mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-09 22:48:57 +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
+27
-1
@@ -12,12 +12,14 @@
|
||||
# ~/.codex/skills/gstack* — Codex skill install + per-skill symlinks
|
||||
# ~/.factory/skills/gstack* — Factory Droid skill install + per-skill symlinks
|
||||
# ~/.kiro/skills/gstack* — Kiro skill install + per-skill symlinks
|
||||
# ~/.cursor/skills/gstack* — Cursor skill install + per-skill symlinks
|
||||
# ~/.gstack/ — global state (config, analytics, sessions, projects,
|
||||
# repos, installation-id, browse error logs)
|
||||
# .claude/skills/gstack* — project-local skill install (--local installs)
|
||||
# .gstack/ — per-project browse state (in current git repo)
|
||||
# .gstack-worktrees/ — per-project test worktrees (in current git repo)
|
||||
# .agents/skills/gstack* — Codex/Gemini/Cursor sidecar (in current git repo)
|
||||
# .agents/skills/gstack* — Codex/Gemini sidecar (in current git repo)
|
||||
# .cursor/skills/gstack* — project-local Cursor skills (in current git repo)
|
||||
# Running browse daemons — stopped via SIGTERM before cleanup
|
||||
#
|
||||
# What is NOT REMOVED:
|
||||
@@ -66,6 +68,7 @@ if [ "$FORCE" -eq 0 ]; then
|
||||
[ -d "$HOME/.codex/skills" ] && echo " ~/.codex/skills/gstack*"
|
||||
[ -d "$HOME/.factory/skills" ] && echo " ~/.factory/skills/gstack*"
|
||||
[ -d "$HOME/.kiro/skills" ] && echo " ~/.kiro/skills/gstack*"
|
||||
[ -d "$HOME/.cursor/skills" ] && echo " ~/.cursor/skills/gstack*"
|
||||
[ "$KEEP_STATE" -eq 0 ] && [ -d "$STATE_DIR" ] && echo " $STATE_DIR"
|
||||
|
||||
if [ -n "$_GIT_ROOT" ]; then
|
||||
@@ -73,6 +76,7 @@ if [ "$FORCE" -eq 0 ]; then
|
||||
[ -d "$_GIT_ROOT/.gstack" ] && echo " $_GIT_ROOT/.gstack/ (browse state + reports)"
|
||||
[ -d "$_GIT_ROOT/.gstack-worktrees" ] && echo " $_GIT_ROOT/.gstack-worktrees/"
|
||||
[ -d "$_GIT_ROOT/.agents/skills" ] && echo " $_GIT_ROOT/.agents/skills/gstack*"
|
||||
[ -d "$_GIT_ROOT/.cursor/skills" ] && echo " $_GIT_ROOT/.cursor/skills/gstack*"
|
||||
fi
|
||||
|
||||
# Preview running daemons
|
||||
@@ -247,6 +251,16 @@ if [ -d "$KIRO_SKILLS" ]; then
|
||||
done
|
||||
fi
|
||||
|
||||
# ─── Remove Cursor skills ───────────────────────────────────
|
||||
CURSOR_SKILLS="$HOME/.cursor/skills"
|
||||
if [ -d "$CURSOR_SKILLS" ]; then
|
||||
for _ITEM in "$CURSOR_SKILLS"/gstack*; do
|
||||
[ -e "$_ITEM" ] || [ -L "$_ITEM" ] || continue
|
||||
rm -rf "$_ITEM"
|
||||
REMOVED+=("cursor/$(basename "$_ITEM")")
|
||||
done
|
||||
fi
|
||||
|
||||
# ─── Remove per-project .agents/ sidecar ─────────────────────
|
||||
if [ -n "$_GIT_ROOT" ] && [ -d "$_GIT_ROOT/.agents/skills" ]; then
|
||||
for _ITEM in "$_GIT_ROOT/.agents/skills"/gstack*; do
|
||||
@@ -271,6 +285,18 @@ if [ -n "$_GIT_ROOT" ] && [ -d "$_GIT_ROOT/.factory/skills" ]; then
|
||||
rmdir "$_GIT_ROOT/.factory" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# ─── Remove per-project .cursor/skills/gstack* ──────────────
|
||||
# Never rmdir .cursor itself — Cursor IDE stores rules and other user config there.
|
||||
if [ -n "$_GIT_ROOT" ] && [ -d "$_GIT_ROOT/.cursor/skills" ]; then
|
||||
for _ITEM in "$_GIT_ROOT/.cursor/skills"/gstack*; do
|
||||
[ -e "$_ITEM" ] || [ -L "$_ITEM" ] || continue
|
||||
rm -rf "$_ITEM"
|
||||
REMOVED+=("cursor/$(basename "$_ITEM")")
|
||||
done
|
||||
|
||||
rmdir "$_GIT_ROOT/.cursor/skills" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# ─── Remove per-project state ───────────────────────────────
|
||||
if [ -n "$_GIT_ROOT" ]; then
|
||||
if [ -d "$_GIT_ROOT/.gstack" ]; then
|
||||
|
||||
@@ -58,6 +58,8 @@ FACTORY_SKILLS="$HOME/.factory/skills"
|
||||
FACTORY_GSTACK="$FACTORY_SKILLS/gstack"
|
||||
OPENCODE_SKILLS="$HOME/.config/opencode/skills"
|
||||
OPENCODE_GSTACK="$OPENCODE_SKILLS/gstack"
|
||||
CURSOR_SKILLS="$HOME/.cursor/skills"
|
||||
CURSOR_GSTACK="$CURSOR_SKILLS/gstack"
|
||||
|
||||
IS_WINDOWS=0
|
||||
case "$(uname -s)" in
|
||||
@@ -239,14 +241,19 @@ INSTALL_CODEX=0
|
||||
INSTALL_KIRO=0
|
||||
INSTALL_FACTORY=0
|
||||
INSTALL_OPENCODE=0
|
||||
INSTALL_CURSOR=0
|
||||
if [ "$HOST" = "auto" ]; then
|
||||
command -v claude >/dev/null 2>&1 && INSTALL_CLAUDE=1
|
||||
command -v codex >/dev/null 2>&1 && INSTALL_CODEX=1
|
||||
command -v kiro-cli >/dev/null 2>&1 && INSTALL_KIRO=1
|
||||
command -v droid >/dev/null 2>&1 && INSTALL_FACTORY=1
|
||||
command -v opencode >/dev/null 2>&1 && INSTALL_OPENCODE=1
|
||||
# Cursor's `cursor` CLI shim isn't always on PATH; ~/.cursor is the
|
||||
# reliable footprint of an installed Cursor IDE.
|
||||
command -v cursor >/dev/null 2>&1 && INSTALL_CURSOR=1
|
||||
[ -d "$HOME/.cursor" ] && INSTALL_CURSOR=1
|
||||
# If none found, default to claude
|
||||
if [ "$INSTALL_CLAUDE" -eq 0 ] && [ "$INSTALL_CODEX" -eq 0 ] && [ "$INSTALL_KIRO" -eq 0 ] && [ "$INSTALL_FACTORY" -eq 0 ] && [ "$INSTALL_OPENCODE" -eq 0 ]; then
|
||||
if [ "$INSTALL_CLAUDE" -eq 0 ] && [ "$INSTALL_CODEX" -eq 0 ] && [ "$INSTALL_KIRO" -eq 0 ] && [ "$INSTALL_FACTORY" -eq 0 ] && [ "$INSTALL_OPENCODE" -eq 0 ] && [ "$INSTALL_CURSOR" -eq 0 ]; then
|
||||
INSTALL_CLAUDE=1
|
||||
fi
|
||||
elif [ "$HOST" = "claude" ]; then
|
||||
@@ -259,6 +266,8 @@ elif [ "$HOST" = "factory" ]; then
|
||||
INSTALL_FACTORY=1
|
||||
elif [ "$HOST" = "opencode" ]; then
|
||||
INSTALL_OPENCODE=1
|
||||
elif [ "$HOST" = "cursor" ]; then
|
||||
INSTALL_CURSOR=1
|
||||
fi
|
||||
|
||||
migrate_direct_codex_install() {
|
||||
@@ -593,6 +602,16 @@ if [ "$INSTALL_OPENCODE" -eq 1 ] && [ "$NEEDS_BUILD" -eq 0 ]; then
|
||||
)
|
||||
fi
|
||||
|
||||
# 1e. Generate .cursor/ Cursor skill docs
|
||||
if [ "$INSTALL_CURSOR" -eq 1 ] && [ "$NEEDS_BUILD" -eq 0 ]; then
|
||||
log "Generating .cursor/ skill docs..."
|
||||
(
|
||||
cd "$SOURCE_GSTACK_DIR"
|
||||
bun_cmd install --frozen-lockfile 2>/dev/null || bun_cmd install
|
||||
bun_cmd run gen:skill-docs --host cursor
|
||||
)
|
||||
fi
|
||||
|
||||
# 2. Ensure Playwright's Chromium is available
|
||||
# Detect Ubuntu 26.04: Playwright does not yet ship a native chromium build for
|
||||
# ubuntu26.04-x64. Override the platform to ubuntu24.04-x64 so the installer
|
||||
@@ -1234,6 +1253,143 @@ link_opencode_skill_dirs() {
|
||||
fi
|
||||
}
|
||||
|
||||
# ─── Helper: create a minimal ~/.cursor/skills/gstack runtime root ──────────
|
||||
# Cursor scans ~/.cursor/skills. Same shape as the Codex/Factory/OpenCode
|
||||
# runtime roots: root SKILL.md from the generated tree + runtime assets only.
|
||||
# Contributed by @szsunyuan (PR #2547), re-derived onto the current installers.
|
||||
create_cursor_runtime_root() {
|
||||
local gstack_dir="$1"
|
||||
local cursor_gstack="$2"
|
||||
local cursor_dir="$gstack_dir/.cursor/skills"
|
||||
local generated_root="$cursor_dir/gstack"
|
||||
|
||||
if [ -L "$cursor_gstack" ]; then
|
||||
rm -f "$cursor_gstack"
|
||||
elif [ -d "$cursor_gstack" ] && [ "$cursor_gstack" != "$gstack_dir" ] && [ "$cursor_gstack" != "$generated_root" ]; then
|
||||
rm -rf "$cursor_gstack"
|
||||
fi
|
||||
|
||||
mkdir -p "$cursor_gstack" "$cursor_gstack/browse" "$cursor_gstack/gstack-upgrade" "$cursor_gstack/review"
|
||||
|
||||
if [ -f "$cursor_dir/gstack/SKILL.md" ]; then
|
||||
_link_or_copy "$cursor_dir/gstack/SKILL.md" "$cursor_gstack/SKILL.md"
|
||||
fi
|
||||
# bin scripts import shared modules via ../lib — bin and lib travel together.
|
||||
if [ -d "$gstack_dir/bin" ]; then
|
||||
_link_or_copy "$gstack_dir/bin" "$cursor_gstack/bin"
|
||||
fi
|
||||
if [ -d "$gstack_dir/lib" ]; then
|
||||
_link_or_copy "$gstack_dir/lib" "$cursor_gstack/lib"
|
||||
fi
|
||||
if [ -d "$gstack_dir/browse/dist" ]; then
|
||||
_link_or_copy "$gstack_dir/browse/dist" "$cursor_gstack/browse/dist"
|
||||
fi
|
||||
if [ -d "$gstack_dir/browse/bin" ]; then
|
||||
_link_or_copy "$gstack_dir/browse/bin" "$cursor_gstack/browse/bin"
|
||||
fi
|
||||
if [ -f "$cursor_dir/gstack-upgrade/SKILL.md" ]; then
|
||||
_link_or_copy "$cursor_dir/gstack-upgrade/SKILL.md" "$cursor_gstack/gstack-upgrade/SKILL.md"
|
||||
fi
|
||||
# Review runtime assets — the cursor host config ships the lean pair.
|
||||
for f in checklist.md TODOS-format.md; do
|
||||
if [ -f "$gstack_dir/review/$f" ]; then
|
||||
_link_or_copy "$gstack_dir/review/$f" "$cursor_gstack/review/$f"
|
||||
fi
|
||||
done
|
||||
if [ -f "$gstack_dir/ETHOS.md" ]; then
|
||||
_link_or_copy "$gstack_dir/ETHOS.md" "$cursor_gstack/ETHOS.md"
|
||||
fi
|
||||
# supabase/config.sh — required by gstack-telemetry-sync to resolve GSTACK_SUPABASE_URL
|
||||
if [ -f "$gstack_dir/supabase/config.sh" ]; then
|
||||
mkdir -p "$cursor_gstack/supabase"
|
||||
_link_or_copy "$gstack_dir/supabase/config.sh" "$cursor_gstack/supabase/config.sh"
|
||||
fi
|
||||
}
|
||||
|
||||
# Plant runtime assets into the repo-local generated skill dir so in-repo
|
||||
# GSTACK_ROOT (preamble prefers $_ROOT/.cursor/skills/gstack) has bin/.
|
||||
# NEVER wipe this directory — it holds the generated SKILL.md files.
|
||||
create_cursor_sidecar() {
|
||||
local repo_root="$1"
|
||||
local cursor_gstack="$repo_root/.cursor/skills/gstack"
|
||||
local cursor_dir="$repo_root/.cursor/skills"
|
||||
|
||||
mkdir -p "$cursor_gstack" "$cursor_gstack/browse" "$cursor_gstack/gstack-upgrade" "$cursor_gstack/review"
|
||||
|
||||
if [ -d "$repo_root/bin" ]; then
|
||||
if [ "$IS_WINDOWS" -eq 1 ] || [ -L "$cursor_gstack/bin" ] || [ ! -e "$cursor_gstack/bin" ]; then
|
||||
_link_or_copy "$repo_root/bin" "$cursor_gstack/bin"
|
||||
fi
|
||||
fi
|
||||
if [ -d "$repo_root/lib" ]; then
|
||||
if [ "$IS_WINDOWS" -eq 1 ] || [ -L "$cursor_gstack/lib" ] || [ ! -e "$cursor_gstack/lib" ]; then
|
||||
_link_or_copy "$repo_root/lib" "$cursor_gstack/lib"
|
||||
fi
|
||||
fi
|
||||
if [ -d "$repo_root/browse/dist" ]; then
|
||||
if [ "$IS_WINDOWS" -eq 1 ] || [ -L "$cursor_gstack/browse/dist" ] || [ ! -e "$cursor_gstack/browse/dist" ]; then
|
||||
_link_or_copy "$repo_root/browse/dist" "$cursor_gstack/browse/dist"
|
||||
fi
|
||||
fi
|
||||
if [ -d "$repo_root/browse/bin" ]; then
|
||||
if [ "$IS_WINDOWS" -eq 1 ] || [ -L "$cursor_gstack/browse/bin" ] || [ ! -e "$cursor_gstack/browse/bin" ]; then
|
||||
_link_or_copy "$repo_root/browse/bin" "$cursor_gstack/browse/bin"
|
||||
fi
|
||||
fi
|
||||
if [ -f "$cursor_dir/gstack-upgrade/SKILL.md" ]; then
|
||||
if [ "$IS_WINDOWS" -eq 1 ] || [ -L "$cursor_gstack/gstack-upgrade/SKILL.md" ] || [ ! -e "$cursor_gstack/gstack-upgrade/SKILL.md" ]; then
|
||||
_link_or_copy "$cursor_dir/gstack-upgrade/SKILL.md" "$cursor_gstack/gstack-upgrade/SKILL.md"
|
||||
fi
|
||||
fi
|
||||
for f in checklist.md TODOS-format.md; do
|
||||
if [ -f "$repo_root/review/$f" ]; then
|
||||
if [ "$IS_WINDOWS" -eq 1 ] || [ -L "$cursor_gstack/review/$f" ] || [ ! -e "$cursor_gstack/review/$f" ]; then
|
||||
_link_or_copy "$repo_root/review/$f" "$cursor_gstack/review/$f"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
if [ -f "$repo_root/ETHOS.md" ]; then
|
||||
if [ "$IS_WINDOWS" -eq 1 ] || [ -L "$cursor_gstack/ETHOS.md" ] || [ ! -e "$cursor_gstack/ETHOS.md" ]; then
|
||||
_link_or_copy "$repo_root/ETHOS.md" "$cursor_gstack/ETHOS.md"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
link_cursor_skill_dirs() {
|
||||
local gstack_dir="$1"
|
||||
local skills_dir="$2"
|
||||
local cursor_dir="$gstack_dir/.cursor/skills"
|
||||
local linked=()
|
||||
|
||||
if [ ! -d "$cursor_dir" ]; then
|
||||
echo " Generating .cursor/ skill docs..."
|
||||
( cd "$gstack_dir" && bun_cmd run gen:skill-docs --host cursor )
|
||||
fi
|
||||
|
||||
if [ ! -d "$cursor_dir" ]; then
|
||||
echo " warning: .cursor/skills/ generation failed — run 'bun run gen:skill-docs --host cursor' manually" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
for skill_dir in "$cursor_dir"/gstack*/; do
|
||||
if [ -f "$skill_dir/SKILL.md" ]; then
|
||||
skill_name="$(basename "$skill_dir")"
|
||||
[ "$skill_name" = "gstack" ] && continue
|
||||
target="$skills_dir/$skill_name"
|
||||
# #2444: IS_WINDOWS bypass — real-dir copies never match -L, so re-runs
|
||||
# skipped the refresh. Only replace a symlink or a missing path
|
||||
# otherwise; never an unowned real Cursor skill dir (#2142).
|
||||
if [ "$IS_WINDOWS" -eq 1 ] || [ -L "$target" ] || [ ! -e "$target" ]; then
|
||||
_link_or_copy "$skill_dir" "$target"
|
||||
linked+=("$skill_name")
|
||||
fi
|
||||
fi
|
||||
done
|
||||
if [ ${#linked[@]} -gt 0 ]; then
|
||||
echo " linked skills: ${linked[*]}"
|
||||
fi
|
||||
}
|
||||
|
||||
# 4. Install for Claude (default)
|
||||
SKILLS_BASENAME="$(basename "$INSTALL_SKILLS_DIR")"
|
||||
SKILLS_PARENT_BASENAME="$(basename "$(dirname "$INSTALL_SKILLS_DIR")")"
|
||||
@@ -1468,6 +1624,19 @@ if [ "$INSTALL_OPENCODE" -eq 1 ]; then
|
||||
echo " opencode skills: $OPENCODE_SKILLS"
|
||||
fi
|
||||
|
||||
# 6d. Install for Cursor
|
||||
if [ "$INSTALL_CURSOR" -eq 1 ]; then
|
||||
mkdir -p "$CURSOR_SKILLS"
|
||||
create_cursor_runtime_root "$SOURCE_GSTACK_DIR" "$CURSOR_GSTACK"
|
||||
# Link before sidecar. Sidecar mkdir -p creates .cursor/skills/gstack, which
|
||||
# would make link_cursor_skill_dirs' "[ ! -d generated ]" gen fallback a no-op.
|
||||
link_cursor_skill_dirs "$SOURCE_GSTACK_DIR" "$CURSOR_SKILLS"
|
||||
create_cursor_sidecar "$SOURCE_GSTACK_DIR"
|
||||
echo "gstack ready (cursor)."
|
||||
echo " browse: $BROWSE_BIN"
|
||||
echo " cursor skills: $CURSOR_SKILLS"
|
||||
fi
|
||||
|
||||
# 7. Create .agents/ sidecar symlinks for the real Codex skill target.
|
||||
# The root Codex skill ends up pointing at $SOURCE_GSTACK_DIR/.agents/skills/gstack,
|
||||
# so the runtime assets must live there for both global and repo-local installs.
|
||||
|
||||
@@ -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