Skip to content

Commit 56dc09d

Browse files
author
Marvin Zhang
committed
feat(templates): enhance template handling to support directory-based templates and improve listing
1 parent a926596 commit 56dc09d

File tree

3 files changed

+109
-34
lines changed

3 files changed

+109
-34
lines changed

packages/cli/src/commands/create.ts

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ export async function createSpec(name: string, options: {
147147
// Resolve template path from .lean-spec/templates/
148148
const templatesDir = path.join(cwd, '.lean-spec', 'templates');
149149
let templateName: string;
150+
let templateDir: string | null = null; // If template is a directory, this will be set
150151

151152
// Determine which template to use
152153
if (options.template) {
@@ -164,10 +165,19 @@ export async function createSpec(name: string, options: {
164165

165166
let templatePath = path.join(templatesDir, templateName);
166167

167-
// Backward compatibility: If template not found, try spec-template.md then README.md
168+
// Check if template is a directory or file
168169
try {
169-
await fs.access(templatePath);
170+
const stat = await fs.stat(templatePath);
171+
if (stat.isDirectory()) {
172+
// Template is a directory - use README.md as main file, copy all .md files
173+
templateDir = templatePath;
174+
templatePath = path.join(templateDir, 'README.md');
175+
// Verify main template file exists
176+
await fs.access(templatePath);
177+
}
178+
// If it's a file, templatePath is already correct
170179
} catch {
180+
// Template not found at configured path, try fallbacks
171181
// Try spec-template.md first (legacy)
172182
const legacyPath = path.join(templatesDir, 'spec-template.md');
173183
try {
@@ -260,20 +270,22 @@ export async function createSpec(name: string, options: {
260270

261271
await fs.writeFile(specFile, content, 'utf-8');
262272

263-
// For detailed templates, copy any additional sub-spec files
264-
// Check if there are other .md files in the templates directory
273+
// Copy additional template files if template is a directory
274+
// This supports multi-file templates (e.g., detailed template with DESIGN.md, PLAN.md, TEST.md)
265275
try {
266-
const templateFiles = await fs.readdir(templatesDir);
267-
const additionalFiles = templateFiles.filter(f =>
268-
f.endsWith('.md') &&
269-
f !== templateName &&
270-
f !== 'spec-template.md' &&
271-
f !== config.structure.defaultFile
272-
);
276+
let additionalFiles: string[] = [];
277+
278+
if (templateDir) {
279+
// Template is a directory - copy all .md files except the main template (README.md)
280+
const templateFiles = await fs.readdir(templateDir);
281+
additionalFiles = templateFiles.filter(f =>
282+
f.endsWith('.md') && f !== 'README.md'
283+
);
284+
}
273285

274286
if (additionalFiles.length > 0) {
275287
for (const file of additionalFiles) {
276-
const srcPath = path.join(templatesDir, file);
288+
const srcPath = path.join(templateDir!, file);
277289
const destPath = path.join(specDir, file);
278290

279291
// Read template file and process variables

packages/cli/src/commands/templates.ts

Lines changed: 84 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -72,10 +72,11 @@ export async function listTemplates(cwd: string = process.cwd()): Promise<void>
7272
return;
7373
}
7474

75-
const files = await fs.readdir(templatesDir);
76-
const templateFiles = files.filter((f) => f.endsWith('.md'));
75+
const entries = await fs.readdir(templatesDir, { withFileTypes: true });
76+
const templateFiles = entries.filter((e) => e.isFile() && e.name.endsWith('.md'));
77+
const templateDirs = entries.filter((e) => e.isDirectory());
7778

78-
if (templateFiles.length === 0) {
79+
if (templateFiles.length === 0 && templateDirs.length === 0) {
7980
console.log(chalk.yellow('No templates found.'));
8081
console.log('');
8182
return;
@@ -87,21 +88,51 @@ export async function listTemplates(cwd: string = process.cwd()): Promise<void>
8788
for (const [name, file] of Object.entries(config.templates)) {
8889
const isDefault = config.template === file;
8990
const marker = isDefault ? chalk.green('✓ (default)') : '';
90-
console.log(` ${chalk.bold(name)}: ${file} ${marker}`);
91+
92+
// Check if it's a directory-based template
93+
const templatePath = path.join(templatesDir, file);
94+
try {
95+
const stat = await fs.stat(templatePath);
96+
if (stat.isDirectory()) {
97+
// List files in the directory
98+
const dirFiles = await fs.readdir(templatePath);
99+
const mdFiles = dirFiles.filter(f => f.endsWith('.md'));
100+
console.log(` ${chalk.bold(name)}: ${file}/ ${marker}`);
101+
console.log(chalk.gray(` Files: ${mdFiles.join(', ')}`));
102+
} else {
103+
console.log(` ${chalk.bold(name)}: ${file} ${marker}`);
104+
}
105+
} catch {
106+
console.log(` ${chalk.bold(name)}: ${file} ${marker} ${chalk.red('(missing)')}`);
107+
}
91108
}
92109
console.log('');
93110
}
94111

95112
// Show all available template files
96-
console.log(chalk.cyan('Available files:'));
97-
for (const file of templateFiles) {
98-
const filePath = path.join(templatesDir, file);
99-
const stat = await fs.stat(filePath);
100-
const sizeKB = (stat.size / 1024).toFixed(1);
101-
console.log(` ${file} (${sizeKB} KB)`);
113+
if (templateFiles.length > 0) {
114+
console.log(chalk.cyan('Available files:'));
115+
for (const entry of templateFiles) {
116+
const filePath = path.join(templatesDir, entry.name);
117+
const stat = await fs.stat(filePath);
118+
const sizeKB = (stat.size / 1024).toFixed(1);
119+
console.log(` ${entry.name} (${sizeKB} KB)`);
120+
}
121+
console.log('');
122+
}
123+
124+
// Show available template directories (multi-file templates)
125+
if (templateDirs.length > 0) {
126+
console.log(chalk.cyan('Available directories (multi-file templates):'));
127+
for (const entry of templateDirs) {
128+
const dirPath = path.join(templatesDir, entry.name);
129+
const dirFiles = await fs.readdir(dirPath);
130+
const mdFiles = dirFiles.filter(f => f.endsWith('.md'));
131+
console.log(` ${entry.name}/ (${mdFiles.length} files: ${mdFiles.join(', ')})`);
132+
}
133+
console.log('');
102134
}
103135

104-
console.log('');
105136
console.log(chalk.gray('Use templates with: lean-spec create <name> --template=<template-name>'));
106137
console.log('');
107138
}
@@ -123,12 +154,33 @@ export async function showTemplate(
123154
const templatePath = path.join(templatesDir, templateFile);
124155

125156
try {
126-
const content = await fs.readFile(templatePath, 'utf-8');
127-
console.log('');
128-
console.log(chalk.cyan(`=== Template: ${templateName} (${templateFile}) ===`));
129-
console.log('');
130-
console.log(content);
131-
console.log('');
157+
const stat = await fs.stat(templatePath);
158+
159+
if (stat.isDirectory()) {
160+
// Directory-based template - show all files
161+
console.log('');
162+
console.log(chalk.cyan(`=== Template: ${templateName} (${templateFile}/) ===`));
163+
console.log('');
164+
165+
const files = await fs.readdir(templatePath);
166+
const mdFiles = files.filter(f => f.endsWith('.md'));
167+
168+
for (const file of mdFiles) {
169+
const filePath = path.join(templatePath, file);
170+
const content = await fs.readFile(filePath, 'utf-8');
171+
console.log(chalk.yellow(`--- ${file} ---`));
172+
console.log(content);
173+
console.log('');
174+
}
175+
} else {
176+
// Single file template
177+
const content = await fs.readFile(templatePath, 'utf-8');
178+
console.log('');
179+
console.log(chalk.cyan(`=== Template: ${templateName} (${templateFile}) ===`));
180+
console.log('');
181+
console.log(content);
182+
console.log('');
183+
}
132184
} catch (error) {
133185
console.error(chalk.red(`Error reading template: ${templateFile}`));
134186
console.error(error);
@@ -145,14 +197,25 @@ export async function addTemplate(
145197
const templatesDir = path.join(cwd, '.lean-spec', 'templates');
146198
const templatePath = path.join(templatesDir, file);
147199

148-
// Check if file exists
200+
// Check if file or directory exists
149201
try {
150-
await fs.access(templatePath);
202+
const stat = await fs.stat(templatePath);
203+
if (stat.isDirectory()) {
204+
// Verify it has a README.md (main template file)
205+
const mainFile = path.join(templatePath, 'README.md');
206+
try {
207+
await fs.access(mainFile);
208+
} catch {
209+
console.error(chalk.red(`Directory template must contain README.md: ${file}/`));
210+
console.error(chalk.gray(`Expected at: ${mainFile}`));
211+
process.exit(1);
212+
}
213+
}
151214
} catch {
152-
console.error(chalk.red(`Template file not found: ${file}`));
215+
console.error(chalk.red(`Template not found: ${file}`));
153216
console.error(chalk.gray(`Expected at: ${templatePath}`));
154217
console.error(
155-
chalk.yellow('Create the file first or use: lean-spec templates copy <source> <target>'),
218+
chalk.yellow('Create the file/directory first or use: lean-spec templates copy <source> <target>'),
156219
);
157220
process.exit(1);
158221
}

0 commit comments

Comments
 (0)