@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/curate.js CHANGED
@@ -7,59 +7,73 @@
7
7
  * node scripts/curate.js <asset_id> approved "Clean silhouette, correct palette" --scores silhouette:0.9,palette:0.8
8
8
  * node scripts/curate.js <asset_id> rejected "3D render look, alpha halo" --failures alpha_halo,3d_render_look
9
9
  * node scripts/curate.js --list (show uncurated candidates)
10
- * sdlab curate <asset_id> approved "explanation" --project star-freight
10
+ * sdlab curate <asset_id> approved "explanation" --project star-freight [--dry-run]
11
11
  */
12
12
 
13
- import { readFile, writeFile, rename, readdir, mkdir } from "node:fs/promises";
13
+ import { writeFile, rename, readdir, mkdir, readFile } from "node:fs/promises";
14
14
  import { join } from "node:path";
15
- import { getProjectName } from "../lib/args.js";
15
+ import { parseArgs, getProjectName } from "../lib/args.js";
16
16
  import { REPO_ROOT } from "../lib/paths.js";
17
-
18
- function getFlagValue(args, flag) {
19
- const prefix = `--${flag}=`;
20
- for (const a of args) {
21
- if (a.startsWith(prefix)) return a.slice(prefix.length);
22
- }
23
- const idx = args.indexOf(`--${flag}`);
24
- if (idx !== -1 && idx + 1 < args.length) return args[idx + 1];
25
- return undefined;
26
- }
17
+ import { readJsonFile } from "../lib/config.js";
18
+ import { inputError, runtimeError, handleCliError } from "../lib/errors.js";
19
+ import { result } from "../lib/log.js";
27
20
 
28
21
  export async function run(argv = process.argv.slice(2)) {
29
- const projectName = getProjectName(argv);
30
- const GAME_ROOT = join(REPO_ROOT, 'projects', projectName);
31
-
32
- // Strip --project/--game and their values from positional parsing
33
- const args = argv.filter((a, i) => {
34
- if (a === '--project' || a === '--game') return false;
35
- if (i > 0 && (argv[i - 1] === '--project' || argv[i - 1] === '--game')) return false;
36
- return true;
22
+ const { flags, positionals } = parseArgs(argv, {
23
+ flags: {
24
+ project: { type: 'string' },
25
+ scores: { type: 'string' },
26
+ failures: { type: 'string' },
27
+ notes: { type: 'string' },
28
+ list: { type: 'boolean', default: false },
29
+ 'dry-run': { type: 'boolean', default: false },
30
+ },
31
+ deprecated: { game: 'project' },
37
32
  });
38
33
 
39
- if (args.includes("--list")) {
34
+ const projectName = flags.project || getProjectName(argv);
35
+ const GAME_ROOT = join(REPO_ROOT, 'projects', projectName);
36
+ const dryRun = flags['dry-run'] || flags.dryRun;
37
+
38
+ if (flags.list) {
40
39
  return listUncurated(GAME_ROOT);
41
40
  }
42
41
 
43
- const [assetId, status, explanation, ...rest] = args;
42
+ const [assetId, status, explanation] = positionals;
44
43
 
45
44
  if (!assetId || !status || !explanation) {
46
- throw new Error("Usage: sdlab curate <asset_id> <approved|rejected|borderline> <explanation> [--scores k:v,...] [--failures f1,f2]");
45
+ throw inputError(
46
+ 'INPUT_MISSING_ARGS',
47
+ 'Usage: sdlab curate <asset_id> <approved|rejected|borderline> <explanation> [--scores k:v,...] [--failures f1,f2] [--dry-run]'
48
+ );
47
49
  }
48
50
 
49
51
  if (!["approved", "rejected", "borderline"].includes(status)) {
50
- throw new Error(`Invalid status: ${status}. Must be approved, rejected, or borderline.`);
52
+ throw inputError(
53
+ 'INPUT_BAD_STATUS',
54
+ `Invalid status: ${status}. Must be approved, rejected, or borderline.`
55
+ );
51
56
  }
52
57
 
53
- // Parse optional flags
54
- const scoresStr = getFlagValue(rest, "scores");
55
- const failuresStr = getFlagValue(rest, "failures");
56
- const notesStr = getFlagValue(rest, "notes");
58
+ const scoresStr = flags.scores;
59
+ const failuresStr = flags.failures;
60
+ const notesStr = flags.notes;
57
61
 
58
62
  const criteria_scores = {};
59
63
  if (scoresStr) {
60
64
  for (const pair of scoresStr.split(",")) {
61
65
  const [k, v] = pair.split(":");
62
- if (k && v) criteria_scores[k.trim()] = parseFloat(v);
66
+ if (k && v) {
67
+ const score = parseFloat(v);
68
+ if (!Number.isFinite(score)) {
69
+ throw inputError(
70
+ 'INPUT_BAD_SCORE',
71
+ `Score value for "${k.trim()}" is not a number: "${v}"`,
72
+ 'Use the form --scores silhouette:0.9,palette:0.8'
73
+ );
74
+ }
75
+ criteria_scores[k.trim()] = score;
76
+ }
63
77
  }
64
78
  }
65
79
 
@@ -69,17 +83,44 @@ export async function run(argv = process.argv.slice(2)) {
69
83
  const recordPath = join(GAME_ROOT, `records/${assetId}.json`);
70
84
  let record;
71
85
  try {
72
- record = JSON.parse(await readFile(recordPath, "utf-8"));
73
- } catch {
74
- throw new Error(`Record not found: ${recordPath}`);
86
+ record = await readJsonFile(recordPath);
87
+ } catch (err) {
88
+ if (err && err.code === 'INPUT_FILE_NOT_FOUND') {
89
+ throw inputError('INPUT_UNKNOWN_RECORD', `Record not found: ${recordPath}`);
90
+ }
91
+ throw err;
75
92
  }
76
93
 
77
- // Update record first (write before move to avoid orphaned images)
78
- const oldPath = join(GAME_ROOT, record.asset_path);
94
+ const originalAssetPath = record.asset_path;
95
+ const oldPath = join(GAME_ROOT, originalAssetPath);
79
96
  const newDir = `outputs/${status}`;
80
97
  const newPath = `${newDir}/${assetId}.png`;
98
+ const newFullPath = join(GAME_ROOT, newPath);
99
+
100
+ if (dryRun) {
101
+ console.log(`\x1b[33m(dry-run)\x1b[0m ${assetId}`);
102
+ console.log(` status: ${status}`);
103
+ console.log(` move: ${originalAssetPath} -> ${newPath}`);
104
+ console.log(` note: ${explanation}`);
105
+ if (failure_modes.length) console.log(` failures: ${failure_modes.join(", ")}`);
106
+ return;
107
+ }
108
+
81
109
  await mkdir(join(GAME_ROOT, newDir), { recursive: true });
82
110
 
111
+ // Move image FIRST — if the rename fails we still have a clean record.
112
+ try {
113
+ await rename(oldPath, newFullPath);
114
+ } catch (err) {
115
+ throw runtimeError(
116
+ 'RUNTIME_MOVE_FAILED',
117
+ `Could not move ${oldPath} to ${newPath}: ${err.message}`,
118
+ 'Check that the source image exists and the destination is writable.',
119
+ err
120
+ );
121
+ }
122
+
123
+ // Now safely update the record. If writing fails, try to restore the image.
83
124
  record.asset_path = newPath;
84
125
  record.judgment = {
85
126
  status,
@@ -92,16 +133,23 @@ export async function run(argv = process.argv.slice(2)) {
92
133
  confidence: 0.9,
93
134
  };
94
135
 
95
- await writeFile(recordPath, JSON.stringify(record, null, 2));
96
-
97
- // Move image file
98
136
  try {
99
- await rename(oldPath, join(GAME_ROOT, newPath));
100
- } catch {
101
- throw new Error(`Could not move ${oldPath} to ${newPath}`);
137
+ await writeFile(recordPath, JSON.stringify(record, null, 2));
138
+ } catch (err) {
139
+ // Attempt rollback: put the image back.
140
+ try { await rename(newFullPath, oldPath); } catch {}
141
+ throw runtimeError(
142
+ 'RUNTIME_WRITE_FAILED',
143
+ `Could not write record ${recordPath}: ${err.message}`,
144
+ null,
145
+ err
146
+ );
102
147
  }
103
148
 
104
- console.log(`\x1b[32m✓\x1b[0m ${assetId} → ${status}`);
149
+ // Final artifact path — always shown, even under --quiet.
150
+ result('curate', `${assetId} → ${status}`);
151
+ result(` image: ${newPath}`);
152
+ result(` record: records/${assetId}.json`);
105
153
  console.log(` ${explanation}`);
106
154
  if (failure_modes.length) console.log(` Failures: ${failure_modes.join(", ")}`);
107
155
  }
@@ -135,8 +183,5 @@ async function listUncurated(GAME_ROOT) {
135
183
 
136
184
  // Direct execution guard
137
185
  if (process.argv[1] && (process.argv[1].endsWith('curate.js') || process.argv[1].endsWith('curate'))) {
138
- run().catch((err) => {
139
- console.error(err.message || err);
140
- process.exit(1);
141
- });
186
+ run().catch(handleCliError);
142
187
  }
package/scripts/doctor.js CHANGED
@@ -15,6 +15,7 @@ import { readFileSync, readdirSync, existsSync } from 'node:fs';
15
15
  import { join } from 'node:path';
16
16
  import { getProjectName } from '../lib/args.js';
17
17
  import { REPO_ROOT } from '../lib/paths.js';
18
+ import { runtimeError, handleCliError } from '../lib/errors.js';
18
19
 
19
20
  const REQUIRED_CONFIG_FILES = [
20
21
  'project.json',
@@ -484,14 +485,11 @@ export async function run(argv = process.argv.slice(2)) {
484
485
  console.log(`═══ ${status} — ${passes} passed, ${failures} failed, ${warnings} warnings ═══`);
485
486
 
486
487
  if (failures > 0) {
487
- throw new Error(`Project "${projectName}" has ${failures} issue(s).`);
488
+ throw runtimeError('PROJECT_UNHEALTHY', `Project "${projectName}" has ${failures} issue(s).`);
488
489
  }
489
490
  }
490
491
 
491
492
  // Direct execution guard
492
493
  if (process.argv[1] && (process.argv[1].endsWith('doctor.js') || process.argv[1].endsWith('doctor'))) {
493
- run().catch((err) => {
494
- console.error(err.message || err);
495
- process.exit(1);
496
- });
494
+ run().catch(handleCliError);
497
495
  }
@@ -4,36 +4,44 @@
4
4
  * eligibility.js — Audit training/eval eligibility for all project records.
5
5
  *
6
6
  * Usage:
7
- * sdlab eligibility audit [--project <name>]
7
+ * sdlab eligibility audit [--project <name>] [--profile <id>]
8
8
  */
9
9
 
10
10
  import { readdir, readFile } from 'node:fs/promises';
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
14
  import { loadProjectConfig, loadSelectionProfile } from '../lib/config.js';
15
+ import { handleCliError } from '../lib/errors.js';
15
16
  import { evaluateEligibility, categorizeExclusions } from '../lib/eligibility.js';
16
17
 
17
18
  export async function run(argv = process.argv.slice(2)) {
18
- const projectName = getProjectName(argv);
19
+ const { flags } = parseArgs(argv, {
20
+ flags: {
21
+ project: { type: 'string' },
22
+ profile: { type: 'string' },
23
+ },
24
+ deprecated: { game: 'project' },
25
+ allowUnknown: true,
26
+ });
27
+
28
+ const projectName = flags.project || getProjectName(argv);
19
29
  const projectRoot = join(REPO_ROOT, 'projects', projectName);
20
30
 
21
- const profileIdx = argv.indexOf('--profile');
22
- const profileId = profileIdx >= 0 ? argv[profileIdx + 1] : null;
23
- const profile = loadSelectionProfile(projectRoot, profileId);
31
+ const profile = loadSelectionProfile(projectRoot, flags.profile);
24
32
  const config = loadProjectConfig(projectRoot);
25
33
 
26
34
  const recordsDir = join(projectRoot, 'records');
27
35
  const files = (await readdir(recordsDir)).filter(f => f.endsWith('.json')).sort();
28
36
 
29
37
  console.log(`\x1b[1msdlab eligibility audit\x1b[0m — ${projectName}`);
30
- console.log(` Profile: ${profileId || 'training-default (built-in)'}`);
38
+ console.log(` Profile: ${flags.profile || 'training-default (built-in)'}`);
31
39
  console.log(` Records: ${files.length}`);
32
40
  console.log('');
33
41
 
34
42
  const eligible = [];
35
43
  const excluded = [];
36
- const nearMiss = []; // Records with exactly 1 failing reason
44
+ const nearMiss = [];
37
45
 
38
46
  for (const file of files) {
39
47
  const raw = await readFile(join(recordsDir, file), 'utf-8');
@@ -55,7 +63,6 @@ export async function run(argv = process.argv.slice(2)) {
55
63
  console.log(` \x1b[31m✗\x1b[0m Excluded: ${excluded.length}`);
56
64
  console.log('');
57
65
 
58
- // Categorize exclusions
59
66
  const categories = categorizeExclusions(excluded);
60
67
  console.log(' Exclusion breakdown:');
61
68
  for (const [cat, count] of Object.entries(categories)) {
@@ -65,7 +72,6 @@ export async function run(argv = process.argv.slice(2)) {
65
72
  }
66
73
  }
67
74
 
68
- // Near-miss improvement opportunities
69
75
  if (nearMiss.length > 0) {
70
76
  console.log(`\n Improvement opportunities (${nearMiss.length} records with 1 failing check):`);
71
77
  const byReason = {};
@@ -84,8 +90,5 @@ export async function run(argv = process.argv.slice(2)) {
84
90
 
85
91
  // Direct execution guard
86
92
  if (process.argv[1] && (process.argv[1].endsWith('eligibility.js') || process.argv[1].endsWith('eligibility'))) {
87
- run().catch((err) => {
88
- console.error(err.message || err);
89
- process.exit(1);
90
- });
93
+ run().catch(handleCliError);
91
94
  }
@@ -11,15 +11,20 @@
11
11
 
12
12
  import { readFile } from 'node:fs/promises';
13
13
  import { join } from 'node:path';
14
- import { getProjectName } from '../lib/args.js';
14
+ import { parseArgs, getProjectName } from '../lib/args.js';
15
15
  import { REPO_ROOT } from '../lib/paths.js';
16
+ import { inputError, handleCliError } from '../lib/errors.js';
16
17
  import { buildEvalPack, listEvalPacks, loadEvalPack } from '../lib/eval-pack.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: { project: { type: 'string' } },
22
+ deprecated: { game: 'project' },
23
+ });
24
+
25
+ const projectName = flags.project || getProjectName(argv);
20
26
  const projectRoot = join(REPO_ROOT, 'projects', projectName);
21
- const subcommand = argv.find(a => !a.startsWith('--')) || 'list';
22
- const args = argv.filter(a => a !== subcommand);
27
+ const subcommand = positionals[0] || 'list';
23
28
 
24
29
  if (subcommand === 'build') {
25
30
  console.log(`\x1b[1msdlab eval-pack build\x1b[0m — ${projectName}\n`);
@@ -48,8 +53,8 @@ export async function run(argv = process.argv.slice(2)) {
48
53
  }
49
54
 
50
55
  } else if (subcommand === 'show') {
51
- const evalId = args.find(a => a.startsWith('eval-'));
52
- if (!evalId) throw new Error('Usage: sdlab eval-pack show <eval-id>');
56
+ const evalId = positionals.find(a => a.startsWith('eval-'));
57
+ if (!evalId) throw inputError('INPUT_MISSING_ARGS', 'Usage: sdlab eval-pack show <eval-id>');
53
58
 
54
59
  const manifest = await loadEvalPack(projectRoot, evalId);
55
60
  console.log(`\x1b[1mEval pack\x1b[0m — ${evalId}\n`);
@@ -80,13 +85,10 @@ export async function run(argv = process.argv.slice(2)) {
80
85
  }
81
86
 
82
87
  } else {
83
- throw new Error(`Unknown subcommand: ${subcommand}. Use: build, list, show`);
88
+ throw inputError('INPUT_BAD_SUBCOMMAND', `Unknown subcommand: ${subcommand}. Use: build, list, show`);
84
89
  }
85
90
  }
86
91
 
87
92
  if (process.argv[1] && (process.argv[1].endsWith('eval-pack.js') || process.argv[1].endsWith('eval-pack'))) {
88
- run().catch((err) => {
89
- console.error(err.message || err);
90
- process.exit(1);
91
- });
93
+ run().catch(handleCliError);
92
94
  }
@@ -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
  }