Skip to content

feat(installer): npx installer CLI (@garrytan/gstack) — interactive wizard + team init#1190

Open
jkrperson wants to merge 3 commits intogarrytan:mainfrom
jkrperson:installer-cli
Open

feat(installer): npx installer CLI (@garrytan/gstack) — interactive wizard + team init#1190
jkrperson wants to merge 3 commits intogarrytan:mainfrom
jkrperson:installer-cli

Conversation

@jkrperson
Copy link
Copy Markdown

Summary

Adds installer/ — a small TypeScript CLI that wraps the existing ./setup bash script behind npx @garrytan/gstack. Replaces the paste-prompt install flow with an interactive wizard. ./setup stays the source of truth; the CLI is a thin orchestrator.

Live demo (published by me under my own npm scope while this PR is in review):

npx @jkresabal/gstack

Try it end-to-end: install, init in a project, status, list, enable/disable, uninstall. When you're ready to ship upstream, the only change is the package name (instructions in installer/PUBLISHING.md).

What it does

  • npx @garrytan/gstack → interactive @clack/prompts wizard, auto-detects git repo + installed hosts, multi-select host registration
  • Verb-based subcommands for scripting: install, init, uninstall [--project], upgrade, doctor, status, list, enable <skill>, disable <skill>
  • Writes a fenced <!-- gstack:begin --> / <!-- gstack:end --> block to CLAUDE.md listing every discovered skill (dynamically scanned, never stale)
  • uninstall walks ~/.claude, ~/.codex, ~/.factory, ~/.config/opencode, ~/.kiro skills dirs and removes symlinks/dirs pointing into the gstack install (realpath-canonicalized for macOS /var/private/var), scrubs gstack-only PreToolUse hooks from .claude/settings.json, preserves non-gstack hooks and other settings keys, preserves ~/.gstack/ session state

Design choices

  • Wrap ./setup, don't port it. Zero logic duplication. One invocation of ./setup --host <id> per selected host. If setup learns a new flag, I expose it with one line.
  • Scoped package (@garrytan/gstack). Safer than unscoped gstack (may be taken); clearly owned.
  • Thin runtime. ~2.3K LOC TypeScript → 16KB published tarball. Two deps: @clack/prompts + picocolors. Node 18+.
  • Doesn't touch voice. Per CLAUDE.md community guardrails, I did not edit README.md or any existing skill templates. This PR is purely additive — the paste-prompt install flow still works unchanged. If you want to mention the new option alongside it, that's your call to make in a follow-up.

Tests

77 tests, ~3s runtime via bun:test:

  • Unit: claude-md (fence insert/update/remove idempotency), skills (YAML frontmatter incl. |, >, quoted, skip dirs), project-config (enable/disable round-trip preserving other keys), cleanup (settings.json scrubbing preserves non-gstack hooks, realpath comparison for symlinked parents), paths (git root walk-up, isInstalled)
  • Integration: spawns dist/cli.js against fake HOME fixtures — exit codes, enable/disable name normalization (qa / /qa / gstack-qa), full uninstall cycle preserving user settings, EPIPE handling under gstack list | head, wizard intro without TTY
cd installer && bun test         # 77 pass, 3s

End-to-end dry-run completed

Tested via npm link and npm pack against a real clone + real build on macOS arm64:

  • Clone → bun installbun run build (browse binary 59MB, Playwright Chromium 162MB download)
  • 40 skills linked in ~/.claude/skills/
  • CLAUDE.md block written
  • doctor green across git, bun, install, version, browse binary, skills count, host registration
  • Project init --tier required.claude/hooks/check-gstack.sh + .claude/settings.json PreToolUse hook + staged for commit
  • uninstall --yes → all 44 host symlinks removed, ~/.gstack/ preserved, 0 zombies

One real bug was caught and fixed during the dry-run (macOS /var vs /private/var symlink comparison in cleanup) and has a regression test.

How to ship

cd installer
npm install
npm run build
npm publish --access public   # publishes @garrytan/gstack

See installer/PUBLISHING.md for full details.

Known non-issue

upgrade calls ./setup --host auto, which auto-detects installed host CLIs and registers with all of them — so upgrading can broaden host registration beyond the original install --host selection. That matches ./setup --host auto semantics upstream. If you want upgrade to remember the original host selection, I can store it in ~/.gstack/config.yaml in a follow-up.

Test plan

  • npx @jkresabal/gstack install --host claude --yes against a fake HOME=$(mktemp -d) completes clean
  • gstack doctor is all-green after install
  • gstack init --tier required inside a scratch git repo creates the hook + staged commit
  • gstack uninstall --yes leaves ~/.claude/skills/ empty and preserves ~/.gstack/
  • cd installer && bun test → 77 pass
  • npm pack → 16KB tarball with 24 files
  • Paste-prompt install flow still works (this PR does not modify setup, README.md, or any existing skill)

Adds installer/ — a TypeScript CLI that wraps the existing ./setup bash
script behind a zero-friction `npx` entry point. Replaces the paste-prompt
install flow with an interactive wizard while preserving ./setup as the
source of truth for host registration and symlinks.

Commands:
  install | init | uninstall (--project) | upgrade | doctor
  status | list | enable <skill> | disable <skill>

No-args launches a @clack/prompts wizard that auto-detects git repos +
installed hosts (claude, codex, factory, opencode, kiro), collects
multi-select host + prefix + CLAUDE.md choices, and routes to install or
team-mode init.

The CLI:
- clones garrytan/gstack into ~/.claude/skills/gstack
- shells out to ./setup once per selected host (or --host auto)
- for init, runs ./setup --team + bin/gstack-team-init <required|optional>,
  stages .claude/ + CLAUDE.md
- inserts/updates a fenced <!-- gstack:begin --> block in CLAUDE.md listing
  every discovered skill
- uninstall walks ~/.claude, ~/.codex, ~/.factory, ~/.config/opencode,
  ~/.kiro skills dirs and removes symlinks/dirs pointing into the gstack
  install (canonicalized with fs.realpathSync to handle macOS /var vs
  /private/var), then removes the CLAUDE.md block and scrubs gstack
  PreToolUse hooks from project .claude/settings.json
- preserves ~/.gstack/ session state across uninstalls

Testing:
- 77 tests, bun:test runner, 3s runtime
- Unit: claude-md (11), skills (12), project-config (11), cleanup (12 + a
  /var realpath regression test), paths (9)
- Integration: spawns dist/cli.js against fake HOME fixtures — verifies
  exit codes, enable/disable round-trip + name normalization, uninstall
  scrubs settings.json preserving non-gstack hooks and top-level keys,
  EPIPE handling under `gstack list | head`, wizard intro without TTY

End-to-end dry-run completed via `npm link` and `npm pack` against a real
clone + real build (clone -> browse binary -> Playwright Chromium -> 40
skills linked -> CLAUDE.md written -> doctor green -> uninstall leaves 0
zombies).

Live demo published under @jkresabal/gstack for testing:
  npx @jkresabal/gstack

Upstream publish (after this PR): cd installer && npm publish --access public

Philosophy: thin wrapper. ~2.3K LOC, 16KB packed, two runtime deps
(@clack/prompts + picocolors), Node 18+. No logic duplicated from ./setup.
If ./setup learns a new flag, the CLI surfaces it with one line.
"Add to this project (team mode)" implied team mode replaces the global
install when it actually stacks on top of it. Relabels the wizard so the
relationship is explicit:

- "Install gstack on this machine" (was "Install globally (on this machine)")
- "Enable team mode for this repo" (was "Add to this project (team mode)")
  with hint: "global install + commits team-sync config to this repo so
  teammates auto-update"

Pre-select intro note now says upfront that there are two install modes
and that team mode is a superset of the machine install.

Required/optional tier hints now say what each actually does (PreToolUse
hook block vs CLAUDE.md nudge) instead of vague "block sessions" /
"nudge teammates".

No behavior changes — labels and hint text only. 77 tests still pass.
Adds a third install scope: `gstack install --local` vendors gstack into
<cwd>/.claude/skills/gstack/ instead of ~/.claude/skills/gstack/. Surfaces
./setup --local, which upstream deprecated in favor of team mode but still
supports. Guarded with an explicit deprecation notice in both the CLI
output and the interactive wizard confirmation step.

Why expose a deprecated mode:
- Some users genuinely want vendored installs (offline machines, strict
  "one project = one dir" policies, air-gapped CI, forked gstack per repo)
- ./setup already supports it, so the CLI would be lying by omission
- The wizard makes the deprecation cost visible before commit

Changes:
- paths.ts: resolveProjectInstallPaths(dir), findLocalInstall(startDir)
  walking up to find <dir>/.claude/skills/gstack, and a resolveActiveInstall
  helper that returns {paths, mode: "global" | "project-local" | "none"}
  so status/doctor/list/upgrade all detect both install kinds
- install.ts: accepts local + projectDir, routes to project paths, passes
  --local through to ./setup, restricts hosts to claude (matching setup
  behavior), prints deprecation warning
- uninstall.ts: new `gstack uninstall --local` removes the project-local
  install (searches cwd upward), distinct from `--project` which removes
  team-mode config
- wizard.ts: fourth top-level option "Install inside this project only
  (vendored)" with a confirm-before-commit step spelling out the tradeoff
- cli.ts: --local flag on install and uninstall, help text updated
- status: shows "Mode: project-local (vendored)" when applicable, yellow

Tests: +8 (85 total, still green)
- resolveProjectInstallPaths roots correctly
- findLocalInstall at cwd + walks up to parent
- status shows project-local mode when only vendored install exists
- list discovers skills in vendored install
- uninstall --local removes the vendored dir
- uninstall --local exits 1 with clear message when no install present
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.

1 participant