Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .changeset/fix-subcommand-help.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
"@bradygaster/squad-cli": patch
---

Fix `squad <command> --help` silently running the command instead of showing help (#1201)

Previously, passing `--help` or `-h` after a subcommand (e.g. `squad init --help`,
`squad triage --help`, `squad doctor --help`) was silently dropped and the command
would execute for real — sometimes with destructive side effects (init scaffolded
files into the cwd, triage/watch started a polling loop). Only `squad loop --help`
and `squad state-mcp --help` were intercepting the flag.

The CLI now intercepts `--help`/`-h` for every registered subcommand in one place
at the top of the router and prints command-specific help via a new
`printCommandHelp(cmd, version)` helper. Unrecognized commands fall back to a
friendly "see `squad help`" message. No side effects are triggered.
66 changes: 26 additions & 40 deletions packages/squad-cli/src/cli-entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ import { BOLD, RESET, DIM, RED, GREEN, YELLOW } from './cli/core/output.js';
import { runInit } from './cli/core/init.js';
import { runCost } from './cli/commands/cost.js';
import { getPackageVersion } from './cli/core/version.js';
import { printCommandHelp, printGenericCommandHelp } from './cli/core/command-help.js';

// Lazy-load squad-sdk to avoid triggering @github/copilot-sdk import on Node 24+
// (Issue: copilot-sdk has broken ESM imports - vscode-jsonrpc/node without .js extension)
Expand Down Expand Up @@ -282,6 +283,27 @@ async function main(): Promise<void> {
return;
}

// --help / -h on a subcommand → print command-specific help and exit.
// Without this intercept the flag was silently dropped and the command
// would execute for real (sometimes with destructive side effects, e.g.
// `squad init --help` scaffolding files, or `squad triage --help` starting
// the polling loop). See #1201.
if (
cmd &&
cmd !== 'help' &&
cmd !== '--help' &&
cmd !== '-h' &&
cmd !== 'version' &&
cmd !== '--version' &&
cmd !== '-v' &&
(args.includes('--help') || args.includes('-h'))
) {
if (!printCommandHelp(cmd, VERSION)) {
printGenericCommandHelp(cmd);
}
return;
}

// No args → launch interactive shell; whitespace-only arg → show help
if (rawCmd === undefined) {
// Fire-and-forget update check — non-blocking, never delays shell startup
Expand Down Expand Up @@ -423,15 +445,10 @@ async function main(): Promise<void> {
return;
}

if (cmd === 'state-mcp') {
if (args.includes('--help') || args.includes('-h')) {
const { printStateMcpHelp } = await import('./cli/commands/state-mcp.js');
printStateMcpHelp();
return;
}
const { runStateMcp } = await import('./cli/commands/state-mcp.js');
await runStateMcp(getSquadStartDir());
return;
if (cmd === 'state-mcp') {
const { runStateMcp } = await import('./cli/commands/state-mcp.js');
await runStateMcp(getSquadStartDir());
return;
}

if (cmd === 'migrate') {
Expand Down Expand Up @@ -621,37 +638,6 @@ async function main(): Promise<void> {
}

if (cmd === 'loop') {
// --help
if (args.includes('--help') || args.includes('-h')) {
console.log(`\n${BOLD}squad loop${RESET} — Prompt-driven continuous work loop\n`);
console.log(`Usage: squad loop [options]\n`);
console.log(`Reads loop.md and runs it as a continuous work loop.\n`);
console.log(`Options:`);
console.log(` ${BOLD}--init${RESET} Generate a boilerplate loop.md`);
console.log(` ${BOLD}--file <path>${RESET} Path to loop file (default: loop.md)`);
console.log(` ${BOLD}--interval <min>${RESET} Override loop interval in minutes`);
console.log(` ${BOLD}--timeout <min>${RESET} Override max minutes per cycle`);
console.log(` ${BOLD}--copilot-flags "..."${RESET} Extra flags for Copilot CLI`);
console.log(` ${BOLD}--agent-cmd <cmd>${RESET} Override the agent command`);
console.log(`\nCapabilities (composable with the loop):`);
console.log(` ${BOLD}--self-pull${RESET} git fetch/pull at round start`);
console.log(` ${BOLD}--monitor-email${RESET} Scan email for actionable items`);
console.log(` ${BOLD}--monitor-teams${RESET} Scan Teams for actionable messages`);
console.log(` ${BOLD}--decision-hygiene${RESET} Auto-merge decision inbox`);
console.log(` ${BOLD}--retro${RESET} Enforce retrospective checks`);
console.log(`\nFrontmatter (in loop.md):`);
console.log(` configured: true ${DIM}(required — confirms intentional setup)${RESET}`);
console.log(` interval: 10 ${DIM}(minutes between cycles)${RESET}`);
console.log(` timeout: 30 ${DIM}(max minutes per cycle)${RESET}`);
console.log(` description: "..." ${DIM}(shown in status output)${RESET}`);
console.log(`\nExamples:`);
console.log(` squad loop ${DIM}# run loop.md${RESET}`);
console.log(` squad loop --init ${DIM}# generate boilerplate${RESET}`);
console.log(` squad loop --file ops/loop.md ${DIM}# custom loop file${RESET}`);
console.log(` squad loop --monitor-email ${DIM}# with email monitoring${RESET}`);
return;
}

const { runLoop, generateLoopFile } = await import('./cli/commands/loop.js');

// --init: scaffold a boilerplate loop.md
Expand Down
Loading
Loading