feat(installer): npx installer CLI (@garrytan/gstack) — interactive wizard + team init#1190
Open
jkrperson wants to merge 3 commits intogarrytan:mainfrom
Open
feat(installer): npx installer CLI (@garrytan/gstack) — interactive wizard + team init#1190jkrperson wants to merge 3 commits intogarrytan:mainfrom
jkrperson wants to merge 3 commits intogarrytan:mainfrom
Conversation
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds
installer/— a small TypeScript CLI that wraps the existing./setupbash script behindnpx @garrytan/gstack. Replaces the paste-prompt install flow with an interactive wizard../setupstays 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):
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/promptswizard, auto-detects git repo + installed hosts, multi-select host registrationinstall,init,uninstall [--project],upgrade,doctor,status,list,enable <skill>,disable <skill><!-- gstack:begin --> / <!-- gstack:end -->block toCLAUDE.mdlisting every discovered skill (dynamically scanned, never stale)uninstallwalks~/.claude,~/.codex,~/.factory,~/.config/opencode,~/.kiroskills 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 stateDesign choices
./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.@garrytan/gstack). Safer than unscopedgstack(may be taken); clearly owned.@clack/prompts+picocolors. Node 18+.CLAUDE.mdcommunity guardrails, I did not editREADME.mdor 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: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)dist/cli.jsagainst fakeHOMEfixtures — exit codes, enable/disable name normalization (qa//qa/gstack-qa), full uninstall cycle preserving user settings, EPIPE handling undergstack list | head, wizard intro without TTYEnd-to-end dry-run completed
Tested via
npm linkandnpm packagainst a real clone + real build on macOS arm64:bun install→bun run build(browse binary 59MB, Playwright Chromium 162MB download)~/.claude/skills/CLAUDE.mdblock writtendoctorgreen across git, bun, install, version, browse binary, skills count, host registrationinit --tier required→.claude/hooks/check-gstack.sh+.claude/settings.jsonPreToolUse hook + staged for commituninstall --yes→ all 44 host symlinks removed,~/.gstack/preserved, 0 zombiesOne real bug was caught and fixed during the dry-run (macOS
/varvs/private/varsymlink comparison in cleanup) and has a regression test.How to ship
See
installer/PUBLISHING.mdfor full details.Known non-issue
upgradecalls./setup --host auto, which auto-detects installed host CLIs and registers with all of them — so upgrading can broaden host registration beyond the originalinstall --hostselection. That matches./setup --host autosemantics upstream. If you wantupgradeto remember the original host selection, I can store it in~/.gstack/config.yamlin a follow-up.Test plan
npx @jkresabal/gstack install --host claude --yesagainst a fakeHOME=$(mktemp -d)completes cleangstack doctoris all-green after installgstack init --tier requiredinside a scratch git repo creates the hook + staged commitgstack uninstall --yesleaves~/.claude/skills/empty and preserves~/.gstack/cd installer && bun test→ 77 passnpm pack→ 16KB tarball with 24 filessetup,README.md, or any existing skill)