diff --git a/.claude/dispatch/inbox.md b/.claude/dispatch/inbox.md
new file mode 100644
index 000000000..c2d5e4f56
--- /dev/null
+++ b/.claude/dispatch/inbox.md
@@ -0,0 +1,3 @@
+# Dispatch inbox
+
+- Investigate the curated Windows-safe suite prerequisites and filtering. `bun run test:windows` currently includes `test/readme-throughput.test.ts`, whose four script subprocess tests return status `-1` on this Windows environment, and `browse/test/tab-isolation.test.ts`, which aborts when the generated `browse/src/server-node.mjs` bundle is absent. Receipt: reproduced identically on detached `origin/main` SHA `7c9df1c568a9ea745508f679a329332b2c338063` after `bun install --frozen-lockfile`, using `bun run test:windows` with Git Bash prepended to `PATH`; result was 27 pass, 5 fail, 1 error, exit 1. Full base log: `C:\Users\kunle\.copilot\session-state\8631d6e3-fb62-412d-8bdc-dc6a8f3a46be\files\main-test-windows.log`. The temporary worktree was removed afterward. This is outside the cross-platform team-hook fix; `test/team-mode.test.ts` passes in full.
diff --git a/README.md b/README.md
index 4bb177c3a..8233848d6 100644
--- a/README.md
+++ b/README.md
@@ -58,7 +58,7 @@ From inside your repo, paste this. Switches you to team mode, bootstraps the rep
(cd ~/.claude/skills/gstack && ./setup --team) && ~/.claude/skills/gstack/bin/gstack-team-init required && git add .claude/ CLAUDE.md && git commit -m "require gstack for AI-assisted work"
```
-No vendored files in your repo, no version drift, no manual upgrades. Every Claude Code session starts with a fast auto-update check (throttled to once/hour, network-failure-safe, completely silent).
+No vendored gstack install in your repo, no version drift, no manual upgrades. Required mode commits one Node 18+ CommonJS enforcement hook, so the same project hook works from POSIX and Windows hook runners without duplicate shell-specific handlers. If the hook runner omits its project directory, enforcement fails closed with an intentional denial instead of a hook error. Every Claude Code session starts with a fast auto-update check (throttled to once/hour, network-failure-safe, completely silent).
Swap `required` for `optional` if you'd rather nudge teammates than block them.
diff --git a/bin/gstack-team-init b/bin/gstack-team-init
index 256735f8b..74cd5af5d 100755
--- a/bin/gstack-team-init
+++ b/bin/gstack-team-init
@@ -65,12 +65,13 @@ cd ~/.claude/skills/gstack && ./setup --team
Skills like /qa, /ship, /review, /investigate, and /browse become available after install.
Use /browse for all web browsing. Use ~/.claude/skills/gstack/... for gstack file paths.'
else
- SNIPPET='## gstack (REQUIRED — global install)
+ SNIPPET=$(cat <<'SNIPPET_EOF'
+## gstack (REQUIRED — global install)
**Before doing ANY work, verify gstack is installed:**
-```bash
-test -d ~/.claude/skills/gstack/bin && echo "GSTACK_OK" || echo "GSTACK_MISSING"
+```
+node -e "const fs=require('node:fs'),os=require('node:os'),p=require('node:path'); console.log(fs.existsSync(p.join(os.homedir(),'.claude','skills','gstack','bin')) ? 'GSTACK_OK' : 'GSTACK_MISSING')"
```
If GSTACK_MISSING: STOP. Do not proceed. Tell the user:
@@ -87,7 +88,9 @@ Do not skip skills, ignore gstack errors, or work around missing gstack.
Using gstack skills: After install, skills like /qa, /ship, /review, /investigate,
and /browse are available. Use /browse for all web browsing.
-Use ~/.claude/skills/gstack/... for gstack file paths (the global path).'
+Use ~/.claude/skills/gstack/... for gstack file paths (the global path).
+SNIPPET_EOF
+)
fi
# Check if CLAUDE.md already has a gstack section
@@ -109,33 +112,60 @@ if [ "$MODE" = "required" ]; then
HOOKS_DIR="$REPO_ROOT/.claude/hooks"
SETTINGS="$REPO_ROOT/.claude/settings.json"
- # Create enforcement hook script
+ # Create a CommonJS hook so consumer package.json module settings cannot change
+ # how Node 18+ loads it.
mkdir -p "$HOOKS_DIR"
- cat > "$HOOKS_DIR/check-gstack.sh" << 'HOOK_EOF'
-#!/bin/bash
-# Block skill usage when gstack is not installed globally.
+ cat > "$HOOKS_DIR/check-gstack.cjs" << 'HOOK_EOF'
+'use strict';
-if [ ! -d "$HOME/.claude/skills/gstack/bin" ]; then
- cat >&2 <<'MSG'
-BLOCKED: gstack is not installed globally.
+// Block skill usage when gstack is not installed globally.
+const fs = require('node:fs');
+const os = require('node:os');
+const path = require('node:path');
-gstack is required for AI-assisted work in this repo.
+const homeDir = process.env.HOME || process.env.USERPROFILE || os.homedir();
+const gstackBin = path.join(homeDir, '.claude', 'skills', 'gstack', 'bin');
+let verificationError = null;
+let installed = false;
+
+try {
+ installed = fs.statSync(gstackBin).isDirectory();
+} catch (error) {
+ if (error && error.code !== 'ENOENT') {
+ verificationError = error;
+ }
+}
+
+if (installed) {
+ process.stdout.write('{}\n');
+} else {
+ const projectDir = process.env.CLAUDE_PROJECT_DIR || '(unknown project)';
+ const heading = verificationError
+ ? 'BLOCKED: the global gstack install could not be verified.'
+ : 'BLOCKED: gstack is not installed globally.';
+ const instructions = `${heading}
+
+gstack is required for AI-assisted work in ${projectDir}.
Install it:
git clone --depth 1 https://github.com/garrytan/gstack.git ~/.claude/skills/gstack
cd ~/.claude/skills/gstack && ./setup --team
Then restart your AI coding tool.
-MSG
- echo '{"permissionDecision":"deny","message":"gstack is required but not installed. See stderr for install instructions."}'
- exit 0
-fi
+`;
-echo '{}'
+ process.stderr.write(instructions);
+ process.stdout.write(`${JSON.stringify({
+ hookSpecificOutput: {
+ hookEventName: 'PreToolUse',
+ permissionDecision: 'deny',
+ permissionDecisionReason: instructions,
+ },
+ })}\n`);
+}
HOOK_EOF
- chmod +x "$HOOKS_DIR/check-gstack.sh"
- GENERATED+=(".claude/hooks/check-gstack.sh")
- echo " + .claude/hooks/check-gstack.sh — enforcement hook"
+ GENERATED+=(".claude/hooks/check-gstack.cjs")
+ echo " + .claude/hooks/check-gstack.cjs — cross-platform enforcement hook"
# Add hook to project-level settings.json
if command -v bun >/dev/null 2>&1; then
@@ -149,18 +179,32 @@ HOOK_EOF
if (!settings.hooks) settings.hooks = {};
if (!settings.hooks.PreToolUse) settings.hooks.PreToolUse = [];
- // Dedup
- const exists = settings.hooks.PreToolUse.some(entry =>
- entry.matcher === 'Skill' &&
- entry.hooks && entry.hooks.some(h => h.command && h.command.includes('check-gstack'))
- );
+ const hookCommand = \"node -e \\\"const projectDir=process.env.CLAUDE_PROJECT_DIR;if(!projectDir){const reason='BLOCKED: CLAUDE_PROJECT_DIR is unavailable, so the required gstack hook cannot be loaded.';console.error(reason);console.log(JSON.stringify({hookSpecificOutput:{hookEventName:'PreToolUse',permissionDecision:'deny',permissionDecisionReason:reason}}))}else{require(require('node:path').join(projectDir, '.claude', 'hooks', 'check-gstack.cjs'))}\\\"\";
+ let found = false;
+ settings.hooks.PreToolUse = settings.hooks.PreToolUse.filter(entry => {
+ if (entry.matcher !== 'Skill' || !entry.hooks) return true;
- if (!exists) {
+ const matchingHooks = entry.hooks.filter(
+ h => h.command && h.command.includes('check-gstack')
+ );
+ if (matchingHooks.length === 0) return true;
+
+ entry.hooks = entry.hooks.filter(
+ h => !h.command || !h.command.includes('check-gstack')
+ );
+ if (!found) {
+ entry.hooks.push({ type: 'command', command: hookCommand });
+ found = true;
+ }
+ return entry.hooks.length > 0;
+ });
+
+ if (!found) {
settings.hooks.PreToolUse.push({
matcher: 'Skill',
hooks: [{
type: 'command',
- command: '\"\$CLAUDE_PROJECT_DIR/.claude/hooks/check-gstack.sh\"'
+ command: hookCommand
}]
});
}
diff --git a/implementation-notes.html b/implementation-notes.html
new file mode 100644
index 000000000..1de10a6d7
--- /dev/null
+++ b/implementation-notes.html
@@ -0,0 +1,36 @@
+
+
+
+
+
+ Cross-platform gstack enforcement hook
+
+
+
+ Cross-platform gstack enforcement hook
+
+ Design decisions
+
+ - Required mode generates
.claude/hooks/check-gstack.cjs. The explicit CommonJS extension is unaffected by a consumer repository's package.json module type.
+ - The one registered command launches Node 18+ and resolves
CLAUDE_PROJECT_DIR inside JavaScript through process.env. It contains no Bash, PowerShell, or cmd environment expansion.
+ - The launcher validates
CLAUDE_PROJECT_DIR before calling path.join. Missing or empty project context emits a structured PreToolUse denial, writes a clear reason to stderr, and exits zero instead of surfacing a hook execution error.
+ - The generated hook resolves the user home in
HOME, USERPROFILE, then os.homedir() order so POSIX, Git Bash, PowerShell, and cmd runners use the expected global install root.
+ - The hook contract follows
docs/spikes/claude-code-hook-mutation.md: pass-through writes {}; denial writes a hookSpecificOutput object for PreToolUse with permissionDecision: "deny".
+ - Missing or unreadable gstack is an intentional denial with exit code zero. Install instructions are present in stderr and
permissionDecisionReason, so the runner does not report a hook execution error.
+ - Rerunning required mode replaces a matching legacy
check-gstack.sh registration and removes duplicate matching registrations.
+
+
+ Deviations
+ None. The implementation uses the requested Node launcher, one hook handler, and the repository's documented hook output schema.
+
+ Tradeoffs
+
+ - The generated project now requires Node 18+ at hook execution time. This removes shell selection ambiguity and matches the requested runtime floor.
+ - The generated hook is invoked through Node rather than as an executable file, so POSIX executable mode bits are no longer part of the contract.
+
+
+ Open questions
+ None.
+
+
+
diff --git a/test/team-mode.test.ts b/test/team-mode.test.ts
index ce8c1d610..a17fb6938 100644
--- a/test/team-mode.test.ts
+++ b/test/team-mode.test.ts
@@ -2,12 +2,17 @@ import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
-import { execSync } from 'child_process';
+import { execSync, spawnSync } from 'child_process';
const ROOT = path.resolve(import.meta.dir, '..');
-const SETTINGS_HOOK = path.join(ROOT, 'bin', 'gstack-settings-hook');
-const SESSION_UPDATE = path.join(ROOT, 'bin', 'gstack-session-update');
-const TEAM_INIT = path.join(ROOT, 'bin', 'gstack-team-init');
+
+function bashCommand(filePath: string): string {
+ return `bash "${filePath.replace(/\\/g, '/')}"`;
+}
+
+const SETTINGS_HOOK = bashCommand(path.join(ROOT, 'bin', 'gstack-settings-hook'));
+const SESSION_UPDATE = bashCommand(path.join(ROOT, 'bin', 'gstack-session-update'));
+const TEAM_INIT = bashCommand(path.join(ROOT, 'bin', 'gstack-team-init'));
function mkTmpDir(): string {
return fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-team-test-'));
@@ -17,7 +22,11 @@ function run(cmd: string, opts: { cwd?: string; env?: Record } =
try {
const stdout = execSync(cmd, {
cwd: opts.cwd,
- env: { ...process.env, ...opts.env },
+ env: {
+ ...process.env,
+ ...(process.platform === 'win32' ? { MSYS_NO_PATHCONV: '1' } : {}),
+ ...opts.env,
+ },
encoding: 'utf-8',
timeout: 10000,
});
@@ -27,6 +36,47 @@ function run(cmd: string, opts: { cwd?: string; env?: Record } =
}
}
+function runHook(
+ command: string,
+ opts: { cwd: string; env: Record },
+): { stdout: string; stderr: string; exitCode: number } {
+ const result = spawnSync(command, {
+ cwd: opts.cwd,
+ env: { ...process.env, ...opts.env },
+ encoding: 'utf-8',
+ shell: true,
+ timeout: 10000,
+ });
+
+ return {
+ stdout: result.stdout || '',
+ stderr: result.stderr || '',
+ exitCode: result.status ?? 1,
+ };
+}
+
+function runHookInPowerShell(
+ command: string,
+ opts: { cwd: string; env: Record },
+): { stdout: string; stderr: string; exitCode: number } {
+ const result = spawnSync(
+ 'powershell.exe',
+ ['-NoProfile', '-NonInteractive', '-Command', command],
+ {
+ cwd: opts.cwd,
+ env: { ...process.env, ...opts.env },
+ encoding: 'utf-8',
+ timeout: 10000,
+ },
+ );
+
+ return {
+ stdout: result.stdout || '',
+ stderr: result.stderr || '',
+ exitCode: result.status ?? 1,
+ };
+}
+
describe('gstack-settings-hook', () => {
let tmpDir: string;
let settingsFile: string;
@@ -227,27 +277,216 @@ describe('gstack-team-init', () => {
const claude = fs.readFileSync(path.join(tmpDir, 'CLAUDE.md'), 'utf-8');
expect(claude).toContain('## gstack (REQUIRED');
expect(claude).toContain('GSTACK_MISSING');
+ expect(claude).toContain("require('node:os')");
+ expect(claude).not.toContain('test -d ~/.claude/skills/gstack/bin');
});
test('required: creates enforcement hook', () => {
run(`${TEAM_INIT} required`, { cwd: tmpDir });
- const hookPath = path.join(tmpDir, '.claude', 'hooks', 'check-gstack.sh');
+ const hookPath = path.join(tmpDir, '.claude', 'hooks', 'check-gstack.cjs');
expect(fs.existsSync(hookPath)).toBe(true);
+ expect(
+ fs.existsSync(path.join(tmpDir, '.claude', 'hooks', 'check-gstack.sh')),
+ ).toBe(false);
const hook = fs.readFileSync(hookPath, 'utf-8');
+ expect(hook).toContain("'use strict'");
+ expect(hook).toContain("require('node:fs')");
+ expect(hook).toContain(
+ 'process.env.HOME || process.env.USERPROFILE || os.homedir()',
+ );
+ expect(hook).toContain('process.env.CLAUDE_PROJECT_DIR');
+ expect(hook).toContain("hookEventName: 'PreToolUse'");
+ expect(hook).toContain("permissionDecision: 'deny'");
expect(hook).toContain('BLOCKED: gstack is not installed');
- // Should be executable
- const stat = fs.statSync(hookPath);
- expect(stat.mode & 0o111).toBeGreaterThan(0);
+ expect(hook).not.toContain('#!/bin/bash');
});
- test('required: creates project settings.json with PreToolUse hook', () => {
+ test('required: registers one shell-neutral project hook', () => {
run(`${TEAM_INIT} required`, { cwd: tmpDir });
const settingsPath = path.join(tmpDir, '.claude', 'settings.json');
expect(fs.existsSync(settingsPath)).toBe(true);
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
expect(settings.hooks.PreToolUse).toHaveLength(1);
- expect(settings.hooks.PreToolUse[0].matcher).toBe('Skill');
- expect(settings.hooks.PreToolUse[0].hooks[0].command).toContain('check-gstack');
+ const entry = settings.hooks.PreToolUse[0];
+ expect(entry.matcher).toBe('Skill');
+ expect(entry.hooks).toHaveLength(1);
+ expect(entry.hooks[0]).not.toHaveProperty('shell');
+ expect(entry.hooks[0].command).toBe(
+ `node -e "const projectDir=process.env.CLAUDE_PROJECT_DIR;if(!projectDir){const reason='BLOCKED: CLAUDE_PROJECT_DIR is unavailable, so the required gstack hook cannot be loaded.';console.error(reason);console.log(JSON.stringify({hookSpecificOutput:{hookEventName:'PreToolUse',permissionDecision:'deny',permissionDecisionReason:reason}}))}else{require(require('node:path').join(projectDir, '.claude', 'hooks', 'check-gstack.cjs'))}"`,
+ );
+ expect(entry.hooks[0].command).not.toMatch(
+ /\$CLAUDE_PROJECT_DIR|\$env:CLAUDE_PROJECT_DIR|%CLAUDE_PROJECT_DIR%/,
+ );
+ expect(entry.hooks[0].command).not.toContain("permissionDecision:'allow'");
+ });
+
+ test('required: hook allows with valid empty JSON when gstack is installed', () => {
+ run(`${TEAM_INIT} required`, { cwd: tmpDir });
+ const settings = JSON.parse(
+ fs.readFileSync(path.join(tmpDir, '.claude', 'settings.json'), 'utf-8'),
+ );
+ const command = settings.hooks.PreToolUse[0].hooks[0].command;
+ const fakeHome = path.join(tmpDir, 'home');
+ const nestedCwd = path.join(tmpDir, 'nested', 'working', 'directory');
+ fs.mkdirSync(path.join(fakeHome, '.claude', 'skills', 'gstack', 'bin'), {
+ recursive: true,
+ });
+ fs.mkdirSync(nestedCwd, { recursive: true });
+
+ const result = runHook(command, {
+ cwd: nestedCwd,
+ env: {
+ CLAUDE_PROJECT_DIR: tmpDir,
+ HOME: fakeHome,
+ USERPROFILE: path.join(tmpDir, 'unused-userprofile'),
+ },
+ });
+
+ expect(result.exitCode).toBe(0);
+ expect(result.stderr).toBe('');
+ expect(JSON.parse(result.stdout)).toEqual({});
+ });
+
+ test('required: missing project env denies through the platform default shell', () => {
+ run(`${TEAM_INIT} required`, { cwd: tmpDir });
+ const settings = JSON.parse(
+ fs.readFileSync(path.join(tmpDir, '.claude', 'settings.json'), 'utf-8'),
+ );
+ const command = settings.hooks.PreToolUse[0].hooks[0].command;
+
+ const result = runHook(command, {
+ cwd: tmpDir,
+ env: { CLAUDE_PROJECT_DIR: '' },
+ });
+
+ expect(result.exitCode).toBe(0);
+ expect(result.stderr).toContain('BLOCKED: CLAUDE_PROJECT_DIR is unavailable');
+ expect(JSON.parse(result.stdout)).toEqual({
+ hookSpecificOutput: {
+ hookEventName: 'PreToolUse',
+ permissionDecision: 'deny',
+ permissionDecisionReason:
+ 'BLOCKED: CLAUDE_PROJECT_DIR is unavailable, so the required gstack hook cannot be loaded.',
+ },
+ });
+ });
+
+ test('required: missing project env denies through PowerShell', () => {
+ if (process.platform !== 'win32') return;
+
+ run(`${TEAM_INIT} required`, { cwd: tmpDir });
+ const settings = JSON.parse(
+ fs.readFileSync(path.join(tmpDir, '.claude', 'settings.json'), 'utf-8'),
+ );
+ const command = settings.hooks.PreToolUse[0].hooks[0].command;
+
+ const result = runHookInPowerShell(command, {
+ cwd: tmpDir,
+ env: { CLAUDE_PROJECT_DIR: '' },
+ });
+
+ expect(result.exitCode).toBe(0);
+ expect(result.stderr).toContain('BLOCKED: CLAUDE_PROJECT_DIR is unavailable');
+ expect(JSON.parse(result.stdout)).toEqual({
+ hookSpecificOutput: {
+ hookEventName: 'PreToolUse',
+ permissionDecision: 'deny',
+ permissionDecisionReason:
+ 'BLOCKED: CLAUDE_PROJECT_DIR is unavailable, so the required gstack hook cannot be loaded.',
+ },
+ });
+ });
+
+ test('required: hook runs in PowerShell with USERPROFILE home fallback', () => {
+ if (process.platform !== 'win32') return;
+
+ run(`${TEAM_INIT} required`, { cwd: tmpDir });
+ const settings = JSON.parse(
+ fs.readFileSync(path.join(tmpDir, '.claude', 'settings.json'), 'utf-8'),
+ );
+ const command = settings.hooks.PreToolUse[0].hooks[0].command;
+ const fakeHome = path.join(tmpDir, 'powershell-home');
+ fs.mkdirSync(path.join(fakeHome, '.claude', 'skills', 'gstack', 'bin'), {
+ recursive: true,
+ });
+
+ const result = runHookInPowerShell(command, {
+ cwd: tmpDir,
+ env: {
+ CLAUDE_PROJECT_DIR: tmpDir,
+ HOME: '',
+ USERPROFILE: fakeHome,
+ },
+ });
+
+ expect(result.exitCode).toBe(0);
+ expect(result.stderr).toBe('');
+ expect(JSON.parse(result.stdout)).toEqual({});
+ });
+
+ test('required: missing gstack returns an intentional deny, not a hook error', () => {
+ run(`${TEAM_INIT} required`, { cwd: tmpDir });
+ const settings = JSON.parse(
+ fs.readFileSync(path.join(tmpDir, '.claude', 'settings.json'), 'utf-8'),
+ );
+ const command = settings.hooks.PreToolUse[0].hooks[0].command;
+ const fakeHome = path.join(tmpDir, 'home-without-gstack');
+ fs.mkdirSync(fakeHome, { recursive: true });
+
+ const result = runHook(command, {
+ cwd: tmpDir,
+ env: {
+ CLAUDE_PROJECT_DIR: tmpDir,
+ HOME: fakeHome,
+ USERPROFILE: fakeHome,
+ },
+ });
+
+ expect(result.exitCode).toBe(0);
+ expect(result.stderr).toContain('BLOCKED: gstack is not installed globally.');
+ expect(result.stderr).toContain('git clone --depth 1');
+ const decision = JSON.parse(result.stdout);
+ expect(decision).toEqual({
+ hookSpecificOutput: {
+ hookEventName: 'PreToolUse',
+ permissionDecision: 'deny',
+ permissionDecisionReason: expect.stringContaining(
+ 'Then restart your AI coding tool.',
+ ),
+ },
+ });
+ });
+
+ test('required: rerun upgrades a legacy Bash hook without duplicates', () => {
+ const hooksDir = path.join(tmpDir, '.claude', 'hooks');
+ fs.mkdirSync(hooksDir, { recursive: true });
+ fs.writeFileSync(path.join(hooksDir, 'check-gstack.sh'), '#!/bin/bash\n');
+ fs.writeFileSync(
+ path.join(tmpDir, '.claude', 'settings.json'),
+ JSON.stringify({
+ hooks: {
+ PreToolUse: [
+ {
+ matcher: 'Skill',
+ hooks: [{
+ type: 'command',
+ command: '"$CLAUDE_PROJECT_DIR/.claude/hooks/check-gstack.sh"',
+ }],
+ },
+ ],
+ },
+ }),
+ );
+
+ run(`${TEAM_INIT} required`, { cwd: tmpDir });
+ const settings = JSON.parse(
+ fs.readFileSync(path.join(tmpDir, '.claude', 'settings.json'), 'utf-8'),
+ );
+ expect(settings.hooks.PreToolUse).toHaveLength(1);
+ expect(settings.hooks.PreToolUse[0].hooks).toHaveLength(1);
+ expect(settings.hooks.PreToolUse[0].hooks[0].command).toContain(
+ 'check-gstack.cjs',
+ );
});
test('idempotent: running twice does not duplicate CLAUDE.md section', () => {
@@ -291,7 +530,11 @@ describe('gstack-team-init', () => {
fs.mkdirSync(skillsDir, { recursive: true });
const targetDir = mkTmpDir();
fs.writeFileSync(path.join(targetDir, 'VERSION'), '0.14.0.0');
- fs.symlinkSync(targetDir, path.join(skillsDir, 'gstack'));
+ fs.symlinkSync(
+ targetDir,
+ path.join(skillsDir, 'gstack'),
+ process.platform === 'win32' ? 'junction' : 'dir',
+ );
const result = run(`${TEAM_INIT} optional`, { cwd: tmpDir });
expect(result.exitCode).toBe(0);
@@ -329,7 +572,7 @@ describe('setup --team / --no-team / -q', () => {
test(
'setup -q produces no stdout',
() => {
- const result = run(`${path.join(ROOT, 'setup')} -q`, { cwd: ROOT });
+ const result = run(`${bashCommand(path.join(ROOT, 'setup'))} -q`, { cwd: ROOT });
// -q should suppress informational output (may still have some output from build)
// The key test is that the "Skill naming:" prompt and "gstack ready" messages are suppressed
expect(result.stdout).not.toContain('Skill naming:');
@@ -341,8 +584,7 @@ describe('setup --team / --no-team / -q', () => {
test(
'setup --local prints deprecation warning',
() => {
- // stderr capture: run via bash redirect so we can capture stderr
- const result = run(`bash -c '${path.join(ROOT, 'setup')} --local -q 2>&1'`, { cwd: ROOT });
+ const result = run(`${bashCommand(path.join(ROOT, 'setup'))} --local -q 2>&1`, { cwd: ROOT });
expect(result.stdout).toContain('deprecated');
},
180_000,