@t3lnet/sceneforge 1.0.9 → 1.0.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +57 -0
- package/cli/cli.js +6 -0
- package/cli/commands/add-audio-to-steps.js +9 -3
- package/cli/commands/concat-final-videos.js +6 -2
- package/cli/commands/context.js +791 -0
- package/cli/commands/split-video.js +3 -1
- package/context/context-builder.ts +318 -0
- package/context/index.ts +52 -0
- package/context/template-loader.ts +161 -0
- package/context/templates/base/actions-reference.md +299 -0
- package/context/templates/base/cli-reference.md +236 -0
- package/context/templates/base/project-overview.md +58 -0
- package/context/templates/base/selectors-guide.md +233 -0
- package/context/templates/base/yaml-schema.md +210 -0
- package/context/templates/skills/balance-timing.md +136 -0
- package/context/templates/skills/debug-selector.md +193 -0
- package/context/templates/skills/generate-actions.md +94 -0
- package/context/templates/skills/optimize-demo.md +218 -0
- package/context/templates/skills/review-demo-yaml.md +164 -0
- package/context/templates/skills/write-step-script.md +136 -0
- package/context/templates/stages/stage1-actions.md +236 -0
- package/context/templates/stages/stage2-scripts.md +197 -0
- package/context/templates/stages/stage3-balancing.md +229 -0
- package/context/templates/stages/stage4-rebalancing.md +228 -0
- package/context/tests/context-builder.test.ts +237 -0
- package/context/tests/template-loader.test.ts +181 -0
- package/context/tests/tool-formatter.test.ts +198 -0
- package/context/tool-formatter.ts +189 -0
- package/dist/index.cjs +416 -11
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +182 -1
- package/dist/index.d.ts +182 -1
- package/dist/index.js +391 -11
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Main context builder for generating LLM instruction files.
|
|
3
|
+
* Orchestrates template loading, composition, and formatting.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as fs from "fs/promises";
|
|
7
|
+
import * as path from "path";
|
|
8
|
+
import {
|
|
9
|
+
loadTemplate,
|
|
10
|
+
loadTemplatesByCategory,
|
|
11
|
+
interpolateVariables,
|
|
12
|
+
composeTemplates,
|
|
13
|
+
listTemplates,
|
|
14
|
+
type LoadedTemplate,
|
|
15
|
+
type TemplateVariables,
|
|
16
|
+
} from "./template-loader.js";
|
|
17
|
+
import {
|
|
18
|
+
formatForTool,
|
|
19
|
+
getOutputPath,
|
|
20
|
+
getToolConfig,
|
|
21
|
+
getSupportedTools,
|
|
22
|
+
formatStageName,
|
|
23
|
+
getStageFileName,
|
|
24
|
+
type TargetTool,
|
|
25
|
+
type DeployFormat,
|
|
26
|
+
} from "./tool-formatter.js";
|
|
27
|
+
|
|
28
|
+
export type Stage = "actions" | "scripts" | "balance" | "rebalance" | "all";
|
|
29
|
+
|
|
30
|
+
export interface ContextBuilderOptions {
|
|
31
|
+
target: TargetTool | "all";
|
|
32
|
+
stage: Stage;
|
|
33
|
+
format: DeployFormat;
|
|
34
|
+
outputDir: string;
|
|
35
|
+
variables?: TemplateVariables;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface DeployResult {
|
|
39
|
+
tool: TargetTool;
|
|
40
|
+
filePath: string;
|
|
41
|
+
stage?: string;
|
|
42
|
+
created: boolean;
|
|
43
|
+
skipped?: boolean;
|
|
44
|
+
error?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface PreviewResult {
|
|
48
|
+
tool: TargetTool;
|
|
49
|
+
stage?: string;
|
|
50
|
+
content: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Build context content for a specific tool and stage.
|
|
55
|
+
*/
|
|
56
|
+
export async function buildContext(
|
|
57
|
+
tool: TargetTool,
|
|
58
|
+
stage: Stage,
|
|
59
|
+
variables?: TemplateVariables
|
|
60
|
+
): Promise<string> {
|
|
61
|
+
const templates: LoadedTemplate[] = [];
|
|
62
|
+
|
|
63
|
+
// Always load base templates
|
|
64
|
+
const baseTemplates = await loadTemplatesByCategory("base");
|
|
65
|
+
templates.push(...baseTemplates);
|
|
66
|
+
|
|
67
|
+
// Load stage-specific templates
|
|
68
|
+
if (stage === "all") {
|
|
69
|
+
const stageTemplates = await loadTemplatesByCategory("stages");
|
|
70
|
+
templates.push(...stageTemplates);
|
|
71
|
+
} else {
|
|
72
|
+
const stageFileName = getStageFileName(stage);
|
|
73
|
+
try {
|
|
74
|
+
const stageTemplate = await loadTemplate("stages", stageFileName);
|
|
75
|
+
templates.push(stageTemplate);
|
|
76
|
+
} catch {
|
|
77
|
+
// Stage template may not exist yet
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Compose templates
|
|
82
|
+
let content = composeTemplates(templates, {
|
|
83
|
+
separator: "\n\n---\n\n",
|
|
84
|
+
includeHeaders: false,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// Interpolate variables
|
|
88
|
+
if (variables) {
|
|
89
|
+
content = interpolateVariables(content, variables);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Format for target tool
|
|
93
|
+
const stageName = stage === "all" ? undefined : formatStageName(stage);
|
|
94
|
+
return formatForTool(tool, content, { stage: stageName });
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Deploy context files to the target directory.
|
|
99
|
+
*/
|
|
100
|
+
export async function deployContext(
|
|
101
|
+
options: ContextBuilderOptions
|
|
102
|
+
): Promise<DeployResult[]> {
|
|
103
|
+
const { target, stage, format, outputDir, variables } = options;
|
|
104
|
+
const results: DeployResult[] = [];
|
|
105
|
+
|
|
106
|
+
// Determine which tools to deploy to
|
|
107
|
+
const tools: TargetTool[] =
|
|
108
|
+
target === "all" ? getSupportedTools() : [target];
|
|
109
|
+
|
|
110
|
+
// Determine which stages to deploy
|
|
111
|
+
const stages: Stage[] =
|
|
112
|
+
stage === "all"
|
|
113
|
+
? ["actions", "scripts", "balance", "rebalance"]
|
|
114
|
+
: [stage];
|
|
115
|
+
|
|
116
|
+
for (const tool of tools) {
|
|
117
|
+
if (format === "combined") {
|
|
118
|
+
// Generate single combined file
|
|
119
|
+
try {
|
|
120
|
+
const content = await buildContext(tool, "all", variables);
|
|
121
|
+
const filePath = getOutputPath(tool, format, outputDir);
|
|
122
|
+
const absolutePath = path.resolve(filePath);
|
|
123
|
+
|
|
124
|
+
// Ensure directory exists
|
|
125
|
+
await fs.mkdir(path.dirname(absolutePath), { recursive: true });
|
|
126
|
+
|
|
127
|
+
// Write file
|
|
128
|
+
await fs.writeFile(absolutePath, content, "utf-8");
|
|
129
|
+
|
|
130
|
+
results.push({
|
|
131
|
+
tool,
|
|
132
|
+
filePath: absolutePath,
|
|
133
|
+
created: true,
|
|
134
|
+
});
|
|
135
|
+
} catch (error) {
|
|
136
|
+
results.push({
|
|
137
|
+
tool,
|
|
138
|
+
filePath: getOutputPath(tool, format, outputDir),
|
|
139
|
+
created: false,
|
|
140
|
+
error: error instanceof Error ? error.message : String(error),
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
} else {
|
|
144
|
+
// Generate split files for each stage
|
|
145
|
+
for (const stg of stages) {
|
|
146
|
+
try {
|
|
147
|
+
const content = await buildContext(tool, stg, variables);
|
|
148
|
+
const stageName = getStageFileName(stg);
|
|
149
|
+
const filePath = getOutputPath(tool, format, outputDir, stageName);
|
|
150
|
+
const absolutePath = path.resolve(filePath);
|
|
151
|
+
|
|
152
|
+
// Ensure directory exists
|
|
153
|
+
await fs.mkdir(path.dirname(absolutePath), { recursive: true });
|
|
154
|
+
|
|
155
|
+
// Write file
|
|
156
|
+
await fs.writeFile(absolutePath, content, "utf-8");
|
|
157
|
+
|
|
158
|
+
results.push({
|
|
159
|
+
tool,
|
|
160
|
+
filePath: absolutePath,
|
|
161
|
+
stage: stg,
|
|
162
|
+
created: true,
|
|
163
|
+
});
|
|
164
|
+
} catch (error) {
|
|
165
|
+
results.push({
|
|
166
|
+
tool,
|
|
167
|
+
filePath: getOutputPath(tool, format, outputDir, stg),
|
|
168
|
+
stage: stg,
|
|
169
|
+
created: false,
|
|
170
|
+
error: error instanceof Error ? error.message : String(error),
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return results;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Preview context content without writing files.
|
|
182
|
+
*/
|
|
183
|
+
export async function previewContext(
|
|
184
|
+
tool: TargetTool,
|
|
185
|
+
stage: Stage,
|
|
186
|
+
variables?: TemplateVariables
|
|
187
|
+
): Promise<PreviewResult> {
|
|
188
|
+
const content = await buildContext(tool, stage, variables);
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
tool,
|
|
192
|
+
stage: stage === "all" ? undefined : stage,
|
|
193
|
+
content,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* List deployed context files in a directory.
|
|
199
|
+
*/
|
|
200
|
+
export async function listDeployedContext(
|
|
201
|
+
outputDir: string
|
|
202
|
+
): Promise<{
|
|
203
|
+
files: Array<{ tool: TargetTool; path: string; exists: boolean }>;
|
|
204
|
+
}> {
|
|
205
|
+
const tools = getSupportedTools();
|
|
206
|
+
const files: Array<{ tool: TargetTool; path: string; exists: boolean }> = [];
|
|
207
|
+
|
|
208
|
+
for (const tool of tools) {
|
|
209
|
+
const config = getToolConfig(tool);
|
|
210
|
+
|
|
211
|
+
// Check combined file
|
|
212
|
+
const combinedPath = path.join(outputDir, config.combinedFile);
|
|
213
|
+
try {
|
|
214
|
+
await fs.access(combinedPath);
|
|
215
|
+
files.push({ tool, path: combinedPath, exists: true });
|
|
216
|
+
} catch {
|
|
217
|
+
files.push({ tool, path: combinedPath, exists: false });
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Check split directory
|
|
221
|
+
const splitDir = path.join(outputDir, config.splitDir);
|
|
222
|
+
try {
|
|
223
|
+
const splitFiles = await fs.readdir(splitDir);
|
|
224
|
+
for (const file of splitFiles) {
|
|
225
|
+
if (file.endsWith(config.fileExtension)) {
|
|
226
|
+
files.push({
|
|
227
|
+
tool,
|
|
228
|
+
path: path.join(splitDir, file),
|
|
229
|
+
exists: true,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
} catch {
|
|
234
|
+
// Split directory doesn't exist
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return { files };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Remove deployed context files.
|
|
243
|
+
*/
|
|
244
|
+
export async function removeContext(
|
|
245
|
+
outputDir: string,
|
|
246
|
+
target: TargetTool | "all"
|
|
247
|
+
): Promise<Array<{ path: string; removed: boolean; error?: string }>> {
|
|
248
|
+
const tools: TargetTool[] =
|
|
249
|
+
target === "all" ? getSupportedTools() : [target];
|
|
250
|
+
const results: Array<{ path: string; removed: boolean; error?: string }> = [];
|
|
251
|
+
|
|
252
|
+
for (const tool of tools) {
|
|
253
|
+
const config = getToolConfig(tool);
|
|
254
|
+
|
|
255
|
+
// Remove combined file
|
|
256
|
+
const combinedPath = path.join(outputDir, config.combinedFile);
|
|
257
|
+
try {
|
|
258
|
+
await fs.unlink(combinedPath);
|
|
259
|
+
results.push({ path: combinedPath, removed: true });
|
|
260
|
+
} catch (error) {
|
|
261
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
|
|
262
|
+
results.push({
|
|
263
|
+
path: combinedPath,
|
|
264
|
+
removed: false,
|
|
265
|
+
error: error instanceof Error ? error.message : String(error),
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Remove split directory
|
|
271
|
+
const splitDir = path.join(outputDir, config.splitDir);
|
|
272
|
+
try {
|
|
273
|
+
await fs.rm(splitDir, { recursive: true });
|
|
274
|
+
results.push({ path: splitDir, removed: true });
|
|
275
|
+
} catch (error) {
|
|
276
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
|
|
277
|
+
results.push({
|
|
278
|
+
path: splitDir,
|
|
279
|
+
removed: false,
|
|
280
|
+
error: error instanceof Error ? error.message : String(error),
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return results;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Get a skill template by name.
|
|
291
|
+
*/
|
|
292
|
+
export async function getSkill(name: string): Promise<{
|
|
293
|
+
name: string;
|
|
294
|
+
content: string;
|
|
295
|
+
} | null> {
|
|
296
|
+
try {
|
|
297
|
+
const template = await loadTemplate("skills", name);
|
|
298
|
+
return { name: template.name, content: template.content };
|
|
299
|
+
} catch {
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* List all available skills.
|
|
306
|
+
*/
|
|
307
|
+
export async function listSkills(): Promise<string[]> {
|
|
308
|
+
const templates = await listTemplates();
|
|
309
|
+
return templates.skills;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Check if templates are available.
|
|
314
|
+
*/
|
|
315
|
+
export async function hasTemplates(): Promise<boolean> {
|
|
316
|
+
const templates = await listTemplates();
|
|
317
|
+
return templates.base.length > 0;
|
|
318
|
+
}
|
package/context/index.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LLM Context Tooling for SceneForge
|
|
3
|
+
*
|
|
4
|
+
* This module provides tools for generating and deploying
|
|
5
|
+
* LLM instruction files for AI coding assistants.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Template loading
|
|
9
|
+
export {
|
|
10
|
+
loadTemplate,
|
|
11
|
+
loadTemplatesByCategory,
|
|
12
|
+
interpolateVariables,
|
|
13
|
+
composeTemplates,
|
|
14
|
+
listTemplates,
|
|
15
|
+
templateExists,
|
|
16
|
+
type LoadedTemplate,
|
|
17
|
+
type TemplateVariables,
|
|
18
|
+
} from "./template-loader.js";
|
|
19
|
+
|
|
20
|
+
// Tool formatting
|
|
21
|
+
export {
|
|
22
|
+
formatForTool,
|
|
23
|
+
getOutputPath,
|
|
24
|
+
getSplitOutputPaths,
|
|
25
|
+
getToolConfig,
|
|
26
|
+
getSupportedTools,
|
|
27
|
+
formatStageName,
|
|
28
|
+
getStageFileName,
|
|
29
|
+
isValidTool,
|
|
30
|
+
isValidFormat,
|
|
31
|
+
TOOL_CONFIGS,
|
|
32
|
+
type TargetTool,
|
|
33
|
+
type DeployFormat,
|
|
34
|
+
type ToolConfig,
|
|
35
|
+
type FormattedOutput,
|
|
36
|
+
} from "./tool-formatter.js";
|
|
37
|
+
|
|
38
|
+
// Context builder
|
|
39
|
+
export {
|
|
40
|
+
buildContext,
|
|
41
|
+
deployContext,
|
|
42
|
+
previewContext,
|
|
43
|
+
listDeployedContext,
|
|
44
|
+
removeContext,
|
|
45
|
+
getSkill,
|
|
46
|
+
listSkills,
|
|
47
|
+
hasTemplates,
|
|
48
|
+
type Stage,
|
|
49
|
+
type ContextBuilderOptions,
|
|
50
|
+
type DeployResult,
|
|
51
|
+
type PreviewResult,
|
|
52
|
+
} from "./context-builder.js";
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Template loading and composition for LLM context files.
|
|
3
|
+
* Loads markdown templates and supports variable interpolation.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as fs from "fs/promises";
|
|
7
|
+
import * as path from "path";
|
|
8
|
+
import { fileURLToPath } from "url";
|
|
9
|
+
|
|
10
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
11
|
+
const __dirname = path.dirname(__filename);
|
|
12
|
+
|
|
13
|
+
export interface TemplateVariables {
|
|
14
|
+
[key: string]: string | number | boolean | undefined;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface LoadedTemplate {
|
|
18
|
+
name: string;
|
|
19
|
+
content: string;
|
|
20
|
+
category: "base" | "stages" | "skills";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Get the templates directory path.
|
|
25
|
+
*/
|
|
26
|
+
function getTemplatesDir(): string {
|
|
27
|
+
return path.join(__dirname, "templates");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Load a single template file by category and name.
|
|
32
|
+
*/
|
|
33
|
+
export async function loadTemplate(
|
|
34
|
+
category: "base" | "stages" | "skills",
|
|
35
|
+
name: string
|
|
36
|
+
): Promise<LoadedTemplate> {
|
|
37
|
+
const templatesDir = getTemplatesDir();
|
|
38
|
+
const filePath = path.join(templatesDir, category, `${name}.md`);
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
42
|
+
return { name, content, category };
|
|
43
|
+
} catch (error) {
|
|
44
|
+
throw new Error(`Failed to load template ${category}/${name}: ${error}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Load all templates from a specific category.
|
|
50
|
+
*/
|
|
51
|
+
export async function loadTemplatesByCategory(
|
|
52
|
+
category: "base" | "stages" | "skills"
|
|
53
|
+
): Promise<LoadedTemplate[]> {
|
|
54
|
+
const templatesDir = getTemplatesDir();
|
|
55
|
+
const categoryDir = path.join(templatesDir, category);
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
const files = await fs.readdir(categoryDir);
|
|
59
|
+
const templates: LoadedTemplate[] = [];
|
|
60
|
+
|
|
61
|
+
for (const file of files) {
|
|
62
|
+
if (file.endsWith(".md")) {
|
|
63
|
+
const name = file.replace(/\.md$/, "");
|
|
64
|
+
const template = await loadTemplate(category, name);
|
|
65
|
+
templates.push(template);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return templates;
|
|
70
|
+
} catch (error) {
|
|
71
|
+
throw new Error(`Failed to load templates from ${category}: ${error}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Interpolate variables in template content.
|
|
77
|
+
* Variables use the format: {{variableName}}
|
|
78
|
+
*/
|
|
79
|
+
export function interpolateVariables(
|
|
80
|
+
content: string,
|
|
81
|
+
variables: TemplateVariables
|
|
82
|
+
): string {
|
|
83
|
+
return content.replace(/\{\{(\w+)\}\}/g, (match, key) => {
|
|
84
|
+
const value = variables[key];
|
|
85
|
+
if (value === undefined) {
|
|
86
|
+
return match; // Leave unmatched variables as-is
|
|
87
|
+
}
|
|
88
|
+
return String(value);
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Compose multiple templates into a single document.
|
|
94
|
+
*/
|
|
95
|
+
export function composeTemplates(
|
|
96
|
+
templates: LoadedTemplate[],
|
|
97
|
+
options?: {
|
|
98
|
+
separator?: string;
|
|
99
|
+
includeHeaders?: boolean;
|
|
100
|
+
}
|
|
101
|
+
): string {
|
|
102
|
+
const { separator = "\n\n---\n\n", includeHeaders = false } = options ?? {};
|
|
103
|
+
|
|
104
|
+
return templates
|
|
105
|
+
.map((template) => {
|
|
106
|
+
if (includeHeaders) {
|
|
107
|
+
return `<!-- Template: ${template.category}/${template.name} -->\n\n${template.content}`;
|
|
108
|
+
}
|
|
109
|
+
return template.content;
|
|
110
|
+
})
|
|
111
|
+
.join(separator);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* List available templates by category.
|
|
116
|
+
*/
|
|
117
|
+
export async function listTemplates(): Promise<{
|
|
118
|
+
base: string[];
|
|
119
|
+
stages: string[];
|
|
120
|
+
skills: string[];
|
|
121
|
+
}> {
|
|
122
|
+
const templatesDir = getTemplatesDir();
|
|
123
|
+
const result: { base: string[]; stages: string[]; skills: string[] } = {
|
|
124
|
+
base: [],
|
|
125
|
+
stages: [],
|
|
126
|
+
skills: [],
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
for (const category of ["base", "stages", "skills"] as const) {
|
|
130
|
+
const categoryDir = path.join(templatesDir, category);
|
|
131
|
+
try {
|
|
132
|
+
const files = await fs.readdir(categoryDir);
|
|
133
|
+
result[category] = files
|
|
134
|
+
.filter((f) => f.endsWith(".md"))
|
|
135
|
+
.map((f) => f.replace(/\.md$/, ""));
|
|
136
|
+
} catch {
|
|
137
|
+
// Directory may not exist yet
|
|
138
|
+
result[category] = [];
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return result;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Check if a template exists.
|
|
147
|
+
*/
|
|
148
|
+
export async function templateExists(
|
|
149
|
+
category: "base" | "stages" | "skills",
|
|
150
|
+
name: string
|
|
151
|
+
): Promise<boolean> {
|
|
152
|
+
const templatesDir = getTemplatesDir();
|
|
153
|
+
const filePath = path.join(templatesDir, category, `${name}.md`);
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
await fs.access(filePath);
|
|
157
|
+
return true;
|
|
158
|
+
} catch {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
}
|