@mcptoolshop/style-dataset-lab 2.2.1 → 3.0.0

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 (65) hide show
  1. package/CHANGELOG.md +125 -0
  2. package/README.es.md +76 -144
  3. package/README.fr.md +75 -143
  4. package/README.hi.md +75 -143
  5. package/README.it.md +76 -144
  6. package/README.ja.md +75 -143
  7. package/README.md +36 -8
  8. package/README.pt-BR.md +75 -143
  9. package/README.zh.md +76 -144
  10. package/SECURITY.md +3 -1
  11. package/bin/sdlab.js +146 -17
  12. package/lib/adapters/comfyui-runner.js +170 -0
  13. package/lib/adapters/comfyui-workflows.js +163 -0
  14. package/lib/batch-compiler.js +118 -0
  15. package/lib/batch-modes.js +111 -0
  16. package/lib/batch-runs.js +168 -0
  17. package/lib/batch-sheet-render.js +206 -0
  18. package/lib/brief-compiler.js +598 -0
  19. package/lib/brief-render.js +205 -0
  20. package/lib/critique-engine.js +413 -0
  21. package/lib/critique-render.js +171 -0
  22. package/lib/errors.js +82 -0
  23. package/lib/generated-provenance.js +87 -0
  24. package/lib/log.js +36 -0
  25. package/lib/paths.js +31 -0
  26. package/lib/refine-briefs.js +329 -0
  27. package/lib/reingest-selected.js +173 -0
  28. package/lib/run-summary.js +131 -0
  29. package/lib/runtime-runs.js +168 -0
  30. package/lib/selections.js +323 -0
  31. package/lib/snapshot.js +5 -2
  32. package/lib/split.js +5 -2
  33. package/lib/workflow-profiles.js +161 -0
  34. package/package.json +8 -6
  35. package/scripts/batch-generate.js +167 -0
  36. package/scripts/batch-sheet.js +46 -0
  37. package/scripts/batch-show.js +86 -0
  38. package/scripts/brief-compile.js +134 -0
  39. package/scripts/brief-show.js +52 -0
  40. package/scripts/critique-show.js +50 -0
  41. package/scripts/critique.js +70 -0
  42. package/scripts/doctor.js +169 -1
  43. package/scripts/init.js +22 -0
  44. package/scripts/refine.js +119 -0
  45. package/scripts/reingest-selected.js +78 -0
  46. package/scripts/reingest.js +7 -1
  47. package/scripts/run-generate.js +76 -0
  48. package/scripts/run-list.js +58 -0
  49. package/scripts/run-show.js +63 -0
  50. package/scripts/select.js +118 -0
  51. package/scripts/selection-show.js +88 -0
  52. package/scripts/snapshot.js +27 -23
  53. package/scripts/split.js +3 -1
  54. package/scripts/workflow-list.js +61 -0
  55. package/scripts/workflow-show.js +96 -0
  56. package/templates/domains/architecture/workflows/profiles/environment-moodboard.json +62 -0
  57. package/templates/domains/architecture/workflows/profiles/interior-variant-pack.json +60 -0
  58. package/templates/domains/character-design/workflows/profiles/character-portrait-set.json +61 -0
  59. package/templates/domains/character-design/workflows/profiles/expression-sheet.json +59 -0
  60. package/templates/domains/creature-design/workflows/profiles/creature-portrait-set.json +61 -0
  61. package/templates/domains/creature-design/workflows/profiles/silhouette-exploration.json +58 -0
  62. package/templates/domains/game-art/workflows/profiles/character-portrait-set.json +62 -0
  63. package/templates/domains/game-art/workflows/profiles/environment-moodboard.json +61 -0
  64. package/templates/domains/vehicle-mech/workflows/profiles/variant-pack.json +60 -0
  65. package/templates/domains/vehicle-mech/workflows/profiles/vehicle-silhouette-pack.json +59 -0
package/scripts/doctor.js CHANGED
@@ -41,6 +41,13 @@ const REQUIRED_DIRS = [
41
41
  'training/packages',
42
42
  'training/eval-runs',
43
43
  'training/implementations',
44
+ 'workflows/profiles',
45
+ 'workflows/batch-modes',
46
+ 'briefs',
47
+ 'runs',
48
+ 'batches',
49
+ 'selections',
50
+ 'inbox/generated',
44
51
  ];
45
52
 
46
53
  function pass(msg) { console.log(` \x1b[32m✓\x1b[0m ${msg}`); }
@@ -284,7 +291,168 @@ export async function run(argv = process.argv.slice(2)) {
284
291
  }
285
292
  }
286
293
 
287
- // ── 9. Records sanity check ──
294
+ // ── 9. Workflow profile validation ──
295
+ const workflowDir = join(projectDir, 'workflows', 'profiles');
296
+ if (existsSync(workflowDir)) {
297
+ console.log('');
298
+ const wfFiles = readdirSync(workflowDir).filter(f => f.endsWith('.json'));
299
+ if (wfFiles.length > 0) {
300
+ const VALID_SUBJECT_MODES = ['required', 'optional', 'forbidden'];
301
+ const VALID_OUTPUT_MODES = ['portrait_set', 'expression_sheet', 'variant_pack', 'moodboard', 'silhouette_sheet', 'turnaround'];
302
+ const laneIds = new Set((lanes?.lanes || []).map(l => l.id));
303
+
304
+ let wfErrors = 0;
305
+ for (const file of wfFiles) {
306
+ const { data, error } = tryParseJson(join(workflowDir, file));
307
+ if (error) { fail(`workflows/profiles/${file} — invalid JSON: ${error}`); wfErrors++; continue; }
308
+
309
+ const missing = ['workflow_id', 'label', 'lane_id', 'output_mode', 'output_count',
310
+ 'prompt_strategy', 'negative_strategy', 'canon_focus', 'drift_guards', 'runtime_defaults']
311
+ .filter(f => data[f] === undefined || data[f] === null);
312
+ if (missing.length > 0) {
313
+ fail(`workflows/profiles/${file} — missing fields: ${missing.join(', ')}`);
314
+ wfErrors++;
315
+ continue;
316
+ }
317
+
318
+ if (data.lane_id && laneIds.size > 0 && !laneIds.has(data.lane_id)) {
319
+ fail(`workflows/profiles/${file} — lane_id "${data.lane_id}" not found in lanes.json`);
320
+ wfErrors++;
321
+ }
322
+
323
+ if (data.subject_mode && !VALID_SUBJECT_MODES.includes(data.subject_mode)) {
324
+ fail(`workflows/profiles/${file} — invalid subject_mode "${data.subject_mode}"`);
325
+ wfErrors++;
326
+ }
327
+
328
+ if (!VALID_OUTPUT_MODES.includes(data.output_mode)) {
329
+ fail(`workflows/profiles/${file} — invalid output_mode "${data.output_mode}"`);
330
+ wfErrors++;
331
+ }
332
+
333
+ const ps = data.prompt_strategy;
334
+ if (!ps || !Array.isArray(ps.style_prefix) || !ps.structure || !Array.isArray(ps.must_include)) {
335
+ fail(`workflows/profiles/${file} — prompt_strategy needs style_prefix[], structure, must_include[]`);
336
+ wfErrors++;
337
+ }
338
+
339
+ const ns = data.negative_strategy;
340
+ if (!ns || !Array.isArray(ns.must_avoid)) {
341
+ fail(`workflows/profiles/${file} — negative_strategy needs must_avoid[]`);
342
+ wfErrors++;
343
+ }
344
+
345
+ if (!data.runtime_defaults?.adapter_target) {
346
+ fail(`workflows/profiles/${file} — runtime_defaults needs adapter_target`);
347
+ wfErrors++;
348
+ }
349
+ }
350
+
351
+ if (wfErrors === 0) {
352
+ pass(`${wfFiles.length} workflow profiles — all valid`);
353
+ passes++;
354
+ } else {
355
+ failures += wfErrors;
356
+ }
357
+ } else {
358
+ pass('workflows/profiles/ — empty (no workflows yet)');
359
+ passes++;
360
+ }
361
+ }
362
+
363
+ // ── 10. Batch mode validation ──
364
+ const batchModesDir = join(projectDir, 'workflows', 'batch-modes');
365
+ if (existsSync(batchModesDir)) {
366
+ console.log('');
367
+ const bmFiles = readdirSync(batchModesDir).filter(f => f.endsWith('.json'));
368
+ if (bmFiles.length > 0) {
369
+ const VALID_BATCH_TYPES = ['expression_sheet', 'environment_board', 'silhouette_pack', 'continuity_variants'];
370
+ const VALID_SUBJECT_MODES_BM = ['required', 'optional', 'forbidden'];
371
+ const VALID_LAYOUTS = ['grid', 'moodboard', 'strip', 'freeform'];
372
+ const wfFiles = existsSync(join(projectDir, 'workflows', 'profiles'))
373
+ ? readdirSync(join(projectDir, 'workflows', 'profiles')).filter(f => f.endsWith('.json')).map(f => f.replace('.json', ''))
374
+ : [];
375
+
376
+ let bmErrors = 0;
377
+ for (const file of bmFiles) {
378
+ const { data, error } = tryParseJson(join(batchModesDir, file));
379
+ if (error) { fail(`workflows/batch-modes/${file} — invalid JSON: ${error}`); bmErrors++; continue; }
380
+
381
+ if (!data.mode_id) { fail(`workflows/batch-modes/${file} — missing mode_id`); bmErrors++; }
382
+ if (!data.batch_type || !VALID_BATCH_TYPES.includes(data.batch_type)) {
383
+ fail(`workflows/batch-modes/${file} — invalid batch_type "${data.batch_type}"`);
384
+ bmErrors++;
385
+ }
386
+ if (data.subject_mode && !VALID_SUBJECT_MODES_BM.includes(data.subject_mode)) {
387
+ fail(`workflows/batch-modes/${file} — invalid subject_mode "${data.subject_mode}"`);
388
+ bmErrors++;
389
+ }
390
+ if (data.base_workflow_id && wfFiles.length > 0 && !wfFiles.includes(data.base_workflow_id)) {
391
+ fail(`workflows/batch-modes/${file} — base_workflow_id "${data.base_workflow_id}" not found in profiles`);
392
+ bmErrors++;
393
+ }
394
+ if (!Array.isArray(data.variant_plan) || data.variant_plan.length === 0) {
395
+ fail(`workflows/batch-modes/${file} — variant_plan must be non-empty array`);
396
+ bmErrors++;
397
+ } else {
398
+ const slotIds = data.variant_plan.map(s => s.slot_id);
399
+ const dupes = slotIds.filter((id, i) => slotIds.indexOf(id) !== i);
400
+ if (dupes.length > 0) {
401
+ fail(`workflows/batch-modes/${file} — duplicate slot_ids: ${[...new Set(dupes)].join(', ')}`);
402
+ bmErrors++;
403
+ }
404
+ }
405
+ if (data.assembly?.layout && !VALID_LAYOUTS.includes(data.assembly.layout)) {
406
+ fail(`workflows/batch-modes/${file} — invalid assembly.layout "${data.assembly.layout}"`);
407
+ bmErrors++;
408
+ }
409
+ }
410
+
411
+ if (bmErrors === 0) {
412
+ pass(`${bmFiles.length} batch modes — all valid`);
413
+ passes++;
414
+ } else {
415
+ failures += bmErrors;
416
+ }
417
+ } else {
418
+ pass('workflows/batch-modes/ — empty (no batch modes yet)');
419
+ passes++;
420
+ }
421
+ }
422
+
423
+ // ── 11. Runtime templates check ──
424
+ {
425
+ console.log('');
426
+ const runtimeDir = join(REPO_ROOT, 'runtime', 'comfyui');
427
+ if (existsSync(runtimeDir)) {
428
+ const templateFiles = readdirSync(runtimeDir).filter(f => f.endsWith('.json'));
429
+ if (templateFiles.length > 0) {
430
+ let templateErrors = 0;
431
+ for (const file of templateFiles) {
432
+ const { data, error } = tryParseJson(join(runtimeDir, file));
433
+ if (error) { fail(`runtime/comfyui/${file}: invalid JSON — ${error}`); templateErrors++; continue; }
434
+ if (!data.template_id) { fail(`runtime/comfyui/${file}: missing template_id`); templateErrors++; }
435
+ if (!data.compatible_modes || !Array.isArray(data.compatible_modes)) {
436
+ fail(`runtime/comfyui/${file}: missing compatible_modes[]`); templateErrors++;
437
+ }
438
+ }
439
+ if (templateErrors === 0) {
440
+ pass(`${templateFiles.length} runtime templates — all valid`);
441
+ passes++;
442
+ } else {
443
+ failures += templateErrors;
444
+ }
445
+ } else {
446
+ warn('runtime/comfyui/ — no templates found');
447
+ warnings++;
448
+ }
449
+ } else {
450
+ warn('runtime/comfyui/ — directory not found');
451
+ warnings++;
452
+ }
453
+ }
454
+
455
+ // ── 12. Records sanity check ──
288
456
  const recordsDir = join(projectDir, 'records');
289
457
  if (existsSync(recordsDir)) {
290
458
  console.log('');
package/scripts/init.js CHANGED
@@ -40,6 +40,13 @@ const PROJECT_DIRS = [
40
40
  'training/packages',
41
41
  'training/eval-runs',
42
42
  'training/implementations',
43
+ 'workflows/profiles',
44
+ 'workflows/batch-modes',
45
+ 'briefs',
46
+ 'runs',
47
+ 'batches',
48
+ 'selections',
49
+ 'inbox/generated',
43
50
  ];
44
51
 
45
52
  function isValidName(name) {
@@ -214,6 +221,21 @@ export async function run(argv = process.argv.slice(2)) {
214
221
  console.log(` \x1b[32m✓\x1b[0m inputs/prompts/example-wave.json`);
215
222
  }
216
223
 
224
+ // Copy domain workflow profiles if available
225
+ if (domainDir) {
226
+ const domainWorkflowsDir = join(domainDir, 'workflows', 'profiles');
227
+ if (existsSync(domainWorkflowsDir)) {
228
+ const wfFiles = (await readdir(domainWorkflowsDir)).filter(f => f.endsWith('.json'));
229
+ for (const file of wfFiles) {
230
+ await copyFile(
231
+ join(domainWorkflowsDir, file),
232
+ join(projectDir, 'workflows', 'profiles', file)
233
+ );
234
+ console.log(` \x1b[32m✓\x1b[0m workflows/profiles/${file}`);
235
+ }
236
+ }
237
+ }
238
+
217
239
  console.log('');
218
240
  console.log(`\x1b[32m✓\x1b[0m Project "${projectName}" initialized`);
219
241
  console.log('');
@@ -0,0 +1,119 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * refine.js — Generate a refined next-pass brief from critique.
5
+ *
6
+ * Usage:
7
+ * sdlab refine --project star-freight --run run_2026-04-15_001 --pick 003.png
8
+ * sdlab refine --run run_2026-04-15_001 --pick 003.png --push "stronger faction read" --suppress "extra ornament"
9
+ * sdlab refine --run run_2026-04-15_001 --pick 003.png --json
10
+ */
11
+
12
+ import { readFileSync } from 'node:fs';
13
+ import { join } from 'node:path';
14
+ import { parseArgs } from '../lib/args.js';
15
+ import { getProjectRoot, getRunsDir } from '../lib/paths.js';
16
+ import { loadCritique } from '../lib/critique-engine.js';
17
+ import {
18
+ extractDelta,
19
+ buildRefinedBrief,
20
+ saveRefinedBrief,
21
+ renderRefinedBriefMarkdown,
22
+ } from '../lib/refine-briefs.js';
23
+ import { info } from '../lib/log.js';
24
+
25
+ export async function run(argv = process.argv.slice(2)) {
26
+ const { flags } = parseArgs(argv, {
27
+ flags: {
28
+ project: { type: 'string', default: 'star-freight' },
29
+ run: { type: 'string' },
30
+ pick: { type: 'string' },
31
+ preserve: { type: 'string' },
32
+ push: { type: 'string' },
33
+ suppress: { type: 'string' },
34
+ json: { type: 'boolean' },
35
+ },
36
+ deprecated: { game: 'project' },
37
+ });
38
+
39
+ if (!flags.run || !flags.pick) {
40
+ console.log('Usage: sdlab refine --run <id> --pick <filename> [--project <name>]');
41
+ console.log('');
42
+ console.log('Options:');
43
+ console.log(' --run <id> Run to refine from (required)');
44
+ console.log(' --pick <filename> Candidate filename to refine from (required)');
45
+ console.log(' --preserve <notes> Comma-separated preserve instructions');
46
+ console.log(' --push <notes> Comma-separated push instructions');
47
+ console.log(' --suppress <notes> Comma-separated suppress instructions');
48
+ console.log(' --json Output raw JSON');
49
+ return;
50
+ }
51
+
52
+ const projectName = flags.project;
53
+ const projectRoot = getProjectRoot(projectName);
54
+ const runId = flags.run;
55
+ const pickedFilename = flags.pick;
56
+
57
+ console.log(`\x1b[1mstyle-dataset-lab\x1b[0m refine`);
58
+ console.log(` Project: ${projectName}`);
59
+ console.log(` Run: ${runId}`);
60
+ console.log(` Picked: ${pickedFilename}`);
61
+ console.log('');
62
+
63
+ // Load critique
64
+ const critique = loadCritique(projectRoot, runId);
65
+
66
+ // Load parent brief from the run
67
+ const runDir = join(getRunsDir(projectRoot), runId);
68
+ const parentBrief = JSON.parse(readFileSync(join(runDir, 'brief.json'), 'utf-8'));
69
+
70
+ // Parse CLI delta overrides
71
+ const cliOverrides = {
72
+ preserve: flags.preserve ? flags.preserve.split(',').map(s => s.trim()).filter(Boolean) : [],
73
+ push: flags.push ? flags.push.split(',').map(s => s.trim()).filter(Boolean) : [],
74
+ suppress: flags.suppress ? flags.suppress.split(',').map(s => s.trim()).filter(Boolean) : [],
75
+ };
76
+
77
+ // Extract delta from critique + overrides
78
+ const delta = extractDelta(critique, pickedFilename, cliOverrides);
79
+
80
+ // Build refined brief
81
+ const refineDir = join(runDir, 'refine');
82
+ const refinedBrief = buildRefinedBrief({
83
+ parentBrief,
84
+ critique,
85
+ pickedCandidate: pickedFilename,
86
+ delta,
87
+ runId,
88
+ refineDir,
89
+ });
90
+
91
+ // Save
92
+ const { jsonPath, mdPath } = await saveRefinedBrief(projectRoot, runId, refinedBrief);
93
+
94
+ if (flags.json) {
95
+ console.log(JSON.stringify(refinedBrief, null, 2));
96
+ } else {
97
+ console.log(`Refined brief: ${refinedBrief.brief_id}`);
98
+ console.log(`Parent: ${refinedBrief.parent_brief_id}`);
99
+ console.log(`Source: ${runId} → ${pickedFilename}`);
100
+ console.log('');
101
+
102
+ if (delta.preserve.length > 0) {
103
+ console.log('Preserve:');
104
+ for (const p of delta.preserve) console.log(` ✓ ${p}`);
105
+ }
106
+ if (delta.push.length > 0) {
107
+ console.log('Push:');
108
+ for (const p of delta.push) console.log(` → ${p}`);
109
+ }
110
+ if (delta.suppress.length > 0) {
111
+ console.log('Suppress:');
112
+ for (const s of delta.suppress) console.log(` ✗ ${s}`);
113
+ }
114
+
115
+ console.log('');
116
+ info(`Saved: runs/${runId}/refine/${refinedBrief.brief_id}.json`);
117
+ info(`Next: sdlab run generate --brief ${refinedBrief.brief_id} (from refine dir)`);
118
+ }
119
+ }
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * reingest-selected.js — Re-ingest selected outputs as candidate records.
5
+ *
6
+ * Usage:
7
+ * sdlab reingest selected --selection selection_2026-04-16_001 --project star-freight
8
+ * sdlab reingest selected --selection selection_2026-04-16_001 --dry-run
9
+ */
10
+
11
+ import { parseArgs } from '../lib/args.js';
12
+ import { getProjectRoot } from '../lib/paths.js';
13
+ import { reingestSelection } from '../lib/reingest-selected.js';
14
+ import { info } from '../lib/log.js';
15
+
16
+ export async function run(argv = process.argv.slice(2)) {
17
+ const { flags } = parseArgs(argv, {
18
+ flags: {
19
+ project: { type: 'string', default: 'star-freight' },
20
+ selection: { type: 'string' },
21
+ 'dry-run': { type: 'boolean' },
22
+ json: { type: 'boolean' },
23
+ },
24
+ deprecated: { game: 'project' },
25
+ });
26
+
27
+ if (!flags.selection) {
28
+ console.log('Usage: sdlab reingest selected --selection <id> [--project <name>]');
29
+ console.log('');
30
+ console.log('Options:');
31
+ console.log(' --selection <id> Selection to re-ingest (required)');
32
+ console.log(' --dry-run Preview without writing records');
33
+ console.log(' --json Output as JSON');
34
+ return;
35
+ }
36
+
37
+ const projectName = flags.project;
38
+ const projectRoot = getProjectRoot(projectName);
39
+ const dryRun = flags['dry-run'] || false;
40
+
41
+ console.log(`\x1b[1mstyle-dataset-lab\x1b[0m reingest selected`);
42
+ console.log(` Selection: ${flags.selection}`);
43
+ console.log(` Project: ${projectName}`);
44
+ if (dryRun) console.log(' ⚠ DRY RUN');
45
+ console.log('');
46
+
47
+ const { created, skipped } = await reingestSelection({
48
+ projectRoot,
49
+ projectId: projectName,
50
+ selectionId: flags.selection,
51
+ dryRun,
52
+ });
53
+
54
+ if (flags.json) {
55
+ console.log(JSON.stringify({ created, skipped }, null, 2));
56
+ return;
57
+ }
58
+
59
+ if (created.length > 0) {
60
+ console.log(` Created ${created.length} candidate record(s):`);
61
+ for (const id of created) {
62
+ console.log(` ✓ ${id}`);
63
+ }
64
+ }
65
+
66
+ if (skipped.length > 0) {
67
+ console.log(` Skipped ${skipped.length} (already exist):`);
68
+ for (const id of skipped) {
69
+ console.log(` ○ ${id}`);
70
+ }
71
+ }
72
+
73
+ console.log('');
74
+ if (!dryRun && created.length > 0) {
75
+ info('Records staged for review. Use sdlab curate to approve/reject.');
76
+ info(`Images copied to inbox/generated/`);
77
+ }
78
+ }
@@ -50,6 +50,12 @@ export async function run(argv = process.argv.slice(2)) {
50
50
  console.log(' 3. Then eligible for future snapshots');
51
51
  }
52
52
 
53
+ } else if (subcommand === 'selected') {
54
+ // Delegate to reingest-selected.js
55
+ const { run: runSelected } = await import('./reingest-selected.js');
56
+ await runSelected(args);
57
+ return;
58
+
53
59
  } else if (subcommand === 'audit') {
54
60
  console.log(`\x1b[1msdlab reingest audit\x1b[0m — ${projectName}\n`);
55
61
 
@@ -77,7 +83,7 @@ export async function run(argv = process.argv.slice(2)) {
77
83
  }
78
84
 
79
85
  } else {
80
- throw new Error(`Unknown subcommand: ${subcommand}. Use: generated, audit`);
86
+ throw new Error(`Unknown subcommand: ${subcommand}. Use: generated, selected, audit`);
81
87
  }
82
88
  }
83
89
 
@@ -0,0 +1,76 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * run-generate.js — Execute a compiled brief through ComfyUI.
5
+ *
6
+ * Usage:
7
+ * sdlab run generate --brief brief_2026-04-15_001 --project star-freight
8
+ * sdlab run generate --brief brief_2026-04-15_001 --project star-freight --dry-run
9
+ * sdlab run generate --brief brief_2026-04-15_001 --project star-freight --seed 42
10
+ */
11
+
12
+ import { parseArgs } from '../lib/args.js';
13
+ import { getProjectRoot, getRunsDir } from '../lib/paths.js';
14
+ import { loadCompiledBrief } from '../lib/brief-compiler.js';
15
+ import { executeRun } from '../lib/adapters/comfyui-runner.js';
16
+ import { saveRunSummary, renderRunText } from '../lib/run-summary.js';
17
+ import { info } from '../lib/log.js';
18
+ import { join } from 'node:path';
19
+
20
+ export async function run(argv = process.argv.slice(2)) {
21
+ const { flags } = parseArgs(argv, {
22
+ flags: {
23
+ project: { type: 'string', default: 'star-freight' },
24
+ brief: { type: 'string' },
25
+ seed: { type: 'string' },
26
+ 'dry-run': { type: 'boolean' },
27
+ json: { type: 'boolean' },
28
+ },
29
+ deprecated: { game: 'project' },
30
+ });
31
+
32
+ if (!flags.brief) {
33
+ console.log('Usage: sdlab run generate --brief <id> [--project <name>]');
34
+ console.log('');
35
+ console.log('Options:');
36
+ console.log(' --brief <id> Compiled brief ID (required)');
37
+ console.log(' --seed <n> Override base seed');
38
+ console.log(' --dry-run Prepare run dir without submitting to ComfyUI');
39
+ console.log(' --json Output manifest as JSON');
40
+ return;
41
+ }
42
+
43
+ const projectName = flags.project;
44
+ const projectRoot = getProjectRoot(projectName);
45
+
46
+ console.log(`\x1b[1mstyle-dataset-lab\x1b[0m run generate`);
47
+ console.log(` Project: ${projectName}`);
48
+ console.log(` Brief: ${flags.brief}`);
49
+ if (flags['dry-run']) console.log(' ⚠ DRY RUN');
50
+ console.log('');
51
+
52
+ // Load brief
53
+ const brief = await loadCompiledBrief(projectRoot, flags.brief);
54
+
55
+ // Execute
56
+ const manifest = await executeRun({
57
+ projectRoot,
58
+ projectName,
59
+ brief,
60
+ baseSeed: flags.seed ? parseInt(flags.seed, 10) : undefined,
61
+ dryRun: flags['dry-run'] || false,
62
+ });
63
+
64
+ // Save summary
65
+ const runDir = join(getRunsDir(projectRoot), manifest.run_id);
66
+ await saveRunSummary(runDir, manifest);
67
+
68
+ console.log('');
69
+ if (flags.json) {
70
+ console.log(JSON.stringify(manifest, null, 2));
71
+ } else {
72
+ console.log(renderRunText(manifest));
73
+ console.log('');
74
+ info(`Run saved to: runs/${manifest.run_id}/`);
75
+ }
76
+ }
@@ -0,0 +1,58 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * run-list.js — List all runs for a project.
5
+ *
6
+ * Usage:
7
+ * sdlab run list --project star-freight
8
+ * sdlab run list --project star-freight --json
9
+ */
10
+
11
+ import { parseArgs } from '../lib/args.js';
12
+ import { getProjectRoot } from '../lib/paths.js';
13
+ import { listRuns } from '../lib/runtime-runs.js';
14
+
15
+ export async function run(argv = process.argv.slice(2)) {
16
+ const { flags } = parseArgs(argv, {
17
+ flags: {
18
+ project: { type: 'string', default: 'star-freight' },
19
+ json: { type: 'boolean' },
20
+ },
21
+ deprecated: { game: 'project' },
22
+ });
23
+
24
+ const projectName = flags.project;
25
+ const projectRoot = getProjectRoot(projectName);
26
+ const runs = listRuns(projectRoot);
27
+
28
+ if (flags.json) {
29
+ console.log(JSON.stringify(runs, null, 2));
30
+ return;
31
+ }
32
+
33
+ if (runs.length === 0) {
34
+ console.log('No runs found. Run: sdlab run generate --brief <id>');
35
+ return;
36
+ }
37
+
38
+ console.log(`\x1b[1mRuns for ${projectName}\x1b[0m (${runs.length})\n`);
39
+
40
+ const header = ' ' +
41
+ 'Run ID'.padEnd(26) +
42
+ 'Brief'.padEnd(26) +
43
+ 'Mode'.padEnd(18) +
44
+ 'Count'.padEnd(8) +
45
+ 'Created';
46
+ console.log(header);
47
+ console.log(' ' + '─'.repeat(90));
48
+
49
+ for (const r of runs) {
50
+ const line = ' ' +
51
+ (r.run_id || '').padEnd(26) +
52
+ (r.brief_id || '').padEnd(26) +
53
+ (r.adapter_target || '?').padEnd(18) +
54
+ String(r.output_count ?? '?').padEnd(8) +
55
+ (r.created_at ? r.created_at.slice(0, 16) : '?');
56
+ console.log(line);
57
+ }
58
+ }
@@ -0,0 +1,63 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * run-show.js — Show details of a completed run.
5
+ *
6
+ * Usage:
7
+ * sdlab run show run_2026-04-16_001 --project star-freight
8
+ * sdlab run show run_2026-04-16_001 --project star-freight --json
9
+ * sdlab run show run_2026-04-16_001 --project star-freight --md
10
+ */
11
+
12
+ import { parseArgs } from '../lib/args.js';
13
+ import { getProjectRoot } from '../lib/paths.js';
14
+ import { loadRun, listRuns } from '../lib/runtime-runs.js';
15
+ import { renderRunText, renderRunMarkdown } from '../lib/run-summary.js';
16
+
17
+ export async function run(argv = process.argv.slice(2)) {
18
+ const { positionals, flags } = parseArgs(argv, {
19
+ flags: {
20
+ project: { type: 'string', default: 'star-freight' },
21
+ json: { type: 'boolean' },
22
+ md: { type: 'boolean' },
23
+ },
24
+ deprecated: { game: 'project' },
25
+ });
26
+
27
+ const runId = positionals[0];
28
+ const projectName = flags.project;
29
+ const projectRoot = getProjectRoot(projectName);
30
+
31
+ // No run ID → list runs
32
+ if (!runId) {
33
+ const runs = listRuns(projectRoot);
34
+ if (runs.length === 0) {
35
+ console.log('No runs found. Run: sdlab run generate --brief <id>');
36
+ return;
37
+ }
38
+
39
+ console.log(`\x1b[1mRuns for ${projectName}\x1b[0m (${runs.length})\n`);
40
+ console.log(' ID Brief Mode Outputs Adapter');
41
+ console.log(' ─'.padEnd(100, '─'));
42
+ for (const r of runs) {
43
+ const id = (r.run_id || '').padEnd(24);
44
+ const brief = (r.brief_id || '').padEnd(24);
45
+ const mode = (r.output_count != null ? `${r.output_count}` : '?').padStart(7);
46
+ const adapter = r.adapter_target || '?';
47
+ // output_mode if we have it
48
+ console.log(` ${id} ${brief} ${mode} ${adapter}`);
49
+ }
50
+ return;
51
+ }
52
+
53
+ // Show single run
54
+ const manifest = await loadRun(projectRoot, runId);
55
+
56
+ if (flags.json) {
57
+ console.log(JSON.stringify(manifest, null, 2));
58
+ } else if (flags.md) {
59
+ console.log(renderRunMarkdown(manifest));
60
+ } else {
61
+ console.log(renderRunText(manifest));
62
+ }
63
+ }