@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.
Files changed (35) hide show
  1. package/README.md +57 -0
  2. package/cli/cli.js +6 -0
  3. package/cli/commands/add-audio-to-steps.js +9 -3
  4. package/cli/commands/concat-final-videos.js +6 -2
  5. package/cli/commands/context.js +791 -0
  6. package/cli/commands/split-video.js +3 -1
  7. package/context/context-builder.ts +318 -0
  8. package/context/index.ts +52 -0
  9. package/context/template-loader.ts +161 -0
  10. package/context/templates/base/actions-reference.md +299 -0
  11. package/context/templates/base/cli-reference.md +236 -0
  12. package/context/templates/base/project-overview.md +58 -0
  13. package/context/templates/base/selectors-guide.md +233 -0
  14. package/context/templates/base/yaml-schema.md +210 -0
  15. package/context/templates/skills/balance-timing.md +136 -0
  16. package/context/templates/skills/debug-selector.md +193 -0
  17. package/context/templates/skills/generate-actions.md +94 -0
  18. package/context/templates/skills/optimize-demo.md +218 -0
  19. package/context/templates/skills/review-demo-yaml.md +164 -0
  20. package/context/templates/skills/write-step-script.md +136 -0
  21. package/context/templates/stages/stage1-actions.md +236 -0
  22. package/context/templates/stages/stage2-scripts.md +197 -0
  23. package/context/templates/stages/stage3-balancing.md +229 -0
  24. package/context/templates/stages/stage4-rebalancing.md +228 -0
  25. package/context/tests/context-builder.test.ts +237 -0
  26. package/context/tests/template-loader.test.ts +181 -0
  27. package/context/tests/tool-formatter.test.ts +198 -0
  28. package/context/tool-formatter.ts +189 -0
  29. package/dist/index.cjs +416 -11
  30. package/dist/index.cjs.map +1 -1
  31. package/dist/index.d.cts +182 -1
  32. package/dist/index.d.ts +182 -1
  33. package/dist/index.js +391 -11
  34. package/dist/index.js.map +1 -1
  35. 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
+ }