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
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
39 changes: 39 additions & 0 deletions scripts/check-template-registry.mjs
Original file line number Diff line number Diff line change
@@ -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');
13 changes: 6 additions & 7 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#!/usr/bin/env node
import { Command } from 'commander';
import { isTemplateKey, listTemplates } from './templates.js';

const program = new Command();

Expand All @@ -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
Expand All @@ -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));
Expand Down
41 changes: 41 additions & 0 deletions src/templates.ts
Original file line number Diff line number Diff line change
@@ -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<TemplateKey, TemplateDefinition> = {
'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);
}