install.sh: clone anywhere, run one command
A manifest declares repo path -> target, and the script walks it. Symlinks, not copies, so an installed file edits the repo and one commit reaches every other machine. Idempotent — a correct link is a no-op, safe after every pull. A real file where a link should go is moved to .bak-<timestamp> and reported, because on a new machine that may be the only copy of something written by hand. Conditional targets are what make it portable: officer.dev/CLAUDE.md and the officer-chat-events hook both name ~/officer-dev/platform, so a laptop with no Officer install gets neither instead of getting a document that lies. mask-secret is a command now rather than a path only true on this machine, and user/CLAUDE.md says so. It is a generated wrapper, not a symlink, and that is forced: mask-secret.py resolves its own path to find mask-hooks.d, so a symlink would reach back into the repo and run every machine's hooks. The wrapper sets MASK_HOOKS_DIR to the ones this machine installed and still runs the script from the repo, so no code is copied. Verified against a scratch HOME: re-run is all ok, the hand-written file survives as .bak, and a machine without Officer skips both conditional lines.
This commit is contained in:
@@ -1,47 +1,35 @@
|
||||
# TODO
|
||||
|
||||
## Bootstrap installer (next session — 2026-08-14)
|
||||
## Bootstrap installer — BUILT 2026-08-20
|
||||
|
||||
**Goal:** clone this repo on any machine, run one command, and every file lands
|
||||
where the tooling expects it.
|
||||
`install.sh` plus `manifest`. Clone the repo, run one command, every file lands where the tooling
|
||||
expects it. See README.md.
|
||||
|
||||
git clone <this repo> ~/projects/claude-mds && ~/projects/claude-mds/install.sh
|
||||
Everything the plan asked for is in: a manifest rather than paths in shell, symlinks rather than
|
||||
copies, idempotent, conditional targets, backups instead of silent clobber, `--dry-run`.
|
||||
|
||||
### Shape
|
||||
Verified on this machine and against a scratch `HOME`: a re-run is all `ok`, a real file in the way is
|
||||
moved to `.bak-<timestamp>` and survives, and a machine with no `~/officer-dev/platform` skips both
|
||||
the workspace file and its hook.
|
||||
|
||||
- **A manifest, not a script full of paths.** Declare `repo path -> target path`
|
||||
in one file and let `install.sh` walk it. Adding a doc later should mean one
|
||||
manifest line, not editing shell.
|
||||
- **Symlinks, not copies.** Same reason as here: two copies drift, and the point
|
||||
of the repo is that an edit lands in git. `ln -sfn`.
|
||||
- **Idempotent.** Safe to re-run after every `git pull`. Re-linking an existing
|
||||
correct link is a no-op.
|
||||
- **Conditional targets.** `user/CLAUDE.md` installs everywhere. `officer.dev/`
|
||||
only where that workspace exists. A manifest entry needs a "only if this path
|
||||
exists" condition.
|
||||
- **Never clobber silently.** If the target is a real file and not our link,
|
||||
back it up (`.bak-<date>`) and say so. That file may be the only copy of
|
||||
something written by hand on that machine.
|
||||
- **Dry run by default, or at least `--dry-run`.** Show what it would link.
|
||||
**`mask-secret` is a command now**, on `~/.local/bin`, and `user/CLAUDE.md` names it instead of the
|
||||
hardcoded path that was only ever true here. It is a generated wrapper, not a symlink, and that is
|
||||
forced: `mask-secret.py` finds hooks at `Path(__file__).resolve().parent/mask-hooks.d`, and `.resolve()`
|
||||
follows a symlink back into the repo — which carries every machine's hooks, not this one's. The
|
||||
wrapper sets `MASK_HOOKS_DIR` and still executes the script from the repo, so no code is copied.
|
||||
|
||||
### Tools are installed, not referenced in place — DECIDED
|
||||
### Still open
|
||||
|
||||
`tools/` stays in the repo as the source, and the installer puts it under
|
||||
`~/.local/claude-scripts/` (confirm the name), with `mask-secret` linked onto
|
||||
`~/.local/bin` so it is a command rather than a path.
|
||||
|
||||
That fixes a real bug: `user/CLAUDE.md` currently hardcodes
|
||||
`~/projects/claude-mds/tools/mask-secret.py`, which is only true on this
|
||||
machine. Once installed, the doc says `mask-secret` and is true everywhere.
|
||||
|
||||
Carry `mask-hooks.d/` across with it, keeping the executable bits. The hooks are
|
||||
the machine-specific half, so the installer should place only the hooks that
|
||||
belong on that machine — same "only if this path exists" condition as the docs.
|
||||
`officer-chat-events` should not land on a laptop with no Officer install, even
|
||||
though it already exits cleanly when it finds no database.
|
||||
|
||||
Update `user/CLAUDE.md` to name the command in the same change, or the doc and
|
||||
the layout disagree the moment this ships.
|
||||
- **Bootstrapping the bootstrap.** The machine needs git, an SSH key for Gitea, and the repo URL
|
||||
before any of this runs. A one-line `curl | sh` would close it, and may be deliberately not worth
|
||||
having.
|
||||
- **`settings.json` and hooks.** Not managed here yet. The repo is named for the docs but already
|
||||
carries `tools/`, so the line is already crossed; the question is whether per-machine settings can
|
||||
be expressed as one file plus conditions, or whether they are genuinely per-machine.
|
||||
- **Which files are universal.** `officer.dev/` is the only machine-specific one so far. The
|
||||
`platform-appstore/` set is a candidate and was never folded in.
|
||||
- **PATH.** `install.sh` warns when `~/.local/bin` is missing from `PATH` but does not edit a shell
|
||||
profile. Editing someone's `.zshrc` from an installer is a bigger intrusion than the warning.
|
||||
|
||||
### Prior art to discuss: akitaonrails/ai-memory
|
||||
|
||||
|
||||
Reference in New Issue
Block a user