From bfeab6fee869c8f3984c866f9f991143c87a60af Mon Sep 17 00:00:00 2001 From: Roger Chappel Date: Wed, 29 Apr 2026 10:37:23 +1000 Subject: [PATCH 1/2] feat: add typed template registry --- src/index.ts | 13 ++++++------- src/templates.ts | 41 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 7 deletions(-) create mode 100644 src/templates.ts diff --git a/src/index.ts b/src/index.ts index 3250232..8d60ef7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,6 @@ #!/usr/bin/env node import { Command } from 'commander'; +import { isTemplateKey, listTemplates } from './templates.js'; const program = new Command(); @@ -13,13 +14,7 @@ program .command('templates') .description('List available templates.') .action(() => { - console.log(JSON.stringify({ - templates: [ - { key: 'oss-cli', description: 'Open-source TypeScript CLI package' }, - { key: 'next-app', description: 'Next.js app scaffold' }, - { key: 'python-api', description: 'Python API scaffold' } - ] - }, null, 2)); + console.log(JSON.stringify({ templates: listTemplates() }, null, 2)); }); program @@ -29,6 +24,10 @@ program .argument('[name]', 'Project directory/name') .option('--dry-run', 'Print planned actions without writing files') .action((template: string, name: string | undefined, options: { dryRun?: boolean }) => { + if (!isTemplateKey(template)) { + throw new Error(`Unknown template: ${template}. Run \`stackforge templates\` to list available templates.`); + } + const projectName = name ?? template; const mode = options.dryRun ? 'dry-run' : 'write'; console.log(JSON.stringify({ ok: true, command: 'init', template, projectName, mode }, null, 2)); diff --git a/src/templates.ts b/src/templates.ts new file mode 100644 index 0000000..50625fa --- /dev/null +++ b/src/templates.ts @@ -0,0 +1,41 @@ +export const TEMPLATE_KEYS = ['next-app', 'oss-cli', 'python-api'] as const; + +export type TemplateKey = typeof TEMPLATE_KEYS[number]; + +export type TemplateCategory = 'app' | 'api' | 'cli'; + +export interface TemplateDefinition { + key: TemplateKey; + name: string; + description: string; + category: TemplateCategory; +} + +export const TEMPLATE_REGISTRY: Record = { + 'next-app': { + key: 'next-app', + name: 'Next.js App', + description: 'Next.js app scaffold', + category: 'app' + }, + 'oss-cli': { + key: 'oss-cli', + name: 'OSS CLI', + description: 'Open-source TypeScript CLI package', + category: 'cli' + }, + 'python-api': { + key: 'python-api', + name: 'Python API', + description: 'Python API scaffold', + category: 'api' + } +}; + +export function listTemplates(): TemplateDefinition[] { + return TEMPLATE_KEYS.map((key) => TEMPLATE_REGISTRY[key]); +} + +export function isTemplateKey(value: string): value is TemplateKey { + return (TEMPLATE_KEYS as readonly string[]).includes(value); +} From 8c18d23dac5399b391465bba45ab734d37ff9cab Mon Sep 17 00:00:00 2001 From: Roger Chappel Date: Wed, 29 Apr 2026 10:37:48 +1000 Subject: [PATCH 2/2] test: add template registry check --- package.json | 3 ++- scripts/check-template-registry.mjs | 39 +++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 scripts/check-template-registry.mjs diff --git a/package.json b/package.json index 19f07b1..9195166 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,8 @@ "build": "tsc", "dev": "tsx src/index.ts", "check": "tsc --noEmit", - "start": "node dist/index.js" + "start": "node dist/index.js", + "check:templates": "pnpm build && node scripts/check-template-registry.mjs" }, "dependencies": { "commander": "^12.1.0" diff --git a/scripts/check-template-registry.mjs b/scripts/check-template-registry.mjs new file mode 100644 index 0000000..97d4cbb --- /dev/null +++ b/scripts/check-template-registry.mjs @@ -0,0 +1,39 @@ +#!/usr/bin/env node +import { execFileSync } from 'node:child_process'; +import { listTemplates, TEMPLATE_KEYS, TEMPLATE_REGISTRY } from '../dist/templates.js'; + +const expectedKeys = ['next-app', 'oss-cli', 'python-api']; +const keys = [...TEMPLATE_KEYS]; + +if (JSON.stringify(keys) !== JSON.stringify(expectedKeys)) { + throw new Error(`Template keys are not deterministic. Expected ${expectedKeys.join(', ')}, got ${keys.join(', ')}`); +} + +const registryKeys = Object.keys(TEMPLATE_REGISTRY).sort(); +if (JSON.stringify(registryKeys) !== JSON.stringify(expectedKeys)) { + throw new Error(`Registry keys mismatch. Expected ${expectedKeys.join(', ')}, got ${registryKeys.join(', ')}`); +} + +const templates = listTemplates(); +if (templates.length !== expectedKeys.length) { + throw new Error(`Expected ${expectedKeys.length} templates, got ${templates.length}`); +} + +for (const [index, template] of templates.entries()) { + const expectedKey = expectedKeys[index]; + if (template.key !== expectedKey) { + throw new Error(`Template at index ${index} should be ${expectedKey}, got ${template.key}`); + } + + if (!template.name || !template.description || !template.category) { + throw new Error(`Template ${template.key} is missing required metadata`); + } +} + +const cliOutput = execFileSync(process.execPath, ['dist/index.js', 'templates'], { encoding: 'utf8' }); +const parsed = JSON.parse(cliOutput); +if (JSON.stringify(parsed.templates) !== JSON.stringify(templates)) { + throw new Error('CLI templates output does not match typed registry'); +} + +console.log('template registry check passed');