@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,791 @@
|
|
|
1
|
+
import * as path from "path";
|
|
2
|
+
import * as fs from "fs/promises";
|
|
3
|
+
import * as readline from "readline";
|
|
4
|
+
import { hasFlag, getFlagValue, getFlagValueOrDefault } from "../utils/args.js";
|
|
5
|
+
import { resolveRoot } from "../utils/paths.js";
|
|
6
|
+
|
|
7
|
+
// ANSI color codes for terminal styling
|
|
8
|
+
const colors = {
|
|
9
|
+
reset: "\x1b[0m",
|
|
10
|
+
bright: "\x1b[1m",
|
|
11
|
+
dim: "\x1b[2m",
|
|
12
|
+
cyan: "\x1b[36m",
|
|
13
|
+
green: "\x1b[32m",
|
|
14
|
+
yellow: "\x1b[33m",
|
|
15
|
+
blue: "\x1b[34m",
|
|
16
|
+
magenta: "\x1b[35m",
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function printHelp() {
|
|
20
|
+
console.log(`
|
|
21
|
+
Manage LLM context files for AI coding assistants
|
|
22
|
+
|
|
23
|
+
Usage:
|
|
24
|
+
sceneforge context <subcommand> [options]
|
|
25
|
+
|
|
26
|
+
Subcommands:
|
|
27
|
+
deploy Deploy context files to target directory (interactive by default)
|
|
28
|
+
list List deployed context files
|
|
29
|
+
remove Remove deployed context files
|
|
30
|
+
preview Preview context content
|
|
31
|
+
skill Manage skills
|
|
32
|
+
|
|
33
|
+
Run "sceneforge context <subcommand> --help" for subcommand options.
|
|
34
|
+
`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function printDeployHelp() {
|
|
38
|
+
console.log(`
|
|
39
|
+
Deploy LLM context files
|
|
40
|
+
|
|
41
|
+
Usage:
|
|
42
|
+
sceneforge context deploy [options]
|
|
43
|
+
|
|
44
|
+
When run without options, an interactive wizard guides you through the setup.
|
|
45
|
+
|
|
46
|
+
Options:
|
|
47
|
+
--target <tool> Target tool: cursor, copilot, claude, codex, all (default: all)
|
|
48
|
+
--stage <stage> Stage: actions, scripts, balance, rebalance, all (default: all)
|
|
49
|
+
--output <path> Target directory (default: cwd)
|
|
50
|
+
--format <type> Format: combined, split (default: combined)
|
|
51
|
+
--force Overwrite without prompting
|
|
52
|
+
--dry-run Preview without writing files
|
|
53
|
+
--no-interactive Skip interactive wizard even if no options provided
|
|
54
|
+
--help, -h Show this help message
|
|
55
|
+
|
|
56
|
+
Examples:
|
|
57
|
+
sceneforge context deploy # Interactive wizard
|
|
58
|
+
sceneforge context deploy --target claude # Deploy for Claude only
|
|
59
|
+
sceneforge context deploy --format split # Create separate files per stage
|
|
60
|
+
`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function printListHelp() {
|
|
64
|
+
console.log(`
|
|
65
|
+
List deployed context files
|
|
66
|
+
|
|
67
|
+
Usage:
|
|
68
|
+
sceneforge context list [options]
|
|
69
|
+
|
|
70
|
+
Options:
|
|
71
|
+
--output <path> Directory to check (default: cwd)
|
|
72
|
+
--json Output as JSON
|
|
73
|
+
--help, -h Show this help message
|
|
74
|
+
`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function printRemoveHelp() {
|
|
78
|
+
console.log(`
|
|
79
|
+
Remove deployed context files
|
|
80
|
+
|
|
81
|
+
Usage:
|
|
82
|
+
sceneforge context remove [options]
|
|
83
|
+
|
|
84
|
+
Options:
|
|
85
|
+
--target <tool> Target to remove: cursor, copilot, claude, codex, all (default: all)
|
|
86
|
+
--output <path> Directory to remove from (default: cwd)
|
|
87
|
+
--force Skip confirmation
|
|
88
|
+
--help, -h Show this help message
|
|
89
|
+
`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function printPreviewHelp() {
|
|
93
|
+
console.log(`
|
|
94
|
+
Preview context content
|
|
95
|
+
|
|
96
|
+
Usage:
|
|
97
|
+
sceneforge context preview [options]
|
|
98
|
+
|
|
99
|
+
Options:
|
|
100
|
+
--target <tool> Target tool: cursor, copilot, claude, codex (required)
|
|
101
|
+
--stage <stage> Stage: actions, scripts, balance, rebalance, all (default: all)
|
|
102
|
+
--help, -h Show this help message
|
|
103
|
+
|
|
104
|
+
Examples:
|
|
105
|
+
sceneforge context preview --target claude
|
|
106
|
+
sceneforge context preview --target cursor --stage actions
|
|
107
|
+
`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function printSkillHelp() {
|
|
111
|
+
console.log(`
|
|
112
|
+
Manage skills
|
|
113
|
+
|
|
114
|
+
Usage:
|
|
115
|
+
sceneforge context skill [options]
|
|
116
|
+
|
|
117
|
+
Options:
|
|
118
|
+
--list List available skills
|
|
119
|
+
--show <name> Display skill content
|
|
120
|
+
--copy <name> Copy skill to clipboard (requires pbcopy/xclip)
|
|
121
|
+
--output <path> Write skill to file
|
|
122
|
+
--help, -h Show this help message
|
|
123
|
+
|
|
124
|
+
Examples:
|
|
125
|
+
sceneforge context skill --list
|
|
126
|
+
sceneforge context skill --show generate-actions
|
|
127
|
+
sceneforge context skill --show debug-selector --output ./skill.md
|
|
128
|
+
`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Interactive prompt utilities
|
|
132
|
+
function createPrompt() {
|
|
133
|
+
const rl = readline.createInterface({
|
|
134
|
+
input: process.stdin,
|
|
135
|
+
output: process.stdout,
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
async question(prompt) {
|
|
140
|
+
return new Promise((resolve) => {
|
|
141
|
+
rl.question(prompt, (answer) => {
|
|
142
|
+
resolve(answer.trim());
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
},
|
|
146
|
+
async select(prompt, options, defaultIndex = 0) {
|
|
147
|
+
console.log(`\n${colors.cyan}${prompt}${colors.reset}\n`);
|
|
148
|
+
|
|
149
|
+
options.forEach((opt, i) => {
|
|
150
|
+
const marker = i === defaultIndex ? `${colors.green}→${colors.reset}` : " ";
|
|
151
|
+
const label = i === defaultIndex ? `${colors.bright}${opt.label}${colors.reset}` : opt.label;
|
|
152
|
+
console.log(` ${marker} ${i + 1}) ${label}`);
|
|
153
|
+
if (opt.description) {
|
|
154
|
+
console.log(` ${colors.dim}${opt.description}${colors.reset}`);
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
const answer = await this.question(`\n${colors.dim}Enter choice [1-${options.length}] (default: ${defaultIndex + 1}):${colors.reset} `);
|
|
159
|
+
|
|
160
|
+
if (!answer) return options[defaultIndex].value;
|
|
161
|
+
|
|
162
|
+
const index = parseInt(answer, 10) - 1;
|
|
163
|
+
if (index >= 0 && index < options.length) {
|
|
164
|
+
return options[index].value;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
console.log(`${colors.yellow}Invalid choice, using default.${colors.reset}`);
|
|
168
|
+
return options[defaultIndex].value;
|
|
169
|
+
},
|
|
170
|
+
async multiSelect(prompt, options) {
|
|
171
|
+
console.log(`\n${colors.cyan}${prompt}${colors.reset}`);
|
|
172
|
+
console.log(`${colors.dim}(Enter numbers separated by commas, or 'all')${colors.reset}\n`);
|
|
173
|
+
|
|
174
|
+
options.forEach((opt, i) => {
|
|
175
|
+
console.log(` ${i + 1}) ${opt.label}`);
|
|
176
|
+
if (opt.description) {
|
|
177
|
+
console.log(` ${colors.dim}${opt.description}${colors.reset}`);
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
const answer = await this.question(`\n${colors.dim}Enter choices (default: all):${colors.reset} `);
|
|
182
|
+
|
|
183
|
+
if (!answer || answer.toLowerCase() === 'all') {
|
|
184
|
+
return options.map(o => o.value);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const indices = answer.split(',').map(s => parseInt(s.trim(), 10) - 1);
|
|
188
|
+
const selected = indices
|
|
189
|
+
.filter(i => i >= 0 && i < options.length)
|
|
190
|
+
.map(i => options[i].value);
|
|
191
|
+
|
|
192
|
+
return selected.length > 0 ? selected : options.map(o => o.value);
|
|
193
|
+
},
|
|
194
|
+
async confirm(prompt, defaultYes = true) {
|
|
195
|
+
const hint = defaultYes ? "[Y/n]" : "[y/N]";
|
|
196
|
+
const answer = await this.question(`${colors.cyan}${prompt}${colors.reset} ${colors.dim}${hint}${colors.reset} `);
|
|
197
|
+
|
|
198
|
+
if (!answer) return defaultYes;
|
|
199
|
+
return answer.toLowerCase().startsWith('y');
|
|
200
|
+
},
|
|
201
|
+
async input(prompt, defaultValue = "") {
|
|
202
|
+
const hint = defaultValue ? ` ${colors.dim}(default: ${defaultValue})${colors.reset}` : "";
|
|
203
|
+
const answer = await this.question(`${colors.cyan}${prompt}${colors.reset}${hint}: `);
|
|
204
|
+
return answer || defaultValue;
|
|
205
|
+
},
|
|
206
|
+
close() {
|
|
207
|
+
rl.close();
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async function runInteractiveDeployWizard() {
|
|
213
|
+
const prompt = createPrompt();
|
|
214
|
+
|
|
215
|
+
console.log(`
|
|
216
|
+
${colors.bright}${colors.blue}╔════════════════════════════════════════════════════════════╗
|
|
217
|
+
║ SceneForge LLM Context Deployment ║
|
|
218
|
+
╚════════════════════════════════════════════════════════════╝${colors.reset}
|
|
219
|
+
|
|
220
|
+
This wizard will help you deploy context files for AI coding assistants.
|
|
221
|
+
These files help AI tools like Cursor, GitHub Copilot, and Claude Code
|
|
222
|
+
understand SceneForge and assist you in creating demos.
|
|
223
|
+
`);
|
|
224
|
+
|
|
225
|
+
try {
|
|
226
|
+
// Step 1: Select target tools
|
|
227
|
+
const targetOptions = [
|
|
228
|
+
{ value: "all", label: "All tools", description: "Deploy for Cursor, Copilot, Claude Code, and Codex" },
|
|
229
|
+
{ value: "claude", label: "Claude Code", description: "Deploy CLAUDE.md for Claude Code CLI" },
|
|
230
|
+
{ value: "cursor", label: "Cursor", description: "Deploy .cursorrules for Cursor IDE" },
|
|
231
|
+
{ value: "copilot", label: "GitHub Copilot", description: "Deploy .github/copilot-instructions.md" },
|
|
232
|
+
{ value: "codex", label: "Codex", description: "Deploy AGENTS.md for OpenAI Codex" },
|
|
233
|
+
];
|
|
234
|
+
|
|
235
|
+
const target = await prompt.select(
|
|
236
|
+
"Which AI coding tool(s) would you like to configure?",
|
|
237
|
+
targetOptions,
|
|
238
|
+
0
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
// Step 2: Select stage focus
|
|
242
|
+
const stageOptions = [
|
|
243
|
+
{ value: "all", label: "All stages (recommended)", description: "Complete context for all demo creation phases" },
|
|
244
|
+
{ value: "actions", label: "Stage 1: Action Generation", description: "Playwright actions, selectors, testing" },
|
|
245
|
+
{ value: "scripts", label: "Stage 2: Script Writing", description: "Voiceover scripts, timing, voice synthesis" },
|
|
246
|
+
{ value: "balance", label: "Stage 3: Step Balancing", description: "Align script duration with action timing" },
|
|
247
|
+
{ value: "rebalance", label: "Stage 4: Rebalancing", description: "Post-audio adjustment cycle" },
|
|
248
|
+
];
|
|
249
|
+
|
|
250
|
+
const stage = await prompt.select(
|
|
251
|
+
"Which stage context would you like to include?",
|
|
252
|
+
stageOptions,
|
|
253
|
+
0
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
// Step 3: Select format
|
|
257
|
+
const formatOptions = [
|
|
258
|
+
{ value: "combined", label: "Combined (recommended)", description: "Single file per tool with all context" },
|
|
259
|
+
{ value: "split", label: "Split", description: "Separate files per stage for modular use" },
|
|
260
|
+
];
|
|
261
|
+
|
|
262
|
+
const format = await prompt.select(
|
|
263
|
+
"How should the context files be organized?",
|
|
264
|
+
formatOptions,
|
|
265
|
+
0
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
// Step 4: Output directory
|
|
269
|
+
const defaultOutput = process.cwd();
|
|
270
|
+
const output = await prompt.input(
|
|
271
|
+
"Where should the files be deployed?",
|
|
272
|
+
defaultOutput
|
|
273
|
+
);
|
|
274
|
+
const outputDir = resolveRoot(output);
|
|
275
|
+
|
|
276
|
+
// Summary
|
|
277
|
+
console.log(`
|
|
278
|
+
${colors.bright}${colors.blue}═══════════════════════════════════════════════════════════${colors.reset}
|
|
279
|
+
${colors.bright}Deployment Summary${colors.reset}
|
|
280
|
+
${colors.blue}═══════════════════════════════════════════════════════════${colors.reset}
|
|
281
|
+
|
|
282
|
+
${colors.cyan}Target:${colors.reset} ${target === "all" ? "All tools (Cursor, Copilot, Claude, Codex)" : target}
|
|
283
|
+
${colors.cyan}Stage:${colors.reset} ${stage === "all" ? "All stages" : stage}
|
|
284
|
+
${colors.cyan}Format:${colors.reset} ${format}
|
|
285
|
+
${colors.cyan}Directory:${colors.reset} ${outputDir}
|
|
286
|
+
`);
|
|
287
|
+
|
|
288
|
+
// Show what files will be created
|
|
289
|
+
const { deployContext, getToolConfig, getSupportedTools } = await import(
|
|
290
|
+
"../../dist/index.js"
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
const tools = target === "all" ? getSupportedTools() : [target];
|
|
294
|
+
|
|
295
|
+
console.log(`${colors.bright}Files to be created:${colors.reset}\n`);
|
|
296
|
+
for (const tool of tools) {
|
|
297
|
+
const config = getToolConfig(tool);
|
|
298
|
+
if (format === "combined") {
|
|
299
|
+
console.log(` ${colors.green}•${colors.reset} ${config.combinedFile} ${colors.dim}(${config.name})${colors.reset}`);
|
|
300
|
+
} else {
|
|
301
|
+
console.log(` ${colors.green}•${colors.reset} ${config.splitDir}/ ${colors.dim}(${config.name})${colors.reset}`);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
console.log("");
|
|
305
|
+
|
|
306
|
+
// Confirm
|
|
307
|
+
const confirmed = await prompt.confirm("Deploy these context files?", true);
|
|
308
|
+
|
|
309
|
+
if (!confirmed) {
|
|
310
|
+
console.log(`\n${colors.yellow}Deployment cancelled.${colors.reset}\n`);
|
|
311
|
+
prompt.close();
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Deploy
|
|
316
|
+
console.log(`\n${colors.dim}Deploying...${colors.reset}\n`);
|
|
317
|
+
|
|
318
|
+
const results = await deployContext({
|
|
319
|
+
target,
|
|
320
|
+
stage,
|
|
321
|
+
format,
|
|
322
|
+
outputDir,
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
let successCount = 0;
|
|
326
|
+
let errorCount = 0;
|
|
327
|
+
|
|
328
|
+
for (const result of results) {
|
|
329
|
+
const relativePath = path.relative(outputDir, result.filePath);
|
|
330
|
+
if (result.created) {
|
|
331
|
+
console.log(` ${colors.green}✓${colors.reset} ${relativePath}`);
|
|
332
|
+
successCount++;
|
|
333
|
+
} else if (result.error) {
|
|
334
|
+
console.log(` ${colors.yellow}✗${colors.reset} ${relativePath}: ${result.error}`);
|
|
335
|
+
errorCount++;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
console.log(`
|
|
340
|
+
${colors.bright}${colors.green}═══════════════════════════════════════════════════════════${colors.reset}
|
|
341
|
+
${colors.green}✓ Deployment complete!${colors.reset} ${successCount} file(s) created.
|
|
342
|
+
${colors.green}═══════════════════════════════════════════════════════════${colors.reset}
|
|
343
|
+
|
|
344
|
+
${colors.bright}Next steps:${colors.reset}
|
|
345
|
+
1. Open your project in your AI coding tool
|
|
346
|
+
2. The tool will automatically read the context files
|
|
347
|
+
3. Ask the AI to help you create or modify SceneForge demos
|
|
348
|
+
|
|
349
|
+
${colors.dim}Tip: Run "sceneforge context skill --list" to see available skills.${colors.reset}
|
|
350
|
+
`);
|
|
351
|
+
|
|
352
|
+
if (errorCount > 0) {
|
|
353
|
+
process.exitCode = 1;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
prompt.close();
|
|
357
|
+
} catch (error) {
|
|
358
|
+
prompt.close();
|
|
359
|
+
throw error;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
async function runDeployCommand(args) {
|
|
364
|
+
const help = hasFlag(args, "--help") || hasFlag(args, "-h");
|
|
365
|
+
if (help) {
|
|
366
|
+
printDeployHelp();
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const noInteractive = hasFlag(args, "--no-interactive");
|
|
371
|
+
const hasOptions = args.some(arg =>
|
|
372
|
+
arg.startsWith("--target") ||
|
|
373
|
+
arg.startsWith("--stage") ||
|
|
374
|
+
arg.startsWith("--format") ||
|
|
375
|
+
arg.startsWith("--output") ||
|
|
376
|
+
arg === "--dry-run" ||
|
|
377
|
+
arg === "--force"
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
// Run interactive wizard if no options provided
|
|
381
|
+
if (!hasOptions && !noInteractive && process.stdin.isTTY) {
|
|
382
|
+
await runInteractiveDeployWizard();
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// CLI mode
|
|
387
|
+
const target = getFlagValueOrDefault(args, "--target", "all");
|
|
388
|
+
const stage = getFlagValueOrDefault(args, "--stage", "all");
|
|
389
|
+
const output = getFlagValue(args, "--output");
|
|
390
|
+
const format = getFlagValueOrDefault(args, "--format", "combined");
|
|
391
|
+
const force = hasFlag(args, "--force");
|
|
392
|
+
const dryRun = hasFlag(args, "--dry-run");
|
|
393
|
+
|
|
394
|
+
const outputDir = resolveRoot(output);
|
|
395
|
+
|
|
396
|
+
// Dynamic import of context module
|
|
397
|
+
const { deployContext, isValidTool, isValidFormat, getSupportedTools } = await import(
|
|
398
|
+
"../../dist/index.js"
|
|
399
|
+
);
|
|
400
|
+
|
|
401
|
+
// Validate target
|
|
402
|
+
if (target !== "all" && !isValidTool(target)) {
|
|
403
|
+
console.error(`[error] Invalid target: ${target}`);
|
|
404
|
+
console.error(`Valid targets: ${getSupportedTools().join(", ")}, all`);
|
|
405
|
+
process.exit(1);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Validate format
|
|
409
|
+
if (!isValidFormat(format)) {
|
|
410
|
+
console.error(`[error] Invalid format: ${format}`);
|
|
411
|
+
console.error("Valid formats: combined, split");
|
|
412
|
+
process.exit(1);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Validate stage
|
|
416
|
+
const validStages = ["actions", "scripts", "balance", "rebalance", "all"];
|
|
417
|
+
if (!validStages.includes(stage)) {
|
|
418
|
+
console.error(`[error] Invalid stage: ${stage}`);
|
|
419
|
+
console.error(`Valid stages: ${validStages.join(", ")}`);
|
|
420
|
+
process.exit(1);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
console.log(`\n[context] Deploying LLM context files`);
|
|
424
|
+
console.log(`[context] Target: ${target}`);
|
|
425
|
+
console.log(`[context] Stage: ${stage}`);
|
|
426
|
+
console.log(`[context] Format: ${format}`);
|
|
427
|
+
console.log(`[context] Output: ${outputDir}`);
|
|
428
|
+
|
|
429
|
+
if (dryRun) {
|
|
430
|
+
console.log(`[context] Dry run mode - no files will be written\n`);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
try {
|
|
434
|
+
const results = await deployContext({
|
|
435
|
+
target,
|
|
436
|
+
stage,
|
|
437
|
+
format,
|
|
438
|
+
outputDir,
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
if (dryRun) {
|
|
442
|
+
console.log(`\n[context] Would create the following files:`);
|
|
443
|
+
for (const result of results) {
|
|
444
|
+
if (result.created || !result.error) {
|
|
445
|
+
const relativePath = path.relative(outputDir, result.filePath);
|
|
446
|
+
console.log(` - ${relativePath}`);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
console.log(`\n[context] Deployment results:`);
|
|
453
|
+
let successCount = 0;
|
|
454
|
+
let errorCount = 0;
|
|
455
|
+
|
|
456
|
+
for (const result of results) {
|
|
457
|
+
const relativePath = path.relative(outputDir, result.filePath);
|
|
458
|
+
if (result.created) {
|
|
459
|
+
console.log(` ✓ ${relativePath}`);
|
|
460
|
+
successCount++;
|
|
461
|
+
} else if (result.error) {
|
|
462
|
+
console.log(` ✗ ${relativePath}: ${result.error}`);
|
|
463
|
+
errorCount++;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
console.log(`\n[context] Deployed ${successCount} file(s)`);
|
|
468
|
+
if (errorCount > 0) {
|
|
469
|
+
console.log(`[context] ${errorCount} error(s)`);
|
|
470
|
+
process.exitCode = 1;
|
|
471
|
+
}
|
|
472
|
+
} catch (error) {
|
|
473
|
+
console.error(`[error] Failed to deploy context: ${error.message}`);
|
|
474
|
+
process.exit(1);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
async function runListCommand(args) {
|
|
479
|
+
const help = hasFlag(args, "--help") || hasFlag(args, "-h");
|
|
480
|
+
if (help) {
|
|
481
|
+
printListHelp();
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
const output = getFlagValue(args, "--output");
|
|
486
|
+
const asJson = hasFlag(args, "--json");
|
|
487
|
+
|
|
488
|
+
const outputDir = resolveRoot(output);
|
|
489
|
+
|
|
490
|
+
const { listDeployedContext } = await import("../../dist/index.js");
|
|
491
|
+
|
|
492
|
+
try {
|
|
493
|
+
const { files } = await listDeployedContext(outputDir);
|
|
494
|
+
|
|
495
|
+
const existingFiles = files.filter((f) => f.exists);
|
|
496
|
+
|
|
497
|
+
if (asJson) {
|
|
498
|
+
console.log(JSON.stringify({ outputDir, files: existingFiles }, null, 2));
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
console.log(`\n[context] Deployed context files in ${outputDir}\n`);
|
|
503
|
+
|
|
504
|
+
if (existingFiles.length === 0) {
|
|
505
|
+
console.log(" No context files found.\n");
|
|
506
|
+
console.log(' Run "sceneforge context deploy" to create context files.\n');
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
for (const file of existingFiles) {
|
|
511
|
+
const relativePath = path.relative(outputDir, file.path);
|
|
512
|
+
console.log(` [${file.tool}] ${relativePath}`);
|
|
513
|
+
}
|
|
514
|
+
console.log(`\n Total: ${existingFiles.length} file(s)\n`);
|
|
515
|
+
} catch (error) {
|
|
516
|
+
console.error(`[error] Failed to list context: ${error.message}`);
|
|
517
|
+
process.exit(1);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
async function runRemoveCommand(args) {
|
|
522
|
+
const help = hasFlag(args, "--help") || hasFlag(args, "-h");
|
|
523
|
+
if (help) {
|
|
524
|
+
printRemoveHelp();
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
const target = getFlagValueOrDefault(args, "--target", "all");
|
|
529
|
+
const output = getFlagValue(args, "--output");
|
|
530
|
+
const force = hasFlag(args, "--force");
|
|
531
|
+
|
|
532
|
+
const outputDir = resolveRoot(output);
|
|
533
|
+
|
|
534
|
+
const { removeContext, isValidTool, getSupportedTools } = await import(
|
|
535
|
+
"../../dist/index.js"
|
|
536
|
+
);
|
|
537
|
+
|
|
538
|
+
// Validate target
|
|
539
|
+
if (target !== "all" && !isValidTool(target)) {
|
|
540
|
+
console.error(`[error] Invalid target: ${target}`);
|
|
541
|
+
console.error(`Valid targets: ${getSupportedTools().join(", ")}, all`);
|
|
542
|
+
process.exit(1);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
if (!force) {
|
|
546
|
+
console.log(`\n[context] This will remove context files for: ${target}`);
|
|
547
|
+
console.log(`[context] Directory: ${outputDir}`);
|
|
548
|
+
console.log(`[context] Use --force to skip this confirmation.\n`);
|
|
549
|
+
// In a real implementation, we'd prompt for confirmation
|
|
550
|
+
// For simplicity, we'll require --force
|
|
551
|
+
console.log('[context] Aborted. Use --force to confirm removal.');
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
console.log(`\n[context] Removing context files for: ${target}`);
|
|
556
|
+
|
|
557
|
+
try {
|
|
558
|
+
const results = await removeContext(outputDir, target);
|
|
559
|
+
|
|
560
|
+
let removedCount = 0;
|
|
561
|
+
for (const result of results) {
|
|
562
|
+
if (result.removed) {
|
|
563
|
+
const relativePath = path.relative(outputDir, result.path);
|
|
564
|
+
console.log(` ✓ Removed: ${relativePath}`);
|
|
565
|
+
removedCount++;
|
|
566
|
+
} else if (result.error) {
|
|
567
|
+
console.log(` ✗ Error: ${result.path}: ${result.error}`);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
if (removedCount === 0) {
|
|
572
|
+
console.log(`\n[context] No files were removed.`);
|
|
573
|
+
} else {
|
|
574
|
+
console.log(`\n[context] Removed ${removedCount} file(s)/directory(ies).`);
|
|
575
|
+
}
|
|
576
|
+
} catch (error) {
|
|
577
|
+
console.error(`[error] Failed to remove context: ${error.message}`);
|
|
578
|
+
process.exit(1);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
async function runPreviewCommand(args) {
|
|
583
|
+
const help = hasFlag(args, "--help") || hasFlag(args, "-h");
|
|
584
|
+
if (help) {
|
|
585
|
+
printPreviewHelp();
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
const target = getFlagValue(args, "--target");
|
|
590
|
+
const stage = getFlagValueOrDefault(args, "--stage", "all");
|
|
591
|
+
|
|
592
|
+
if (!target) {
|
|
593
|
+
console.error("[error] --target is required for preview");
|
|
594
|
+
printPreviewHelp();
|
|
595
|
+
process.exit(1);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
const { previewContext, isValidTool, getSupportedTools } = await import(
|
|
599
|
+
"../../dist/index.js"
|
|
600
|
+
);
|
|
601
|
+
|
|
602
|
+
// Validate target
|
|
603
|
+
if (!isValidTool(target)) {
|
|
604
|
+
console.error(`[error] Invalid target: ${target}`);
|
|
605
|
+
console.error(`Valid targets: ${getSupportedTools().join(", ")}`);
|
|
606
|
+
process.exit(1);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// Validate stage
|
|
610
|
+
const validStages = ["actions", "scripts", "balance", "rebalance", "all"];
|
|
611
|
+
if (!validStages.includes(stage)) {
|
|
612
|
+
console.error(`[error] Invalid stage: ${stage}`);
|
|
613
|
+
console.error(`Valid stages: ${validStages.join(", ")}`);
|
|
614
|
+
process.exit(1);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
try {
|
|
618
|
+
const result = await previewContext(target, stage);
|
|
619
|
+
|
|
620
|
+
console.log(`\n${"=".repeat(60)}`);
|
|
621
|
+
console.log(`Preview: ${target}${stage !== "all" ? ` (${stage})` : ""}`);
|
|
622
|
+
console.log(`${"=".repeat(60)}\n`);
|
|
623
|
+
console.log(result.content);
|
|
624
|
+
console.log(`\n${"=".repeat(60)}\n`);
|
|
625
|
+
} catch (error) {
|
|
626
|
+
console.error(`[error] Failed to preview context: ${error.message}`);
|
|
627
|
+
process.exit(1);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
async function runSkillCommand(args) {
|
|
632
|
+
const help = hasFlag(args, "--help") || hasFlag(args, "-h");
|
|
633
|
+
if (help) {
|
|
634
|
+
printSkillHelp();
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
const list = hasFlag(args, "--list");
|
|
639
|
+
const show = getFlagValue(args, "--show");
|
|
640
|
+
const copy = getFlagValue(args, "--copy");
|
|
641
|
+
const output = getFlagValue(args, "--output");
|
|
642
|
+
|
|
643
|
+
const { listSkills, getSkill } = await import("../../dist/index.js");
|
|
644
|
+
|
|
645
|
+
if (list) {
|
|
646
|
+
try {
|
|
647
|
+
const skills = await listSkills();
|
|
648
|
+
|
|
649
|
+
console.log(`\n${colors.bright}Available SceneForge Skills${colors.reset}\n`);
|
|
650
|
+
if (skills.length === 0) {
|
|
651
|
+
console.log(" No skills found.\n");
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
const skillDescriptions = {
|
|
656
|
+
"generate-actions": "Generate demo actions for a web page",
|
|
657
|
+
"write-step-script": "Write voiceover script for actions",
|
|
658
|
+
"balance-timing": "Analyze and balance step timing",
|
|
659
|
+
"review-demo-yaml": "Review and improve a demo definition",
|
|
660
|
+
"debug-selector": "Debug why a selector isn't working",
|
|
661
|
+
"optimize-demo": "Optimize a demo for better flow",
|
|
662
|
+
};
|
|
663
|
+
|
|
664
|
+
for (const skill of skills) {
|
|
665
|
+
const desc = skillDescriptions[skill] || "";
|
|
666
|
+
console.log(` ${colors.cyan}${skill}${colors.reset}`);
|
|
667
|
+
if (desc) {
|
|
668
|
+
console.log(` ${colors.dim}${desc}${colors.reset}`);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
console.log(`\n Use "${colors.bright}sceneforge context skill --show <name>${colors.reset}" to view a skill.\n`);
|
|
672
|
+
} catch (error) {
|
|
673
|
+
console.error(`[error] Failed to list skills: ${error.message}`);
|
|
674
|
+
process.exit(1);
|
|
675
|
+
}
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
const skillName = show || copy;
|
|
680
|
+
if (!skillName) {
|
|
681
|
+
console.error("[error] Specify --list, --show <name>, or --copy <name>");
|
|
682
|
+
printSkillHelp();
|
|
683
|
+
process.exit(1);
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
try {
|
|
687
|
+
const skill = await getSkill(skillName);
|
|
688
|
+
|
|
689
|
+
if (!skill) {
|
|
690
|
+
console.error(`[error] Skill not found: ${skillName}`);
|
|
691
|
+
const skills = await listSkills();
|
|
692
|
+
console.error(`Available skills: ${skills.join(", ")}`);
|
|
693
|
+
process.exit(1);
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
if (output) {
|
|
697
|
+
const outputPath = path.resolve(output);
|
|
698
|
+
await fs.writeFile(outputPath, skill.content, "utf-8");
|
|
699
|
+
console.log(`[context] Skill written to: ${outputPath}`);
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
if (copy) {
|
|
704
|
+
// Try to copy to clipboard using pbcopy (macOS) or xclip (Linux)
|
|
705
|
+
const { exec } = await import("child_process");
|
|
706
|
+
const { promisify } = await import("util");
|
|
707
|
+
const execAsync = promisify(exec);
|
|
708
|
+
|
|
709
|
+
const platform = process.platform;
|
|
710
|
+
let copyCommand;
|
|
711
|
+
|
|
712
|
+
if (platform === "darwin") {
|
|
713
|
+
copyCommand = "pbcopy";
|
|
714
|
+
} else if (platform === "linux") {
|
|
715
|
+
copyCommand = "xclip -selection clipboard";
|
|
716
|
+
} else if (platform === "win32") {
|
|
717
|
+
copyCommand = "clip";
|
|
718
|
+
} else {
|
|
719
|
+
console.error(`[error] Clipboard not supported on ${platform}`);
|
|
720
|
+
console.log("\nSkill content:\n");
|
|
721
|
+
console.log(skill.content);
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
try {
|
|
726
|
+
const child = exec(copyCommand);
|
|
727
|
+
child.stdin.write(skill.content);
|
|
728
|
+
child.stdin.end();
|
|
729
|
+
await new Promise((resolve, reject) => {
|
|
730
|
+
child.on("close", (code) => {
|
|
731
|
+
if (code === 0) resolve();
|
|
732
|
+
else reject(new Error(`Copy command failed with code ${code}`));
|
|
733
|
+
});
|
|
734
|
+
});
|
|
735
|
+
console.log(`${colors.green}✓${colors.reset} Skill "${skillName}" copied to clipboard.`);
|
|
736
|
+
} catch (copyError) {
|
|
737
|
+
console.error(`[error] Failed to copy to clipboard: ${copyError.message}`);
|
|
738
|
+
console.log("\nSkill content:\n");
|
|
739
|
+
console.log(skill.content);
|
|
740
|
+
}
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
// Show skill content
|
|
745
|
+
console.log(`\n${colors.blue}${"═".repeat(60)}${colors.reset}`);
|
|
746
|
+
console.log(`${colors.bright}Skill: ${skill.name}${colors.reset}`);
|
|
747
|
+
console.log(`${colors.blue}${"═".repeat(60)}${colors.reset}\n`);
|
|
748
|
+
console.log(skill.content);
|
|
749
|
+
console.log(`\n${colors.blue}${"═".repeat(60)}${colors.reset}\n`);
|
|
750
|
+
} catch (error) {
|
|
751
|
+
console.error(`[error] Failed to get skill: ${error.message}`);
|
|
752
|
+
process.exit(1);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
export async function runContextCommand(argv) {
|
|
757
|
+
const args = argv ?? process.argv.slice(2);
|
|
758
|
+
|
|
759
|
+
// Get subcommand (first arg after "context")
|
|
760
|
+
const subcommand = args[0];
|
|
761
|
+
const subArgs = args.slice(1);
|
|
762
|
+
|
|
763
|
+
if (!subcommand || subcommand === "help" || subcommand === "--help" || subcommand === "-h") {
|
|
764
|
+
printHelp();
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
switch (subcommand.toLowerCase()) {
|
|
769
|
+
case "deploy":
|
|
770
|
+
await runDeployCommand(subArgs);
|
|
771
|
+
break;
|
|
772
|
+
case "list":
|
|
773
|
+
await runListCommand(subArgs);
|
|
774
|
+
break;
|
|
775
|
+
case "remove":
|
|
776
|
+
case "rm":
|
|
777
|
+
await runRemoveCommand(subArgs);
|
|
778
|
+
break;
|
|
779
|
+
case "preview":
|
|
780
|
+
await runPreviewCommand(subArgs);
|
|
781
|
+
break;
|
|
782
|
+
case "skill":
|
|
783
|
+
case "skills":
|
|
784
|
+
await runSkillCommand(subArgs);
|
|
785
|
+
break;
|
|
786
|
+
default:
|
|
787
|
+
console.error(`[error] Unknown subcommand: ${subcommand}`);
|
|
788
|
+
printHelp();
|
|
789
|
+
process.exit(1);
|
|
790
|
+
}
|
|
791
|
+
}
|