feat(setup): wire --host cursor through the full install path

'./setup --host cursor' was accepted by the flag parser and then did
nothing: no INSTALL_CURSOR branch existed, so the script built binaries,
printed no 'ready' line, and installed zero skills — Cursor users had no
way to install gstack at all.

Full install slice, re-derived from PR #2547 by @szsunyuan onto the
current installers: generate .cursor/ skill docs (host config already
existed), create a minimal ~/.cursor/skills/gstack runtime root (root
SKILL.md + bin/lib/browse assets + review checklist pair + ETHOS.md +
supabase config — bin and lib travel together because bin scripts import
../lib), link the generated gstack-* skills, and plant the repo-local
.cursor/skills/gstack sidecar WITHOUT ever wiping the generated SKILL.md
files it shares a directory with (link-before-sidecar ordering keeps the
generation fallback alive). Auto mode detects Cursor via the cursor
binary or the ~/.cursor footprint. gstack-uninstall removes
~/.cursor/skills/gstack* and per-project .cursor/skills/gstack* — and
never rmdir's .cursor itself, where Cursor stores user rules.

Re-derivation deltas from the PR: the link guards carry the #2444
IS_WINDOWS bypass (re-runs refresh real-dir copies), lib/ and
supabase/config.sh ride along like every other runtime root, and the
hosts/cursor.ts sidecar field is omitted (HostConfig no longer carries
one — sidecar behavior lives in setup).

Fixes #1358

Co-authored-by: Yuan Sun <forrest.sun527@gmail.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-08-16 09:49:31 -07:00
co-authored by Yuan Sun Claude Fable 5
parent c84246845e
commit 2be9bd0660
5 changed files with 320 additions and 5 deletions
+80
View File
@@ -2490,6 +2490,86 @@ describe('setup script validation', () => {
expect(setupContent).toContain('OPENCODE_GSTACK="$OPENCODE_SKILLS/gstack"');
});
// --host cursor full install slice (#1358, PR #2547 by @szsunyuan re-derived)
test('auto mode detects Cursor via binary or ~/.cursor directory', () => {
expect(setupContent).toContain('command -v cursor');
expect(setupContent).toContain('[ -d "$HOME/.cursor" ] && INSTALL_CURSOR=1');
});
test('setup supports --host cursor with install section and Cursor skill path vars', () => {
expect(setupContent).toContain('INSTALL_CURSOR=');
expect(setupContent).toContain('CURSOR_SKILLS="$HOME/.cursor/skills"');
expect(setupContent).toContain('CURSOR_GSTACK="$CURSOR_SKILLS/gstack"');
expect(setupContent).toContain('create_cursor_runtime_root');
expect(setupContent).toContain('create_cursor_sidecar');
expect(setupContent).toContain('link_cursor_skill_dirs');
expect(setupContent).toContain('gstack ready (cursor).');
});
test('create_cursor_runtime_root exposes only Cursor runtime assets', () => {
const fnStart = setupContent.indexOf('create_cursor_runtime_root()');
const fnEnd = setupContent.indexOf('create_cursor_sidecar()', fnStart);
const fnBody = setupContent.slice(fnStart, fnEnd);
expect(fnBody).toContain('gstack/SKILL.md');
expect(fnBody).toContain('browse/dist');
expect(fnBody).toContain('browse/bin');
expect(fnBody).toContain('gstack-upgrade/SKILL.md');
expect(fnBody).toContain('checklist.md');
expect(fnBody).toContain('TODOS-format.md');
// bin scripts import ../lib — the two must travel together.
expect(fnBody).toContain('$cursor_gstack/lib');
expect(fnBody).not.toContain('design-checklist.md');
expect(fnBody).not.toContain('greptile-triage.md');
expect(fnBody).not.toContain('review/specialists');
expect(fnBody).not.toContain('qa/templates');
expect(fnBody).not.toContain('_link_or_copy "$gstack_dir" "$cursor_gstack"');
});
test('create_cursor_sidecar plants runtime assets without wiping generated SKILL.md', () => {
const fnStart = setupContent.indexOf('create_cursor_sidecar()');
const fnEnd = setupContent.indexOf('link_cursor_skill_dirs()', fnStart);
const fnBody = setupContent.slice(fnStart, fnEnd);
expect(fnBody).toContain('.cursor/skills/gstack');
expect(fnBody).toContain('bin');
expect(fnBody).toContain('browse/dist');
expect(fnBody).toContain('browse/bin');
expect(fnBody).toContain('ETHOS.md');
expect(fnBody).not.toContain('rm -rf');
});
test('link_cursor_skill_dirs skips the gstack runtime root directory', () => {
const fnStart = setupContent.indexOf('link_cursor_skill_dirs()');
const fnEnd = setupContent.indexOf('}', setupContent.indexOf('linked[@]', fnStart));
const fnBody = setupContent.slice(fnStart, fnEnd);
expect(fnBody).toContain('[ "$skill_name" = "gstack" ] && continue');
// #2444-aware guard: Windows bypass, else only replace symlink-or-missing.
expect(fnBody).toContain('[ "$IS_WINDOWS" -eq 1 ] || [ -L "$target" ] || [ ! -e "$target" ]');
});
// #2142 deleted existing ~/.cursor/skills/<name> dirs with `rm -rf "$target"`
// before relinking. That can wipe unowned Cursor skills. Only replace a
// symlink or a missing path; never the whole skills directory.
test('link_cursor_skill_dirs does not delete unowned Cursor skill directories', () => {
const fnStart = setupContent.indexOf('link_cursor_skill_dirs()');
const fnEnd = setupContent.indexOf('}', setupContent.indexOf('linked[@]', fnStart));
const fnBody = setupContent.slice(fnStart, fnEnd);
expect(fnBody).not.toContain('rm -rf "$target"');
expect(fnBody).not.toContain('rm -rf "$skills_dir"');
expect(setupContent).not.toContain('rm -rf "$CURSOR_SKILLS"');
});
test('Cursor install links generated skills before planting the sidecar', () => {
const cursorInstall = setupContent.slice(
setupContent.indexOf('# 6d. Install for Cursor'),
setupContent.indexOf('# 7. Create .agents/ sidecar'),
);
const linkCall = cursorInstall.indexOf('link_cursor_skill_dirs "$SOURCE_GSTACK_DIR"');
const sidecarCall = cursorInstall.indexOf('create_cursor_sidecar "$SOURCE_GSTACK_DIR"');
expect(linkCall).toBeGreaterThan(-1);
expect(sidecarCall).toBeGreaterThan(-1);
expect(linkCall).toBeLessThan(sidecarCall);
});
test('setup installs OpenCode skills into a nested gstack runtime root', () => {
expect(setupContent).toContain('create_opencode_runtime_root');
expect(setupContent).toContain('.opencode/skills');