@mcptoolshop/style-dataset-lab 2.3.0 → 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 (51) hide show
  1. package/CHANGELOG.md +125 -0
  2. package/README.md +36 -8
  3. package/bin/sdlab.js +129 -0
  4. package/lib/adapters/comfyui-runner.js +170 -0
  5. package/lib/adapters/comfyui-workflows.js +163 -0
  6. package/lib/batch-compiler.js +118 -0
  7. package/lib/batch-modes.js +111 -0
  8. package/lib/batch-runs.js +168 -0
  9. package/lib/batch-sheet-render.js +206 -0
  10. package/lib/brief-compiler.js +598 -0
  11. package/lib/brief-render.js +205 -0
  12. package/lib/critique-engine.js +413 -0
  13. package/lib/critique-render.js +171 -0
  14. package/lib/generated-provenance.js +87 -0
  15. package/lib/paths.js +31 -0
  16. package/lib/refine-briefs.js +329 -0
  17. package/lib/reingest-selected.js +173 -0
  18. package/lib/run-summary.js +131 -0
  19. package/lib/runtime-runs.js +168 -0
  20. package/lib/selections.js +323 -0
  21. package/lib/workflow-profiles.js +161 -0
  22. package/package.json +6 -3
  23. package/scripts/batch-generate.js +167 -0
  24. package/scripts/batch-sheet.js +46 -0
  25. package/scripts/batch-show.js +86 -0
  26. package/scripts/brief-compile.js +134 -0
  27. package/scripts/brief-show.js +52 -0
  28. package/scripts/critique-show.js +50 -0
  29. package/scripts/critique.js +70 -0
  30. package/scripts/doctor.js +169 -1
  31. package/scripts/init.js +22 -0
  32. package/scripts/refine.js +119 -0
  33. package/scripts/reingest-selected.js +78 -0
  34. package/scripts/reingest.js +7 -1
  35. package/scripts/run-generate.js +76 -0
  36. package/scripts/run-list.js +58 -0
  37. package/scripts/run-show.js +63 -0
  38. package/scripts/select.js +118 -0
  39. package/scripts/selection-show.js +88 -0
  40. package/scripts/workflow-list.js +61 -0
  41. package/scripts/workflow-show.js +96 -0
  42. package/templates/domains/architecture/workflows/profiles/environment-moodboard.json +62 -0
  43. package/templates/domains/architecture/workflows/profiles/interior-variant-pack.json +60 -0
  44. package/templates/domains/character-design/workflows/profiles/character-portrait-set.json +61 -0
  45. package/templates/domains/character-design/workflows/profiles/expression-sheet.json +59 -0
  46. package/templates/domains/creature-design/workflows/profiles/creature-portrait-set.json +61 -0
  47. package/templates/domains/creature-design/workflows/profiles/silhouette-exploration.json +58 -0
  48. package/templates/domains/game-art/workflows/profiles/character-portrait-set.json +62 -0
  49. package/templates/domains/game-art/workflows/profiles/environment-moodboard.json +61 -0
  50. package/templates/domains/vehicle-mech/workflows/profiles/variant-pack.json +60 -0
  51. package/templates/domains/vehicle-mech/workflows/profiles/vehicle-silhouette-pack.json +59 -0
@@ -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
+ }
@@ -0,0 +1,118 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * select.js — Approve outputs from runs or batches.
5
+ *
6
+ * Usage:
7
+ * sdlab select --run run_2026-04-15_001 --approve 001.png,003.png
8
+ * sdlab select --batch batch_2026-04-16_001 --approve slot_neutral:001.png,slot_anger:001.png
9
+ * sdlab select --run run_2026-04-15_001 --approve 001.png --reason "best continuity" --tags portrait,keeper
10
+ */
11
+
12
+ import { parseArgs } from '../lib/args.js';
13
+ import { getProjectRoot } from '../lib/paths.js';
14
+ import {
15
+ createSelectionFromRun,
16
+ createSelectionFromBatch,
17
+ saveSelection,
18
+ } from '../lib/selections.js';
19
+ import { info } from '../lib/log.js';
20
+
21
+ export async function run(argv = process.argv.slice(2)) {
22
+ const { flags } = parseArgs(argv, {
23
+ flags: {
24
+ project: { type: 'string', default: 'star-freight' },
25
+ run: { type: 'string' },
26
+ batch: { type: 'string' },
27
+ approve: { type: 'string' },
28
+ reason: { type: 'string' },
29
+ tags: { type: 'string' },
30
+ json: { type: 'boolean' },
31
+ },
32
+ deprecated: { game: 'project' },
33
+ });
34
+
35
+ if (!flags.approve || (!flags.run && !flags.batch)) {
36
+ console.log('Usage: sdlab select --run <id> --approve <files>');
37
+ console.log(' sdlab select --batch <id> --approve <slot:file,...>');
38
+ console.log('');
39
+ console.log('Options:');
40
+ console.log(' --run <id> Source run');
41
+ console.log(' --batch <id> Source batch');
42
+ console.log(' --approve <list> Comma-separated files (run) or slot:file pairs (batch)');
43
+ console.log(' --reason <text> Why these outputs were chosen');
44
+ console.log(' --tags <list> Comma-separated tags');
45
+ console.log(' --json Output manifest as JSON');
46
+ return;
47
+ }
48
+
49
+ const projectName = flags.project;
50
+ const projectRoot = getProjectRoot(projectName);
51
+ const reason = flags.reason || undefined;
52
+ const tags = flags.tags ? flags.tags.split(',').map(t => t.trim()) : undefined;
53
+
54
+ let result;
55
+
56
+ if (flags.run) {
57
+ // Run selection: --approve 001.png,003.png
58
+ const approvedFiles = flags.approve.split(',').map(f => f.trim());
59
+
60
+ result = await createSelectionFromRun({
61
+ projectRoot,
62
+ projectId: projectName,
63
+ runId: flags.run,
64
+ approvedFiles,
65
+ reason,
66
+ tags,
67
+ });
68
+ } else if (flags.batch) {
69
+ // Batch selection: --approve slot_neutral:001.png,slot_anger:001.png
70
+ const pairs = flags.approve.split(',').map(p => p.trim());
71
+ const approvedSlots = pairs.map(pair => {
72
+ const colonIdx = pair.indexOf(':');
73
+ if (colonIdx < 0) {
74
+ throw new Error(`Invalid batch approve format: "${pair}". Use slot_id:filename`);
75
+ }
76
+ return {
77
+ slotId: pair.slice(0, colonIdx),
78
+ filename: pair.slice(colonIdx + 1),
79
+ };
80
+ });
81
+
82
+ result = await createSelectionFromBatch({
83
+ projectRoot,
84
+ projectId: projectName,
85
+ batchId: flags.batch,
86
+ approvedSlots,
87
+ reason,
88
+ tags,
89
+ });
90
+ }
91
+
92
+ const { selectionId, selectionDir, manifest } = result;
93
+
94
+ // Save the selection
95
+ await saveSelection(selectionDir, manifest);
96
+
97
+ if (flags.json) {
98
+ console.log(JSON.stringify(manifest, null, 2));
99
+ return;
100
+ }
101
+
102
+ console.log(`\x1b[1mstyle-dataset-lab\x1b[0m select`);
103
+ console.log('');
104
+ console.log(` Selection: ${selectionId}`);
105
+ console.log(` Source: ${manifest.source_type} ${manifest.source_id}`);
106
+ if (manifest.workflow_id) console.log(` Workflow: ${manifest.workflow_id}`);
107
+ if (manifest.subject_id) console.log(` Subject: ${manifest.subject_id}`);
108
+ console.log(` Items: ${manifest.items.length}`);
109
+ console.log('');
110
+
111
+ for (const item of manifest.items) {
112
+ console.log(` ✓ ${item.slot_or_output.padEnd(28)} → ${item.filename} ${item.reason}`);
113
+ }
114
+
115
+ console.log('');
116
+ info(`Selection: selections/${selectionId}/manifest.json`);
117
+ info(`Chosen files: selections/${selectionId}/chosen/`);
118
+ }
@@ -0,0 +1,88 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * selection-show.js — Show selection details or list all selections.
5
+ *
6
+ * Usage:
7
+ * sdlab selection show --project star-freight
8
+ * sdlab selection show --selection selection_2026-04-16_001
9
+ * sdlab selection show selection_2026-04-16_001 --json
10
+ */
11
+
12
+ import { parseArgs } from '../lib/args.js';
13
+ import { getProjectRoot } from '../lib/paths.js';
14
+ import { loadSelection, listSelections, getSelectionsDir } from '../lib/selections.js';
15
+ import { join } from 'node:path';
16
+ import { existsSync, readFileSync } from 'node:fs';
17
+
18
+ export async function run(argv = process.argv.slice(2)) {
19
+ const { flags, positionals } = parseArgs(argv, {
20
+ flags: {
21
+ project: { type: 'string', default: 'star-freight' },
22
+ selection: { type: 'string' },
23
+ json: { type: 'boolean' },
24
+ },
25
+ deprecated: { game: 'project' },
26
+ });
27
+
28
+ const projectName = flags.project;
29
+ const projectRoot = getProjectRoot(projectName);
30
+ const selectionId = positionals[0] || flags.selection;
31
+
32
+ // List mode
33
+ if (!selectionId) {
34
+ const selections = listSelections(projectRoot);
35
+ if (selections.length === 0) {
36
+ console.log('No selections found.');
37
+ return;
38
+ }
39
+ console.log(`\x1b[1mstyle-dataset-lab\x1b[0m selections (${projectName})`);
40
+ console.log('');
41
+ for (const id of selections) {
42
+ try {
43
+ const m = loadSelection(projectRoot, id);
44
+ const items = m.items?.length || 0;
45
+ const src = `${m.source_type}:${m.source_id}`;
46
+ const ready = m.reingest_ready ? '✓' : '○';
47
+ const subject = m.subject_id ? ` (${m.subject_id})` : '';
48
+ console.log(` ${ready} ${id} ${src.padEnd(32)} ${items} items${subject}`);
49
+ } catch {
50
+ console.log(` ? ${id} (manifest unreadable)`);
51
+ }
52
+ }
53
+ console.log('');
54
+ return;
55
+ }
56
+
57
+ // Detail mode
58
+ const manifest = loadSelection(projectRoot, selectionId);
59
+
60
+ if (flags.json) {
61
+ console.log(JSON.stringify(manifest, null, 2));
62
+ return;
63
+ }
64
+
65
+ console.log(`\x1b[1mstyle-dataset-lab\x1b[0m selection ${selectionId}`);
66
+ console.log('');
67
+ console.log(` Source: ${manifest.source_type} ${manifest.source_id}`);
68
+ if (manifest.workflow_id) console.log(` Workflow: ${manifest.workflow_id}`);
69
+ if (manifest.subject_id) console.log(` Subject: ${manifest.subject_id}`);
70
+ console.log(` Created: ${manifest.created_at}`);
71
+ console.log(` Re-ingest ready: ${manifest.reingest_ready ? 'yes' : 'no'}`);
72
+ console.log('');
73
+ console.log(' Chosen outputs:');
74
+ for (const item of manifest.items || []) {
75
+ const seedStr = item.seed !== undefined ? `seed=${item.seed}` : '';
76
+ console.log(` ✓ ${item.slot_or_output.padEnd(28)} → ${item.filename} ${seedStr}`);
77
+ if (item.reason) console.log(` ${item.reason}`);
78
+ if (item.tags?.length) console.log(` tags: ${item.tags.join(', ')}`);
79
+ }
80
+
81
+ // Check provenance log
82
+ const provPath = join(getSelectionsDir(projectRoot), selectionId, 'provenance.jsonl');
83
+ if (existsSync(provPath)) {
84
+ console.log('');
85
+ console.log(' Provenance log: provenance.jsonl');
86
+ }
87
+ console.log('');
88
+ }
@@ -0,0 +1,61 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * workflow-list.js — List workflow profiles for a project.
5
+ *
6
+ * Usage:
7
+ * sdlab workflow list --project star-freight
8
+ * sdlab workflow list --project star-freight --json
9
+ */
10
+
11
+ import { getProjectName, parseArgs } from '../lib/args.js';
12
+ import { getProjectRoot } from '../lib/paths.js';
13
+ import { listWorkflowProfiles } from '../lib/workflow-profiles.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 profiles = await listWorkflowProfiles(projectRoot);
27
+
28
+ if (flags.json) {
29
+ console.log(JSON.stringify(profiles, null, 2));
30
+ return;
31
+ }
32
+
33
+ console.log(`\x1b[1msdlab workflow list\x1b[0m — ${projectName}\n`);
34
+
35
+ if (profiles.length === 0) {
36
+ console.log(' No workflow profiles found.');
37
+ console.log(` Add profiles to projects/${projectName}/workflows/profiles/`);
38
+ return;
39
+ }
40
+
41
+ // Table header
42
+ const idW = Math.max(12, ...profiles.map(p => p.workflow_id.length));
43
+ const laneW = Math.max(6, ...profiles.map(p => p.lane_id.length));
44
+ const modeW = Math.max(7, ...profiles.map(p => (p.subject_mode || 'optional').length));
45
+ const outW = Math.max(11, ...profiles.map(p => p.output_mode.length));
46
+
47
+ const header = ` ${'WORKFLOW'.padEnd(idW)} ${'LANE'.padEnd(laneW)} ${'SUBJECT'.padEnd(modeW)} ${'OUTPUT MODE'.padEnd(outW)} COUNT`;
48
+ console.log(header);
49
+ console.log(' ' + '─'.repeat(header.length - 2));
50
+
51
+ for (const p of profiles) {
52
+ const line = ` ${p.workflow_id.padEnd(idW)} ${p.lane_id.padEnd(laneW)} ${(p.subject_mode || 'optional').padEnd(modeW)} ${p.output_mode.padEnd(outW)} ${p.output_count}`;
53
+ console.log(line);
54
+ }
55
+
56
+ console.log(`\n ${profiles.length} workflow(s)`);
57
+ }
58
+
59
+ if (process.argv[1] && (process.argv[1].endsWith('workflow-list.js') || process.argv[1].endsWith('workflow-list'))) {
60
+ run().catch((err) => { console.error(err.message || err); process.exit(1); });
61
+ }