@mcptoolshop/style-dataset-lab 3.0.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 (88) hide show
  1. package/CHANGELOG.md +68 -15
  2. package/CODE_OF_CONDUCT.md +9 -0
  3. package/CONTRIBUTING.md +71 -0
  4. package/README.md +46 -1
  5. package/SECURITY.md +6 -5
  6. package/bin/sdlab.js +281 -62
  7. package/lib/adapters/diffusers-lora.js +7 -2
  8. package/lib/adapters/generic-image-caption.js +6 -1
  9. package/lib/args.js +200 -13
  10. package/lib/batch-compiler.js +7 -3
  11. package/lib/batch-runs.js +100 -31
  12. package/lib/batch-sheet-render.js +5 -2
  13. package/lib/brief-compiler.js +38 -11
  14. package/lib/card.js +11 -4
  15. package/lib/comfyui.js +100 -6
  16. package/lib/config.js +68 -3
  17. package/lib/deprecation.js +1 -1
  18. package/lib/eval-pack.js +94 -4
  19. package/lib/eval-runs.js +42 -3
  20. package/lib/export.js +144 -34
  21. package/lib/implementation-packs.js +15 -1
  22. package/lib/log.js +41 -4
  23. package/lib/paths.js +41 -3
  24. package/lib/refine-briefs.js +16 -9
  25. package/lib/reingest-selected.js +14 -3
  26. package/lib/reingest.js +15 -1
  27. package/lib/run-summary.js +3 -2
  28. package/lib/runtime-runs.js +59 -9
  29. package/lib/selections.js +54 -2
  30. package/lib/snapshot.js +100 -7
  31. package/lib/split.js +175 -17
  32. package/lib/training-manifests.js +17 -3
  33. package/lib/training-packages.js +28 -14
  34. package/package.json +8 -2
  35. package/scripts/batch-generate.js +16 -7
  36. package/scripts/batch-sheet.js +3 -3
  37. package/scripts/batch-show.js +3 -3
  38. package/scripts/brief-compile.js +8 -8
  39. package/scripts/brief-show.js +4 -3
  40. package/scripts/canon-bind.js +7 -9
  41. package/scripts/card.js +22 -25
  42. package/scripts/compare.js +38 -33
  43. package/scripts/critique-show.js +3 -3
  44. package/scripts/critique.js +3 -3
  45. package/scripts/curate.js +91 -46
  46. package/scripts/doctor.js +3 -5
  47. package/scripts/eligibility.js +17 -14
  48. package/scripts/eval-pack.js +13 -11
  49. package/scripts/eval-run.js +30 -29
  50. package/scripts/export.js +27 -31
  51. package/scripts/generate-controlnet.js +31 -17
  52. package/scripts/generate-identity.js +37 -20
  53. package/scripts/generate-ipadapter.js +22 -16
  54. package/scripts/generate.js +68 -13
  55. package/scripts/implementation-pack.js +19 -14
  56. package/scripts/init.js +115 -17
  57. package/scripts/painterly.js +8 -10
  58. package/scripts/refine.js +3 -3
  59. package/scripts/reingest-selected.js +3 -3
  60. package/scripts/reingest.js +33 -20
  61. package/scripts/run-generate.js +4 -4
  62. package/scripts/run-list.js +3 -3
  63. package/scripts/run-show.js +3 -3
  64. package/scripts/select.js +5 -4
  65. package/scripts/selection-show.js +3 -3
  66. package/scripts/snapshot.js +22 -24
  67. package/scripts/split.js +26 -31
  68. package/scripts/training-manifest.js +29 -24
  69. package/scripts/training-package.js +24 -20
  70. package/scripts/training-profile.js +13 -8
  71. package/scripts/workflow-list.js +2 -2
  72. package/scripts/workflow-show.js +4 -3
  73. package/templates/domains/architecture/inputs/prompts/example-wave.json +32 -0
  74. package/templates/domains/architecture/terminology.json +10 -8
  75. package/templates/domains/character-design/inputs/prompts/example-wave.json +32 -0
  76. package/templates/domains/character-design/project.json +4 -2
  77. package/templates/domains/creature-design/inputs/prompts/example-wave.json +32 -0
  78. package/templates/domains/creature-design/project.json +4 -2
  79. package/templates/domains/creature-design/terminology.json +1 -0
  80. package/templates/domains/game-art/inputs/prompts/example-wave.json +37 -0
  81. package/templates/domains/game-art/project.json +4 -2
  82. package/templates/domains/vehicle-mech/inputs/prompts/example-wave.json +37 -0
  83. package/templates/domains/vehicle-mech/project.json +4 -2
  84. package/scripts/bulk-curate-wave2-5.js +0 -138
  85. package/scripts/bulk-curate-waves11-18.js +0 -413
  86. package/scripts/curate-wave25.js +0 -209
  87. package/scripts/migrate-records.js +0 -141
  88. package/templates/inputs/prompts/example-wave.json +0 -27
package/scripts/init.js CHANGED
@@ -13,6 +13,7 @@ import { mkdir, readdir, readFile, writeFile, copyFile } from 'node:fs/promises'
13
13
  import { join, basename } from 'node:path';
14
14
  import { existsSync } from 'node:fs';
15
15
  import { REPO_ROOT } from '../lib/paths.js';
16
+ import { inputError, handleCliError } from '../lib/errors.js';
16
17
 
17
18
  const TEMPLATES_DIR = join(REPO_ROOT, 'templates');
18
19
  const PROJECTS_DIR = join(REPO_ROOT, 'projects');
@@ -82,15 +83,28 @@ export async function run(argv = process.argv.slice(2)) {
82
83
  }
83
84
 
84
85
  if (!isValidName(projectName)) {
85
- throw new Error(
86
- `Invalid project name "${projectName}".\n` +
86
+ throw inputError(
87
+ 'INPUT_BAD_NAME',
88
+ `Invalid project name "${projectName}".`,
87
89
  `Use lowercase letters, numbers, and hyphens (e.g. "my-project").`
88
90
  );
89
91
  }
90
92
 
93
+ // Validate --domain to prevent path traversal via template lookup.
94
+ if (!isValidName(domain)) {
95
+ throw inputError(
96
+ 'INPUT_BAD_DOMAIN',
97
+ `Invalid --domain "${domain}".`,
98
+ `Use lowercase letters, numbers, and hyphens (e.g. "character-design").`
99
+ );
100
+ }
101
+
91
102
  const projectDir = join(PROJECTS_DIR, projectName);
92
103
  if (existsSync(projectDir)) {
93
- throw new Error(`Project "${projectName}" already exists at ${projectDir}`);
104
+ throw inputError(
105
+ 'INPUT_PROJECT_EXISTS',
106
+ `Project "${projectName}" already exists at ${projectDir}`
107
+ );
94
108
  }
95
109
 
96
110
  // Resolve domain template source
@@ -99,8 +113,9 @@ export async function run(argv = process.argv.slice(2)) {
99
113
  domainDir = join(TEMPLATES_DIR, 'domains', domain);
100
114
  if (!existsSync(domainDir)) {
101
115
  const domains = await listDomains();
102
- throw new Error(
103
- `Domain "${domain}" not found.\n` +
116
+ throw inputError(
117
+ 'INPUT_UNKNOWN_DOMAIN',
118
+ `Domain "${domain}" not found.`,
104
119
  `Available: generic, ${domains.join(', ')}`
105
120
  );
106
121
  }
@@ -206,18 +221,30 @@ export async function run(argv = process.argv.slice(2)) {
206
221
 
207
222
  // Copy canon markdown templates
208
223
  const canonTemplates = ['constitution.md', 'review-rubric.md'];
224
+ const wroteCanon = {};
209
225
  for (const file of canonTemplates) {
210
226
  const src = join(TEMPLATES_DIR, 'canon', file);
211
227
  if (existsSync(src)) {
212
228
  await copyFile(src, join(projectDir, 'canon', file));
229
+ wroteCanon[file] = true;
213
230
  console.log(` \x1b[32m✓\x1b[0m canon/${file}`);
214
231
  }
215
232
  }
216
233
 
217
- // Copy example prompt pack
218
- const examplePack = join(TEMPLATES_DIR, 'inputs', 'prompts', 'example-wave.json');
219
- if (existsSync(examplePack)) {
234
+ // Copy example prompt pack — prefer per-domain example (subjects match domain lane id_patterns),
235
+ // fall back to legacy global template if it exists (pre-v3.0.1 installs may still ship it).
236
+ let wroteExamplePack = false;
237
+ const domainExamplePack = domainDir
238
+ ? join(domainDir, 'inputs', 'prompts', 'example-wave.json')
239
+ : null;
240
+ const globalExamplePack = join(TEMPLATES_DIR, 'inputs', 'prompts', 'example-wave.json');
241
+ const examplePack =
242
+ domainExamplePack && existsSync(domainExamplePack) ? domainExamplePack
243
+ : existsSync(globalExamplePack) ? globalExamplePack
244
+ : null;
245
+ if (examplePack) {
220
246
  await copyFile(examplePack, join(projectDir, 'inputs', 'prompts', 'example-wave.json'));
247
+ wroteExamplePack = true;
221
248
  console.log(` \x1b[32m✓\x1b[0m inputs/prompts/example-wave.json`);
222
249
  }
223
250
 
@@ -236,21 +263,92 @@ export async function run(argv = process.argv.slice(2)) {
236
263
  }
237
264
  }
238
265
 
266
+ // Write a starter README.md inside the project so users have a home base
267
+ // with copy-pasteable next commands and a place for their notes.
268
+ const readmeLines = [
269
+ `# ${projectName}`,
270
+ '',
271
+ `A \`style-dataset-lab\` project — domain: **${domain}**.`,
272
+ '',
273
+ '## Quick commands',
274
+ '',
275
+ '```bash',
276
+ `# Validate that the scaffold is healthy`,
277
+ `sdlab project doctor --project ${projectName}`,
278
+ '',
279
+ ];
280
+ if (wroteExamplePack) {
281
+ readmeLines.push(
282
+ `# Generate candidates from the example prompt pack`,
283
+ `sdlab generate inputs/prompts/example-wave.json --project ${projectName}`,
284
+ ''
285
+ );
286
+ } else {
287
+ readmeLines.push(
288
+ `# Generate candidates (add a prompt pack under inputs/prompts/ first)`,
289
+ `sdlab generate <pack-path> --project ${projectName}`,
290
+ ''
291
+ );
292
+ }
293
+ readmeLines.push(
294
+ `# Curate a candidate (approved | rejected | borderline)`,
295
+ `sdlab curate <asset_id> approved "reads clean" --project ${projectName}`,
296
+ '',
297
+ `# Bind approved records to constitution rules`,
298
+ `sdlab canon-bind --project ${projectName}`,
299
+ '',
300
+ `# Create a frozen dataset snapshot`,
301
+ `sdlab snapshot create --project ${projectName}`,
302
+ '```',
303
+ '',
304
+ '## Project layout',
305
+ '',
306
+ '- `project.json` — defaults (checkpoint, width, height, sampler)',
307
+ '- `constitution.json` — machine-readable style rules',
308
+ '- `canon/` — human-authored style reference (constitution.md, review-rubric.md)',
309
+ '- `inputs/prompts/` — generation prompt packs',
310
+ '- `outputs/candidates/` — raw generated PNGs',
311
+ '- `outputs/approved/` · `outputs/rejected/` · `outputs/borderline/` — curated outputs',
312
+ '- `records/` — provenance + judgment records (one JSON per candidate)',
313
+ '- `snapshots/` · `splits/` · `exports/` — frozen dataset artifacts',
314
+ '',
315
+ '## Docs',
316
+ '',
317
+ '- Handbook: https://mcp-tool-shop-org.github.io/style-dataset-lab/',
318
+ '- CLI reference: `sdlab --help` (or `sdlab <cmd> --help` for any command)',
319
+ '',
320
+ '## TODO',
321
+ '',
322
+ '- [ ] Fill in `canon/constitution.md` with your style rules',
323
+ '- [ ] Encode those rules in `constitution.json` for canon-bind',
324
+ '- [ ] Add at least one prompt pack under `inputs/prompts/`',
325
+ '- [ ] Run `sdlab project doctor` and fix any warnings',
326
+ ''
327
+ );
328
+ await writeFile(join(projectDir, 'README.md'), readmeLines.join('\n'));
329
+ console.log(` \x1b[32m✓\x1b[0m README.md`);
330
+
239
331
  console.log('');
240
332
  console.log(`\x1b[32m✓\x1b[0m Project "${projectName}" initialized`);
241
333
  console.log('');
242
334
  console.log('Next steps:');
243
- console.log(` 1. Edit ${projectName}/canon/constitution.md — define your visual rules`);
244
- console.log(` 2. Edit ${projectName}/constitution.json — encode rules for canon-bind`);
245
- console.log(` 3. Create prompt packs in ${projectName}/inputs/prompts/`);
246
- console.log(` 4. Run: sdlab generate <pack> --project ${projectName}`);
247
- console.log(` 5. Run: sdlab project doctor --project ${projectName}`);
335
+ let step = 1;
336
+ if (wroteCanon['constitution.md']) {
337
+ console.log(` ${step++}. Edit ${projectName}/canon/constitution.md — define your visual rules`);
338
+ }
339
+ console.log(` ${step++}. Edit ${projectName}/constitution.json — encode rules for canon-bind`);
340
+ if (wroteExamplePack) {
341
+ console.log(` ${step++}. Run: sdlab generate inputs/prompts/example-wave.json --project ${projectName}`);
342
+ } else {
343
+ console.log(` ${step++}. Create prompt packs under ${projectName}/inputs/prompts/`);
344
+ console.log(` ${step++}. Run: sdlab generate <pack> --project ${projectName}`);
345
+ }
346
+ console.log(` ${step++}. Run: sdlab project doctor --project ${projectName}`);
347
+ console.log('');
348
+ console.log(`See ${projectName}/README.md for the full quick-start.`);
248
349
  }
249
350
 
250
351
  // Direct execution guard
251
352
  if (process.argv[1] && (process.argv[1].endsWith('init.js') || process.argv[1].endsWith('init'))) {
252
- run().catch((err) => {
253
- console.error(err.message || err);
254
- process.exit(1);
255
- });
353
+ run().catch(handleCliError);
256
354
  }
@@ -18,8 +18,9 @@
18
18
  import { writeFile, mkdir, readdir } from "node:fs/promises";
19
19
  import { join } from "node:path";
20
20
  import { existsSync } from "node:fs";
21
- import { getProjectName } from "../lib/args.js";
22
- import { REPO_ROOT } from "../lib/paths.js";
21
+ import { getProjectName, parseNumberFlag } from "../lib/args.js";
22
+ import { REPO_ROOT, resolveSafeProjectPath } from "../lib/paths.js";
23
+ import { runtimeError, handleCliError } from "../lib/errors.js";
23
24
  import { comfyHealth, submitAndWait, downloadImage, uploadImage } from "../lib/comfyui.js";
24
25
 
25
26
  // ── Config ──
@@ -156,11 +157,11 @@ export async function run(argv = process.argv.slice(2)) {
156
157
 
157
158
  for (let i = 0; i < argv.length; i++) {
158
159
  if (argv[i] === "--source" && argv[i + 1]) sourceDir = argv[++i];
159
- if (argv[i] === "--limit" && argv[i + 1]) limit = parseInt(argv[++i]);
160
- if (argv[i] === "--offset" && argv[i + 1]) offset = parseInt(argv[++i]);
160
+ else if (argv[i] === "--limit" && argv[i + 1]) limit = parseNumberFlag('limit', argv[++i], { int: true, min: 1 });
161
+ else if (argv[i] === "--offset" && argv[i + 1]) offset = parseNumberFlag('offset', argv[++i], { int: true, min: 0 });
161
162
  }
162
163
 
163
- const fullSourceDir = join(GAME_ROOT, sourceDir);
164
+ const fullSourceDir = resolveSafeProjectPath(GAME_ROOT, sourceDir, { flagName: 'source' });
164
165
  const outDir = join(GAME_ROOT, "outputs/painterly");
165
166
  await mkdir(outDir, { recursive: true });
166
167
 
@@ -195,7 +196,7 @@ export async function run(argv = process.argv.slice(2)) {
195
196
  if (!dryRun) {
196
197
  const online = await comfyHealth(COMFY_URL);
197
198
  if (!online) {
198
- throw new Error("ComfyUI not reachable at " + COMFY_URL);
199
+ throw runtimeError('RUNTIME_COMFY_UNREACHABLE', "ComfyUI not reachable at " + COMFY_URL);
199
200
  }
200
201
  console.log("\x1b[32m✓\x1b[0m ComfyUI online");
201
202
  }
@@ -263,8 +264,5 @@ export async function run(argv = process.argv.slice(2)) {
263
264
 
264
265
  // Direct execution guard
265
266
  if (process.argv[1] && (process.argv[1].endsWith('painterly.js') || process.argv[1].endsWith('painterly'))) {
266
- run().catch((err) => {
267
- console.error(err.message || err);
268
- process.exit(1);
269
- });
267
+ run().catch(handleCliError);
270
268
  }
package/scripts/refine.js CHANGED
@@ -11,7 +11,7 @@
11
11
 
12
12
  import { readFileSync } from 'node:fs';
13
13
  import { join } from 'node:path';
14
- import { parseArgs } from '../lib/args.js';
14
+ import { parseArgs, getProjectName } from '../lib/args.js';
15
15
  import { getProjectRoot, getRunsDir } from '../lib/paths.js';
16
16
  import { loadCritique } from '../lib/critique-engine.js';
17
17
  import {
@@ -25,7 +25,7 @@ import { info } from '../lib/log.js';
25
25
  export async function run(argv = process.argv.slice(2)) {
26
26
  const { flags } = parseArgs(argv, {
27
27
  flags: {
28
- project: { type: 'string', default: 'star-freight' },
28
+ project: { type: 'string' },
29
29
  run: { type: 'string' },
30
30
  pick: { type: 'string' },
31
31
  preserve: { type: 'string' },
@@ -49,7 +49,7 @@ export async function run(argv = process.argv.slice(2)) {
49
49
  return;
50
50
  }
51
51
 
52
- const projectName = flags.project;
52
+ const projectName = flags.project || getProjectName(argv);
53
53
  const projectRoot = getProjectRoot(projectName);
54
54
  const runId = flags.run;
55
55
  const pickedFilename = flags.pick;
@@ -8,7 +8,7 @@
8
8
  * sdlab reingest selected --selection selection_2026-04-16_001 --dry-run
9
9
  */
10
10
 
11
- import { parseArgs } from '../lib/args.js';
11
+ import { parseArgs, getProjectName } from '../lib/args.js';
12
12
  import { getProjectRoot } from '../lib/paths.js';
13
13
  import { reingestSelection } from '../lib/reingest-selected.js';
14
14
  import { info } from '../lib/log.js';
@@ -16,7 +16,7 @@ import { info } from '../lib/log.js';
16
16
  export async function run(argv = process.argv.slice(2)) {
17
17
  const { flags } = parseArgs(argv, {
18
18
  flags: {
19
- project: { type: 'string', default: 'star-freight' },
19
+ project: { type: 'string' },
20
20
  selection: { type: 'string' },
21
21
  'dry-run': { type: 'boolean' },
22
22
  json: { type: 'boolean' },
@@ -34,7 +34,7 @@ export async function run(argv = process.argv.slice(2)) {
34
34
  return;
35
35
  }
36
36
 
37
- const projectName = flags.project;
37
+ const projectName = flags.project || getProjectName(argv);
38
38
  const projectRoot = getProjectRoot(projectName);
39
39
  const dryRun = flags['dry-run'] || false;
40
40
 
@@ -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) {
@@ -51,9 +64,9 @@ export async function run(argv = process.argv.slice(2)) {
51
64
  }
52
65
 
53
66
  } else if (subcommand === 'selected') {
54
- // Delegate to reingest-selected.js
67
+ // Delegate to reingest-selected.js — pass the full argv so it can re-parse
55
68
  const { run: runSelected } = await import('./reingest-selected.js');
56
- await runSelected(args);
69
+ await runSelected(argv.slice(argv.indexOf(subcommand) + 1));
57
70
  return;
58
71
 
59
72
  } else if (subcommand === 'audit') {
@@ -83,10 +96,10 @@ export async function run(argv = process.argv.slice(2)) {
83
96
  }
84
97
 
85
98
  } else {
86
- throw new Error(`Unknown subcommand: ${subcommand}. Use: generated, selected, audit`);
99
+ throw inputError('INPUT_BAD_SUBCOMMAND', `Unknown subcommand: ${subcommand}. Use: generated, selected, audit`);
87
100
  }
88
101
  }
89
102
 
90
103
  if (process.argv[1] && (process.argv[1].endsWith('reingest.js') || process.argv[1].endsWith('reingest'))) {
91
- run().catch((err) => { console.error(err.message || err); process.exit(1); });
104
+ run().catch(handleCliError);
92
105
  }
@@ -9,7 +9,7 @@
9
9
  * sdlab run generate --brief brief_2026-04-15_001 --project star-freight --seed 42
10
10
  */
11
11
 
12
- import { parseArgs } from '../lib/args.js';
12
+ import { parseArgs, getProjectName, parseNumberFlag } from '../lib/args.js';
13
13
  import { getProjectRoot, getRunsDir } from '../lib/paths.js';
14
14
  import { loadCompiledBrief } from '../lib/brief-compiler.js';
15
15
  import { executeRun } from '../lib/adapters/comfyui-runner.js';
@@ -20,7 +20,7 @@ import { join } from 'node:path';
20
20
  export async function run(argv = process.argv.slice(2)) {
21
21
  const { flags } = parseArgs(argv, {
22
22
  flags: {
23
- project: { type: 'string', default: 'star-freight' },
23
+ project: { type: 'string' },
24
24
  brief: { type: 'string' },
25
25
  seed: { type: 'string' },
26
26
  'dry-run': { type: 'boolean' },
@@ -40,7 +40,7 @@ export async function run(argv = process.argv.slice(2)) {
40
40
  return;
41
41
  }
42
42
 
43
- const projectName = flags.project;
43
+ const projectName = flags.project || getProjectName(argv);
44
44
  const projectRoot = getProjectRoot(projectName);
45
45
 
46
46
  console.log(`\x1b[1mstyle-dataset-lab\x1b[0m run generate`);
@@ -57,7 +57,7 @@ export async function run(argv = process.argv.slice(2)) {
57
57
  projectRoot,
58
58
  projectName,
59
59
  brief,
60
- baseSeed: flags.seed ? parseInt(flags.seed, 10) : undefined,
60
+ baseSeed: flags.seed ? parseNumberFlag('seed', flags.seed, { int: true, min: 0 }) : undefined,
61
61
  dryRun: flags['dry-run'] || false,
62
62
  });
63
63
 
@@ -8,20 +8,20 @@
8
8
  * sdlab run list --project star-freight --json
9
9
  */
10
10
 
11
- import { parseArgs } from '../lib/args.js';
11
+ import { parseArgs, getProjectName } from '../lib/args.js';
12
12
  import { getProjectRoot } from '../lib/paths.js';
13
13
  import { listRuns } from '../lib/runtime-runs.js';
14
14
 
15
15
  export async function run(argv = process.argv.slice(2)) {
16
16
  const { flags } = parseArgs(argv, {
17
17
  flags: {
18
- project: { type: 'string', default: 'star-freight' },
18
+ project: { type: 'string' },
19
19
  json: { type: 'boolean' },
20
20
  },
21
21
  deprecated: { game: 'project' },
22
22
  });
23
23
 
24
- const projectName = flags.project;
24
+ const projectName = flags.project || getProjectName(argv);
25
25
  const projectRoot = getProjectRoot(projectName);
26
26
  const runs = listRuns(projectRoot);
27
27
 
@@ -9,7 +9,7 @@
9
9
  * sdlab run show run_2026-04-16_001 --project star-freight --md
10
10
  */
11
11
 
12
- import { parseArgs } from '../lib/args.js';
12
+ import { parseArgs, getProjectName } from '../lib/args.js';
13
13
  import { getProjectRoot } from '../lib/paths.js';
14
14
  import { loadRun, listRuns } from '../lib/runtime-runs.js';
15
15
  import { renderRunText, renderRunMarkdown } from '../lib/run-summary.js';
@@ -17,7 +17,7 @@ import { renderRunText, renderRunMarkdown } from '../lib/run-summary.js';
17
17
  export async function run(argv = process.argv.slice(2)) {
18
18
  const { positionals, flags } = parseArgs(argv, {
19
19
  flags: {
20
- project: { type: 'string', default: 'star-freight' },
20
+ project: { type: 'string' },
21
21
  json: { type: 'boolean' },
22
22
  md: { type: 'boolean' },
23
23
  },
@@ -25,7 +25,7 @@ export async function run(argv = process.argv.slice(2)) {
25
25
  });
26
26
 
27
27
  const runId = positionals[0];
28
- const projectName = flags.project;
28
+ const projectName = flags.project || getProjectName(argv);
29
29
  const projectRoot = getProjectRoot(projectName);
30
30
 
31
31
  // No run ID → list runs
package/scripts/select.js CHANGED
@@ -9,8 +9,9 @@
9
9
  * sdlab select --run run_2026-04-15_001 --approve 001.png --reason "best continuity" --tags portrait,keeper
10
10
  */
11
11
 
12
- import { parseArgs } from '../lib/args.js';
12
+ import { parseArgs, getProjectName } from '../lib/args.js';
13
13
  import { getProjectRoot } from '../lib/paths.js';
14
+ import { inputError, handleCliError } from '../lib/errors.js';
14
15
  import {
15
16
  createSelectionFromRun,
16
17
  createSelectionFromBatch,
@@ -21,7 +22,7 @@ import { info } from '../lib/log.js';
21
22
  export async function run(argv = process.argv.slice(2)) {
22
23
  const { flags } = parseArgs(argv, {
23
24
  flags: {
24
- project: { type: 'string', default: 'star-freight' },
25
+ project: { type: 'string' },
25
26
  run: { type: 'string' },
26
27
  batch: { type: 'string' },
27
28
  approve: { type: 'string' },
@@ -46,7 +47,7 @@ export async function run(argv = process.argv.slice(2)) {
46
47
  return;
47
48
  }
48
49
 
49
- const projectName = flags.project;
50
+ const projectName = flags.project || getProjectName(argv);
50
51
  const projectRoot = getProjectRoot(projectName);
51
52
  const reason = flags.reason || undefined;
52
53
  const tags = flags.tags ? flags.tags.split(',').map(t => t.trim()) : undefined;
@@ -71,7 +72,7 @@ export async function run(argv = process.argv.slice(2)) {
71
72
  const approvedSlots = pairs.map(pair => {
72
73
  const colonIdx = pair.indexOf(':');
73
74
  if (colonIdx < 0) {
74
- throw new Error(`Invalid batch approve format: "${pair}". Use slot_id:filename`);
75
+ throw inputError('INPUT_BAD_FORMAT', `Invalid batch approve format: "${pair}". Use slot_id:filename`);
75
76
  }
76
77
  return {
77
78
  slotId: pair.slice(0, colonIdx),
@@ -9,7 +9,7 @@
9
9
  * sdlab selection show selection_2026-04-16_001 --json
10
10
  */
11
11
 
12
- import { parseArgs } from '../lib/args.js';
12
+ import { parseArgs, getProjectName } from '../lib/args.js';
13
13
  import { getProjectRoot } from '../lib/paths.js';
14
14
  import { loadSelection, listSelections, getSelectionsDir } from '../lib/selections.js';
15
15
  import { join } from 'node:path';
@@ -18,14 +18,14 @@ import { existsSync, readFileSync } from 'node:fs';
18
18
  export async function run(argv = process.argv.slice(2)) {
19
19
  const { flags, positionals } = parseArgs(argv, {
20
20
  flags: {
21
- project: { type: 'string', default: 'star-freight' },
21
+ project: { type: 'string' },
22
22
  selection: { type: 'string' },
23
23
  json: { type: 'boolean' },
24
24
  },
25
25
  deprecated: { game: 'project' },
26
26
  });
27
27
 
28
- const projectName = flags.project;
28
+ const projectName = flags.project || getProjectName(argv);
29
29
  const projectRoot = getProjectRoot(projectName);
30
30
  const selectionId = positionals[0] || flags.selection;
31
31
 
@@ -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
  }