* test: reproduce CJS dispatcher rejecting dotted form (#3243) runGsdTools assertions confirm that generate-slug.hello-world, current-timestamp.date, validate.plan, roadmap.analyze, phases.list, and check.decision-coverage-plan all fail with "Unknown command: <dotted>" — the dispatcher switch only accepts the spaced form. Edge cases (no dots unchanged, leading-dot rejected, unknown dotted form suggests spaced equivalent) are also specified; those three pass already because the shim is not yet implemented. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(cjs): accept dotted canonical command form (#3243) Add a shim at the top of main() in gsd-tools.cjs that splits args[0] on the first dot when present, normalizing "state.update" → command='state' args=['state','update',...] before the switch statement is reached. Any caller that bypasses the SDK (stale npm-installed binary, workflow shell-out, third-party script) can now use the canonical dotted form natively without hitting "Unknown command: <domain>.<subcommand>". The shim guards against empty head/rest so ".hidden" and bare "." args are unchanged and fall through to the existing "Unknown command" path. Also improves the default "Unknown command" error message to suggest the spaced equivalent when a dotted form was passed — e.g. for "foo.bar" the error now reads: Unknown command: foo — did you mean: "foo bar"? Parallel to dottedCommandToCjsArgv in sdk/src/query/query-fallback-bridge-adapter.ts; intentionally kept separate to avoid SDK coupling. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * changeset: pr=3248 for #3243 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * test: tighten dotted-form suggestion assertion (CR nitpick) * fix(cjs): suggestion uses first-dot split (CR finding 1, multi-dot consistency) The "did you mean" hint in the Unknown-command default case was replacing ALL dots with spaces (state.update.foo → "state update foo"), but the dispatcher shim only splits on the FIRST dot (state.update.foo → head=state, rest=update.foo). Apply CR's exact patch to use indexOf+slice so suggestion matches dispatch behavior. Add a multi-dot regression test (a.b.c must suggest "a b.c", not "a b c"). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -241,7 +241,7 @@ function parseMultiwordArg(args, flag) {
|
||||
// ─── CLI Router ───────────────────────────────────────────────────────────────
|
||||
|
||||
async function main() {
|
||||
const args = process.argv.slice(2);
|
||||
let args = process.argv.slice(2);
|
||||
|
||||
// Optional cwd override for sandboxed subagents running outside project root.
|
||||
let cwd = process.cwd();
|
||||
@@ -339,7 +339,30 @@ async function main() {
|
||||
args.splice(defaultIdx, 2);
|
||||
}
|
||||
|
||||
const command = args[0];
|
||||
let command = args[0];
|
||||
|
||||
// #3243: accept dotted canonical form (e.g. `state.update`) as well as the
|
||||
// spaced form (`state update`). Workflow files and stale SDK binaries pass
|
||||
// the dotted canonical form directly; any caller that bypasses the SDK
|
||||
// client-side split hit "Unknown command" before this shim.
|
||||
//
|
||||
// Split on the FIRST dot only — `check.decision-coverage-plan` becomes
|
||||
// command='check', args=['check','decision-coverage-plan',...rest].
|
||||
// Parallel to dottedCommandToCjsArgv in sdk/src/query/query-fallback-bridge-adapter.ts;
|
||||
// kept separate here to avoid SDK coupling (see TODO: extract to shared helper).
|
||||
//
|
||||
// Guard: head and rest must both be non-empty (rejects leading-dot args like
|
||||
// ".hidden" and bare-dot ".").
|
||||
const originalCommand = command; // preserved for "Unknown command" suggestion
|
||||
if (typeof command === 'string' && command.includes('.')) {
|
||||
const dotIdx = command.indexOf('.');
|
||||
const head = command.slice(0, dotIdx);
|
||||
const rest = command.slice(dotIdx + 1);
|
||||
if (head && rest) {
|
||||
command = head;
|
||||
args = [head, rest, ...args.slice(1)];
|
||||
}
|
||||
}
|
||||
|
||||
// Top-level usage string — emitted by `gsd-tools` (no args) and by
|
||||
// `gsd-tools --help` / any `--help` request below.
|
||||
@@ -424,7 +447,7 @@ async function main() {
|
||||
}
|
||||
};
|
||||
try {
|
||||
await runCommand(command, args, cwd, raw, defaultValue);
|
||||
await runCommand(command, args, cwd, raw, defaultValue, originalCommand);
|
||||
cleanup();
|
||||
} catch (e) {
|
||||
fs.writeSync = origWriteSync;
|
||||
@@ -445,7 +468,7 @@ async function main() {
|
||||
return origWriteSync2.call(fs, fd, data, ...rest);
|
||||
};
|
||||
try {
|
||||
await runCommand(command, args, cwd, raw, defaultValue);
|
||||
await runCommand(command, args, cwd, raw, defaultValue, originalCommand);
|
||||
} finally {
|
||||
fs.writeSync = origWriteSync2;
|
||||
}
|
||||
@@ -479,7 +502,7 @@ function extractField(obj, fieldPath) {
|
||||
return current;
|
||||
}
|
||||
|
||||
async function runCommand(command, args, cwd, raw, defaultValue) {
|
||||
async function runCommand(command, args, cwd, raw, defaultValue, originalCommand) {
|
||||
switch (command) {
|
||||
case 'state': {
|
||||
routeStateCommand({
|
||||
@@ -1163,8 +1186,25 @@ async function runCommand(command, args, cwd, raw, defaultValue) {
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
error(`Unknown command: ${command}`);
|
||||
default: {
|
||||
// #3243: if the caller passed a dotted form (e.g. "foo.bar"), the shim
|
||||
// above split it so `command` here is the head ("foo"). Use
|
||||
// originalCommand to reconstruct the original dotted form and suggest
|
||||
// the spaced equivalent — surfacing a useful diagnostic instead of just
|
||||
// "Unknown command: foo".
|
||||
const wasDotted =
|
||||
typeof originalCommand === 'string' &&
|
||||
originalCommand !== command &&
|
||||
originalCommand.includes('.');
|
||||
let suggestion = '';
|
||||
if (wasDotted) {
|
||||
const dotIdx = originalCommand.indexOf('.');
|
||||
const head = originalCommand.slice(0, dotIdx);
|
||||
const rest = originalCommand.slice(dotIdx + 1);
|
||||
suggestion = ` — did you mean: "${head} ${rest}"?`;
|
||||
}
|
||||
error(`Unknown command: ${command}${suggestion}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user