pixelkiln 0.35.0 → 0.36.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.
package/CONTRIBUTING.md CHANGED
@@ -19,8 +19,15 @@ npm run test:security
19
19
  npm test
20
20
  npm run build
21
21
  npm run test:package
22
+ npm run test:gallery
22
23
  ```
23
24
 
25
+ `test:gallery` serves a gallery over `FakeProvider` and drives it in headless
26
+ Chrome: the Generate and Regenerate buttons, the busy badge while a job runs,
27
+ the previous generation kept and restored for free, and zero console errors.
28
+ It needs a Chrome or Chromium binary (`CHROME=/path/to/chrome` when it is not
29
+ in a usual place) and skips without one outside CI.
30
+
24
31
  The Next.js marketing/documentation site is isolated in `website/`:
25
32
 
26
33
  ```bash
package/dist/cli.js CHANGED
@@ -1,5 +1,71 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ // src/errors.ts
4
+ var EXIT_CODES = {
5
+ usage: 2,
6
+ project: 3,
7
+ provider: 4,
8
+ "refused-overwrite": 5,
9
+ budget: 6,
10
+ capability: 7
11
+ };
12
+ var PixelKilnError = class extends Error {
13
+ code;
14
+ hint;
15
+ constructor(message6, code, opts = {}) {
16
+ super(message6, opts.cause === void 0 ? void 0 : { cause: opts.cause });
17
+ this.name = "PixelKilnError";
18
+ this.code = code;
19
+ if (opts.hint) this.hint = opts.hint;
20
+ }
21
+ };
22
+ var UsageError = class extends PixelKilnError {
23
+ constructor(message6, opts) {
24
+ super(message6, "usage", opts);
25
+ this.name = "UsageError";
26
+ }
27
+ };
28
+ var ProjectError = class extends PixelKilnError {
29
+ constructor(message6, opts) {
30
+ super(message6, "project", opts);
31
+ this.name = "ProjectError";
32
+ }
33
+ };
34
+ var ProviderError = class extends PixelKilnError {
35
+ /** Registry id of the provider that failed. */
36
+ provider;
37
+ /** HTTP status when the failure was a response, otherwise undefined. */
38
+ status;
39
+ constructor(provider, message6, opts = {}) {
40
+ super(message6, "provider", opts);
41
+ this.name = "ProviderError";
42
+ this.provider = provider;
43
+ if (opts.status !== void 0) this.status = opts.status;
44
+ }
45
+ };
46
+ var OverwriteRefusedError = class extends PixelKilnError {
47
+ /** The file(s) left untouched. */
48
+ files;
49
+ constructor(message6, files, opts) {
50
+ super(message6, "refused-overwrite", opts);
51
+ this.name = "OverwriteRefusedError";
52
+ this.files = files;
53
+ }
54
+ };
55
+ var BudgetError = class extends PixelKilnError {
56
+ constructor(message6, opts) {
57
+ super(message6, "budget", opts);
58
+ this.name = "BudgetError";
59
+ }
60
+ };
61
+ function errorCode(err) {
62
+ return err instanceof PixelKilnError ? err.code : void 0;
63
+ }
64
+ function exitCodeFor(err) {
65
+ const code = errorCode(err);
66
+ return code ? EXIT_CODES[code] : 1;
67
+ }
68
+
3
69
  // src/cli/main.ts
4
70
  import { existsSync as existsSync31 } from "fs";
5
71
  import { readFile as readFile27 } from "fs/promises";
@@ -103,7 +169,7 @@ var TOOLS = ["editor"];
103
169
  function parseArgs(argv) {
104
170
  const [command = "help"] = argv;
105
171
  if (!COMMANDS.includes(command)) {
106
- throw new Error(`Unknown command "${command}". Run \`pixelkiln help\` for the list.`);
172
+ throw new UsageError(`Unknown command "${command}". Run \`pixelkiln help\` for the list.`);
107
173
  }
108
174
  let rest = argv.slice(1);
109
175
  let subcommand;
@@ -111,10 +177,10 @@ function parseArgs(argv) {
111
177
  if (command === "quality") {
112
178
  subcommand = rest[0];
113
179
  if (subcommand === void 0 || subcommand.startsWith("-")) {
114
- throw new Error(`quality needs a subcommand: ${QUALITY_SUBCOMMANDS.join(", ")}`);
180
+ throw new UsageError(`quality needs a subcommand: ${QUALITY_SUBCOMMANDS.join(", ")}`);
115
181
  }
116
182
  if (!QUALITY_SUBCOMMANDS.includes(subcommand)) {
117
- throw new Error(
183
+ throw new UsageError(
118
184
  `Unknown quality subcommand "${subcommand}". Known: ${QUALITY_SUBCOMMANDS.join(", ")}`
119
185
  );
120
186
  }
@@ -122,10 +188,10 @@ function parseArgs(argv) {
122
188
  } else if (command === "recipe") {
123
189
  subcommand = rest[0];
124
190
  if (subcommand === void 0 || subcommand.startsWith("-")) {
125
- throw new Error(`recipe needs a subcommand: ${RECIPE_SUBCOMMANDS.join(", ")}`);
191
+ throw new UsageError(`recipe needs a subcommand: ${RECIPE_SUBCOMMANDS.join(", ")}`);
126
192
  }
127
193
  if (!RECIPE_SUBCOMMANDS.includes(subcommand)) {
128
- throw new Error(
194
+ throw new UsageError(
129
195
  `Unknown recipe subcommand "${subcommand}". Known: ${RECIPE_SUBCOMMANDS.join(", ")}`
130
196
  );
131
197
  }
@@ -133,17 +199,17 @@ function parseArgs(argv) {
133
199
  if (subcommand !== "list") {
134
200
  target = rest[0];
135
201
  if (target === void 0 || target.startsWith("-")) {
136
- throw new Error(`recipe ${subcommand} needs a recipe id, selector, or path.`);
202
+ throw new UsageError(`recipe ${subcommand} needs a recipe id, selector, or path.`);
137
203
  }
138
204
  rest = rest.slice(1);
139
205
  }
140
206
  } else if (command === "workspace") {
141
207
  subcommand = rest[0];
142
208
  if (subcommand === void 0 || subcommand.startsWith("-")) {
143
- throw new Error(`workspace needs a subcommand: ${WORKSPACE_SUBCOMMANDS.join(", ")}`);
209
+ throw new UsageError(`workspace needs a subcommand: ${WORKSPACE_SUBCOMMANDS.join(", ")}`);
144
210
  }
145
211
  if (!WORKSPACE_SUBCOMMANDS.includes(subcommand)) {
146
- throw new Error(
212
+ throw new UsageError(
147
213
  `Unknown workspace subcommand "${subcommand}". Known: ${WORKSPACE_SUBCOMMANDS.join(", ")}`
148
214
  );
149
215
  }
@@ -151,7 +217,7 @@ function parseArgs(argv) {
151
217
  if (subcommand === "add" || subcommand === "remove") {
152
218
  target = rest[0];
153
219
  if (target === void 0 || target.startsWith("-")) {
154
- throw new Error(
220
+ throw new UsageError(
155
221
  subcommand === "add" ? "workspace add needs a manifest path." : "workspace remove needs a project id or manifest path."
156
222
  );
157
223
  }
@@ -160,29 +226,29 @@ function parseArgs(argv) {
160
226
  } else if (command === "edit") {
161
227
  subcommand = rest[0]?.startsWith("-") || rest[0] === void 0 ? "start" : rest[0];
162
228
  if (!EDIT_SUBCOMMANDS.includes(subcommand)) {
163
- throw new Error(`Unknown edit subcommand "${subcommand}". Known: ${EDIT_SUBCOMMANDS.join(", ")}`);
229
+ throw new UsageError(`Unknown edit subcommand "${subcommand}". Known: ${EDIT_SUBCOMMANDS.join(", ")}`);
164
230
  }
165
231
  if (rest[0] === subcommand) rest = rest.slice(1);
166
232
  } else if (command === "tools") {
167
233
  subcommand = rest[0]?.startsWith("-") || rest[0] === void 0 ? "status" : rest[0];
168
234
  if (!TOOLS_SUBCOMMANDS.includes(subcommand)) {
169
- throw new Error(`Unknown tools subcommand "${subcommand}". Known: ${TOOLS_SUBCOMMANDS.join(", ")}`);
235
+ throw new UsageError(`Unknown tools subcommand "${subcommand}". Known: ${TOOLS_SUBCOMMANDS.join(", ")}`);
170
236
  }
171
237
  if (rest[0] === subcommand) rest = rest.slice(1);
172
238
  target = rest[0]?.startsWith("-") ? void 0 : rest[0];
173
239
  if (subcommand === "install" && target === void 0) {
174
- throw new Error(`tools install needs a tool name: ${TOOLS.join(", ")}`);
240
+ throw new UsageError(`tools install needs a tool name: ${TOOLS.join(", ")}`);
175
241
  }
176
242
  if (target !== void 0) {
177
243
  if (!TOOLS.includes(target)) {
178
- throw new Error(`Unknown tool "${target}". Known: ${TOOLS.join(", ")}`);
244
+ throw new UsageError(`Unknown tool "${target}". Known: ${TOOLS.join(", ")}`);
179
245
  }
180
246
  rest = rest.slice(1);
181
247
  }
182
248
  } else if (command === "refine") {
183
249
  subcommand = rest[0]?.startsWith("-") || rest[0] === void 0 ? "run" : rest[0];
184
250
  if (!REFINE_SUBCOMMANDS.includes(subcommand)) {
185
- throw new Error(
251
+ throw new UsageError(
186
252
  `Unknown refine subcommand "${subcommand}". Known: ${REFINE_SUBCOMMANDS.join(", ")}`
187
253
  );
188
254
  }
@@ -191,19 +257,19 @@ function parseArgs(argv) {
191
257
  for (let i = 0; i < rest.length; i++) {
192
258
  const token = rest[i];
193
259
  if (!token.startsWith("-")) {
194
- throw new Error(`Unexpected argument "${token}". Options must be passed with a named flag.`);
260
+ throw new UsageError(`Unexpected argument "${token}". Options must be passed with a named flag.`);
195
261
  }
196
262
  if (BOOL_FLAGS.includes(token)) continue;
197
263
  if (VALUE_FLAGS.includes(token)) {
198
264
  const value = rest[i + 1];
199
265
  const looksLikeFlag = value !== void 0 && value.startsWith("-") && !Number.isFinite(Number(value));
200
266
  if (value === void 0 || looksLikeFlag) {
201
- throw new Error(`${token} needs a value.`);
267
+ throw new UsageError(`${token} needs a value.`);
202
268
  }
203
269
  i++;
204
270
  continue;
205
271
  }
206
- throw new Error(
272
+ throw new UsageError(
207
273
  `Unknown flag "${token}". Known flags: ${[...VALUE_FLAGS, ...BOOL_FLAGS].join(", ")}`
208
274
  );
209
275
  }
@@ -227,7 +293,7 @@ function parseArgs(argv) {
227
293
  if (rawColumns !== void 0) {
228
294
  columns = Number(rawColumns);
229
295
  if (!Number.isInteger(columns) || columns < 1 || columns > 1024) {
230
- throw new Error(`--columns must be a whole number between 1 and 1024, got "${rawColumns}"`);
296
+ throw new UsageError(`--columns must be a whole number between 1 and 1024, got "${rawColumns}"`);
231
297
  }
232
298
  }
233
299
  const rawPort = get("--port");
@@ -235,7 +301,7 @@ function parseArgs(argv) {
235
301
  if (rawPort !== void 0) {
236
302
  port = Number(rawPort);
237
303
  if (!Number.isInteger(port) || port < 1 || port > 65535) {
238
- throw new Error(`--port must be a whole number between 1 and 65535, got "${rawPort}"`);
304
+ throw new UsageError(`--port must be a whole number between 1 and 65535, got "${rawPort}"`);
239
305
  }
240
306
  }
241
307
  let budget;
@@ -248,30 +314,30 @@ function parseArgs(argv) {
248
314
  const rawAmount = rawBudget.slice(separator + 1).trim();
249
315
  const amount2 = Number(rawAmount);
250
316
  if (!providerId || /[=\s]/.test(providerId) || !Number.isFinite(amount2) || amount2 < 0) {
251
- throw new Error(
317
+ throw new UsageError(
252
318
  `--budget must be a non-negative number or provider=number, got "${rawBudget}".`
253
319
  );
254
320
  }
255
321
  if (Object.hasOwn(providerBudgets, providerId)) {
256
- throw new Error(`--budget repeats provider "${providerId}".`);
322
+ throw new UsageError(`--budget repeats provider "${providerId}".`);
257
323
  }
258
324
  providerBudgets[providerId] = amount2;
259
325
  continue;
260
326
  }
261
327
  const amount = Number(rawBudget);
262
328
  if (!Number.isFinite(amount) || amount < 0) {
263
- throw new Error(`--budget must be a non-negative number, got "${rawBudget}".`);
329
+ throw new UsageError(`--budget must be a non-negative number, got "${rawBudget}".`);
264
330
  }
265
- if (budget !== void 0) throw new Error("Only one unkeyed --budget may be passed.");
331
+ if (budget !== void 0) throw new UsageError("Only one unkeyed --budget may be passed.");
266
332
  budget = amount;
267
333
  }
268
334
  if (budget !== void 0 && Object.keys(providerBudgets).length) {
269
- throw new Error("Do not mix an unkeyed --budget with provider-keyed budgets.");
335
+ throw new UsageError("Do not mix an unkeyed --budget with provider-keyed budgets.");
270
336
  }
271
337
  const manifest = get("--manifest") ?? "pixelkiln.manifest.json";
272
338
  const rawFormat = get("--format");
273
339
  if (rawFormat && rawFormat !== "generic" && rawFormat !== "tiled" && rawFormat !== "godot") {
274
- throw new Error(`--format must be generic, tiled, or godot, got "${rawFormat}"`);
340
+ throw new UsageError(`--format must be generic, tiled, or godot, got "${rawFormat}"`);
275
341
  }
276
342
  const numberOption = (flag, opts) => {
277
343
  const raw = get(flag);
@@ -279,13 +345,13 @@ function parseArgs(argv) {
279
345
  const value = Number(raw);
280
346
  if (!Number.isFinite(value) || value < opts.min || opts.max !== void 0 && value > opts.max || opts.integer && !Number.isInteger(value)) {
281
347
  const range = opts.max === void 0 ? `at least ${opts.min}` : `${opts.min} to ${opts.max}`;
282
- throw new Error(`${flag} must be ${opts.integer ? "a whole number " : "a number "}${range}, got "${raw}"`);
348
+ throw new UsageError(`${flag} must be ${opts.integer ? "a whole number " : "a number "}${range}, got "${raw}"`);
283
349
  }
284
350
  return value;
285
351
  };
286
352
  const rawGridConfidence = get("--min-grid-confidence");
287
353
  if (rawGridConfidence !== void 0 && rawGridConfidence !== "low" && rawGridConfidence !== "medium" && rawGridConfidence !== "high") {
288
- throw new Error(
354
+ throw new UsageError(
289
355
  `--min-grid-confidence must be low, medium, or high, got "${rawGridConfidence}"`
290
356
  );
291
357
  }
@@ -505,10 +571,11 @@ function measureBalanceChange(before, after) {
505
571
  };
506
572
  }
507
573
  var DEFAULT_RATE_LIMIT = { spacingMs: 2500, maxInFlight: 8 };
508
- var UnsupportedCapabilityError = class extends Error {
574
+ var UnsupportedCapabilityError = class extends PixelKilnError {
509
575
  constructor(providerId, capability) {
510
576
  super(
511
- `Provider "${providerId}" does not support ${capability}. That command is unavailable with this backend.`
577
+ `Provider "${providerId}" does not support ${capability}. That command is unavailable with this backend.`,
578
+ "capability"
512
579
  );
513
580
  this.name = "UnsupportedCapabilityError";
514
581
  }
@@ -1086,9 +1153,10 @@ async function loadLock(lockPath) {
1086
1153
  try {
1087
1154
  return parseLock(JSON.parse(await readFile(lockPath, "utf8")));
1088
1155
  } catch (err) {
1089
- throw new Error(
1156
+ throw new ProjectError(
1090
1157
  `Lockfile at ${lockPath} is malformed:
1091
- ${err instanceof Error ? err.message : String(err)}`
1158
+ ${err instanceof Error ? err.message : String(err)}`,
1159
+ { cause: err }
1092
1160
  );
1093
1161
  }
1094
1162
  }
@@ -1122,8 +1190,9 @@ async function writeLockWhileHeld(file, lock) {
1122
1190
  try {
1123
1191
  disk = parseLock(JSON.parse(await readFile(file, "utf8")));
1124
1192
  } catch (err) {
1125
- throw new Error(
1126
- `Refusing to overwrite malformed lockfile at ${file}: ${err instanceof Error ? err.message : String(err)}`
1193
+ throw new ProjectError(
1194
+ `Refusing to overwrite malformed lockfile at ${file}: ${err instanceof Error ? err.message : String(err)}`,
1195
+ { cause: err }
1127
1196
  );
1128
1197
  }
1129
1198
  }
@@ -2051,10 +2120,11 @@ async function writeManagedArtifactBundle(manifestPath, outputs, provenance, opt
2051
2120
  if (!expected || digest(current) !== expected) conflicts.push(destination);
2052
2121
  }
2053
2122
  if (conflicts.length) {
2054
- throw new Error(
2123
+ throw new OverwriteRefusedError(
2055
2124
  `Refusing to overwrite modified or unowned artifact file(s):
2056
2125
  ` + conflicts.map((file) => ` ${file}`).join("\n") + `
2057
- Pass { force: true } (CLI: --force) to take ownership and replace the complete bundle.`
2126
+ Pass { force: true } (CLI: --force) to take ownership and replace the complete bundle.`,
2127
+ conflicts
2058
2128
  );
2059
2129
  }
2060
2130
  }
@@ -3706,8 +3776,10 @@ var ComfyUIClient = class {
3706
3776
  value = null;
3707
3777
  }
3708
3778
  if (!response.ok) {
3709
- throw new Error(
3710
- `ComfyUI image upload failed (${response.status})` + (apiError(value) ? `: ${apiError(value)}` : "")
3779
+ throw new ProviderError(
3780
+ "comfyui",
3781
+ `ComfyUI image upload failed (${response.status})` + (apiError(value) ? `: ${apiError(value)}` : ""),
3782
+ { status: response.status }
3711
3783
  );
3712
3784
  }
3713
3785
  if (!isObject(value) || typeof value.name !== "string" || !value.name) {
@@ -3730,7 +3802,7 @@ var ComfyUIClient = class {
3730
3802
  let target = source;
3731
3803
  if (source.startsWith("comfyui://")) target = this.viewUrl(parseSource(source));
3732
3804
  const response = await this.request(target);
3733
- if (!response.ok) throw new Error(`ComfyUI download failed (${response.status})`);
3805
+ if (!response.ok) throw new ProviderError("comfyui", `ComfyUI download failed (${response.status})`, { status: response.status });
3734
3806
  return Buffer.from(await response.arrayBuffer());
3735
3807
  }
3736
3808
  endpoint(route) {
@@ -3755,8 +3827,10 @@ var ComfyUIClient = class {
3755
3827
  }
3756
3828
  if (!response.ok) {
3757
3829
  const detail = apiError(value);
3758
- throw new Error(
3759
- `ComfyUI request failed (${response.status}) at ${url.pathname}` + (detail ? `: ${detail}` : "")
3830
+ throw new ProviderError(
3831
+ "comfyui",
3832
+ `ComfyUI request failed (${response.status}) at ${url.pathname}` + (detail ? `: ${detail}` : ""),
3833
+ { status: response.status }
3760
3834
  );
3761
3835
  }
3762
3836
  return value;
@@ -4592,14 +4666,12 @@ function validateResponse(schema, raw, operation) {
4592
4666
  const issues = parsed.error.issues.slice(0, 4).map((i) => `${i.path.join(".") || "response"}: ${i.message}`).join("; ");
4593
4667
  throw new Error(`Invalid PixelLab response for ${operation}: ${issues}`);
4594
4668
  }
4595
- var PixelLabError = class extends Error {
4669
+ var PixelLabError = class extends ProviderError {
4596
4670
  constructor(message6, status, body) {
4597
- super(message6);
4598
- this.status = status;
4671
+ super("pixellab", message6, { status });
4599
4672
  this.body = body;
4600
4673
  this.name = "PixelLabError";
4601
4674
  }
4602
- status;
4603
4675
  body;
4604
4676
  };
4605
4677
  var PixelLabClient = class {
@@ -5180,7 +5252,7 @@ var RetroDiffusionClient = class {
5180
5252
  /** Result URLs are signed; no token header, and the same retry policy as the API. */
5181
5253
  async download(url) {
5182
5254
  const response = await this.request(url);
5183
- if (!response.ok) throw new Error(`Retro Diffusion download failed (${response.status})`);
5255
+ if (!response.ok) throw new ProviderError("retrodiffusion", `Retro Diffusion download failed (${response.status})`, { status: response.status });
5184
5256
  return Buffer.from(await response.arrayBuffer());
5185
5257
  }
5186
5258
  async call(path43, init = {}) {
@@ -5203,8 +5275,10 @@ var RetroDiffusionClient = class {
5203
5275
  if (!response.ok) {
5204
5276
  const retry = response.headers.get("retry-after");
5205
5277
  const detail = retroError(value);
5206
- throw new Error(
5207
- `Retro Diffusion request failed (${response.status}): ${detail}` + (retry ? `; retry after ${retry}s` : "")
5278
+ throw new ProviderError(
5279
+ "retrodiffusion",
5280
+ `Retro Diffusion request failed (${response.status}): ${detail}` + (retry ? `; retry after ${retry}s` : ""),
5281
+ { status: response.status }
5208
5282
  );
5209
5283
  }
5210
5284
  return value;
@@ -5596,7 +5670,7 @@ var ScenarioClient = class {
5596
5670
  throw new Error("Scenario downloads must use HTTP or HTTPS");
5597
5671
  }
5598
5672
  const response = await this.request(parsed);
5599
- if (!response.ok) throw new Error(`Scenario download failed (${response.status})`);
5673
+ if (!response.ok) throw new ProviderError("scenario", `Scenario download failed (${response.status})`, { status: response.status });
5600
5674
  const declared = Number(response.headers.get("content-length"));
5601
5675
  if (Number.isFinite(declared) && declared > MAX_DOWNLOAD_BYTES) {
5602
5676
  throw new Error(`Scenario download exceeds the ${MAX_DOWNLOAD_BYTES}-byte safety limit`);
@@ -5643,8 +5717,10 @@ var ScenarioClient = class {
5643
5717
  if (!response.ok) {
5644
5718
  const retry = response.headers.get("retry-after");
5645
5719
  const detail = redactScenarioError(value, [this.apiKey, this.apiSecret, auth]);
5646
- throw new Error(
5647
- `Scenario request failed (${response.status}): ${detail}` + (retry ? `; retry after ${retry}s` : "")
5720
+ throw new ProviderError(
5721
+ "scenario",
5722
+ `Scenario request failed (${response.status}): ${detail}` + (retry ? `; retry after ${retry}s` : ""),
5723
+ { status: response.status }
5648
5724
  );
5649
5725
  }
5650
5726
  return value;
@@ -6043,18 +6119,18 @@ import { existsSync as existsSync9 } from "fs";
6043
6119
  import path13 from "path";
6044
6120
  async function loadManifest(manifestPath) {
6045
6121
  const abs = path13.resolve(manifestPath);
6046
- if (!existsSync9(abs)) throw new Error(`No manifest at ${abs}`);
6122
+ if (!existsSync9(abs)) throw new ProjectError(`No manifest at ${abs}`);
6047
6123
  const input = ManifestInputSchema.safeParse(JSON.parse(await readFile6(abs, "utf8")));
6048
6124
  if (!input.success) {
6049
6125
  const issues = formatManifestIssues(input.error.issues);
6050
- throw new Error(`Manifest at ${abs} is invalid:
6126
+ throw new ProjectError(`Manifest at ${abs} is invalid:
6051
6127
  ${issues}`);
6052
6128
  }
6053
6129
  let styles;
6054
6130
  try {
6055
6131
  styles = resolveStyleInheritance(input.data.styles);
6056
6132
  } catch (error) {
6057
- throw new Error(
6133
+ throw new ProjectError(
6058
6134
  `Manifest at ${abs} is invalid:
6059
6135
  ${error instanceof Error ? error.message : String(error)}`
6060
6136
  );
@@ -6062,7 +6138,7 @@ ${issues}`);
6062
6138
  const parsed = ManifestSchema.safeParse({ ...input.data, styles });
6063
6139
  if (!parsed.success) {
6064
6140
  const issues = formatManifestIssues(parsed.error.issues);
6065
- throw new Error(`Manifest at ${abs} is invalid:
6141
+ throw new ProjectError(`Manifest at ${abs} is invalid:
6066
6142
  ${issues}`);
6067
6143
  }
6068
6144
  const styleIds = new Set(Object.keys(parsed.data.styles));
@@ -6112,7 +6188,7 @@ ${issues}`);
6112
6188
  };
6113
6189
  for (const assetId of Object.keys(parsed.data.assets)) visitRevision(assetId, []);
6114
6190
  if (unknownReferences.length) {
6115
- throw new Error(`Manifest at ${abs} is invalid:
6191
+ throw new ProjectError(`Manifest at ${abs} is invalid:
6116
6192
  ${unknownReferences.map((i) => ` ${i}`).join("\n")}`);
6117
6193
  }
6118
6194
  return { manifest: parsed.data, root: path13.dirname(abs), path: abs };
@@ -6191,14 +6267,14 @@ async function resolveSpecs(loaded, filter) {
6191
6267
  );
6192
6268
  for (const unknownStyle of filter?.styles ?? []) {
6193
6269
  if (!manifest.styles[unknownStyle]) {
6194
- throw new Error(
6270
+ throw new UsageError(
6195
6271
  `Unknown style "${unknownStyle}". Defined: ${Object.keys(manifest.styles).join(", ") || "(none)"}`
6196
6272
  );
6197
6273
  }
6198
6274
  }
6199
6275
  for (const unknownAsset of filter?.assets ?? []) {
6200
6276
  if (!manifest.assets[unknownAsset]) {
6201
- throw new Error(`Unknown asset "${unknownAsset}".`);
6277
+ throw new UsageError(`Unknown asset "${unknownAsset}".`);
6202
6278
  }
6203
6279
  }
6204
6280
  const requestedAssetIds = new Set(filter?.assets?.length ? filter.assets : Object.keys(manifest.assets));
@@ -7232,7 +7308,7 @@ var WorkspaceSchema = z4.object({
7232
7308
  function parseWorkspace(raw) {
7233
7309
  const parsed = WorkspaceSchema.safeParse(raw);
7234
7310
  if (parsed.success) return parsed.data;
7235
- throw new Error(
7311
+ throw new ProjectError(
7236
7312
  `Workspace catalog is not valid v1:
7237
7313
  ${parsed.error.issues.slice(0, 5).map((i) => ` ${i.path.join(".")}: ${i.message}`).join("\n")}`
7238
7314
  );
@@ -7243,7 +7319,7 @@ async function loadWorkspace(workspacePath) {
7243
7319
  try {
7244
7320
  raw = JSON.parse(await readFile11(workspacePath, "utf8"));
7245
7321
  } catch (err) {
7246
- throw new Error(
7322
+ throw new ProjectError(
7247
7323
  `Workspace catalog at ${workspacePath} is malformed:
7248
7324
  ${err instanceof Error ? err.message : String(err)}`
7249
7325
  );
@@ -7885,14 +7961,14 @@ function accountProviderId(manifest, requested, command) {
7885
7961
  const providers = manifestProviderIds(manifest);
7886
7962
  if (requested) {
7887
7963
  if (!providers.includes(requested)) {
7888
- throw new Error(
7964
+ throw new UsageError(
7889
7965
  `Provider "${requested}" is not used by this manifest. Used: ${providers.join(", ")}.`
7890
7966
  );
7891
7967
  }
7892
7968
  return requested;
7893
7969
  }
7894
7970
  if (providers.length > 1) {
7895
- throw new Error(
7971
+ throw new UsageError(
7896
7972
  `${command} is account-scoped and this manifest uses ${providers.join(", ")}. Pass --provider <id>.`
7897
7973
  );
7898
7974
  }
@@ -7903,13 +7979,13 @@ function budgetsForPlan(plan, args) {
7903
7979
  const keyed = Object.entries(args.providerBudgets);
7904
7980
  for (const [provider] of keyed) {
7905
7981
  if (!providerIds.has(provider)) {
7906
- throw new Error(
7982
+ throw new BudgetError(
7907
7983
  `Budget names provider "${provider}", but this run has work only for ${[...providerIds].join(", ") || "no providers"}.`
7908
7984
  );
7909
7985
  }
7910
7986
  }
7911
7987
  if (plan.groups.length > 1 && args.budget !== void 0) {
7912
- throw new Error(
7988
+ throw new BudgetError(
7913
7989
  "A mixed-provider run needs provider-keyed budgets, for example --budget pixellab=40 --budget retrodiffusion=1.25."
7914
7990
  );
7915
7991
  }
@@ -7917,7 +7993,7 @@ function budgetsForPlan(plan, args) {
7917
7993
  for (const group of plan.groups) {
7918
7994
  const ceiling = args.providerBudgets[group.provider];
7919
7995
  if (plan.groups.length > 1 && group.costUnit !== "free" && group.cost > 0 && ceiling === void 0) {
7920
- throw new Error(
7996
+ throw new BudgetError(
7921
7997
  `Mixed-provider run is missing --budget ${group.provider}=<amount> for ${formatCost(group.costUnit, group.cost)} of planned work.`
7922
7998
  );
7923
7999
  }
@@ -7927,14 +8003,14 @@ function budgetsForPlan(plan, args) {
7927
8003
  }
7928
8004
  async function requireCompleteWorkspaceClaims(workspacePath) {
7929
8005
  if (!existsSync15(workspacePath)) {
7930
- throw new Error(`Workspace catalog not found: ${workspacePath}`);
8006
+ throw new ProjectError(`Workspace catalog not found: ${workspacePath}`);
7931
8007
  }
7932
8008
  const dir = path20.dirname(path20.resolve(workspacePath));
7933
8009
  const ws = await loadWorkspace(workspacePath);
7934
8010
  const diagnostics = validateWorkspace(ws, dir);
7935
8011
  const errors = diagnostics.filter((d) => d.level === "error");
7936
8012
  if (errors.length) {
7937
- throw new Error(
8013
+ throw new ProjectError(
7938
8014
  `Workspace catalog at ${workspacePath} is not safe to derive a claim set from:
7939
8015
  ` + errors.map((d) => ` ${d.id}: ${d.message}`).join("\n")
7940
8016
  );
@@ -7953,7 +8029,7 @@ async function openAccountProject(args) {
7953
8029
  }
7954
8030
  async function open(args) {
7955
8031
  if (!existsSync15(path20.resolve(args.manifest))) {
7956
- throw new Error(
8032
+ throw new ProjectError(
7957
8033
  `No manifest at ${path20.resolve(args.manifest)}. Pass --manifest, or run \`pixelkiln init --from <dir>\`.`
7958
8034
  );
7959
8035
  }
@@ -7962,7 +8038,7 @@ async function open(args) {
7962
8038
  let specs = project.specs;
7963
8039
  const accountProvider = ACCOUNT_COMMANDS.has(args.command) ? accountProviderId(loaded.manifest, args.provider, args.command) : void 0;
7964
8040
  if (args.provider && !accountProvider) {
7965
- throw new Error("--provider is only used by balance, adopt, salvage, purge, or workspace add.");
8041
+ throw new UsageError("--provider is only used by balance, adopt, salvage, purge, or workspace add.");
7966
8042
  }
7967
8043
  if (accountProvider) specs = specs.filter((spec) => spec.provider === accountProvider);
7968
8044
  const accountManifest = accountProvider ? {
@@ -7975,7 +8051,7 @@ async function open(args) {
7975
8051
  )
7976
8052
  } : loaded.manifest;
7977
8053
  if (accountProvider && args.styles.some((styleId) => !accountManifest.styles[styleId])) {
7978
- throw new Error(`Selected style is not assigned to provider "${accountProvider}".`);
8054
+ throw new UsageError(`Selected style is not assigned to provider "${accountProvider}".`);
7979
8055
  }
7980
8056
  return { loaded, specs, lock, accountProvider, accountManifest };
7981
8057
  }
@@ -9010,8 +9086,9 @@ async function revertGeneration(provider, spec, lock, lockPath, opts) {
9010
9086
  const file = currentEntryOutputPath(entry, spec, i);
9011
9087
  if (!existsSync18(file)) continue;
9012
9088
  if (await sha256File(file) !== entry.outputs[i].sha256 && !opts.force) {
9013
- throw new Error(
9014
- `${path26.relative(process.cwd(), file)} was changed after download; keep it with pixelkiln edit, or pass --force to replace it`
9089
+ throw new OverwriteRefusedError(
9090
+ `${path26.relative(process.cwd(), file)} was changed after download; keep it with pixelkiln edit, or pass --force to replace it`,
9091
+ [file]
9015
9092
  );
9016
9093
  }
9017
9094
  }
@@ -10080,7 +10157,7 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
10080
10157
  if (opts.budget != null) {
10081
10158
  const cost = [...estimates.values()].reduce((sum, estimate) => sum + estimate.amount, 0);
10082
10159
  if (cost > opts.budget) {
10083
- throw new Error(
10160
+ throw new BudgetError(
10084
10161
  `This run would spend ${formatCost(unit, cost)} but the budget is ${formatCost(unit, opts.budget)}. Narrow it with --only/--style, or raise --budget.`
10085
10162
  );
10086
10163
  }
@@ -11690,12 +11767,12 @@ async function runGenerate(args) {
11690
11767
  balance: ${formatCost(balance.unit, balance.remaining)} remaining (${group.provider})`
11691
11768
  );
11692
11769
  if (balance.unit !== group.costUnit) {
11693
- throw new Error(
11770
+ throw new BudgetError(
11694
11771
  `Provider ${group.provider} estimate unit ${group.costUnit} does not match balance unit ${balance.unit}.`
11695
11772
  );
11696
11773
  }
11697
11774
  if (balance.unit !== "free" && group.cost > balance.remaining) {
11698
- throw new Error(
11775
+ throw new BudgetError(
11699
11776
  `${group.provider} needs ${formatCost(balance.unit, group.cost)} but only ${formatCost(balance.unit, balance.remaining)} remain.`
11700
11777
  );
11701
11778
  }
@@ -11707,7 +11784,7 @@ async function runGenerate(args) {
11707
11784
  }
11708
11785
  const ceiling = budgets.get(group.provider);
11709
11786
  if (ceiling !== void 0 && group.cost > ceiling) {
11710
- throw new Error(
11787
+ throw new BudgetError(
11711
11788
  `${group.provider} would spend ${formatCost(group.costUnit, group.cost)} but its budget is ${formatCost(group.costUnit, ceiling)}.`
11712
11789
  );
11713
11790
  }
@@ -13522,7 +13599,10 @@ async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
13522
13599
  if (existsSync28(absolute) && !options.force) {
13523
13600
  const current = await readFile24(absolute);
13524
13601
  if (!current.equals(data)) {
13525
- throw new Error(`Quality baseline already exists with different content: ${absolute}. Pass --force to replace it.`);
13602
+ throw new OverwriteRefusedError(
13603
+ `Quality baseline already exists with different content: ${absolute}. Pass --force to replace it.`,
13604
+ [absolute]
13605
+ );
13526
13606
  }
13527
13607
  }
13528
13608
  const result = await writeArtifactBundle([{ path: absolute, data }]);
@@ -14051,8 +14131,9 @@ async function installRecipe(target, options = {}) {
14051
14131
  if (!existsSync29(file.path)) continue;
14052
14132
  const current = await readFile26(file.path);
14053
14133
  if (!current.equals(file.data)) {
14054
- throw new Error(
14055
- `Recipe destination has local changes: ${file.path}. Choose another --out or pass --force to replace declared recipe files.`
14134
+ throw new OverwriteRefusedError(
14135
+ `Recipe destination has local changes: ${file.path}. Choose another --out or pass --force to replace declared recipe files.`,
14136
+ [file.path]
14056
14137
  );
14057
14138
  }
14058
14139
  }
@@ -14519,9 +14600,12 @@ async function main(argv = process.argv.slice(2)) {
14519
14600
 
14520
14601
  // src/cli.ts
14521
14602
  main().catch((err) => {
14603
+ const message6 = err instanceof Error ? err.message : String(err);
14604
+ const hint = err instanceof PixelKilnError && err.hint ? ` ${err.hint}
14605
+ ` : "";
14522
14606
  console.error(`
14523
- error: ${err instanceof Error ? err.message : String(err)}
14524
- `);
14525
- process.exit(1);
14607
+ error: ${message6}
14608
+ ${hint}`);
14609
+ process.exit(exitCodeFor(err));
14526
14610
  });
14527
14611
  //# sourceMappingURL=cli.js.map