@mcptoolshop/style-dataset-lab 2.3.0 → 3.0.1

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 (106) hide show
  1. package/CHANGELOG.md +178 -0
  2. package/CODE_OF_CONDUCT.md +9 -0
  3. package/CONTRIBUTING.md +71 -0
  4. package/README.md +82 -9
  5. package/SECURITY.md +6 -5
  6. package/bin/sdlab.js +359 -11
  7. package/lib/adapters/comfyui-runner.js +170 -0
  8. package/lib/adapters/comfyui-workflows.js +163 -0
  9. package/lib/adapters/diffusers-lora.js +7 -2
  10. package/lib/adapters/generic-image-caption.js +6 -1
  11. package/lib/args.js +200 -13
  12. package/lib/batch-compiler.js +122 -0
  13. package/lib/batch-modes.js +111 -0
  14. package/lib/batch-runs.js +237 -0
  15. package/lib/batch-sheet-render.js +209 -0
  16. package/lib/brief-compiler.js +625 -0
  17. package/lib/brief-render.js +205 -0
  18. package/lib/card.js +11 -4
  19. package/lib/comfyui.js +100 -6
  20. package/lib/config.js +68 -3
  21. package/lib/critique-engine.js +413 -0
  22. package/lib/critique-render.js +171 -0
  23. package/lib/deprecation.js +1 -1
  24. package/lib/eval-pack.js +94 -4
  25. package/lib/eval-runs.js +42 -3
  26. package/lib/export.js +144 -34
  27. package/lib/generated-provenance.js +87 -0
  28. package/lib/implementation-packs.js +15 -1
  29. package/lib/log.js +41 -4
  30. package/lib/paths.js +72 -3
  31. package/lib/refine-briefs.js +336 -0
  32. package/lib/reingest-selected.js +184 -0
  33. package/lib/reingest.js +15 -1
  34. package/lib/run-summary.js +132 -0
  35. package/lib/runtime-runs.js +218 -0
  36. package/lib/selections.js +375 -0
  37. package/lib/snapshot.js +100 -7
  38. package/lib/split.js +175 -17
  39. package/lib/training-manifests.js +17 -3
  40. package/lib/training-packages.js +28 -14
  41. package/lib/workflow-profiles.js +161 -0
  42. package/package.json +13 -4
  43. package/scripts/batch-generate.js +176 -0
  44. package/scripts/batch-sheet.js +46 -0
  45. package/scripts/batch-show.js +86 -0
  46. package/scripts/brief-compile.js +134 -0
  47. package/scripts/brief-show.js +53 -0
  48. package/scripts/canon-bind.js +7 -9
  49. package/scripts/card.js +22 -25
  50. package/scripts/compare.js +38 -33
  51. package/scripts/critique-show.js +50 -0
  52. package/scripts/critique.js +70 -0
  53. package/scripts/curate.js +91 -46
  54. package/scripts/doctor.js +172 -6
  55. package/scripts/eligibility.js +17 -14
  56. package/scripts/eval-pack.js +13 -11
  57. package/scripts/eval-run.js +30 -29
  58. package/scripts/export.js +27 -31
  59. package/scripts/generate-controlnet.js +31 -17
  60. package/scripts/generate-identity.js +37 -20
  61. package/scripts/generate-ipadapter.js +22 -16
  62. package/scripts/generate.js +68 -13
  63. package/scripts/implementation-pack.js +19 -14
  64. package/scripts/init.js +137 -17
  65. package/scripts/painterly.js +8 -10
  66. package/scripts/refine.js +119 -0
  67. package/scripts/reingest-selected.js +78 -0
  68. package/scripts/reingest.js +37 -18
  69. package/scripts/run-generate.js +76 -0
  70. package/scripts/run-list.js +58 -0
  71. package/scripts/run-show.js +63 -0
  72. package/scripts/select.js +119 -0
  73. package/scripts/selection-show.js +88 -0
  74. package/scripts/snapshot.js +22 -24
  75. package/scripts/split.js +26 -31
  76. package/scripts/training-manifest.js +29 -24
  77. package/scripts/training-package.js +24 -20
  78. package/scripts/training-profile.js +13 -8
  79. package/scripts/workflow-list.js +61 -0
  80. package/scripts/workflow-show.js +97 -0
  81. package/templates/domains/architecture/inputs/prompts/example-wave.json +32 -0
  82. package/templates/domains/architecture/terminology.json +10 -8
  83. package/templates/domains/architecture/workflows/profiles/environment-moodboard.json +62 -0
  84. package/templates/domains/architecture/workflows/profiles/interior-variant-pack.json +60 -0
  85. package/templates/domains/character-design/inputs/prompts/example-wave.json +32 -0
  86. package/templates/domains/character-design/project.json +4 -2
  87. package/templates/domains/character-design/workflows/profiles/character-portrait-set.json +61 -0
  88. package/templates/domains/character-design/workflows/profiles/expression-sheet.json +59 -0
  89. package/templates/domains/creature-design/inputs/prompts/example-wave.json +32 -0
  90. package/templates/domains/creature-design/project.json +4 -2
  91. package/templates/domains/creature-design/terminology.json +1 -0
  92. package/templates/domains/creature-design/workflows/profiles/creature-portrait-set.json +61 -0
  93. package/templates/domains/creature-design/workflows/profiles/silhouette-exploration.json +58 -0
  94. package/templates/domains/game-art/inputs/prompts/example-wave.json +37 -0
  95. package/templates/domains/game-art/project.json +4 -2
  96. package/templates/domains/game-art/workflows/profiles/character-portrait-set.json +62 -0
  97. package/templates/domains/game-art/workflows/profiles/environment-moodboard.json +61 -0
  98. package/templates/domains/vehicle-mech/inputs/prompts/example-wave.json +37 -0
  99. package/templates/domains/vehicle-mech/project.json +4 -2
  100. package/templates/domains/vehicle-mech/workflows/profiles/variant-pack.json +60 -0
  101. package/templates/domains/vehicle-mech/workflows/profiles/vehicle-silhouette-pack.json +59 -0
  102. package/scripts/bulk-curate-wave2-5.js +0 -138
  103. package/scripts/bulk-curate-waves11-18.js +0 -413
  104. package/scripts/curate-wave25.js +0 -209
  105. package/scripts/migrate-records.js +0 -141
  106. package/templates/inputs/prompts/example-wave.json +0 -27
@@ -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, getProjectName } 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' },
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 || getProjectName(argv);
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
+ }
@@ -5,38 +5,51 @@
5
5
  *
6
6
  * Usage:
7
7
  * sdlab reingest generated --source <dir> --manifest <id> [--dry-run] [--project <name>]
8
+ * sdlab reingest selected --selection <id> [--project <name>]
8
9
  * sdlab reingest audit [--project <name>]
9
10
  */
10
11
 
11
12
  import { join } from 'node:path';
12
- import { getProjectName } from '../lib/args.js';
13
- import { REPO_ROOT } from '../lib/paths.js';
13
+ import { parseArgs, getProjectName } from '../lib/args.js';
14
+ import { REPO_ROOT, resolveSafeProjectPath } from '../lib/paths.js';
15
+ import { inputError, handleCliError } from '../lib/errors.js';
14
16
  import { reingestGenerated, auditReingest } from '../lib/reingest.js';
15
17
 
16
18
  export async function run(argv = process.argv.slice(2)) {
17
- const projectName = getProjectName(argv);
19
+ const { flags, positionals } = parseArgs(argv, {
20
+ flags: {
21
+ project: { type: 'string' },
22
+ source: { type: 'string' },
23
+ manifest: { type: 'string' },
24
+ selection: { type: 'string' },
25
+ 'dry-run': { type: 'boolean', default: false },
26
+ },
27
+ deprecated: { game: 'project' },
28
+ allowUnknown: true, // forward unknowns to delegated 'selected' path
29
+ });
30
+
31
+ const projectName = flags.project || getProjectName(argv);
18
32
  const projectRoot = join(REPO_ROOT, 'projects', projectName);
19
- const subcommand = argv.find(a => !a.startsWith('--')) || 'audit';
20
- const args = argv.filter(a => a !== subcommand);
33
+ const subcommand = positionals[0] || 'audit';
21
34
 
22
35
  if (subcommand === 'generated') {
23
- const sourceIdx = args.indexOf('--source');
24
- if (sourceIdx < 0) throw new Error('--source <dir> is required (directory of generated images)');
25
- const sourcePath = args[sourceIdx + 1];
26
-
27
- const manifestIdx = args.indexOf('--manifest');
28
- if (manifestIdx < 0) throw new Error('--manifest <id> is required (training manifest that produced the outputs)');
29
- const manifestId = args[manifestIdx + 1];
36
+ if (!flags.source) {
37
+ throw inputError('INPUT_MISSING_FLAG', '--source <dir> is required (directory of generated images)');
38
+ }
39
+ if (!flags.manifest) {
40
+ throw inputError('INPUT_MISSING_FLAG', '--manifest <id> is required (training manifest that produced the outputs)');
41
+ }
30
42
 
31
- const dryRun = args.includes('--dry-run');
43
+ const safeSource = resolveSafeProjectPath(projectRoot, flags.source, { baseRoot: REPO_ROOT, flagName: 'source' });
44
+ const dryRun = flags['dry-run'] || flags.dryRun;
32
45
 
33
46
  console.log(`\x1b[1msdlab reingest generated\x1b[0m — ${projectName}`);
34
- console.log(` Source: ${sourcePath}`);
35
- console.log(` Manifest: ${manifestId}`);
47
+ console.log(` Source: ${flags.source}`);
48
+ console.log(` Manifest: ${flags.manifest}`);
36
49
  if (dryRun) console.log(` Mode: DRY RUN (no records written)`);
37
50
  console.log('');
38
51
 
39
- const result = await reingestGenerated(projectRoot, sourcePath, manifestId, { dryRun });
52
+ const result = await reingestGenerated(projectRoot, safeSource, flags.manifest, { dryRun });
40
53
 
41
54
  console.log(` \x1b[32m✓\x1b[0m Created: ${result.created} records`);
42
55
  if (result.skipped > 0) {
@@ -50,6 +63,12 @@ export async function run(argv = process.argv.slice(2)) {
50
63
  console.log(' 3. Then eligible for future snapshots');
51
64
  }
52
65
 
66
+ } else if (subcommand === 'selected') {
67
+ // Delegate to reingest-selected.js — pass the full argv so it can re-parse
68
+ const { run: runSelected } = await import('./reingest-selected.js');
69
+ await runSelected(argv.slice(argv.indexOf(subcommand) + 1));
70
+ return;
71
+
53
72
  } else if (subcommand === 'audit') {
54
73
  console.log(`\x1b[1msdlab reingest audit\x1b[0m — ${projectName}\n`);
55
74
 
@@ -77,10 +96,10 @@ export async function run(argv = process.argv.slice(2)) {
77
96
  }
78
97
 
79
98
  } else {
80
- throw new Error(`Unknown subcommand: ${subcommand}. Use: generated, audit`);
99
+ throw inputError('INPUT_BAD_SUBCOMMAND', `Unknown subcommand: ${subcommand}. Use: generated, selected, audit`);
81
100
  }
82
101
  }
83
102
 
84
103
  if (process.argv[1] && (process.argv[1].endsWith('reingest.js') || process.argv[1].endsWith('reingest'))) {
85
- run().catch((err) => { console.error(err.message || err); process.exit(1); });
104
+ run().catch(handleCliError);
86
105
  }
@@ -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, getProjectName, parseNumberFlag } 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' },
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 || getProjectName(argv);
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 ? parseNumberFlag('seed', flags.seed, { int: true, min: 0 }) : 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, getProjectName } 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' },
19
+ json: { type: 'boolean' },
20
+ },
21
+ deprecated: { game: 'project' },
22
+ });
23
+
24
+ const projectName = flags.project || getProjectName(argv);
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, getProjectName } 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' },
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 || getProjectName(argv);
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,119 @@
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, getProjectName } from '../lib/args.js';
13
+ import { getProjectRoot } from '../lib/paths.js';
14
+ import { inputError, handleCliError } from '../lib/errors.js';
15
+ import {
16
+ createSelectionFromRun,
17
+ createSelectionFromBatch,
18
+ saveSelection,
19
+ } from '../lib/selections.js';
20
+ import { info } from '../lib/log.js';
21
+
22
+ export async function run(argv = process.argv.slice(2)) {
23
+ const { flags } = parseArgs(argv, {
24
+ flags: {
25
+ project: { type: 'string' },
26
+ run: { type: 'string' },
27
+ batch: { type: 'string' },
28
+ approve: { type: 'string' },
29
+ reason: { type: 'string' },
30
+ tags: { type: 'string' },
31
+ json: { type: 'boolean' },
32
+ },
33
+ deprecated: { game: 'project' },
34
+ });
35
+
36
+ if (!flags.approve || (!flags.run && !flags.batch)) {
37
+ console.log('Usage: sdlab select --run <id> --approve <files>');
38
+ console.log(' sdlab select --batch <id> --approve <slot:file,...>');
39
+ console.log('');
40
+ console.log('Options:');
41
+ console.log(' --run <id> Source run');
42
+ console.log(' --batch <id> Source batch');
43
+ console.log(' --approve <list> Comma-separated files (run) or slot:file pairs (batch)');
44
+ console.log(' --reason <text> Why these outputs were chosen');
45
+ console.log(' --tags <list> Comma-separated tags');
46
+ console.log(' --json Output manifest as JSON');
47
+ return;
48
+ }
49
+
50
+ const projectName = flags.project || getProjectName(argv);
51
+ const projectRoot = getProjectRoot(projectName);
52
+ const reason = flags.reason || undefined;
53
+ const tags = flags.tags ? flags.tags.split(',').map(t => t.trim()) : undefined;
54
+
55
+ let result;
56
+
57
+ if (flags.run) {
58
+ // Run selection: --approve 001.png,003.png
59
+ const approvedFiles = flags.approve.split(',').map(f => f.trim());
60
+
61
+ result = await createSelectionFromRun({
62
+ projectRoot,
63
+ projectId: projectName,
64
+ runId: flags.run,
65
+ approvedFiles,
66
+ reason,
67
+ tags,
68
+ });
69
+ } else if (flags.batch) {
70
+ // Batch selection: --approve slot_neutral:001.png,slot_anger:001.png
71
+ const pairs = flags.approve.split(',').map(p => p.trim());
72
+ const approvedSlots = pairs.map(pair => {
73
+ const colonIdx = pair.indexOf(':');
74
+ if (colonIdx < 0) {
75
+ throw inputError('INPUT_BAD_FORMAT', `Invalid batch approve format: "${pair}". Use slot_id:filename`);
76
+ }
77
+ return {
78
+ slotId: pair.slice(0, colonIdx),
79
+ filename: pair.slice(colonIdx + 1),
80
+ };
81
+ });
82
+
83
+ result = await createSelectionFromBatch({
84
+ projectRoot,
85
+ projectId: projectName,
86
+ batchId: flags.batch,
87
+ approvedSlots,
88
+ reason,
89
+ tags,
90
+ });
91
+ }
92
+
93
+ const { selectionId, selectionDir, manifest } = result;
94
+
95
+ // Save the selection
96
+ await saveSelection(selectionDir, manifest);
97
+
98
+ if (flags.json) {
99
+ console.log(JSON.stringify(manifest, null, 2));
100
+ return;
101
+ }
102
+
103
+ console.log(`\x1b[1mstyle-dataset-lab\x1b[0m select`);
104
+ console.log('');
105
+ console.log(` Selection: ${selectionId}`);
106
+ console.log(` Source: ${manifest.source_type} ${manifest.source_id}`);
107
+ if (manifest.workflow_id) console.log(` Workflow: ${manifest.workflow_id}`);
108
+ if (manifest.subject_id) console.log(` Subject: ${manifest.subject_id}`);
109
+ console.log(` Items: ${manifest.items.length}`);
110
+ console.log('');
111
+
112
+ for (const item of manifest.items) {
113
+ console.log(` ✓ ${item.slot_or_output.padEnd(28)} → ${item.filename} ${item.reason}`);
114
+ }
115
+
116
+ console.log('');
117
+ info(`Selection: selections/${selectionId}/manifest.json`);
118
+ info(`Chosen files: selections/${selectionId}/chosen/`);
119
+ }
@@ -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, getProjectName } 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' },
22
+ selection: { type: 'string' },
23
+ json: { type: 'boolean' },
24
+ },
25
+ deprecated: { game: 'project' },
26
+ });
27
+
28
+ const projectName = flags.project || getProjectName(argv);
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
+ }
@@ -12,30 +12,33 @@
12
12
 
13
13
  import { readFile } from 'node:fs/promises';
14
14
  import { join } from 'node:path';
15
- import { existsSync } from 'node:fs';
16
- import { getProjectName } from '../lib/args.js';
15
+ import { parseArgs, getProjectName } from '../lib/args.js';
17
16
  import { REPO_ROOT } from '../lib/paths.js';
18
17
  import { loadSelectionProfile } from '../lib/config.js';
18
+ import { inputError, handleCliError } from '../lib/errors.js';
19
19
  import { createSnapshot, loadSnapshot, listSnapshots, diffSnapshots } from '../lib/snapshot.js';
20
- import { evaluateEligibility } from '../lib/eligibility.js';
21
20
  import { categorizeExclusions } from '../lib/eligibility.js';
22
21
 
23
22
  export async function run(argv = process.argv.slice(2)) {
24
- const projectName = getProjectName(argv);
25
- const projectRoot = join(REPO_ROOT, 'projects', projectName);
26
- const subcommand = argv.find(a => !a.startsWith('--')) || 'list';
23
+ const { flags, positionals } = parseArgs(argv, {
24
+ flags: {
25
+ project: { type: 'string' },
26
+ profile: { type: 'string' },
27
+ 'dry-run': { type: 'boolean', default: false },
28
+ },
29
+ deprecated: { game: 'project' },
30
+ });
27
31
 
28
- // Strip subcommand from argv for flag parsing
29
- const args = argv.filter(a => a !== subcommand);
32
+ const projectName = flags.project || getProjectName(argv);
33
+ const projectRoot = join(REPO_ROOT, 'projects', projectName);
34
+ const subcommand = positionals[0] || 'list';
35
+ const dryRun = flags['dry-run'] || flags.dryRun;
30
36
 
31
37
  if (subcommand === 'create') {
32
- const profileIdx = args.indexOf('--profile');
33
- const profileId = profileIdx >= 0 ? args[profileIdx + 1] : null;
34
- const profile = loadSelectionProfile(projectRoot, profileId);
35
- const dryRun = argv.includes('--dry-run');
38
+ const profile = loadSelectionProfile(projectRoot, flags.profile);
36
39
 
37
40
  console.log(`\x1b[1msdlab snapshot create\x1b[0m — ${projectName}`);
38
- console.log(` Profile: ${profileId || 'training-default (built-in)'}`);
41
+ console.log(` Profile: ${flags.profile || 'training-default (built-in)'}`);
39
42
  if (dryRun) console.log(' Mode: DRY RUN (no files written)');
40
43
  console.log('');
41
44
 
@@ -46,7 +49,6 @@ export async function run(argv = process.argv.slice(2)) {
46
49
  console.log(` Excluded: ${result.excluded}`);
47
50
 
48
51
  if (!dryRun) {
49
- // Read and display summary
50
52
  const summary = JSON.parse(
51
53
  await readFile(join(projectRoot, 'snapshots', result.snapshotId, 'summary.json'), 'utf-8')
52
54
  );
@@ -59,7 +61,6 @@ export async function run(argv = process.argv.slice(2)) {
59
61
  console.log(` ${f}: ${c}`);
60
62
  }
61
63
 
62
- // Read and categorize exclusions
63
64
  const excludedRaw = await readFile(
64
65
  join(projectRoot, 'snapshots', result.snapshotId, 'excluded.jsonl'), 'utf-8'
65
66
  );
@@ -83,8 +84,8 @@ export async function run(argv = process.argv.slice(2)) {
83
84
  }
84
85
 
85
86
  } else if (subcommand === 'show') {
86
- const snapshotId = args.find(a => a.startsWith('snap-'));
87
- if (!snapshotId) throw new Error('Usage: sdlab snapshot show <snapshot-id>');
87
+ const snapshotId = positionals[1] || positionals.find(a => a.startsWith('snap-'));
88
+ if (!snapshotId) throw inputError('INPUT_MISSING_ARGS', 'Usage: sdlab snapshot show <snapshot-id>');
88
89
 
89
90
  const manifest = await loadSnapshot(projectRoot, snapshotId);
90
91
  console.log(`\x1b[1mSnapshot\x1b[0m — ${snapshotId}\n`);
@@ -102,8 +103,8 @@ export async function run(argv = process.argv.slice(2)) {
102
103
  }
103
104
 
104
105
  } else if (subcommand === 'diff') {
105
- const snapIds = args.filter(a => a.startsWith('snap-'));
106
- if (snapIds.length < 2) throw new Error('Usage: sdlab snapshot diff <id-a> <id-b>');
106
+ const snapIds = positionals.slice(1).filter(a => a.startsWith('snap-'));
107
+ if (snapIds.length < 2) throw inputError('INPUT_MISSING_ARGS', 'Usage: sdlab snapshot diff <id-a> <id-b>');
107
108
 
108
109
  const diff = await diffSnapshots(projectRoot, snapIds[0], snapIds[1]);
109
110
  console.log(`\x1b[1mSnapshot diff\x1b[0m — ${snapIds[0]} vs ${snapIds[1]}\n`);
@@ -123,14 +124,11 @@ export async function run(argv = process.argv.slice(2)) {
123
124
  }
124
125
 
125
126
  } else {
126
- throw new Error(`Unknown subcommand: ${subcommand}. Use: create, list, show, diff`);
127
+ throw inputError('INPUT_BAD_SUBCOMMAND', `Unknown subcommand: ${subcommand}. Use: create, list, show, diff`);
127
128
  }
128
129
  }
129
130
 
130
131
  // Direct execution guard
131
132
  if (process.argv[1] && (process.argv[1].endsWith('snapshot.js') || process.argv[1].endsWith('snapshot'))) {
132
- run().catch((err) => {
133
- console.error(err.message || err);
134
- process.exit(1);
135
- });
133
+ run().catch(handleCliError);
136
134
  }