Skip to content

feat: run more than one vault-cortex install on a machine with INSTANCE_NAME - #686

Merged
aliasunder merged 57 commits into
mainfrom
worktree-multi-instance-container-name
Oct 21, 2026
Merged

aliasunder merged 57 commits into
mainfrom
worktree-multi-instance-container-name

Conversation

@aliasunder

@aliasunder aliasunder commented Oct 20, 2026 •

Copy link
Copy Markdown
Owner

What does this PR do?

Closes #681.

Two vault-cortex installs can now run side by side on one machine. Before this change, every install used the same container name and the same volumes, so a second Compose project or npx vault-cortex init either failed on the taken container name or attached to the first install's volumes. On the :remote image, a second vault on the first vault's volumes is synced into the same folder, merging the two vaults through Obsidian Sync.

A new INSTANCE_NAME setting names an install's Compose project, container and volumes. Unset, or set to vault-cortex, every name stays exactly as it is today, so existing installs keep their volumes.

The :remote image also stops a boot whose volumes hold the Obsidian Sync setup for a different vault than VAULT_NAME, before ob sync-setup writes anything.

Who is affected

  • Compose (local and remote quickstarts): name: and container_name: follow INSTANCE_NAME. With it unset or vault-cortex, docker compose config renders byte-identical to main. -p and COMPOSE_PROJECT_NAME now rename the container along with the project.
  • CLI: every command reads INSTANCE_NAME from the install's .env and acts only on that name's container and volumes. init asks for a new name and port when the default name's container or volumes already exist, and new --instance-name and --port flags cover scripted installs. A re-run of init keeps an install's name, port and URL. init --yes into a new folder now exits when the default name is taken, until --instance-name is given.
    • When a container with the install's name already exists, init's start question says so ("This stops and replaces the existing … container. Its data volumes are kept."), and a yes replaces it the way start does. Before, docker run failed on the taken name.
    • Commands match a container by its exact name. Docker falls back to container-ID prefixes, so before this an install named with hex digits (2025, cafe) could have matched, and removed, an unrelated container.
    • Before any command stops, replaces or streams the logs of the container that holds the install's name (start, restart, upgrade, configure's restart, down, logs, and init's start offer), it checks that container's image. When the image isn't Vault Cortex, the command leaves the container alone, names the container and its image, and offers two fixes: rename that container, which keeps the install's data, or change INSTANCE_NAME, which starts on new, empty volumes. upgrade checks before it downloads the image.
    • When another app's container holds the name a new install asks for, init doesn't offer to reuse its data: it asks for a new name, and --yes or --instance-name stops instead.
    • init checks a new install's port, a re-run's --port, and the PORT picked from the settings list (init and configure) against the host ports other containers publish, stopped ones included; a published range counts every port in it. A taken port is asked again with the first free port suggested, counting up from 8000; --yes stops instead and names a free port.
    • init refuses a folder whose .env it wrote for an install of the other mode (local or remote), as soon as it knows the folder, and points at start or another --dir. Another app's .env gets the usual re-run question.
    • start, restart, upgrade and init refuse a .env whose PORT isn't a whole number from 1 to 65535 (it was read as 8000) and, for remote installs too, an empty or missing PUBLIC_URL. After writing, init checks the .env on disk the way start does, so a kept file decides what starts.
    • Remote init offers to start without a Sync token. The container then uses a login saved on its volume, or shows its sign-in page at PUBLIC_URL, and the closing message says how to sign in there.
  • Every :remote deployment (Compose, CLI, docker run, Render, Railway, the Lightsail workflow): a boot stops with an error when the Sync client would sync a vault other than VAULT_NAME. That state syncs the wrong vault today, so it is not a supported setup, and this is not flagged as a breaking change. A setup file the Sync client would silently ignore (unreadable, unparseable, or not a setup object) also stops the boot, and so does an answer from the Sync client that names no vault (each with its own error and the client's reason). An install that already uses the right vault but also holds another vault's dormant setup for the same vault folder boots with a warning.
  • The repo-root compose files used by the Lightsail deployment are unchanged; their explicit volume names keep that deployment's data in place.

Changes

  • rootfs/etc/s6-overlay/scripts/check-sync-vault (new, read-only): resolves VAULT_NAME to a vault ID the way ob sync-setup does, validates every stored setup file, and compares the vault the Sync client would use for the vault folder with that ID. It prints the ID for the second check and its decision on stderr, and can be run by hand inside a running container.
  • init-setup-vault runs the check before ob sync-setup, then confirms after sync-setup that the client uses VAULT_NAME's vault before any sync-config call writes to the setup.
  • Compose and env files: both deploy compose files and .env.example files, with the CLI's .env blocks resynced. templates.test.ts counts active optional lines too and skips COMPOSE_PROJECT_NAME, which Compose sets itself.
  • CLI: docker.ts, lifecycle.ts, messages.ts, scaffold.ts, init.ts, program.ts, optional-settings.ts and a new instance-identity.ts. Printed commands and connector names use the install's name.
  • Docs: "Run more than one vault" in the local and remote guides, an INSTANCE_NAME settings row and an "Another vault's Sync setup" section in the remote guide, troubleshooting entries in the Render and Railway guides, and notes in cli/README.md, llms-install.md, DEPLOY.md, ARCHITECTURE.md and AGENTS.md (the Sync client outputs the check relies on).
  • Tests: a new check-sync-vault.test.ts that replays outputs captured from the pinned Sync client; init-setup-vault.test.ts runs the real script beside a stub check; a remote-boot scenario seeds another vault's setup on the config volume and asserts the boot stops before sync-setup with the setup unchanged; CLI unit and PTY tests for the name-in-use prompt, the flags, re-init, custom-name commands, and the image and port checks. The PTY fake docker models containers and volumes by name and reports each container's image.

Decisions

  • The check compares vault IDs from the Sync client's own answers, not stored vault names (they go stale on a rename), and never deletes or rewrites a setup. Comparing setups before and after sync-setup and removing new ones was ruled out, because a setup sync-setup rewrote could be removed with real state in it.
  • The check cannot tell which vault the files in the vault folder came from when no setup points at the folder. The refusal message and the guides say to reset the vault folder and the Sync config together when switching vaults.
  • configure does not offer INSTANCE_NAME: renaming an existing install moves it to empty volumes.
  • The port check counts ports that stopped containers declare, since starting them later would clash, and it counts every port of a published range.

Type of change

  • New feature
  • Documentation
  • Infrastructure (SST, Docker)

Checklist

  • The pre-commit hook passed: typecheck, knip, and ESLint, Prettier, markdownlint and ShellCheck on the staged files
  • npm test passes at the head, c9518b81, after rebasing onto main (5,740 passed, 2 skipped)
  • npm run build succeeds; npm run lint (0 errors) and knip pass
  • npm run test:remote-boot passed 57/57 and npm run test:cli-pty 18/18 locally at 730e6fec, the last commit before the rebase, which brought in only main's release-script and dependency changes; the CLI PTY workflow passed at 8176a2a2
  • New MCP tools follow the naming and description conventions in AGENTS.md (no tool changes)
  • README or ARCHITECTURE.md updated (if user-facing behavior changed)

Live Test Results

Real Obsidian Sync refusal (this Mac, disposable volumes, image built from 4420b49b, sha256:d20552b3; the container, volumes and image were removed afterwards). A fresh config volume held a hand-written setup for a made-up vault ID with vaultPath /vault, which the real Sync client picked. The boot used VAULT_NAME for the real vault, SYNC_MODE=pull-only, a throwaway DEVICE_NAME and no VAULT_PASSWORD, so a missed check would have stopped at ob sync-setup's password check.

Probe Before (v0.54.16) After (4420b49b)
Boot with another vault's setup on the config volume not in v0.54.16 (no vault check) stopped after 4 s at the vault check, before ob sync-setup: "This install is set up to sync a different vault than VAULT_NAME", naming both vaults, the made-up one as "no longer in your account"
Setups on the config volume afterwards — only the planted one, its config.json unchanged
Vault folder afterwards — empty
Sync device registered — none: ob sync-setup never ran

Container name matching (real Docker on this Mac): docker container inspect --format '{{.Name}}' 62f6ea, a 6-character prefix of the ID of a container named mitest-hexprobe, exited 0 and printed /mitest-hexprobe. On main, an install named 62f6ea would have treated that container as its own. The CLI now compares the printed name.

Image and port checks (real Docker 29.8.2 on this Mac, the branch CLI at 730e6fec, throwaway containers created but never started, all removed afterwards):

Probe Before (main) After (730e6fec)
docker container inspect --format '{{.Name}} {{.Config.Image}}' on two containers not in main /probe-foreign-686 umm-actually-test:latest, /probe-own-686 ghcr.io/aliasunder/vault-cortex:latest, the shape containerImage parses
The published-ports template over three IDs not in main one line per container; only the one created with -p 18686:8000 lists 18686
logs and down on an install whose name a non-Vault Cortex container holds not in main (main's down removes any container named vault-cortex, whatever its image) both exit 1 naming the container and its image, with the rename and INSTANCE_NAME fixes; the container is still there
down on an install whose container runs ghcr.io/aliasunder/vault-cortex:latest not in main (no INSTANCE_NAME) removed
init --yes --port 18686 re-run while a stopped container publishes 18686 not in main exits 1: "Port 18686 is already used by the "probe-port-686" container on this machine. Re-run with a free port, such as --port 8000."
init --yes --instance-name probe-foreign-686 for a new install not in main exits 1: "… runs umm-actually-test:latest, not Vault Cortex, so this install needs another name …"; no .env written
init --yes --port 18691 re-run while a stopped container publishes the range 18690-18692 not in main exits 1: "Port 18691 is already used by the "probe-range-686" container …"

Podman and nerdctl were not run. Their source shows both report .Config.Image in container inspect: Podman's InspectContainerConfig.Image, and nerdctl's Docker-compatible Config.Image, set from the reference the container was created with.

Production boot at cdc50ef7. Since then the boot scripts gained fail-closed checks for an unreadable vault list and for a Sync client answer without a vault ID, their own error messages, and a warning when the setup listing fails; the dormant-setup warning now lists only setups for this vault folder. The normal path, where the Sync client already uses VAULT_NAME's vault, is unchanged; the script tests and the remote-boot suite cover it at 730e6fec. On a single-vault install like this one, the warning printed nothing before the narrowing and prints nothing after it, so the boot wasn't redeployed for it. Test Deploy run 39356144477 built and pushed :test from cdc50ef7. The container was recreated from ghcr.io/aliasunder/vault-cortex:test with org.opencontainers.image.revision=cdc50ef7fea73b2d7059510b894899d9f68904f0 and is healthy. "Before" is the previous :test build (revision 5c145387), with this branch's check-sync-vault run by hand inside it.

Probe Before (5c145387) After (cdc50ef7)
Vault check by hand: Vault check: continue — set up for '<vault>', no warning boot log: Vault check: continue — set up for '<vault>', no warning
First sync — First sync complete., continuous sync service up
/healthz (host and public URL) {"ok":true} {"ok":true}
MCP tool call — vault_recent_notes answers

Side by side on one machine at cdc50ef7 (macOS, Docker Desktop, the branch CLI against real Docker, throwaway vaults; everything removed afterwards):

Scenario Result
Install A: init --yes --instance-name … --port 8010, then start --dir healthy on 8010
Install B: interactive init with the default name already in use the name-in-use prompt appeared; own name and port 8011 written; healthy on 8011; OAuth issuer http://localhost:8011/
down --dir B B's container removed, A still running
Two Compose projects from copies of deploy/local with different INSTANCE_NAME both healthy; containers and <name>_mcp_data volumes named per project
Two remote installs without a Sync token, init --mode remote then start --dir both in setup mode side by side, each with its own three volumes

🤖 Generated with Claude Code

Comment thread cli/src/instance-identity.ts Outdated
Comment thread deploy/render/README.md Outdated
Comment thread src/vault-mcp/__tests__/init-setup-vault.test.ts
Comment thread src/vault-mcp/__tests__/init-setup-vault.test.ts
Comment thread src/vault-mcp/__tests__/init-setup-vault.test.ts Outdated
Comment thread src/vault-mcp/__tests__/init-setup-vault.test.ts Outdated
@umm-actually

umm-actually Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

umm-actually re-reviewed at d53c947

No new findings (53 tracked finding(s) across all runs).

Review phase correctness-security did not complete; its findings are missing from this run. See the check run for details.

The review deadline expired; results from completed phases are shown.

Context notes
  • Priority docs already in context: ARCHITECTURE.md, deploy/railway/README.md, deploy/render/README.md, deploy/local/README.md, deploy/remote/README.md

umm-actually · z-ai/glm-5.3-flash

@aliasunder
aliasunder force-pushed the worktree-multi-instance-container-name branch from cdc50ef to 6c9c857 Compare October 21, 2026 00:00
Comment thread cli/src/instance-identity.ts
Comment thread rootfs/etc/s6-overlay/scripts/check-sync-vault Outdated
Comment thread src/vault-mcp/__tests__/init-setup-vault.test.ts Outdated
Comment thread cli/src/instance-identity.ts
Comment thread cli/src/instance-identity.ts Outdated
Comment thread cli/src/optional-settings.ts Outdated
Comment thread src/vault-mcp/__tests__/check-sync-vault.test.ts
@umm-actually

umm-actually Bot commented Oct 21, 2026

Copy link
Copy Markdown

Update DEPLOY.md's stale two-service compose description
Medium severity · conventions · medium confidence

DEPLOY.md:1 — beyond the diff's line ranges, in code the changes touch or depend on.

Pre-existing: DEPLOY.md still says docker-compose runs two services in order and credits the forked sync image, describing the architecture that commit dd2489f replaced with the single vault-cortex:remote container (the current deploy/remote/docker-compose.yml defines exactly one service and bundles Obsidian Sync under s6). It also points readers at an 'ARCHITECTURE.md § Docker Compose Startup' section that the same commit replaced with the s6 startup section, so a reader following DEPLOY.md gets both an outdated topology and a stale cross-reference. The PR touches DEPLOY.md, so this stale passage sits in a file the diff modifies.

Failure scenario: A user reading DEPLOY.md to deploy the remote stack expects two services (obsidian-sync plus the MCP server) and searches ARCHITECTURE.md for a 'Docker Compose Startup' section that no longer exists, then cannot map the documented topology onto the single-service compose file they downloaded.

Suggested fix
Rewrite the passage to describe the single-container stack ('docker compose runs one vault-cortex service; the s6 init chain performs login and sync-setup inside the container') and point to the current ARCHITECTURE.md s6 startup section by its actual heading.

umm-actually · z-ai/glm-5.3-flash

@aliasunder

Copy link
Copy Markdown
Owner Author

Re the umm-actually PR-level finding "Update DEPLOY.md's stale two-service compose description" (comment 6305929440): false positive. DEPLOY.md line 100 already says Compose runs one vault-cortex container (the :remote image) with s6-overlay running the init chain and supervising the sync process and the MCP server, and its link points at ARCHITECTURE.md#container-startup, which matches the current ### Container startup heading. No passage in DEPLOY.md mentions two services or a Docker Compose Startup section.


🔍 ship-check · pr-monitor · claude-opus-5-5

Comment thread cli/src/instance-identity.ts
Comment thread cli/src/instance-identity.ts Outdated
Comment thread cli/src/scaffold.ts
Comment thread cli/src/init.ts
Comment thread cli/src/instance-identity.ts Outdated
@umm-actually

umm-actually Bot commented Oct 21, 2026

Copy link
Copy Markdown

Drop configure.test.ts's local createRecordingDocker in favor of the shared stub
Low severity · conventions · high confidence

cli/src/__tests__/configure.test.ts:26 — beyond the diff's line ranges, in code the changes touch or depend on.

This PR adds createRecordingDocker to command-stubs.ts and switches init, lifecycle and upgrade tests to it, but configure.test.ts still defines its own local copy with a narrower shape (no calls/runParams recording beyond dockerRun). The duplicate can drift from the shared one, as it already has (it lacks the inspect/rm/logs recording the shared version now provides).

Failure scenario: A future change to DockerRunner or the recording shape updates command-stubs' helper but not configure.test.ts's local copy, so configure tests silently assert against a stale stub shape or fail to record operations the shared helper would capture.

Suggested fix
Import createRecordingDocker from ./command-stubs.js in configure.test.ts and delete the local definition.

umm-actually · z-ai/glm-5.3-flash

Comment thread cli/src/init.ts Outdated
Comment thread deploy/remote/README.md Outdated
@umm-actually

umm-actually Bot commented Oct 21, 2026

Copy link
Copy Markdown

Init's success message references .env settings for a kept legacy file it never validated
Low severity · subtle bugs · medium confidence

cli/src/init.ts:563 — beyond the diff's line ranges, in code the changes touch or depend on.

runRemoteInit's start offer runs after the optional-settings and write steps, and for a kept legacy .env (one whose PUBLIC_URL line is empty or absent) offerDockerRun proceeds straight to docker run — resolveDeployment's PUBLIC_URL validation lives only in restart/upgrade/configure paths. A remote .env kept by init with PUBLIC_URL= empty boots a container whose OAuth issuer is unset, which the server rejects at startup; init's gate ladder doesn't check it the way the other start paths do, so the failure surfaces as docker's health timeout instead of the clear PUBLIC_URL message.

Failure scenario: A user re-runs init --mode remote over a compose-era directory whose kept .env has an empty PUBLIC_URL= line, accepts the overwrite=false keep, and answers yes to 'Start the server now?': the container starts, OAuth binding fails at boot, and after 4 minutes init prints only the generic health-timeout message with no pointer to the missing PUBLIC_URL, while restart for the same directory would have named it.

Suggested fix
In runRemoteInit, before offering the start, apply the same PUBLIC_URL presence check resolveDeployment uses (or reuse resolveDeployment's validation) so a kept legacy .env gets the explicit 'PUBLIC_URL not found in <dir>/.env' error instead of a health timeout.

umm-actually · z-ai/glm-5.3-flash

@aliasunder

Copy link
Copy Markdown
Owner Author

Re the umm-actually PR-level finding "Drop configure.test.ts's local createRecordingDocker in favor of the shared stub" (comment 6306297657): fixed in be40c0e. configure.test.ts now imports createRecordingDocker from command-stubs.ts and asserts on its runParams record; the local copy is gone.


🔍 ship-check · pr-monitor · claude-opus-5-5

@aliasunder

Copy link
Copy Markdown
Owner Author

Re the umm-actually PR-level finding "Init's success message references .env settings for a kept legacy file it never validated" (comment 6306372327): false positive. The finding says restart would have named the missing PUBLIC_URL, but resolveDeployment in cli/src/lifecycle.ts checks PUBLIC_URL only for local installs, so start, restart and init all behave the same for a remote .env. For a remote container, the server's own required-variable error names PUBLIC_URL in the container log (print-derived-env leaves it unset on purpose so that error fires), and every remote .env the CLI writes includes PUBLIC_URL.


🔍 ship-check · pr-monitor · claude-opus-5-5

Comment thread cli/src/instance-identity.ts Outdated
Comment thread rootfs/etc/s6-overlay/scripts/check-sync-vault
Comment thread deploy/remote/docker-compose.yml
Comment thread deploy/local/docker-compose.yml
Comment thread deploy/remote/README.md Outdated
Comment thread llms-install.md
@aliasunder
aliasunder marked this pull request as ready for review October 21, 2026 04:19
Comment thread cli/src/init.ts Outdated
Comment thread cli/src/instance-identity.ts
Comment thread cli/src/__tests__/integration/fixtures/docker Outdated
Comment thread cli/src/instance-identity.ts Outdated
Comment thread cli/src/messages.ts Outdated
Comment thread deploy/remote/README.md
Comment thread rootfs/etc/s6-overlay/scripts/check-sync-vault
Comment thread llms-install.md
@umm-actually

umm-actually Bot commented Oct 21, 2026

Copy link
Copy Markdown

Fix the stale start-offer test that passes only because Docker is down
Medium severity · tests · high confidence

cli/src/__tests__/init.test.ts:774 — beyond the diff's line ranges, in code the changes touch or depend on.

The remote-flow test named 'skips the start offer when the sync token was left blank' no longer verifies any token-dependent behavior: runRemoteInit now offers the start regardless of token state (setup mode handles a blank one), and the offer is skipped only because the test's dockerDown stub short-circuits offerDockerRun at its daemon check, before the confirm is ever reached. The sibling test in the same describe asserts the offer IS asked with a blank token, so this test cannot fail for the behavior its name claims.

Failure scenario: With Docker actually running, the same blank-token remote init asks 'Start the server now?' — the sibling 'leaves OBSIDIAN_AUTH_TOKEN blank when auto-capture is declined' test (dockerDaemonOnly) asserts exactly that. Mutating or removing any token-related logic cannot make this test fail, because dockerDown returns 'not-started' from offerDockerRun's daemon gate before the confirm; a reader trusting the test name concludes init refuses to start token-less installs, the opposite of the PR's stated 'Remote init offers to start without a Sync token' behavior.

Suggested fix
Rename the test to what it actually pins (e.g. "does not offer to start when Docker is down, even with a blank token") or fold it into the daemon-down coverage without the token framing; if the blank-token start behavior is what should be pinned, drive it with a running-daemon stub (dockerDaemonOnly/dockerReadyEmptyHost) and assert the offer is asked, matching the sibling test.

umm-actually · z-ai/glm-5.3-flash

@umm-actually

umm-actually Bot commented Oct 21, 2026

Copy link
Copy Markdown

Fix the remote-init test whose blank-token skip no longer exists
Medium severity · tests · high confidence

cli/src/__tests__/init.test.ts:810 — beyond the diff's line ranges, in code the changes touch or depend on.

Remote init now offers the start step even when OBSIDIAN_AUTH_TOKEN is blank (the container serves its sign-in page), so the behavior this test's name claims no longer exists. The assertion still holds only because the fixture runs with the Docker daemon down, which suppresses the start question for any token state.

Failure scenario: A maintainer editing the remote init flow reads the test name, concludes a blank Sync token still suppresses the start offer, and relies on that contract; the suite stays green through any rewrite of the offer gating because dockerDown's daemon check — not token logic — is what keeps 'Start the server now?' out of the asked prompts.

Suggested fix
Rename the test to the gate it actually pins (the not-running daemon suppresses the start question), or run it against a ready daemon and assert the offer now appears for a blank token, matching the new sign-in-page flow.

umm-actually · z-ai/glm-5.3-flash

@aliasunder

Copy link
Copy Markdown
Owner Author

Re the umm-actually PR-level findings "Fix the stale start-offer test that passes only because Docker is down" (comment 6321777433) and "Fix the remote-init test whose blank-token skip no longer exists" (comment 6321777720): both point at the same test, and both are valid. Fixed in 65e03ba.

The test "skips the start offer when the sync token was left blank" described behaviour this PR removed. Remote init now offers to start without a Sync token, and two other tests pin that: "leaves OBSIDIAN_AUTH_TOKEN blank when auto-capture is declined" asks "Start the server now?" with a blank token, and "starts the server without a Sync token and points at its sign-in page" runs it. The test is now "warns and skips the start offer when the Docker daemon is down". It asserts the remote flow's exact warnings (the skipped name check, then the daemon-not-running message with the start command), mirroring the local flow's daemon-down test. No other test name claims the old blank-token skip.


🔍 ship-check · pr-monitor · claude-opus-5-5

Comment thread cli/src/instance-identity.ts Outdated
Comment thread cli/src/lifecycle.ts Outdated
Comment thread cli/src/init.ts Outdated
Comment thread deploy/local/README.md
Comment thread cli/src/docker.ts
Comment thread src/vault-mcp/__tests__/check-sync-vault.test.ts
Comment thread cli/src/lifecycle.ts
Comment thread src/__tests__/docker/fixtures/ob
aliasunder and others added 22 commits October 21, 2026 16:27
…rect the no-token start comment

readEnvPublicUrl now trims inside the quotes, so `PUBLIC_URL="   "` stops
start with the empty-or-missing error instead of passing the gate and
timing out on a server that refuses the blank value.

Ship-Check: bug-check · claude-opus-5-5
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…init start without a Sync token

- readEnvPort reports a PORT that isn't a whole number from 1 to 65535
  instead of using 8000; start, restart, upgrade and init stop on it,
  and an init re-run stops on a kept invalid PORT unless --port
  replaces it. Before, the CLI mapped 8000 while a derived PUBLIC_URL
  could carry the bad value.
- start, restart and upgrade refuse an empty or missing PUBLIC_URL for
  a remote install too, suggesting the server's public address.
- Remote init offers to start the server without a Sync token: the
  container keeps a login saved on its volume, or serves its sign-in
  page, and the closing message says how to sign in there (over
  HTTPS). The generated .env comment no longer says the token must be
  filled in before starting.

Ship-Check: pr-monitor · claude-opus-5-5
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nd show sign-in steps for a kept .env without a Sync token

- A PORT that isn't a port no longer strands a default-port localhost PUBLIC_URL when init --port or configure replaces it, and configure no longer pre-fills that value in the port prompt.
- Remote init decides whether to print the sign-in steps from the .env on disk, so a token captured this run but not saved to a kept .env no longer hides them.
- Comments listing the start checks now include PORT and every command that runs them.

Ship-Check: bug-check · claude-opus-5-5

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…dropped Sync token, and warn when --port strands PUBLIC_URL

- After writing .env, init runs start's own checks on the file on disk:
  a kept .env with no PUBLIC_URL, VAULT_PATH, or a valid PORT or
  INSTANCE_NAME stops init with start's error instead of starting a
  server that can't boot, and a local start mounts the kept file's
  vault folder, as start does.
- Remote init warns when it kept an existing .env and so dropped the
  Sync token captured in this run, and prints the command that adds it.
- A re-run whose --port leaves a custom PUBLIC_URL on the old port gets
  the same warning a port picked in the settings menu gives.

Ship-Check: pr-monitor · claude-opus-5-5
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e --port PUBLIC_URL warning fires

- resolveDeployment's docstring lists init, which now runs start's .env checks.
- describeStrandedCarriedPublicUrl's docstring states that any carried PUBLIC_URL that doesn't follow the new port warns, not only one that names a port.

Ship-Check: bug-check · claude-opus-5-5

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… already names the new port

Ship-Check: pr-monitor · claude-opus-5-5
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rs publish

init already refuses a name another install uses; it now does the same
for the port. Docker lists every container's published host ports,
stopped ones included:
- the own-name prompt offers the first free port above 8000 and asks
  again for one that's taken, so a third install no longer gets 8001
- a taken --port or default port stops --yes and is asked again
  interactively
- a reuse doesn't count the reused install's own container

Ship-Check: pr-monitor · claude-opus-5-5
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… CLI init notes

The Compose steps in the local and remote guides now name the settings a
second install needs and link where each is described, instead of listing
example values. The CLI README's init notes are split by subject: remote
setups, re-running init, and running a second vault as numbered steps.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…guides

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each install syncs a different vault, so their changes never share a sync
history and a shared device name has nothing to tell apart. The settings
table row goes back to describing what the name labels.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nto .env

The optional settings text in each deploy .env.example is copied into the
.env the CLI writes, where Compose wording points CLI users at a tool they
never use. The shared text now names no setup method: settings apply when
the container is re-created, and renaming an install starts with stopping
its container.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…iew findings

- init stops before asking anything when the folder's .env belongs to an
  install of the other mode, pointing at start or another --dir, so a re-run
  never writes or starts the other mode over it.
- After a taken port is entered, the port prompt suggests the first free
  port, and a reuse whose default port another container holds suggests a
  free one instead of the taken default.
- The sign-in lines name the page first, then say it asks for the auth token
  below, so the URL no longer reads as the token.
- The boot warning about other vaults' Sync setups lists only setups stored
  for this vault folder, the only ones ob can pick.
- The PTY tests' fake docker answers `container ls -aq` with the present
  names instead of failing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… blank-token skip

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nd in the settings list

- A re-run's --port gets the same check as a new install's port, with the
  install's own container left out, so --yes stops and an interactive run
  asks again before .env is written.
- The port picked in the optional-settings list, in init and configure, is
  refused when another container publishes it; the install's own port is
  accepted. The port-collision helpers move beside the port prompt they
  build on.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
start, restart, upgrade, configure's restart, down, and init's start offer
now read the image of the container that holds the install's name. When it
is not a Vault Cortex image they stop with a message naming the container and
its image, instead of removing it.

Also:
- suggestFreePort starts at the default port, so init suggests 8000 when it
  is free, as the guides say.
- init passes the deployment's vault path straight through; the fallback for
  a kept remote .env could not be reached once init refuses a mode mismatch.
- derivePublicUrlOverride is now withPublicUrlOverride and takes named inputs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Installs the CLI started before v0.27.0 still run ghcr.io/aliasunder/vault-mcp,
so the foreign-container check now accepts that image too, and start,
restart, upgrade and down keep managing those installs.

The fake docker in the PTY tests now answers `container inspect` for every
name it is given, as docker does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The port check read Docker's published ports into a map keyed by host port,
so when the install's own container and another container both declared a
port, whichever was inspected last won. If that was the install's own
container, the other container's claim disappeared and the port read as
free. The ports now stay one entry per container, and the install's own
container is left out before the map is built.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The --yes error for a taken port told a user who had passed --port to
re-run with --port. It now names the first free port instead, which fits
whether the taken port came from --port or the default.

configure also reuses the instance name requireInitializedDir has already
checked, instead of reading INSTANCE_NAME a second time.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
logs streamed whatever container held the install's name, so it could show
another app's output while down and restart refused that same container. It
now refuses it with the same message.

Every command now finds the install's container through containerImage, so
containerExists is removed, along with its stale comment about guarding
removals.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Bug-check findings:
- init no longer offers to reuse the data of another app's container that
  holds the requested name: --yes and --instance-name stop, and the prompt
  asks for a new name straight away.
- upgrade refuses another app's container before downloading the image.
- configure asks Docker again before offering the restart, since it may
  have stopped or started while the prompts were open.
- A host-port range ("8000-8010") counts every port in it as taken.
- The mode-mismatch refusal applies only to a .env init wrote (one with
  MCP_AUTH_TOKEN); another app's .env gets the usual re-run question.
- The refusal names both remedies: rename the other container, which keeps
  the install's data, or change INSTANCE_NAME, which starts on new volumes.
- The pre-rename vault-mcp image is no longer accepted: the CLI named those
  containers vault-mcp, so the INSTANCE_NAME lookup never reaches them.

Code-quality findings:
- One checkInstanceContainer helper replaces four copies of the image check.
- askPort folds into askFreePort; portInUseMessage takes named inputs;
  askOptionalSettings requires portsInUse.
- Stale comments and docs corrected (start's replace rule, init's re-run
  bullets, the second-vault step, the dormant-setup comment, re-created).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@aliasunder
aliasunder force-pushed the worktree-multi-instance-container-name branch from 730e6fe to c9518b8 Compare October 21, 2026 20:29
Comment thread cli/src/init.ts
aliasunder and others added 2 commits October 21, 2026 16:40
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ting one

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@aliasunder
aliasunder merged commit 772a869 into main Oct 21, 2026
18 checks passed
@aliasunder
aliasunder deleted the worktree-multi-instance-container-name branch October 21, 2026 21:19
aliasunder added a commit that referenced this pull request Oct 22, 2026
## Summary

The CLI release notes list every commit that touches `cli/`, so they
include changes no CLI user gets: the release's own version bump,
test-only commits, and commits that only reword comments (including the
comment text of the `.env` templates in `cli/src/env.ts`).

A commit is now listed only when its changes under `cli/` add or remove
a line that none of these rules skips:

- blank lines
- lines in files under `cli/src/__tests__/`
- the `"version"` line of `cli/package.json` (the release bump)
- in `.ts` files, lines that start, after any indent, with `#`, `//`,
`/*` or `*`: comments, and the `.env` templates' comment text. A `#`
line shaped like a setting (`# KEY=value`) still counts, because the
templates ship optional settings commented out for the user to uncomment

In other files, `cli/README.md` included, every non-blank line counts.
Dependency changes in `cli/package.json` stay listed, and the "Published
to npm" footer is unchanged. With no earlier CLI tag, the range is still
the whole history, and the same rules decide which of its commits are
listed.

- `.github/scripts/generate-cli-notes.sh`: new script that prints the
listed commits since the previous CLI tag, one `- <subject>` line each,
newest first.
- `.github/workflows/cli_release.yml`: the "Create GitHub release" step
runs the script with `bash` instead of `git log -- cli/`.
- `src/__tests__/generate-cli-notes.test.ts`: runs the real script under
`bash` against a temporary git repository.
- `scripts/__tests__/lint-shell.test.ts`: lists the new script among the
shell scripts ShellCheck checks.
- `CONTRIBUTING.md`: the release-notes section states the CLI rule.
- `AGENTS.md`: the script-test conventions list the new test.

## Test plan

- [x] The script for every `cli-v*` tag (55): cli-v0.16.0 lists exactly
#686, #682 and #677 and leaves out its own bump, #665 and #642, whose
`cli/` changes are comment text only. Counting setting lines changed the
result for five commits in the whole history (#225, #314, #468, #484,
#617), each adding or renaming a commented-out `.env` setting; without
it, cli-v0.10.6 and cli-v0.12.0 would have had no listed commits.
- [x] `npx vitest run src/__tests__/generate-cli-notes.test.ts
scripts/__tests__/lint-shell.test.ts`: 30 tests pass; each rule was
mutated once and fails the test that covers it.
- [x] The same output under BSD awk (macOS) and mawk (the Ubuntu
runners' awk), and under macOS `/bin/bash` 3.2.
- [x] `npm run lint:shell`, markdownlint and prettier pass.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support for multiple vaults

1 participant