Commit graph

3 commits

Author SHA1 Message Date
dan-claude-bot
199cf6ecaf fix: the flip and the uninstall must run on macOS — rename(2) via node, no mapfile
claude-bot's round-2 catch: cast is the sibling that runs on the
operator's own machine, and the layout port carried two Linux
assumptions in with it.

- The atomic current flip spelled 'replace, don't descend' the GNU way
  (mv -Tf); BSD/macOS mv has no -T and dies. The flip now rides node's
  fs.renameSync — rename(2) is POSIX, node is a cast prerequisite on
  every platform — as one flip_current(), byte-identical in install.sh
  and bin/cast, added to the anti-drift diff.
- cmd_uninstall's de-dup used mapfile — bash 4, and macOS ships bash
  3.2. Now a portable while-read append.
- readlink -f: Apple's readlink grew -f in macOS 12.3 (March 2022); the
  installer now probes it once among the prerequisites and refuses
  loudly on older systems instead of failing weirdly mid-flip.
- A portability test pins both spellings out of the two scripts, so a
  reintroduction fails in CI, not on the first operator Mac.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 21:39:29 +00:00
dan-claude-bot
5cd5968cf2 refactor: rescope to versioned installations — the release flow moves out
Maintainer direction: this PR's one goal is the versioned layout, the same
one box#79 built and rig#36 ported — the release flow (tags, release.yml,
prebuilt assets, CHANGELOG) is its own PR later, the shape rig#40 has.

So: release.yml, changelog-section.sh, CHANGELOG.md and the asset-aware
installer channels leave this branch, and in their place cast gets the
family layout for real:

- install.sh lands each build at $DEST/versions/<package.json version>,
  'current' names the default (atomic rename flips), $BINDIR/cast points
  through it. Converging no-op on an installed version (nothing rebuilt),
  CAST_REINSTALL=1 replaces, a new version installs beside and becomes
  default. Pre-versioning flat installs migrate in place, bit for bit.
  CAST_INSTALL_SOURCE=<dir|tarball> installs locally (CI/tests, rig's
  RIG_INSTALL_SOURCE precedent). No flip gate: box refuses under live
  boxes, rig warns on a converged host — cast is an API client, a flip
  strands nothing, 'cast use <old>' is one command away.
- bin/cast grows the layout verbs in bash (they must work when dist/ is
  broken): versions (marks current+running), use (atomic flip, then
  asserts the chain ANSWERS the new version), uninstall (consent gate,
  CURRENT guard, dangling-current guard, ends with the absence assert).
  valid_version/pkg_version are byte-identical copies in both files; a
  test diffs them so the gates cannot drift.
- cast --version stays: package.json is the single source of truth,
  printed with the install root, rig-style.
- ci.yml gains the install job: the real installer, from this checkout,
  layout asserted, converge no-op asserted, uninstall --all asserted
  absent — the box CI precedent.
- Tests drive the REAL install.sh and bin/cast offline (npm shim, local
  source): the layout, the chain answering end to end, no-op/reinstall/
  side-by-side/migration semantics, the hostile-version gates, refs/heads
  download, every uninstall refusal.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 21:17:59 +00:00
a10349d835 feat: cast — the Coolify executor, extracted from the infra state repo
Public tool, private state. cast holds no hostnames, no bindings, no
secrets: it joins a product repo's .infra/ manifest with a state directory
you point it at, and makes Coolify match.

Extracted from heavy-duty/infra, which was half tool and half state — the
inconsistency that made it impossible to say whether "infra" named a CLI
or a runbook. rig builds the boxes; cast fills them; infra is what they
are filled with.

Two changes were required to make it genuinely stateless and publishable:

- The implicit cwd contract (environments.yaml / secrets/ / .coolify.env
  resolved against the working directory, silently reading the wrong file
  from the wrong place) is now an explicit --state <dir> / $CAST_STATE.
- BANNED_IN_PROD — a hardcoded list of one product's ALLOW_* flags, the
  only product knowledge in the executor — becomes the generic, operator-
  owned environments.<env>.forbidden_var_patterns. The guard now lives in
  private state, so a product-side change cannot lower its own guard, and
  it is a pattern rather than a list, so it catches unforeseen siblings.

Age identities resolve as $CAST_AGE_KEY_FILE_<ENV> then
~/.config/cast/age-<env>.key — which is the entire attended-vs-unattended
apply mechanism, with no environment names known to the tool.

Instance identity (org names, the GitHub App name, founder domains) is out
of the fixtures and out of register-github-app.sh, which took APP_NAME and
ORG as arguments rather than baking them in.

69 tests green; bin/cast + curl installer mirror rig's shape.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 12:25:44 +00:00