* test(3251): red — assert 14 missing commands in command-aliases.generated.cjs Parametrized test covering all 14 commands from issue #3251. Requires the CJS manifest and asserts structurally (never greps source). Currently fails because NON_FAMILY_COMMAND_ALIASES is not exported and all 14 are missing. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(3251): add 14 missing commands to command-aliases.generated.cjs Adds NON_FAMILY_COMMAND_ALIASES export to command-aliases.generated.cjs and extends sdk/src/query/command-manifest.non-family.ts with the 10 commands that were registered in static catalogs but absent from the manifest source-of-truth: - check.decision-coverage-plan / check.decision-coverage-verify - frontmatter.get - phase.mvp-mode - progress.bar - stats.json - task.is-behavior-adding - todo.match-phase - uat.render-checkpoint - workstream.list The other 4 (frontmatter.set, learnings.copy, milestone.complete, requirements.mark-complete) were already in non-family.ts but unexported. Generator (sdk/scripts/gen-command-aliases.ts) now produces both the TS and CJS artifacts including the non-family section, sorted by canonical for determinism. Freshness check (sdk/scripts/check-command-aliases-fresh.mjs) verifies TS and CJS non-family parity against the manifest source-of-truth. Fixes #3251 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore(3251): add changeset and CHANGELOG entry for PR #3305 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(gen-command-aliases): preserve TS type interfaces and annotations on regen (#3251) - Add FamilyCommandAlias and NonFamilyCommandAlias interface declarations to tsBody so regen never strips them - Replace JSON.stringify with single-line compact serialisers for TS output (matches committed file format) - Apply typed annotations (readonly FamilyCommandAlias[] / readonly NonFamilyCommandAlias[]) to all exported TS constants - CJS path unchanged: remains untyped pure-JS with JSON.stringify multi-line format - Regenerate sdk/src/query/command-aliases.generated.ts to sync with updated generator Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: drop redundant CHANGELOG.md edit (use .changeset/ fragment per CONTRIBUTING.md) --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
197 lines
9.0 KiB
JavaScript
197 lines
9.0 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Build-time alias generator skeleton for command-manifest-driven routing.
|
|
*
|
|
* This pilot commits generated artifacts directly; this script documents and
|
|
* preserves the generation seam so future command families can be migrated
|
|
* without hand-maintained alias duplication.
|
|
*/
|
|
|
|
import { writeFile } from 'node:fs/promises';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
import { COMMAND_DEFINITIONS_BY_FAMILY } from '../src/query/command-definition.js';
|
|
import { NON_FAMILY_COMMAND_MANIFEST } from '../src/query/command-manifest.non-family.js';
|
|
|
|
function toSubcommand(canonical: string, family: 'state' | 'verify' | 'init' | 'phase' | 'phases' | 'validate' | 'roadmap'): string {
|
|
const prefix = `${family}.`;
|
|
return canonical.startsWith(prefix) ? canonical.slice(prefix.length) : canonical;
|
|
}
|
|
|
|
async function main(): Promise<void> {
|
|
const stateEntries = COMMAND_DEFINITIONS_BY_FAMILY.state.map((entry) => ({
|
|
canonical: entry.canonical,
|
|
aliases: entry.aliases,
|
|
subcommand: toSubcommand(entry.canonical, 'state'),
|
|
mutation: entry.mutation,
|
|
}));
|
|
|
|
const verifyEntries = COMMAND_DEFINITIONS_BY_FAMILY.verify.map((entry) => ({
|
|
canonical: entry.canonical,
|
|
aliases: entry.aliases,
|
|
subcommand: toSubcommand(entry.canonical, 'verify'),
|
|
mutation: entry.mutation,
|
|
}));
|
|
|
|
const initEntries = COMMAND_DEFINITIONS_BY_FAMILY.init.map((entry) => ({
|
|
canonical: entry.canonical,
|
|
aliases: entry.aliases,
|
|
subcommand: toSubcommand(entry.canonical, 'init'),
|
|
mutation: entry.mutation,
|
|
}));
|
|
|
|
const phaseEntries = COMMAND_DEFINITIONS_BY_FAMILY.phase.map((entry) => ({
|
|
canonical: entry.canonical,
|
|
aliases: entry.aliases,
|
|
subcommand: toSubcommand(entry.canonical, 'phase'),
|
|
mutation: entry.mutation,
|
|
}));
|
|
|
|
const phasesEntries = COMMAND_DEFINITIONS_BY_FAMILY.phases.map((entry) => ({
|
|
canonical: entry.canonical,
|
|
aliases: entry.aliases,
|
|
subcommand: toSubcommand(entry.canonical, 'phases'),
|
|
mutation: entry.mutation,
|
|
}));
|
|
|
|
const validateEntries = COMMAND_DEFINITIONS_BY_FAMILY.validate.map((entry) => ({
|
|
canonical: entry.canonical,
|
|
aliases: entry.aliases,
|
|
subcommand: toSubcommand(entry.canonical, 'validate'),
|
|
mutation: entry.mutation,
|
|
}));
|
|
|
|
const roadmapEntries = COMMAND_DEFINITIONS_BY_FAMILY.roadmap.map((entry) => ({
|
|
canonical: entry.canonical,
|
|
aliases: entry.aliases,
|
|
subcommand: toSubcommand(entry.canonical, 'roadmap'),
|
|
mutation: entry.mutation,
|
|
}));
|
|
|
|
// Non-family entries — sorted by canonical for deterministic output.
|
|
const nonFamilyEntries = [...NON_FAMILY_COMMAND_MANIFEST]
|
|
.sort((a, b) => a.canonical.localeCompare(b.canonical))
|
|
.map((entry) => ({
|
|
canonical: entry.canonical,
|
|
aliases: [...entry.aliases],
|
|
mutation: entry.mutation,
|
|
}));
|
|
|
|
// Serialise a FamilyCommandAlias entry as a single-line TS literal.
|
|
function serializeFamily(e: { canonical: string; aliases: string[]; subcommand: string; mutation: boolean }): string {
|
|
const aliases = `[${e.aliases.map((a) => `'${a}'`).join(', ')}]`;
|
|
return `{ canonical: '${e.canonical}', aliases: ${aliases}, subcommand: '${e.subcommand}', mutation: ${e.mutation} }`;
|
|
}
|
|
|
|
// Serialise a NonFamilyCommandAlias entry as a single-line TS literal.
|
|
function serializeNonFamily(e: { canonical: string; aliases: string[]; mutation: boolean }): string {
|
|
const aliases = `[${e.aliases.map((a) => `'${a}'`).join(', ')}]`;
|
|
return `{ canonical: '${e.canonical}', aliases: ${aliases}, mutation: ${e.mutation} }`;
|
|
}
|
|
|
|
function renderFamilyArray(entries: { canonical: string; aliases: string[]; subcommand: string; mutation: boolean }[]): string {
|
|
return `[\n${entries.map((e) => ` ${serializeFamily(e)},`).join('\n')}\n]`;
|
|
}
|
|
|
|
function renderNonFamilyArray(entries: { canonical: string; aliases: string[]; mutation: boolean }[]): string {
|
|
return `[\n${entries.map((e) => ` ${serializeNonFamily(e)},`).join('\n')}\n]`;
|
|
}
|
|
|
|
const tsOutPath = fileURLToPath(new URL('../src/query/command-aliases.generated.ts', import.meta.url));
|
|
const tsHeader = `/**\n * GENERATED FILE — command alias expansion for state.*, verify.*, init.*, phase.*, phases.*, validate.*, roadmap.*, and non-family commands.\n * Source: sdk/src/query/command-manifest.{state,verify,init,phase,phases,validate,roadmap,non-family}.ts\n */\n\n`;
|
|
const tsBody = [
|
|
'export interface FamilyCommandAlias {',
|
|
' canonical: string;',
|
|
' aliases: string[];',
|
|
' subcommand: string;',
|
|
' mutation: boolean;',
|
|
'}',
|
|
'',
|
|
`export const STATE_COMMAND_ALIASES: readonly FamilyCommandAlias[] = ${renderFamilyArray(stateEntries)} as const;`,
|
|
'',
|
|
`export const VERIFY_COMMAND_ALIASES: readonly FamilyCommandAlias[] = ${renderFamilyArray(verifyEntries)} as const;`,
|
|
'',
|
|
`export const INIT_COMMAND_ALIASES: readonly FamilyCommandAlias[] = ${renderFamilyArray(initEntries)} as const;`,
|
|
'',
|
|
`export const PHASE_COMMAND_ALIASES: readonly FamilyCommandAlias[] = ${renderFamilyArray(phaseEntries)} as const;`,
|
|
'',
|
|
`export const PHASES_COMMAND_ALIASES: readonly FamilyCommandAlias[] = ${renderFamilyArray(phasesEntries)} as const;`,
|
|
'',
|
|
`export const VALIDATE_COMMAND_ALIASES: readonly FamilyCommandAlias[] = ${renderFamilyArray(validateEntries)} as const;`,
|
|
'',
|
|
`export const ROADMAP_COMMAND_ALIASES: readonly FamilyCommandAlias[] = ${renderFamilyArray(roadmapEntries)} as const;`,
|
|
'',
|
|
'export interface NonFamilyCommandAlias {',
|
|
' canonical: string;',
|
|
' aliases: string[];',
|
|
' mutation: boolean;',
|
|
'}',
|
|
'',
|
|
`export const NON_FAMILY_COMMAND_ALIASES: readonly NonFamilyCommandAlias[] = ${renderNonFamilyArray(nonFamilyEntries)} as const;`,
|
|
'',
|
|
'export const STATE_SUBCOMMANDS = new Set<string>(STATE_COMMAND_ALIASES.map((entry) => entry.subcommand));',
|
|
'export const VERIFY_SUBCOMMANDS = new Set<string>(VERIFY_COMMAND_ALIASES.map((entry) => entry.subcommand));',
|
|
'export const INIT_SUBCOMMANDS = new Set<string>(INIT_COMMAND_ALIASES.map((entry) => entry.subcommand));',
|
|
'export const PHASE_SUBCOMMANDS = new Set<string>(PHASE_COMMAND_ALIASES.map((entry) => entry.subcommand));',
|
|
'export const PHASES_SUBCOMMANDS = new Set<string>(PHASES_COMMAND_ALIASES.map((entry) => entry.subcommand));',
|
|
'export const VALIDATE_SUBCOMMANDS = new Set<string>(VALIDATE_COMMAND_ALIASES.map((entry) => entry.subcommand));',
|
|
'export const ROADMAP_SUBCOMMANDS = new Set<string>(ROADMAP_COMMAND_ALIASES.map((entry) => entry.subcommand));',
|
|
'',
|
|
].join('\n');
|
|
await writeFile(tsOutPath, tsHeader + tsBody, 'utf-8');
|
|
|
|
// Also generate the CJS mirror used by get-shit-done/bin/lib/ seams.
|
|
// CJS is plain JavaScript — no type annotations.
|
|
const cjsOutPath = fileURLToPath(new URL('../../get-shit-done/bin/lib/command-aliases.generated.cjs', import.meta.url));
|
|
const cjsHeader = `'use strict';\n\n/**\n * GENERATED FILE — state.*, verify.*, init.*, phase.*, phases.*, validate.*, roadmap.*, and non-family alias/subcommand metadata for CJS routing.\n * Source: sdk/src/query/command-manifest.{state,verify,init,phase,phases,validate,roadmap,non-family}.ts\n */\n\n`;
|
|
const cjsBody = [
|
|
`const STATE_COMMAND_ALIASES = ${JSON.stringify(stateEntries, null, 2)};`,
|
|
'',
|
|
`const VERIFY_COMMAND_ALIASES = ${JSON.stringify(verifyEntries, null, 2)};`,
|
|
'',
|
|
`const INIT_COMMAND_ALIASES = ${JSON.stringify(initEntries, null, 2)};`,
|
|
'',
|
|
`const PHASE_COMMAND_ALIASES = ${JSON.stringify(phaseEntries, null, 2)};`,
|
|
'',
|
|
`const PHASES_COMMAND_ALIASES = ${JSON.stringify(phasesEntries, null, 2)};`,
|
|
'',
|
|
`const VALIDATE_COMMAND_ALIASES = ${JSON.stringify(validateEntries, null, 2)};`,
|
|
'',
|
|
`const ROADMAP_COMMAND_ALIASES = ${JSON.stringify(roadmapEntries, null, 2)};`,
|
|
'',
|
|
`const NON_FAMILY_COMMAND_ALIASES = ${JSON.stringify(nonFamilyEntries, null, 2)};`,
|
|
'',
|
|
`const STATE_SUBCOMMANDS = STATE_COMMAND_ALIASES.map((entry) => entry.subcommand);`,
|
|
`const VERIFY_SUBCOMMANDS = VERIFY_COMMAND_ALIASES.map((entry) => entry.subcommand);`,
|
|
`const INIT_SUBCOMMANDS = INIT_COMMAND_ALIASES.map((entry) => entry.subcommand);`,
|
|
`const PHASE_SUBCOMMANDS = PHASE_COMMAND_ALIASES.map((entry) => entry.subcommand);`,
|
|
`const PHASES_SUBCOMMANDS = PHASES_COMMAND_ALIASES.map((entry) => entry.subcommand);`,
|
|
`const VALIDATE_SUBCOMMANDS = VALIDATE_COMMAND_ALIASES.map((entry) => entry.subcommand);`,
|
|
`const ROADMAP_SUBCOMMANDS = ROADMAP_COMMAND_ALIASES.map((entry) => entry.subcommand);`,
|
|
'',
|
|
`module.exports = {`,
|
|
` STATE_COMMAND_ALIASES,`,
|
|
` VERIFY_COMMAND_ALIASES,`,
|
|
` INIT_COMMAND_ALIASES,`,
|
|
` PHASE_COMMAND_ALIASES,`,
|
|
` PHASES_COMMAND_ALIASES,`,
|
|
` VALIDATE_COMMAND_ALIASES,`,
|
|
` ROADMAP_COMMAND_ALIASES,`,
|
|
` NON_FAMILY_COMMAND_ALIASES,`,
|
|
` STATE_SUBCOMMANDS,`,
|
|
` VERIFY_SUBCOMMANDS,`,
|
|
` INIT_SUBCOMMANDS,`,
|
|
` PHASE_SUBCOMMANDS,`,
|
|
` PHASES_SUBCOMMANDS,`,
|
|
` VALIDATE_SUBCOMMANDS,`,
|
|
` ROADMAP_SUBCOMMANDS,`,
|
|
`};`,
|
|
].join('\n');
|
|
await writeFile(cjsOutPath, cjsHeader + cjsBody, 'utf-8');
|
|
}
|
|
|
|
main().catch((err) => {
|
|
console.error(err);
|
|
process.exit(1);
|
|
});
|