mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-10 23:19:09 +02:00
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:
co-authored by
Yuan Sun
Claude Fable 5
parent
c84246845e
commit
2be9bd0660
@@ -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');
|
||||
|
||||
@@ -34,16 +34,23 @@ function extractFn(name: string): string {
|
||||
const WINDOWS_BYPASS = '[ "$IS_WINDOWS" -eq 1 ] || [ -L ';
|
||||
|
||||
describe('setup: Windows re-run refresh — static guard sites (#2444)', () => {
|
||||
test('all five install guards carry the IS_WINDOWS bypass', () => {
|
||||
const sites = SETUP_SRC.split(WINDOWS_BYPASS).length - 1;
|
||||
expect(sites).toBe(5);
|
||||
test('no install guard is missing the IS_WINDOWS bypass', () => {
|
||||
// Every `[ -L ...] || [ ! -e ...]` refresh guard in setup must carry the
|
||||
// bypass — a bare guard is a Windows re-run no-op waiting to happen.
|
||||
const bareGuards = SETUP_SRC
|
||||
.split('\n')
|
||||
.filter((l) => /\[ -L "\$[A-Za-z_/${}.]+" \] \|\| \[ ! -e /.test(l) && !l.includes('IS_WINDOWS'));
|
||||
expect(bareGuards).toEqual([]);
|
||||
expect(SETUP_SRC.split(WINDOWS_BYPASS).length - 1).toBeGreaterThanOrEqual(5);
|
||||
});
|
||||
|
||||
test.each([
|
||||
'link_codex_skill_dirs',
|
||||
'link_factory_skill_dirs',
|
||||
'link_opencode_skill_dirs',
|
||||
'link_cursor_skill_dirs',
|
||||
'create_agents_sidecar',
|
||||
'create_cursor_sidecar',
|
||||
])('%s bypasses the symlink-or-missing guard on Windows', (fn) => {
|
||||
expect(extractFn(fn)).toContain(WINDOWS_BYPASS);
|
||||
});
|
||||
|
||||
@@ -161,5 +161,38 @@ describe('gstack-uninstall', () => {
|
||||
// Non-gstack should survive
|
||||
expect(fs.existsSync(path.join(mockHome, '.claude', 'skills', 'other-tool'))).toBe(true);
|
||||
});
|
||||
|
||||
test('--force removes Cursor gstack skills and leaves other Cursor skills', () => {
|
||||
fs.mkdirSync(path.join(mockHome, '.cursor', 'skills', 'gstack'), { recursive: true });
|
||||
fs.writeFileSync(path.join(mockHome, '.cursor', 'skills', 'gstack', 'SKILL.md'), 'test');
|
||||
fs.mkdirSync(path.join(mockHome, '.cursor', 'skills', 'gstack-review'), { recursive: true });
|
||||
fs.writeFileSync(path.join(mockHome, '.cursor', 'skills', 'gstack-review', 'SKILL.md'), 'test');
|
||||
fs.mkdirSync(path.join(mockHome, '.cursor', 'skills', 'frontend-design'), { recursive: true });
|
||||
fs.writeFileSync(path.join(mockHome, '.cursor', 'skills', 'frontend-design', 'SKILL.md'), 'keep');
|
||||
|
||||
fs.mkdirSync(path.join(mockGitRoot, '.cursor', 'skills', 'gstack-ship'), { recursive: true });
|
||||
fs.writeFileSync(path.join(mockGitRoot, '.cursor', 'skills', 'gstack-ship', 'SKILL.md'), 'test');
|
||||
fs.mkdirSync(path.join(mockGitRoot, '.cursor', 'rules'), { recursive: true });
|
||||
fs.writeFileSync(path.join(mockGitRoot, '.cursor', 'rules', 'keep.md'), 'keep');
|
||||
|
||||
const result = spawnSync('bash', [UNINSTALL, '--force'], {
|
||||
stdio: 'pipe',
|
||||
env: {
|
||||
...process.env,
|
||||
HOME: mockHome,
|
||||
GSTACK_DIR: path.join(mockHome, '.claude', 'skills', 'gstack'),
|
||||
GSTACK_STATE_DIR: path.join(mockHome, '.gstack'),
|
||||
},
|
||||
cwd: mockGitRoot,
|
||||
});
|
||||
|
||||
expect(result.status).toBe(0);
|
||||
|
||||
expect(fs.existsSync(path.join(mockHome, '.cursor', 'skills', 'gstack'))).toBe(false);
|
||||
expect(fs.existsSync(path.join(mockHome, '.cursor', 'skills', 'gstack-review'))).toBe(false);
|
||||
expect(fs.existsSync(path.join(mockHome, '.cursor', 'skills', 'frontend-design'))).toBe(true);
|
||||
expect(fs.existsSync(path.join(mockGitRoot, '.cursor', 'skills', 'gstack-ship'))).toBe(false);
|
||||
expect(fs.existsSync(path.join(mockGitRoot, '.cursor', 'rules', 'keep.md'))).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user