@mcptoolshop/style-dataset-lab 3.0.0 → 3.1.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 (99) hide show
  1. package/CHANGELOG.md +108 -15
  2. package/CODE_OF_CONDUCT.md +9 -0
  3. package/CONTRIBUTING.md +71 -0
  4. package/README.es.md +99 -8
  5. package/README.fr.md +100 -9
  6. package/README.hi.md +102 -9
  7. package/README.it.md +99 -8
  8. package/README.ja.md +100 -9
  9. package/README.md +64 -1
  10. package/README.pt-BR.md +99 -8
  11. package/README.zh.md +100 -9
  12. package/SECURITY.md +6 -5
  13. package/bin/sdlab.js +289 -62
  14. package/lib/adapters/comfyui-runner.js +11 -15
  15. package/lib/adapters/diffusers-lora.js +7 -2
  16. package/lib/adapters/generic-image-caption.js +6 -1
  17. package/lib/args.js +200 -13
  18. package/lib/batch-compiler.js +7 -3
  19. package/lib/batch-runs.js +162 -31
  20. package/lib/batch-sheet-render.js +5 -2
  21. package/lib/brief-compiler.js +38 -11
  22. package/lib/card.js +11 -4
  23. package/lib/comfyui-output.js +70 -0
  24. package/lib/comfyui.js +100 -6
  25. package/lib/config.js +68 -3
  26. package/lib/deprecation.js +1 -1
  27. package/lib/eval-pack.js +94 -4
  28. package/lib/eval-runs.js +42 -3
  29. package/lib/export.js +144 -34
  30. package/lib/implementation-packs.js +15 -1
  31. package/lib/log.js +41 -4
  32. package/lib/paths.js +41 -3
  33. package/lib/refine-briefs.js +16 -9
  34. package/lib/reingest-selected.js +14 -3
  35. package/lib/reingest.js +15 -1
  36. package/lib/run-summary.js +3 -2
  37. package/lib/runtime-runs.js +59 -9
  38. package/lib/selections.js +54 -2
  39. package/lib/snapshot.js +100 -7
  40. package/lib/split.js +175 -17
  41. package/lib/training-adapters.js +79 -0
  42. package/lib/training-manifests.js +17 -3
  43. package/lib/training-packages.js +29 -28
  44. package/lib/training-profiles.js +10 -0
  45. package/package.json +27 -3
  46. package/scripts/batch-generate.js +72 -20
  47. package/scripts/batch-sheet.js +3 -3
  48. package/scripts/batch-show.js +3 -3
  49. package/scripts/brief-compile.js +8 -8
  50. package/scripts/brief-show.js +4 -3
  51. package/scripts/canon-bind.js +7 -9
  52. package/scripts/card.js +22 -25
  53. package/scripts/compare.js +38 -33
  54. package/scripts/critique-show.js +3 -3
  55. package/scripts/critique.js +3 -3
  56. package/scripts/curate.js +91 -46
  57. package/scripts/doctor.js +3 -5
  58. package/scripts/eligibility.js +17 -14
  59. package/scripts/eval-pack.js +13 -11
  60. package/scripts/eval-run.js +30 -29
  61. package/scripts/export.js +27 -31
  62. package/scripts/generate-controlnet.js +31 -17
  63. package/scripts/generate-identity.js +37 -20
  64. package/scripts/generate-ipadapter.js +22 -16
  65. package/scripts/generate.js +96 -29
  66. package/scripts/implementation-pack.js +19 -14
  67. package/scripts/init.js +115 -17
  68. package/scripts/painterly.js +8 -10
  69. package/scripts/refine.js +3 -3
  70. package/scripts/reingest-selected.js +3 -3
  71. package/scripts/reingest.js +33 -20
  72. package/scripts/run-generate.js +4 -4
  73. package/scripts/run-list.js +3 -3
  74. package/scripts/run-show.js +3 -3
  75. package/scripts/select.js +5 -4
  76. package/scripts/selection-show.js +3 -3
  77. package/scripts/snapshot.js +22 -24
  78. package/scripts/split.js +26 -31
  79. package/scripts/training-manifest.js +29 -24
  80. package/scripts/training-package.js +24 -20
  81. package/scripts/training-profile.js +13 -8
  82. package/scripts/workflow-list.js +2 -2
  83. package/scripts/workflow-show.js +4 -3
  84. package/templates/domains/architecture/inputs/prompts/example-wave.json +32 -0
  85. package/templates/domains/architecture/terminology.json +10 -8
  86. package/templates/domains/character-design/inputs/prompts/example-wave.json +32 -0
  87. package/templates/domains/character-design/project.json +4 -2
  88. package/templates/domains/creature-design/inputs/prompts/example-wave.json +32 -0
  89. package/templates/domains/creature-design/project.json +4 -2
  90. package/templates/domains/creature-design/terminology.json +1 -0
  91. package/templates/domains/game-art/inputs/prompts/example-wave.json +37 -0
  92. package/templates/domains/game-art/project.json +4 -2
  93. package/templates/domains/vehicle-mech/inputs/prompts/example-wave.json +37 -0
  94. package/templates/domains/vehicle-mech/project.json +4 -2
  95. package/scripts/bulk-curate-wave2-5.js +0 -138
  96. package/scripts/bulk-curate-waves11-18.js +0 -413
  97. package/scripts/curate-wave25.js +0 -209
  98. package/scripts/migrate-records.js +0 -141
  99. package/templates/inputs/prompts/example-wave.json +0 -27
@@ -10,40 +10,42 @@
10
10
  * sdlab eval-run list [--project <name>]
11
11
  */
12
12
 
13
- import { readFile } from 'node:fs/promises';
14
13
  import { join } from 'node:path';
15
- import { getProjectName } from '../lib/args.js';
16
- import { REPO_ROOT } from '../lib/paths.js';
14
+ import { parseArgs, getProjectName } from '../lib/args.js';
15
+ import { REPO_ROOT, resolveSafeProjectPath } from '../lib/paths.js';
16
+ import { inputError, handleCliError } from '../lib/errors.js';
17
17
  import { listTrainingManifests } from '../lib/training-manifests.js';
18
18
  import { listEvalPacks } from '../lib/eval-pack.js';
19
19
  import { createEvalRun, scoreEvalRun, loadEvalRun, listEvalRuns } from '../lib/eval-runs.js';
20
20
 
21
21
  export async function run(argv = process.argv.slice(2)) {
22
- const projectName = getProjectName(argv);
22
+ const { flags, positionals } = parseArgs(argv, {
23
+ flags: {
24
+ project: { type: 'string' },
25
+ manifest: { type: 'string' },
26
+ 'eval-pack': { type: 'string' },
27
+ outputs: { type: 'string' },
28
+ },
29
+ deprecated: { game: 'project' },
30
+ });
31
+
32
+ const projectName = flags.project || getProjectName(argv);
23
33
  const projectRoot = join(REPO_ROOT, 'projects', projectName);
24
- const subcommand = argv.find(a => !a.startsWith('--') && !a.startsWith('er-')) || 'list';
25
- const args = argv.filter(a => a !== subcommand);
34
+ // First non-flag positional that isn't an er-id is the subcommand
35
+ const subcommand = positionals.find(a => !a.startsWith('er-')) || 'list';
26
36
 
27
37
  if (subcommand === 'create') {
28
- // Find manifest
29
- let manifestId;
30
- const manifestIdx = args.indexOf('--manifest');
31
- if (manifestIdx >= 0) {
32
- manifestId = args[manifestIdx + 1];
33
- } else {
38
+ let manifestId = flags.manifest;
39
+ if (!manifestId) {
34
40
  const manifests = await listTrainingManifests(projectRoot);
35
- if (manifests.length === 0) throw new Error('No training manifests. Run: sdlab training-manifest create');
41
+ if (manifests.length === 0) throw inputError('INPUT_NO_MANIFEST', 'No training manifests. Run: sdlab training-manifest create');
36
42
  manifestId = manifests[manifests.length - 1].id;
37
43
  }
38
44
 
39
- // Find eval pack
40
- let evalPackId;
41
- const evalIdx = args.indexOf('--eval-pack');
42
- if (evalIdx >= 0) {
43
- evalPackId = args[evalIdx + 1];
44
- } else {
45
+ let evalPackId = flags['eval-pack'] || flags.evalPack;
46
+ if (!evalPackId) {
45
47
  const packs = await listEvalPacks(projectRoot);
46
- if (packs.length === 0) throw new Error('No eval packs. Run: sdlab eval-pack build');
48
+ if (packs.length === 0) throw inputError('INPUT_NO_EVALPACK', 'No eval packs. Run: sdlab eval-pack build');
47
49
  evalPackId = packs[packs.length - 1].id;
48
50
  }
49
51
 
@@ -57,12 +59,11 @@ export async function run(argv = process.argv.slice(2)) {
57
59
  console.log(` Status: created (use "sdlab eval-run score" to score outputs)`);
58
60
 
59
61
  } else if (subcommand === 'score') {
60
- const evalRunId = args.find(a => a.startsWith('er-'));
61
- if (!evalRunId) throw new Error('Usage: sdlab eval-run score <eval-run-id> --outputs <path>');
62
+ const evalRunId = positionals.find(a => a.startsWith('er-'));
63
+ if (!evalRunId) throw inputError('INPUT_MISSING_ARGS', 'Usage: sdlab eval-run score <eval-run-id> --outputs <path>');
62
64
 
63
- const outputsIdx = args.indexOf('--outputs');
64
- if (outputsIdx < 0) throw new Error('--outputs <path> is required (JSONL with record_id, accept, notes)');
65
- const outputsPath = args[outputsIdx + 1];
65
+ if (!flags.outputs) throw inputError('INPUT_MISSING_FLAG', '--outputs <path> is required (JSONL with record_id, accept, notes)');
66
+ const outputsPath = resolveSafeProjectPath(projectRoot, flags.outputs, { baseRoot: REPO_ROOT, flagName: 'outputs' });
66
67
 
67
68
  console.log(`\x1b[1msdlab eval-run score\x1b[0m — ${evalRunId}\n`);
68
69
 
@@ -80,8 +81,8 @@ export async function run(argv = process.argv.slice(2)) {
80
81
  }
81
82
 
82
83
  } else if (subcommand === 'show') {
83
- const evalRunId = args.find(a => a.startsWith('er-'));
84
- if (!evalRunId) throw new Error('Usage: sdlab eval-run show <eval-run-id>');
84
+ const evalRunId = positionals.find(a => a.startsWith('er-'));
85
+ if (!evalRunId) throw inputError('INPUT_MISSING_ARGS', 'Usage: sdlab eval-run show <eval-run-id>');
85
86
 
86
87
  const run = await loadEvalRun(projectRoot, evalRunId);
87
88
  console.log(`\x1b[1mEval run\x1b[0m — ${evalRunId}\n`);
@@ -112,10 +113,10 @@ export async function run(argv = process.argv.slice(2)) {
112
113
  }
113
114
 
114
115
  } else {
115
- throw new Error(`Unknown subcommand: ${subcommand}. Use: create, score, show, list`);
116
+ throw inputError('INPUT_BAD_SUBCOMMAND', `Unknown subcommand: ${subcommand}. Use: create, score, show, list`);
116
117
  }
117
118
  }
118
119
 
119
120
  if (process.argv[1] && (process.argv[1].endsWith('eval-run.js') || process.argv[1].endsWith('eval-run'))) {
120
- run().catch((err) => { console.error(err.message || err); process.exit(1); });
121
+ run().catch(handleCliError);
121
122
  }
package/scripts/export.js CHANGED
@@ -9,53 +9,52 @@
9
9
  */
10
10
 
11
11
  import { join } from 'node:path';
12
- import { getProjectName } from '../lib/args.js';
12
+ import { parseArgs, getProjectName } from '../lib/args.js';
13
13
  import { REPO_ROOT } from '../lib/paths.js';
14
+ import { inputError, handleCliError } from '../lib/errors.js';
14
15
  import { listSnapshots } from '../lib/snapshot.js';
15
16
  import { listSplits } from '../lib/split.js';
16
- import { buildExport, listExports, loadExport } from '../lib/export.js';
17
+ import { buildExport, listExports } from '../lib/export.js';
17
18
 
18
19
  export async function run(argv = process.argv.slice(2)) {
19
- const projectName = getProjectName(argv);
20
+ const { flags, positionals } = parseArgs(argv, {
21
+ flags: {
22
+ project: { type: 'string' },
23
+ snapshot: { type: 'string' },
24
+ split: { type: 'string' },
25
+ profile: { type: 'string' },
26
+ copy: { type: 'boolean', default: false },
27
+ },
28
+ deprecated: { game: 'project' },
29
+ });
30
+
31
+ const projectName = flags.project || getProjectName(argv);
20
32
  const projectRoot = join(REPO_ROOT, 'projects', projectName);
21
- const subcommand = argv.find(a => !a.startsWith('--') && a !== 'true') || 'list';
22
- const args = argv.filter(a => a !== subcommand);
33
+ const subcommand = positionals[0] || 'list';
23
34
 
24
35
  if (subcommand === 'build') {
25
- // Find snapshot
26
- let snapshotId;
27
- const snapIdx = args.indexOf('--snapshot');
28
- if (snapIdx >= 0) {
29
- snapshotId = args[snapIdx + 1];
30
- } else {
36
+ let snapshotId = flags.snapshot;
37
+ if (!snapshotId) {
31
38
  const snapshots = await listSnapshots(projectRoot);
32
- if (snapshots.length === 0) throw new Error('No snapshots found. Run: sdlab snapshot create');
39
+ if (snapshots.length === 0) throw inputError('INPUT_NO_SNAPSHOT', 'No snapshots found. Run: sdlab snapshot create');
33
40
  snapshotId = snapshots[snapshots.length - 1].id;
34
41
  }
35
42
 
36
- // Find split
37
- let splitId;
38
- const splitIdx = args.indexOf('--split');
39
- if (splitIdx >= 0) {
40
- splitId = args[splitIdx + 1];
41
- } else {
43
+ let splitId = flags.split;
44
+ if (!splitId) {
42
45
  const splits = await listSplits(projectRoot);
43
- if (splits.length === 0) throw new Error('No splits found. Run: sdlab split build');
46
+ if (splits.length === 0) throw inputError('INPUT_NO_SPLIT', 'No splits found. Run: sdlab split build');
44
47
  splitId = splits[splits.length - 1].id;
45
48
  }
46
49
 
47
- const profileIdx = args.indexOf('--profile');
48
- const profileId = profileIdx >= 0 ? args[profileIdx + 1] : null;
49
- const copy = args.includes('--copy');
50
-
51
50
  console.log(`\x1b[1msdlab export build\x1b[0m — ${projectName}`);
52
51
  console.log(` Snapshot: ${snapshotId}`);
53
52
  console.log(` Split: ${splitId}`);
54
- console.log(` Profile: ${profileId || 'canonical-package (built-in)'}`);
55
- console.log(` Images: ${copy ? 'copy' : 'symlink'}`);
53
+ console.log(` Profile: ${flags.profile || 'canonical-package (built-in)'}`);
54
+ console.log(` Images: ${flags.copy ? 'copy' : 'symlink'}`);
56
55
  console.log('');
57
56
 
58
- const result = await buildExport(projectRoot, snapshotId, splitId, { profileId, copy });
57
+ const result = await buildExport(projectRoot, snapshotId, splitId, { profileId: flags.profile, copy: flags.copy });
59
58
 
60
59
  console.log(` \x1b[32m✓\x1b[0m Export: ${result.exportId}`);
61
60
  console.log(` Records: ${result.recordCount}`);
@@ -73,13 +72,10 @@ export async function run(argv = process.argv.slice(2)) {
73
72
  }
74
73
 
75
74
  } else {
76
- throw new Error(`Unknown subcommand: ${subcommand}. Use: build, list`);
75
+ throw inputError('INPUT_BAD_SUBCOMMAND', `Unknown subcommand: ${subcommand}. Use: build, list`);
77
76
  }
78
77
  }
79
78
 
80
79
  if (process.argv[1] && (process.argv[1].endsWith('export.js') || process.argv[1].endsWith('export'))) {
81
- run().catch((err) => {
82
- console.error(err.message || err);
83
- process.exit(1);
84
- });
80
+ run().catch(handleCliError);
85
81
  }
@@ -12,10 +12,12 @@
12
12
  * sdlab generate:controlnet --subject naia --guide <path> --seeds 4 --project star-freight
13
13
  */
14
14
 
15
- import { readFile, writeFile, mkdir } from "node:fs/promises";
15
+ import { writeFile, mkdir } from "node:fs/promises";
16
16
  import { join } from "node:path";
17
- import { getProjectName } from "../lib/args.js";
18
- import { REPO_ROOT } from "../lib/paths.js";
17
+ import { getProjectName, parseNumberFlag } from "../lib/args.js";
18
+ import { REPO_ROOT, resolveSafeProjectPath } from "../lib/paths.js";
19
+ import { readJsonFile } from "../lib/config.js";
20
+ import { inputError, runtimeError, handleCliError } from "../lib/errors.js";
19
21
  import { comfyHealth, submitAndWait, downloadImage, uploadImage } from "../lib/comfyui.js";
20
22
 
21
23
  const DEFAULTS = {
@@ -33,7 +35,12 @@ const DEFAULTS = {
33
35
  function parseLocalArgs(argv) {
34
36
  const get = (flag, def) => {
35
37
  const i = argv.indexOf(flag);
36
- return i >= 0 && i + 1 < argv.length ? argv[i + 1] : def;
38
+ if (i < 0 || i + 1 >= argv.length) return def;
39
+ const v = argv[i + 1];
40
+ if (typeof v === 'string' && v.startsWith('--')) {
41
+ throw inputError('INPUT_MISSING_VALUE', `Flag ${flag} is missing its value (got another flag: ${v}).`);
42
+ }
43
+ return v;
37
44
  };
38
45
  return {
39
46
  subject: get("--subject", "unknown"),
@@ -41,9 +48,9 @@ function parseLocalArgs(argv) {
41
48
  prompt: get("--prompt", null),
42
49
  promptFile: get("--prompt-file", null),
43
50
  negative: get("--negative", null),
44
- seeds: parseInt(get("--seeds", "4"), 10),
45
- weight: parseFloat(get("--weight", "0.70")),
46
- guidanceEnd: parseFloat(get("--guidance-end", "0.65")),
51
+ seeds: parseNumberFlag('seeds', get("--seeds", "4"), { int: true, min: 1 }),
52
+ weight: parseNumberFlag('weight', get("--weight", "0.70"), { min: 0, max: 2 }),
53
+ guidanceEnd: parseNumberFlag('guidance-end', get("--guidance-end", "0.65"), { min: 0, max: 1 }),
47
54
  controlModel: get("--model", "controlnet-canny-sdxl-1.0.safetensors"),
48
55
  dryRun: argv.includes("--dry-run"),
49
56
  };
@@ -167,21 +174,25 @@ export async function run(argv = process.argv.slice(2)) {
167
174
  const opts = parseLocalArgs(argv);
168
175
 
169
176
  if (!opts.guidePath) {
170
- throw new Error("--guide <path> is required");
177
+ throw inputError('INPUT_MISSING_FLAG', "--guide <path> is required");
171
178
  }
172
179
 
180
+ // Validate guide stays inside the project tree.
181
+ const safeGuide = resolveSafeProjectPath(GAME_ROOT, opts.guidePath, { flagName: 'guide' });
182
+
173
183
  // Resolve prompt
174
184
  let prompt = opts.prompt;
175
185
  let negative = opts.negative;
176
186
 
177
187
  if (opts.promptFile && !prompt) {
178
- const pf = JSON.parse(await readFile(join(GAME_ROOT, opts.promptFile), "utf-8"));
188
+ const safePromptFile = resolveSafeProjectPath(GAME_ROOT, opts.promptFile, { flagName: 'prompt-file' });
189
+ const pf = await readJsonFile(safePromptFile, { requiredFields: ['prompt'] });
179
190
  prompt = pf.prompt;
180
191
  negative = negative || pf.negative;
181
192
  }
182
193
 
183
194
  if (!prompt) {
184
- throw new Error("--prompt or --prompt-file required");
195
+ throw inputError('INPUT_MISSING_FLAG', "--prompt or --prompt-file required");
185
196
  }
186
197
 
187
198
  if (!negative) {
@@ -200,12 +211,12 @@ export async function run(argv = process.argv.slice(2)) {
200
211
  if (!opts.dryRun) {
201
212
  const online = await comfyHealth(COMFY_URL);
202
213
  if (!online) {
203
- throw new Error("ComfyUI not reachable at " + COMFY_URL);
214
+ throw runtimeError('RUNTIME_COMFY_UNREACHABLE', "ComfyUI not reachable at " + COMFY_URL);
204
215
  }
205
216
  console.log("\x1b[32m+\x1b[0m ComfyUI online");
206
217
  }
207
218
 
208
- const guideFullPath = join(GAME_ROOT, opts.guidePath);
219
+ const guideFullPath = safeGuide;
209
220
  const guideFilename = `cn_guide_${opts.subject}.png`;
210
221
 
211
222
  if (!opts.dryRun) {
@@ -215,6 +226,7 @@ export async function run(argv = process.argv.slice(2)) {
215
226
 
216
227
  await mkdir(join(GAME_ROOT, "outputs/candidates"), { recursive: true });
217
228
 
229
+ let successes = 0, failures = 0;
218
230
  for (let si = 0; si < opts.seeds; si++) {
219
231
  const seed = DEFAULTS.base_seed + si;
220
232
  const assetId = `${opts.subject}_cn_s${si}`;
@@ -254,19 +266,21 @@ export async function run(argv = process.argv.slice(2)) {
254
266
  const imgData = await downloadImage(imageFile, imageSubfolder, COMFY_URL);
255
267
  await writeFile(join(GAME_ROOT, destPath), imgData);
256
268
  console.log(` \x1b[32m+\x1b[0m ${destPath} (${elapsed}ms, ${imgData.length} bytes)`);
269
+ successes++;
257
270
  } catch (err) {
258
271
  console.log(` \x1b[31mx\x1b[0m ${err.message}`);
272
+ failures++;
259
273
  }
260
274
  }
261
275
 
262
- console.log("\n\x1b[32m+\x1b[0m ControlNet discovery complete");
276
+ console.log(`\n\x1b[32m+\x1b[0m ControlNet discovery complete (${successes} success, ${failures} failed)`);
263
277
  if (opts.dryRun) console.log(" (dry run)");
278
+ if (!opts.dryRun && failures > 0 && successes === 0) {
279
+ throw runtimeError('RUNTIME_ALL_FAILED', `All ${opts.seeds} ControlNet generation attempts failed.`);
280
+ }
264
281
  }
265
282
 
266
283
  // Direct execution guard
267
284
  if (process.argv[1] && (process.argv[1].endsWith('generate-controlnet.js') || process.argv[1].endsWith('generate-controlnet'))) {
268
- run().catch((err) => {
269
- console.error(err.message || err);
270
- process.exit(1);
271
- });
285
+ run().catch(handleCliError);
272
286
  }
@@ -20,10 +20,12 @@
20
20
  * --denoise N Denoise strength for follow_on phase (default: 0.38)
21
21
  */
22
22
 
23
- import { readFile, writeFile, mkdir } from "node:fs/promises";
23
+ import { writeFile, mkdir } from "node:fs/promises";
24
24
  import { join } from "node:path";
25
- import { getProjectName } from "../lib/args.js";
26
- import { REPO_ROOT } from "../lib/paths.js";
25
+ import { getProjectName, parseNumberFlag } from "../lib/args.js";
26
+ import { REPO_ROOT, resolveSafeProjectPath } from "../lib/paths.js";
27
+ import { readJsonFile } from "../lib/config.js";
28
+ import { inputError, runtimeError, handleCliError } from "../lib/errors.js";
27
29
  import { comfyHealth, submitAndWait, downloadImage } from "../lib/comfyui.js";
28
30
 
29
31
  // ── Defaults (match existing lab pipeline) ──
@@ -378,13 +380,24 @@ export async function run(argv = process.argv.slice(2)) {
378
380
  const GAME_ROOT = join(REPO_ROOT, 'projects', projectName);
379
381
  const COMFY_URL = process.env.COMFY_URL || "http://127.0.0.1:8188";
380
382
 
381
- const packPath = argv.find((a) => !a.startsWith("--"));
383
+ // Collect positionals, skipping values that belong to known flags
384
+ const knownFlagsWithValue = new Set(['--subject', '--seeds', '--phase', '--anchor', '--denoise', '--project', '--game']);
385
+ const positionals = [];
386
+ for (let k = 0; k < argv.length; k++) {
387
+ const a = argv[k];
388
+ if (a.startsWith('--')) {
389
+ if (!a.includes('=') && knownFlagsWithValue.has(a)) k++;
390
+ continue;
391
+ }
392
+ positionals.push(a);
393
+ }
394
+ const packPath = positionals[0];
382
395
  const dryRun = argv.includes("--dry-run");
383
396
  const subjectFilter = argv.includes("--subject")
384
397
  ? argv[argv.indexOf("--subject") + 1]
385
398
  : null;
386
399
  const seedCount = argv.includes("--seeds")
387
- ? parseInt(argv[argv.indexOf("--seeds") + 1], 10)
400
+ ? parseNumberFlag('seeds', argv[argv.indexOf("--seeds") + 1], { int: true, min: 1 })
388
401
  : 3;
389
402
  const phase = argv.includes("--phase")
390
403
  ? argv[argv.indexOf("--phase") + 1]
@@ -393,30 +406,31 @@ export async function run(argv = process.argv.slice(2)) {
393
406
  ? argv[argv.indexOf("--anchor") + 1]
394
407
  : null;
395
408
  const denoise = argv.includes("--denoise")
396
- ? parseFloat(argv[argv.indexOf("--denoise") + 1])
409
+ ? parseNumberFlag('denoise', argv[argv.indexOf("--denoise") + 1], { min: 0, max: 1 })
397
410
  : 0.38;
398
411
 
399
412
  if (!packPath) {
400
- throw new Error("Usage: sdlab generate:identity <packet-file> [options]");
413
+ throw inputError('INPUT_MISSING_ARGS', "Usage: sdlab generate:identity <packet-file> [options]");
401
414
  }
402
415
 
403
416
  if (phase === "follow_on" && !anchorPath) {
404
- throw new Error("--phase follow_on requires --anchor <image-path>");
417
+ throw inputError('INPUT_MISSING_FLAG', "--phase follow_on requires --anchor <image-path>");
405
418
  }
406
419
 
407
- const fullPackPath = join(GAME_ROOT, packPath);
408
- const pack = JSON.parse(await readFile(fullPackPath, "utf-8"));
420
+ const fullPackPath = resolveSafeProjectPath(GAME_ROOT, packPath, { flagName: 'packet-file' });
421
+ if (anchorPath) resolveSafeProjectPath(GAME_ROOT, anchorPath, { flagName: 'anchor' });
422
+ const pack = await readJsonFile(fullPackPath, { requiredFields: ['subjects'] });
409
423
 
410
- const errors = validatePacket(pack);
411
- if (errors.length > 0) {
412
- throw new Error("Packet validation failed:\n " + errors.join("\n "));
424
+ const vErrors = validatePacket(pack);
425
+ if (vErrors.length > 0) {
426
+ throw inputError('INPUT_BAD_PACKET', "Packet validation failed:\n " + vErrors.join("\n "));
413
427
  }
414
428
 
415
429
  let subjects = pack.subjects;
416
430
  if (subjectFilter) {
417
431
  subjects = subjects.filter((s) => s.subject_name === subjectFilter);
418
432
  if (subjects.length === 0) {
419
- throw new Error(`No subject found with name '${subjectFilter}'`);
433
+ throw inputError('INPUT_UNKNOWN_SUBJECT', `No subject found with name '${subjectFilter}'`);
420
434
  }
421
435
  }
422
436
 
@@ -439,7 +453,7 @@ export async function run(argv = process.argv.slice(2)) {
439
453
  if (!dryRun) {
440
454
  const online = await comfyHealth(COMFY_URL);
441
455
  if (!online) {
442
- throw new Error("ComfyUI not reachable at " + COMFY_URL);
456
+ throw runtimeError('RUNTIME_COMFY_UNREACHABLE', "ComfyUI not reachable at " + COMFY_URL);
443
457
  }
444
458
  console.log("\x1b[32m+\x1b[0m ComfyUI online");
445
459
  }
@@ -448,6 +462,7 @@ export async function run(argv = process.argv.slice(2)) {
448
462
  await mkdir(join(GAME_ROOT, "records"), { recursive: true });
449
463
 
450
464
  let generated = 0;
465
+ let errors = 0;
451
466
  let imageIndex = 0;
452
467
 
453
468
  for (const subject of subjects) {
@@ -532,6 +547,7 @@ export async function run(argv = process.argv.slice(2)) {
532
547
  console.log(` \x1b[32m+\x1b[0m ${destPath} (${elapsed}ms, ${imgData.length} bytes)`);
533
548
  } catch (err) {
534
549
  console.log(` \x1b[31mx\x1b[0m ${err.message}`);
550
+ errors++;
535
551
  }
536
552
 
537
553
  generated++;
@@ -540,8 +556,12 @@ export async function run(argv = process.argv.slice(2)) {
540
556
  }
541
557
  }
542
558
 
543
- console.log(`\n\x1b[32m+\x1b[0m Generated ${generated} images`);
559
+ const succeeded = generated - errors;
560
+ console.log(`\n\x1b[32m+\x1b[0m Generated ${succeeded} images (${errors} errors)`);
544
561
  if (dryRun) console.log(" (dry run — no images produced)");
562
+ if (!dryRun && errors > 0 && succeeded === 0) {
563
+ throw runtimeError('RUNTIME_ALL_FAILED', `All ${generated} generation attempts failed.`);
564
+ }
545
565
 
546
566
  console.log("\nNext steps:");
547
567
  if (phase === "discovery") {
@@ -558,8 +578,5 @@ export async function run(argv = process.argv.slice(2)) {
558
578
 
559
579
  // Direct execution guard
560
580
  if (process.argv[1] && (process.argv[1].endsWith('generate-identity.js') || process.argv[1].endsWith('generate-identity'))) {
561
- run().catch((err) => {
562
- console.error(err.message || err);
563
- process.exit(1);
564
- });
581
+ run().catch(handleCliError);
565
582
  }
@@ -11,10 +11,11 @@
11
11
  * sdlab generate:ipadapter --subject corrigan --ref <image> --seeds 4 --project star-freight
12
12
  */
13
13
 
14
- import { readFile, writeFile, mkdir } from "node:fs/promises";
14
+ import { writeFile, mkdir } from "node:fs/promises";
15
15
  import { join } from "node:path";
16
- import { getProjectName } from "../lib/args.js";
17
- import { REPO_ROOT } from "../lib/paths.js";
16
+ import { getProjectName, parseNumberFlag } from "../lib/args.js";
17
+ import { REPO_ROOT, resolveSafeProjectPath } from "../lib/paths.js";
18
+ import { inputError, runtimeError, handleCliError } from "../lib/errors.js";
18
19
  import { comfyHealth, submitAndWait, downloadImage, uploadImage } from "../lib/comfyui.js";
19
20
 
20
21
  const DEFAULTS = {
@@ -34,17 +35,22 @@ const DEFAULTS = {
34
35
  function parseLocalArgs(argv) {
35
36
  const get = (flag, def) => {
36
37
  const i = argv.indexOf(flag);
37
- return i >= 0 && i + 1 < argv.length ? argv[i + 1] : def;
38
+ if (i < 0 || i + 1 >= argv.length) return def;
39
+ const v = argv[i + 1];
40
+ if (typeof v === 'string' && v.startsWith('--')) {
41
+ throw inputError('INPUT_MISSING_VALUE', `Flag ${flag} is missing its value (got another flag: ${v}).`);
42
+ }
43
+ return v;
38
44
  };
39
45
  return {
40
46
  subject: get("--subject", "unknown"),
41
47
  refPath: get("--ref", null),
42
48
  prompt: get("--prompt", null),
43
49
  negative: get("--negative", null),
44
- seeds: parseInt(get("--seeds", "4"), 10),
45
- weight: parseFloat(get("--weight", "0.55")),
46
- start: parseFloat(get("--start", "0.0")),
47
- end: parseFloat(get("--end", "0.8")),
50
+ seeds: parseNumberFlag('seeds', get("--seeds", "4"), { int: true, min: 1 }),
51
+ weight: parseNumberFlag('weight', get("--weight", "0.55"), { min: 0, max: 2 }),
52
+ start: parseNumberFlag('start', get("--start", "0.0"), { min: 0, max: 1 }),
53
+ end: parseNumberFlag('end', get("--end", "0.8"), { min: 0, max: 1 }),
48
54
  dryRun: argv.includes("--dry-run"),
49
55
  };
50
56
  }
@@ -174,12 +180,15 @@ export async function run(argv = process.argv.slice(2)) {
174
180
  const opts = parseLocalArgs(argv);
175
181
 
176
182
  if (!opts.refPath) {
177
- throw new Error("--ref <path> is required");
183
+ throw inputError('INPUT_MISSING_FLAG', "--ref <path> is required");
178
184
  }
179
185
  if (!opts.prompt) {
180
- throw new Error("--prompt is required");
186
+ throw inputError('INPUT_MISSING_FLAG', "--prompt is required");
181
187
  }
182
188
 
189
+ // Validate --ref path stays inside project tree.
190
+ const safeRefPath = resolveSafeProjectPath(GAME_ROOT, opts.refPath, { flagName: 'ref' });
191
+
183
192
  const negative = opts.negative ||
184
193
  "photorealistic, photograph, 3d render, anime, cartoon, text, watermark, blurry, low quality";
185
194
 
@@ -196,11 +205,11 @@ export async function run(argv = process.argv.slice(2)) {
196
205
  if (!opts.dryRun) {
197
206
  const online = await comfyHealth(COMFY_URL);
198
207
  if (!online) {
199
- throw new Error("ComfyUI not reachable");
208
+ throw runtimeError('RUNTIME_COMFY_UNREACHABLE', "ComfyUI not reachable at " + COMFY_URL);
200
209
  }
201
210
  console.log("\x1b[32m+\x1b[0m ComfyUI online");
202
211
 
203
- await uploadImage(join(GAME_ROOT, opts.refPath), refFilename, COMFY_URL);
212
+ await uploadImage(safeRefPath, refFilename, COMFY_URL);
204
213
  console.log(`\x1b[32m+\x1b[0m Reference uploaded: ${refFilename}`);
205
214
  }
206
215
 
@@ -255,8 +264,5 @@ export async function run(argv = process.argv.slice(2)) {
255
264
 
256
265
  // Direct execution guard
257
266
  if (process.argv[1] && (process.argv[1].endsWith('generate-ipadapter.js') || process.argv[1].endsWith('generate-ipadapter'))) {
258
- run().catch((err) => {
259
- console.error(err.message || err);
260
- process.exit(1);
261
- });
267
+ run().catch(handleCliError);
262
268
  }