Files
claude-mem/plans/2026-04-29-installer-streamline.md
T
2026-05-04 20:22:29 -07:00

532 lines
24 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Installer Streamline — Eliminate 30s Silent Dead Air
**Goal:** Move all heavy install work (Bun/uv install, `bun install` in plugin cache) into the `npx claude-mem install` flow with a visible spinner. Make hooks runtime-only — never installers.
**Net effect:**
- `smart-install.js` runs in normal Claude Code lifecycle: 3 → 0 (or 1 via `npx claude-mem repair` after `claude plugin update`)
- 30s silent dead air → visible spinner during `npx`
- `npx claude-mem repair` becomes the canonical recovery entry point
- ~420 lines of code deleted (smart-install.js × 2 + tests + docs)
**Out of scope:** `bun-runner.js` deletion (independent rework with Windows/stdin verification needs — ship later).
---
## Phase 0 — Documentation Discovery (already complete)
These facts came from a discovery agent + direct file reads. Each implementation phase below cites them by line number; do not re-derive.
### Allowed APIs / patterns to copy
| Item | Location | What to copy |
|---|---|---|
| NPX command dispatcher | `src/npx-cli/index.ts:39141` | Manual `switch (command)` on `process.argv.slice(2)`. Each case dynamic-imports its handler. |
| `install` case (template for `repair`) | `src/npx-cli/index.ts:4652` | `const { runInstallCommand } = await import('./commands/install.js'); await runInstallCommand({ ide: ideValue });` |
| Plugin cache dir helper | `src/npx-cli/utils/paths.ts:3234` | `pluginCacheDirectory(version)``~/.claude/plugins/cache/thedotmack/claude-mem/{version}/` |
| `.install-version` marker readers | `src/services/context/ContextBuilder.ts:36,45` and `src/services/worker/BranchManager.ts:173,228` | These read/delete the marker. Marker schema (`{ version, bun, uv, installedAt }`) MUST be preserved. |
| `clack` task pattern | `src/npx-cli/commands/install.ts:604664` | `runTasks([{ title, task: async (message) => { … return 'Done OK' } }])` |
### Anti-patterns / API methods that DO NOT exist (avoid inventing)
- There is no existing `version-check.js` helper in `plugin/scripts/`. Phase 4 must create it.
- `package.json#files` already globs `plugin/scripts/*.js` (line 50), so deleting `plugin/scripts/smart-install.js` requires no `package.json` change.
- `scripts/smart-install.js` and `plugin/scripts/smart-install.js` are **both source files** kept in sync manually — there is no build step that copies one to the other. Both must be deleted in Phase 5.
- `runSmartInstall()` (install.ts:325345) shells `node smart-install.js`. After Phase 1 you can call the new module directly — do NOT shell out.
- The `claude plugin install` exec at install.ts:113 has **only one caller** in the entire repo. Safe to remove.
### File inventory used by this plan
| File | Lines | Disposition |
|---|---|---|
| `src/npx-cli/commands/install.ts` | 761 | Edited heavily (Phase 2) |
| `src/npx-cli/index.ts` | 147 | One case added (Phase 3) |
| `plugin/hooks/hooks.json` | 93 | Setup hook command rewritten, SessionStart smart-install entry deleted (Phase 4) |
| `scripts/smart-install.js` | 264 | DELETED (Phase 5) |
| `plugin/scripts/smart-install.js` | ≈264 | DELETED (Phase 5) |
| `tests/smart-install.test.ts` | 310 | DELETED (Phase 5) |
| `tests/plugin-scripts-line-endings.test.ts` | 33 | One array entry removed (Phase 5) |
| `plugin/scripts/version-check.js` | NEW | CREATED (Phase 4) |
| `src/npx-cli/install/setup-runtime.ts` | NEW | CREATED (Phase 1) |
| Docs (`docs/public/*.mdx`, `docs/architecture-overview.md`) | misc | Light edit (Phase 6) |
---
## Phase 1 — Create `src/npx-cli/install/setup-runtime.ts`
**What to implement:** Port the smart-install.js logic to a TypeScript module that takes a target directory parameter (so it can install into the plugin cache dir, not just the marketplace dir). Three exported functions plus internal helpers.
**File to create:** `src/npx-cli/install/setup-runtime.ts`
**API surface (these names are used by Phase 2 and Phase 3 — do not rename):**
```ts
export async function ensureBun(): Promise<{ bunPath: string; version: string }>;
export async function ensureUv(): Promise<{ uvPath: string; version: string }>;
export async function installPluginDependencies(targetDir: string, bunPath: string): Promise<void>;
export function readInstallMarker(targetDir: string): { version: string; bun?: string; uv?: string; installedAt?: string } | null;
export function writeInstallMarker(targetDir: string, version: string, bunVersion: string, uvVersion: string): void;
export function isInstallCurrent(targetDir: string, expectedVersion: string): boolean;
```
**Reference implementation to port from:** `scripts/smart-install.js:1264`. Map old → new:
| smart-install.js | setup-runtime.ts |
|---|---|
| `getBunPath()` / `isBunInstalled()` / `installBun()` (lines 42152) | private helpers consumed by `ensureBun()` |
| `getUvPath()` / `isUvInstalled()` / `installUv()` (lines 77194) | private helpers consumed by `ensureUv()` |
| `needsInstall()` (lines 196205) | `isInstallCurrent()` + `readInstallMarker()` |
| `installDeps()` (lines 207226) | `installPluginDependencies(targetDir, bunPath)` — accepts target dir as parameter |
| `verifyCriticalModules()` (lines 228246) | private helper called inside `installPluginDependencies` |
| `MARKER` constant (line 32) | derive inside each function: `join(targetDir, '.install-version')` |
| Top-level `try { … }` (lines 248264) | DELETE — caller orchestrates |
**Key behavioral differences from smart-install.js:**
- All functions take `targetDir` as a parameter (was a top-level `ROOT` constant).
- `ensureBun()` / `ensureUv()` return their version strings rather than logging — caller decides what to display.
- All functions throw on failure with descriptive `Error.message`. The `clack` `runTasks` wrapper in Phase 2 catches and renders.
- `console.error` calls in install/uninstall paths become structured: throw a single `Error` with the manual install instructions in the message body.
- Marker schema is preserved exactly (`{ version, bun, uv, installedAt }`) so existing readers in `ContextBuilder.ts:36` and `BranchManager.ts:173,228` continue to work.
**Verification checklist:**
- [ ] `bun build src/npx-cli/install/setup-runtime.ts --target=node` succeeds (or whatever the project's TS check command is — confirm via `package.json#scripts`)
- [ ] Marker file format is byte-identical to smart-install.js output (write a marker, diff against a marker written by the old code)
- [ ] `grep -rn "ROOT" src/npx-cli/install/setup-runtime.ts` returns nothing — no top-level constants
**Anti-pattern guards:**
- ❌ Do not invent a `bunInstall.ts` or `uvInstall.ts` split — keep all three in one file. They share helper code (paths, version probing).
- ❌ Do not import from `plugin/scripts/smart-install.js` — it gets deleted in Phase 5.
- ❌ Do not change the marker schema. Existing readers depend on `{ version }` field.
---
## Phase 2 — Rework `src/npx-cli/commands/install.ts`
**What to implement:** Drop `needsManualInstall` gating (always run copy/register/enable for every IDE), add a new unconditional "Setting up runtime" task before `setupIDEs`, neuter the claude-code `execSync` shell-out, delete `runSmartInstall()`, and add a `runRepairCommand()` export.
**File to edit:** `src/npx-cli/commands/install.ts`
### Edit 2A — Add import for setup-runtime (top of file, after other imports)
Insert after line 10 (after the `ensureWorkerStarted` import):
```ts
import {
ensureBun,
ensureUv,
installPluginDependencies,
writeInstallMarker,
isInstallCurrent,
} from '../install/setup-runtime.js';
```
### Edit 2B — Delete `runSmartInstall()` function
**Delete lines 325345** (the entire `function runSmartInstall(): boolean { … }` block).
### Edit 2C — Drop `needsManualInstall` gating, ungate the runTasks block
**Line 589** currently reads:
```ts
const needsManualInstall = selectedIDEs.some((id) => id !== 'claude-code');
```
**Delete line 589.** Update line 593's `if (needsManualInstall) {` to just `{` (or unwrap the block — preferred). The `runTasks` block at lines 604664 now runs unconditionally.
**Within that runTasks block:** delete the "Setting up Bun and uv" task entry (lines 656663). Replace its slot with the new "Setting up runtime" task (Edit 2D).
### Edit 2D — Insert "Setting up runtime" task
Replace the deleted "Setting up Bun and uv" task (lines 656663) with:
```ts
{
title: 'Setting up runtime (first install can take ~30s)',
task: async (message) => {
message('Checking Bun…');
const { version: bunVersion } = await ensureBun();
message('Checking uv…');
const { version: uvVersion } = await ensureUv();
const cacheDir = pluginCacheDirectory(version);
if (!isInstallCurrent(cacheDir, version)) {
message('Installing plugin dependencies…');
const { bunPath } = await ensureBun();
await installPluginDependencies(cacheDir, bunPath);
writeInstallMarker(cacheDir, version, bunVersion, uvVersion);
}
return `Runtime ready (Bun ${bunVersion}, uv ${uvVersion}) ${pc.green('OK')}`;
},
},
```
Place this AFTER the "Installing dependencies" (npm install) task — same ordering position the deleted task occupied.
### Edit 2E — Neuter the claude-code shell-out in `setupIDEs`
**Lines 110123 currently:**
```ts
case 'claude-code': {
try {
execSync(
'claude plugin marketplace add thedotmack/claude-mem && claude plugin install claude-mem',
{ stdio: 'inherit' },
);
log.success('Claude Code: plugin installed via CLI.');
} catch (error: unknown) {
console.error('[install] Claude Code plugin install error:', );
log.error('Claude Code: plugin install failed. Is `claude` CLI on your PATH?');
failedIDEs.push(ideId);
}
break;
}
```
**Replace with:**
```ts
case 'claude-code': {
log.success('Claude Code: plugin registered (cache + settings written by npx).');
break;
}
```
The cache dir, marketplace registration, plugin registration, and `enabledPlugins` flag have all been written by the (now ungated) runTasks block before `setupIDEs` is called. `claude plugin install` was duplicating that work and triggering the silent Setup hook — both reasons to drop it.
### Edit 2F — Add `runRepairCommand()` export
After `runInstallCommand()` (after line 761), append:
```ts
export async function runRepairCommand(): Promise<void> {
const version = readPluginVersion();
const cacheDir = pluginCacheDirectory(version);
if (isInteractive) {
p.intro(pc.bgCyan(pc.black(' claude-mem repair ')));
} else {
console.log('claude-mem repair');
}
log.info(`Version: ${pc.cyan(version)}`);
await runTasks([
{
title: 'Setting up runtime',
task: async (message) => {
message('Checking Bun…');
const { version: bunVersion } = await ensureBun();
message('Checking uv…');
const { version: uvVersion } = await ensureUv();
message('Reinstalling plugin dependencies…');
const { bunPath } = await ensureBun();
await installPluginDependencies(cacheDir, bunPath);
writeInstallMarker(cacheDir, version, bunVersion, uvVersion);
return `Runtime ready (Bun ${bunVersion}, uv ${uvVersion}) ${pc.green('OK')}`;
},
},
]);
if (isInteractive) {
p.outro(pc.green('claude-mem repair complete.'));
} else {
console.log('claude-mem repair complete.');
}
}
```
`runRepairCommand` always runs the install (no `isInstallCurrent` short-circuit) — the user invoked `repair` because something is wrong, so don't gate on the marker.
**Verification checklist:**
- [ ] `grep -n "needsManualInstall" src/npx-cli/commands/install.ts` returns nothing
- [ ] `grep -n "runSmartInstall" src/npx-cli/commands/install.ts` returns nothing
- [ ] `grep -n "claude plugin install" src/npx-cli/commands/install.ts` returns nothing
- [ ] `grep -n "claude plugin marketplace add" src/npx-cli/commands/install.ts` returns nothing
- [ ] `runRepairCommand` is exported and TypeScript compiles
- [ ] `runInstallCommand` still exports the same `InstallOptions` shape (Phase 3 needs it untouched)
**Anti-pattern guards:**
- ❌ Do not delete `runNpmInstallInMarketplace()` — it's still needed for the marketplace dir copy step (other IDEs use that dir).
- ❌ Do not delete `copyPluginToMarketplace()` — non-claude-code IDEs read from `marketplaceDirectory()`.
- ❌ Do not delete the `if (alreadyInstalled)` overwrite-confirm block (lines 538562) — user-facing UX preserved.
---
## Phase 3 — Wire `npx claude-mem repair`
**What to implement:** Add a `repair` case to the npx-cli command dispatcher.
**File to edit:** `src/npx-cli/index.ts`
### Edit 3A — Add `repair` case
In the `switch` block (lines 39141), copy the `install` case pattern from lines 4652 and adapt:
```ts
case 'repair': {
const { runRepairCommand } = await import('./commands/install.js');
await runRepairCommand();
break;
}
```
Place it adjacent to the `install` case for discoverability.
### Edit 3B — Help text update (if applicable)
If `src/npx-cli/index.ts` has a help/usage block (look for `case 'help':` or default case), add `repair` to the list of commands with description: `Repair claude-mem runtime (re-runs Bun/uv setup and bun install in plugin cache).`
**Verification checklist:**
- [ ] `npx claude-mem repair --help` (after build) shows the command
- [ ] `npx claude-mem repair` runs `runRepairCommand` end to end on a corrupted cache (delete `.install-version` then run; should reinstall)
- [ ] Help/usage output (if it exists) lists `repair`
**Anti-pattern guards:**
- ❌ Do not add CLI flag parsing for `repair` (no flags needed).
- ❌ Do not duplicate the `runRepairCommand` body in `index.ts` — dynamic import only.
---
## Phase 4 — Strip smart-install from hooks; add `version-check.js`
**What to implement:** Replace the Setup hook's `node smart-install.js` call with a fast version-marker check. Delete the SessionStart smart-install hook entry entirely.
### Edit 4A — Create `plugin/scripts/version-check.js`
**File to create:** `plugin/scripts/version-check.js` (new)
```js
#!/usr/bin/env node
import { existsSync, readFileSync } from 'fs';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
function resolveRoot() {
if (process.env.CLAUDE_PLUGIN_ROOT) {
const root = process.env.CLAUDE_PLUGIN_ROOT;
if (existsSync(join(root, 'package.json'))) return root;
}
try {
const scriptDir = dirname(fileURLToPath(import.meta.url));
const candidate = dirname(scriptDir);
if (existsSync(join(candidate, 'package.json'))) return candidate;
} catch {}
return null;
}
const ROOT = resolveRoot();
if (!ROOT) process.exit(0);
try {
const pkg = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf-8'));
const markerPath = join(ROOT, '.install-version');
if (!existsSync(markerPath)) {
console.error('claude-mem: runtime not yet set up — run: npx claude-mem repair');
process.exit(0);
}
const marker = JSON.parse(readFileSync(markerPath, 'utf-8'));
if (marker.version !== pkg.version) {
console.error(`claude-mem: upgraded to v${pkg.version} — run: npx claude-mem repair`);
}
} catch {
console.error('claude-mem: install marker unreadable — run: npx claude-mem repair');
}
process.exit(0);
```
**Behavior:**
- Sub-100ms (two synchronous file reads + JSON.parse + string compare).
- Always exits 0 (non-blocking) per the project's exit-code strategy in CLAUDE.md.
- Stderr message tells the user exactly what to run if a mismatch is detected.
### Edit 4B — Rewrite Setup hook command in `plugin/hooks/hooks.json`
**Lines 415** — replace the existing Setup hook command. Current command ends with `node "$_R/scripts/smart-install.js"`. Change it to `node "$_R/scripts/version-check.js"`. Everything before that (PATH export, `_R` resolution, cygpath) stays.
Concretely: the only change to line 11 is the trailing `smart-install.js``version-check.js`.
### Edit 4C — Delete SessionStart smart-install entry in `plugin/hooks/hooks.json`
**Lines 1740** — the SessionStart hook array currently has THREE hook entries:
1. `node "$_R/scripts/smart-install.js"` (lines 2126) — DELETE this entire entry
2. `node "$_R/scripts/bun-runner.js" "$_R/scripts/worker-service.cjs" start` (lines 2732) — KEEP
3. `node "$_R/scripts/bun-runner.js" "$_R/scripts/worker-service.cjs" hook claude-code context` (lines 3338) — KEEP
After edit, the SessionStart `hooks` array has 2 entries instead of 3.
**Verification checklist:**
- [ ] `cat plugin/hooks/hooks.json | jq '.hooks.Setup[0].hooks[0].command' | grep version-check.js` succeeds
- [ ] `cat plugin/hooks/hooks.json | jq '.hooks.SessionStart[0].hooks | length'` returns `2`
- [ ] `grep -c "smart-install" plugin/hooks/hooks.json` returns `0`
- [ ] `node plugin/scripts/version-check.js` exits 0 in <500ms (time it)
- [ ] On a fresh checkout (no `.install-version` marker), version-check stderr says "run: npx claude-mem repair"
**Anti-pattern guards:**
- ❌ Do not change the exit code from 0 — Windows Terminal tab management depends on it (CLAUDE.md exit-code strategy).
- ❌ Do not call out to Bun in version-check.js — Node-only, since this runs before we know Bun exists.
- ❌ Do not add fancy logic (semver compare, partial recovery). String equality is correct: any version mismatch warrants a repair.
---
## Phase 5 — Delete dead code
**What to implement:** Delete smart-install source files and update tests.
### Edit 5A — Delete files
```
rm scripts/smart-install.js
rm plugin/scripts/smart-install.js
rm tests/smart-install.test.ts
```
### Edit 5B — Trim `tests/plugin-scripts-line-endings.test.ts`
**Line 12 (the `SHEBANG_SCRIPTS` array):** remove the `'smart-install.js'` entry. Keep the rest of the array intact.
If the array becomes empty after the removal, also remove the entry — but per discovery report it has multiple entries, so just delete the one line.
### Edit 5C — Add new test for setup-runtime module (optional but recommended)
**File to create:** `tests/setup-runtime.test.ts`
Cover:
- `readInstallMarker` returns `null` for missing file
- `writeInstallMarker` produces a JSON object matching the smart-install.js schema (`{ version, bun, uv, installedAt }`)
- `isInstallCurrent` returns `false` for missing marker, `false` for version mismatch, `true` for match
- (Skip Bun/uv install integration tests — those need a sandbox and fall outside this PR's scope.)
If you skip this, document why in the PR description.
**Verification checklist:**
- [ ] `find . -name "smart-install*" -not -path "*/node_modules/*"` returns no results
- [ ] `grep -rn "smart-install" tests/` returns no results
- [ ] `npm test` (or whatever the project uses) passes
- [ ] If `tests/setup-runtime.test.ts` was added, it passes
**Anti-pattern guards:**
- ❌ Do not delete `tests/plugin-scripts-line-endings.test.ts` entirely — it tests other scripts too.
- ❌ Do not delete `tests/bun-runner.test.ts` — bun-runner.js stays in this PR.
---
## Phase 6 — Update docs
**What to implement:** Sweep documentation to reflect the new install flow.
### Edit 6A — `docs/architecture-overview.md:36`
Update reference to smart-install. New copy: "On first install, `npx claude-mem install` sets up Bun and uv globally and runs `bun install` in the plugin cache. The Setup hook then runs a sub-100ms version check on every Claude Code startup; if the plugin was upgraded externally, the user is prompted to run `npx claude-mem repair`."
### Edit 6B — `docs/public/configuration.mdx:139,163` and `docs/public/development.mdx:42`
Replace any mention of smart-install behavior with the version-check + repair model. Two-sentence patches; preserve surrounding context.
### Edit 6C — `docs/public/hooks-architecture.mdx` (11 references)
This is the largest doc change. Walk each reference (lines 77, 103, 119, 127, 432, 695696, 703 per discovery report). Update text describing the Setup hook to say it runs `version-check.js` (sub-100ms) instead of `smart-install.js`. Update SessionStart description to reflect 2 entries (worker start + context fetch) instead of 3.
### Edit 6D — `docs/public/architecture/` references (lines 149, 193)
Same pattern — replace smart-install lifecycle description with the npx-installer + version-check model.
### Edit 6E — Skip CHANGELOG
CLAUDE.md says: "No need to edit the changelog ever, it's generated automatically." Don't touch it.
### Edit 6F — Skip historical incident-report backfills
The old `docs/reports/` archive was removed during later cleanup. Do not recreate it as part of this installer work.
**Verification checklist:**
- [ ] `grep -rn "smart-install" docs/public/` returns no results
- [ ] `grep -rn "smart-install" docs/architecture-overview.md` returns no results
- [ ] (Optional) Render docs locally via Mintlify dev server and visually scan the architecture page
**Anti-pattern guards:**
- ❌ Do not recreate the removed `docs/reports/` archive from this plan.
- ❌ Do not edit CHANGELOG.md.
---
## Phase 7 — Build, test, manual verify
**What to implement:** End-to-end validation. This phase is run by the implementer before opening the PR.
### Edit 7A — Build
```bash
npm run build-and-sync
```
This must succeed. If TypeScript fails on the new `setup-runtime.ts`, fix in place.
### Edit 7B — Test suite
```bash
npm test
```
Must be green. Likely failures to anticipate:
- `plugin-scripts-line-endings.test.ts` if the `'smart-install.js'` entry was missed in Phase 5
- Any test that imports from `scripts/smart-install.js` (discovery report says only `tests/smart-install.test.ts`, which Phase 5 deletes)
### Edit 7C — Manual fresh-install verification
1. On a clean machine (or after `rm -rf ~/.claude/plugins/marketplaces/thedotmack ~/.claude/plugins/cache/thedotmack ~/.claude-mem`):
```bash
npx claude-mem install
```
Confirm:
- Spinner says "Setting up runtime (first install can take ~30s)"
- No silent dead air
- Worker starts at the end
2. Open Claude Code in any project. Confirm:
- Setup hook fires fast (<200ms total)
- SessionStart fires fast (no smart-install delay)
- No "claude plugin install" output
3. Simulate a stale install:
```bash
rm ~/.claude/plugins/cache/thedotmack/claude-mem/<version>/.install-version
```
Open a new Claude Code session. Confirm version-check.js prints the "run: npx claude-mem repair" message to stderr.
4. Run repair:
```bash
npx claude-mem repair
```
Confirm spinner runs through Bun/uv check + bun install + marker write, then exits clean.
### Edit 7D — Commit and open PR
Per the PR creation flow in the user's outer task. Don't auto-merge; the user wants a review loop.
**Verification checklist:**
- [ ] `npm run build-and-sync` exits 0
- [ ] `npm test` exits 0
- [ ] Manual fresh install completes with visible spinner, no silent dead air
- [ ] Setup hook fires <200ms after rebuild
- [ ] `npx claude-mem repair` runs end-to-end
**Anti-pattern guards:**
- ❌ Do not skip the manual verification — the whole point of this PR is UX (eliminating dead air). Type checks won't catch a regression.
- ❌ Do not bump the version — version bump is handled separately by the version-bump skill.
---
## Summary of file changes
| Type | Path | Phase |
|---|---|---|
| Created | `src/npx-cli/install/setup-runtime.ts` | 1 |
| Edited | `src/npx-cli/commands/install.ts` | 2 |
| Edited | `src/npx-cli/index.ts` | 3 |
| Created | `plugin/scripts/version-check.js` | 4 |
| Edited | `plugin/hooks/hooks.json` | 4 |
| Deleted | `scripts/smart-install.js` | 5 |
| Deleted | `plugin/scripts/smart-install.js` | 5 |
| Deleted | `tests/smart-install.test.ts` | 5 |
| Edited | `tests/plugin-scripts-line-endings.test.ts` | 5 |
| Created | `tests/setup-runtime.test.ts` (optional) | 5 |
| Edited | `docs/architecture-overview.md` | 6 |
| Edited | `docs/public/configuration.mdx` | 6 |
| Edited | `docs/public/development.mdx` | 6 |
| Edited | `docs/public/hooks-architecture.mdx` | 6 |
| Edited | `docs/public/architecture/*.md` | 6 |
Estimated diff: **+250 / 500 lines** (net deletion).