pixelkiln 0.23.0 → 0.24.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/dist/cli.js CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import path26 from "path";
5
- import { existsSync as existsSync21 } from "fs";
4
+ import path28 from "path";
5
+ import { existsSync as existsSync22 } from "fs";
6
6
  import { readFile as readFile22 } from "fs/promises";
7
7
 
8
8
  // src/env.ts
@@ -134,10 +134,10 @@ import { createReadStream } from "fs";
134
134
  function sha256(data) {
135
135
  return createHash("sha256").update(data).digest("hex");
136
136
  }
137
- async function sha256File(path27) {
137
+ async function sha256File(path29) {
138
138
  return new Promise((resolve, reject) => {
139
139
  const hash = createHash("sha256");
140
- const stream = createReadStream(path27);
140
+ const stream = createReadStream(path29);
141
141
  stream.on("data", (chunk2) => hash.update(chunk2));
142
142
  stream.on("error", reject);
143
143
  stream.on("end", () => resolve(hash.digest("hex")));
@@ -1617,11 +1617,11 @@ var PixelLabClient = class {
1617
1617
  * common than the failure mode of retrying (a duplicate object), and a
1618
1618
  * duplicate is visible and free to delete whereas a silent gap is neither.
1619
1619
  */
1620
- async request(path27, init, attempt = 0) {
1620
+ async request(path29, init, attempt = 0) {
1621
1621
  const auth = this.apiKey.startsWith("Bearer ") ? this.apiKey : `Bearer ${this.apiKey}`;
1622
1622
  let res;
1623
1623
  try {
1624
- res = await fetch(`${BASE}${path27}`, {
1624
+ res = await fetch(`${BASE}${path29}`, {
1625
1625
  ...init,
1626
1626
  signal: init?.signal ?? AbortSignal.timeout(this.timeoutMs),
1627
1627
  headers: {
@@ -1633,24 +1633,24 @@ var PixelLabClient = class {
1633
1633
  } catch (err) {
1634
1634
  if (attempt < MAX_RETRIES) {
1635
1635
  await sleep(backoffMs(attempt));
1636
- return this.request(path27, init, attempt + 1);
1636
+ return this.request(path29, init, attempt + 1);
1637
1637
  }
1638
1638
  throw err;
1639
1639
  }
1640
1640
  if (!res.ok && shouldRetry(res.status) && attempt < MAX_RETRIES) {
1641
1641
  const waitMs = retryAfterMs(res.headers.get("retry-after")) ?? backoffMs(attempt);
1642
1642
  await sleep(waitMs);
1643
- return this.request(path27, init, attempt + 1);
1643
+ return this.request(path29, init, attempt + 1);
1644
1644
  }
1645
1645
  const text = await res.text();
1646
1646
  if (!res.ok) {
1647
- throw new PixelLabError(`${init?.method ?? "GET"} ${path27} \u2192 ${res.status}`, res.status, text);
1647
+ throw new PixelLabError(`${init?.method ?? "GET"} ${path29} \u2192 ${res.status}`, res.status, text);
1648
1648
  }
1649
1649
  if (!text) return {};
1650
1650
  try {
1651
1651
  return JSON.parse(text);
1652
1652
  } catch {
1653
- throw new Error(`${init?.method ?? "GET"} ${path27} returned invalid JSON`);
1653
+ throw new Error(`${init?.method ?? "GET"} ${path29} returned invalid JSON`);
1654
1654
  }
1655
1655
  }
1656
1656
  async balance() {
@@ -2112,6 +2112,15 @@ var AssetSchema = z2.object({
2112
2112
  * that wasn't. `prompt` still records what was asked for.
2113
2113
  */
2114
2114
  source: z2.string().optional(),
2115
+ /**
2116
+ * Per-style replacement for `source`, keyed by style id.
2117
+ *
2118
+ * A hand edit belongs to one generation: the same asset generated in two
2119
+ * styles is two different pictures, so the touch-up of one must not be
2120
+ * placed for the other. `pixelkiln edit` writes here when an asset is in
2121
+ * more than one style, and to `source` when it is in exactly one.
2122
+ */
2123
+ sourceByStyle: z2.record(z2.string().min(1)).default({}),
2115
2124
  /** Controlled generation derived from another asset in the same style. */
2116
2125
  revision: RevisionSchema.optional(),
2117
2126
  /**
@@ -2146,7 +2155,7 @@ var AssetSchema = z2.object({
2146
2155
  z2.array(z2.union([z2.string(), z2.number().finite(), z2.boolean()])).min(2).max(64)
2147
2156
  ])).default({})
2148
2157
  }).strict().superRefine((asset, context) => {
2149
- if (asset.source && asset.revision) {
2158
+ if ((asset.source || Object.keys(asset.sourceByStyle).length) && asset.revision) {
2150
2159
  context.addIssue({
2151
2160
  code: z2.ZodIssueCode.custom,
2152
2161
  message: "source and revision are mutually exclusive",
@@ -2613,9 +2622,9 @@ var RetroDiffusionClient = class {
2613
2622
  }
2614
2623
  return balance;
2615
2624
  }
2616
- async call(path27, init = {}) {
2625
+ async call(path29, init = {}) {
2617
2626
  if (!this.token) throw new Error("RD_API_KEY is not set");
2618
- const response = await this.request(`${this.baseUrl}${path27}`, {
2627
+ const response = await this.request(`${this.baseUrl}${path29}`, {
2619
2628
  ...init,
2620
2629
  headers: {
2621
2630
  "Content-Type": "application/json",
@@ -3784,6 +3793,11 @@ ${issues}`);
3784
3793
  unknownReferences.push(`assets.${assetId}.promptByStyle: unknown style "${styleId}"`);
3785
3794
  }
3786
3795
  }
3796
+ for (const styleId of Object.keys(asset.sourceByStyle)) {
3797
+ if (!styleIds.has(styleId)) {
3798
+ unknownReferences.push(`assets.${assetId}.sourceByStyle: unknown style "${styleId}"`);
3799
+ }
3800
+ }
3787
3801
  if (asset.revision) {
3788
3802
  if (!parsed.data.assets[asset.revision.from]) {
3789
3803
  unknownReferences.push(
@@ -4048,7 +4062,7 @@ async function resolveSpecs(loaded, filter) {
4048
4062
  }
4049
4063
  } : {},
4050
4064
  tags,
4051
- source: asset.source,
4065
+ source: asset.sourceByStyle[styleId] ?? asset.source,
4052
4066
  // Revision identity is attached after every dependency in this style
4053
4067
  // has a concrete output target. Finalization below computes the hash.
4054
4068
  specHash: ""
@@ -4306,16 +4320,24 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
4306
4320
  }
4307
4321
  const prefix = `${styleId}/`;
4308
4322
  const entries = Object.entries(lock.entries).filter(([key]) => key.startsWith(prefix));
4309
- if (!entries.length) {
4323
+ const sources = options.sources ?? {};
4324
+ if (!entries.length && !Object.keys(sources).length) {
4310
4325
  throw new Error(
4311
4326
  `No locked assets for style "${styleId}". Run \`pixelkiln gen --style ${styleId}\` first.`
4312
4327
  );
4313
4328
  }
4314
4329
  const inputs = [];
4315
4330
  const noOutput = [];
4331
+ const locked = /* @__PURE__ */ new Set();
4316
4332
  for (const [key, entry] of entries) {
4317
4333
  const id = key.slice(prefix.length);
4334
+ locked.add(id);
4318
4335
  const outputs = resolveEntryOutputs(entry, id, manifestDir);
4336
+ const source = sources[id];
4337
+ if (source && outputs.length <= 1) {
4338
+ inputs.push({ id, path: path7.resolve(manifestDir, source) });
4339
+ continue;
4340
+ }
4319
4341
  let selected = outputs;
4320
4342
  if (options.outputRoles?.length) {
4321
4343
  selected = outputs.filter(
@@ -4332,6 +4354,9 @@ function packStyle(lock, styleId, manifestDir, options = {}) {
4332
4354
  }
4333
4355
  for (const output of selected) inputs.push({ id: output.id, path: output.absolutePath });
4334
4356
  }
4357
+ for (const [id, source] of Object.entries(sources)) {
4358
+ if (!locked.has(id)) inputs.push({ id, path: path7.resolve(manifestDir, source) });
4359
+ }
4335
4360
  if (!inputs.length) {
4336
4361
  throw new Error(`No readable sprites for style "${styleId}" \u2014 ${noOutput.length} skipped.`);
4337
4362
  }
@@ -6821,10 +6846,10 @@ function isSha256Hash(value) {
6821
6846
  function parseCache(value) {
6822
6847
  return HashCacheSchema.parse(value);
6823
6848
  }
6824
- async function loadCache(path27) {
6825
- if (!existsSync11(path27)) return { version: 1, hashes: {} };
6849
+ async function loadCache(path29) {
6850
+ if (!existsSync11(path29)) return { version: 1, hashes: {} };
6826
6851
  try {
6827
- const parsed = HashCacheSchema.safeParse(JSON.parse(await readFile8(path27, "utf8")));
6852
+ const parsed = HashCacheSchema.safeParse(JSON.parse(await readFile8(path29, "utf8")));
6828
6853
  if (!parsed.success) return { version: 1, hashes: {} };
6829
6854
  return {
6830
6855
  version: 1,
@@ -6836,18 +6861,18 @@ async function loadCache(path27) {
6836
6861
  return { version: 1, hashes: {} };
6837
6862
  }
6838
6863
  }
6839
- async function saveCache(path27, cache) {
6864
+ async function saveCache(path29, cache) {
6840
6865
  const sorted = {};
6841
6866
  for (const key of Object.keys(cache.hashes).sort()) {
6842
6867
  const hash = cache.hashes[key];
6843
6868
  if (!isSha256Hash(hash)) throw new Error(`Refusing to cache invalid SHA-256 for ${key}`);
6844
6869
  sorted[key] = hash;
6845
6870
  }
6846
- await mkdir4(pathModule.dirname(pathModule.resolve(path27)), { recursive: true });
6847
- const tmp = `${path27}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
6871
+ await mkdir4(pathModule.dirname(pathModule.resolve(path29)), { recursive: true });
6872
+ const tmp = `${path29}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
6848
6873
  try {
6849
6874
  await writeFile4(tmp, JSON.stringify({ version: 1, hashes: sorted }, null, 2) + "\n");
6850
- await rename4(tmp, path27);
6875
+ await rename4(tmp, path29);
6851
6876
  } finally {
6852
6877
  await rm5(tmp, { force: true });
6853
6878
  }
@@ -7096,6 +7121,7 @@ function buildManifest(name, styleId, generator, outDir, scanned) {
7096
7121
  assets[asset.id] = {
7097
7122
  prompt: "",
7098
7123
  promptByStyle: {},
7124
+ sourceByStyle: {},
7099
7125
  category: asset.category || void 0,
7100
7126
  file: asset.file,
7101
7127
  tags: [],
@@ -7789,9 +7815,9 @@ async function runPicker(provider, lock, lockPath, opts = {}) {
7789
7815
 
7790
7816
  // src/gallery/snapshot.ts
7791
7817
  import { createHash as createHash3 } from "crypto";
7792
- import { existsSync as existsSync16 } from "fs";
7818
+ import { existsSync as existsSync17 } from "fs";
7793
7819
  import { readFile as readFile15, stat as stat3 } from "fs/promises";
7794
- import path20 from "path";
7820
+ import path22 from "path";
7795
7821
 
7796
7822
  // src/workspace.ts
7797
7823
  import { mkdir as mkdir5, readFile as readFile13, rename as rename5, rm as rm6, writeFile as writeFile7 } from "fs/promises";
@@ -7927,6 +7953,10 @@ function validateWorkspace(ws, dir) {
7927
7953
  }
7928
7954
 
7929
7955
  // src/gallery/edit.ts
7956
+ import path21 from "path";
7957
+ import { z as z6 } from "zod";
7958
+
7959
+ // src/manifest-edit.ts
7930
7960
  import { readFile as readFile14, rename as rename6, rm as rm7, writeFile as writeFile8 } from "fs/promises";
7931
7961
  import path19 from "path";
7932
7962
  import { z as z5 } from "zod";
@@ -7973,6 +8003,22 @@ var ManifestEditSchema = z5.discriminatedUnion("action", [
7973
8003
  expectedSha256: HexSha,
7974
8004
  asset: NewAssetSchema
7975
8005
  }).strict(),
8006
+ z5.object({
8007
+ action: z5.literal("set-source"),
8008
+ project: z5.string().min(1).optional(),
8009
+ assetId: z5.string().min(1),
8010
+ styleId: z5.string().min(1),
8011
+ /** Manifest-relative path of the art that stands in for this asset in this style. */
8012
+ source: z5.string().min(1),
8013
+ expectedSha256: HexSha
8014
+ }).strict(),
8015
+ z5.object({
8016
+ action: z5.literal("clear-source"),
8017
+ project: z5.string().min(1).optional(),
8018
+ assetId: z5.string().min(1),
8019
+ styleId: z5.string().min(1),
8020
+ expectedSha256: HexSha
8021
+ }).strict(),
7976
8022
  z5.object({
7977
8023
  action: z5.literal("patch-style"),
7978
8024
  project: z5.string().min(1).optional(),
@@ -8023,8 +8069,38 @@ function setOrDelete(target, key, value) {
8023
8069
  if (value === null || value === void 0) delete target[key];
8024
8070
  else target[key] = value;
8025
8071
  }
8072
+ function assetStyles(raw, asset) {
8073
+ const own = Array.isArray(asset.styles) ? asset.styles : [];
8074
+ return own.length ? own : Object.keys(raw.styles ?? {});
8075
+ }
8026
8076
  function applyEdit(raw, edit) {
8027
8077
  raw.assets ??= {};
8078
+ if (edit.action === "set-source" || edit.action === "clear-source") {
8079
+ const asset2 = Object.hasOwn(raw.assets, edit.assetId) ? raw.assets[edit.assetId] : void 0;
8080
+ if (!asset2) throw new ManifestEditError(`asset "${edit.assetId}" is not declared by the manifest`);
8081
+ if (!raw.styles || !Object.hasOwn(raw.styles, edit.styleId)) {
8082
+ throw new ManifestEditError(`unknown style "${edit.styleId}"`);
8083
+ }
8084
+ if (!assetStyles(raw, asset2).includes(edit.styleId)) {
8085
+ throw new ManifestEditError(`asset "${edit.assetId}" is not in style "${edit.styleId}"`);
8086
+ }
8087
+ const single = assetStyles(raw, asset2).length === 1;
8088
+ const byStyle = { ...asset2.sourceByStyle ?? {} };
8089
+ if (edit.action === "set-source") {
8090
+ if (single) {
8091
+ asset2.source = edit.source;
8092
+ delete byStyle[edit.styleId];
8093
+ } else {
8094
+ byStyle[edit.styleId] = edit.source;
8095
+ }
8096
+ } else {
8097
+ delete byStyle[edit.styleId];
8098
+ if (single) delete asset2.source;
8099
+ }
8100
+ if (Object.keys(byStyle).length) asset2.sourceByStyle = byStyle;
8101
+ else delete asset2.sourceByStyle;
8102
+ return;
8103
+ }
8028
8104
  if (edit.action === "patch-style") {
8029
8105
  const style = raw.styles && Object.hasOwn(raw.styles, edit.styleId) ? raw.styles[edit.styleId] : void 0;
8030
8106
  if (!style) throw new ManifestEditError(`style "${edit.styleId}" is not declared by the manifest`);
@@ -8128,10 +8204,134 @@ async function applyManifestEdit(manifestPath, edit) {
8128
8204
  }
8129
8205
  return { manifestPath: absolute, sha256: sha256(next), changed: true };
8130
8206
  }
8207
+
8208
+ // src/pipeline/hand-edit.ts
8209
+ import { spawn as spawn2 } from "child_process";
8210
+ import { existsSync as existsSync16 } from "fs";
8211
+ import { copyFile, mkdir as mkdir6 } from "fs/promises";
8212
+ import path20 from "path";
8213
+ var HAND_EDIT_DIR = "edits";
8214
+ function handEditPath(loaded, spec) {
8215
+ const style = loaded.manifest.styles[spec.styleId];
8216
+ const outDir = path20.resolve(loaded.root, style?.outDir ?? ".");
8217
+ const relative = path20.relative(outDir, spec.outFile);
8218
+ const inside = relative && !relative.startsWith("..") && !path20.isAbsolute(relative);
8219
+ const file = inside ? relative : path20.basename(spec.outFile);
8220
+ return path20.join(outDir, HAND_EDIT_DIR, file.replace(/\.gif$/i, ".png"));
8221
+ }
8222
+ function handEditBase(spec, lock) {
8223
+ const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
8224
+ if (entry) {
8225
+ if (entry.outputs.length !== 1) return null;
8226
+ const primary = primaryOutput(entry);
8227
+ if (!primary || primary.mediaType && primary.mediaType !== "image/png") return null;
8228
+ const file = currentEntryOutputPath(entry, spec, 0);
8229
+ return existsSync16(file) ? file : null;
8230
+ }
8231
+ return existsSync16(spec.outFile) ? spec.outFile : null;
8232
+ }
8233
+ async function startHandEdit(loaded, lock, spec, opts = {}) {
8234
+ const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
8235
+ if (entry && entry.outputs.length > 1) {
8236
+ throw new ManifestEditError(
8237
+ `${spec.styleId}/${spec.assetId} has ${entry.outputs.length} outputs; hand edits currently cover single-image assets`
8238
+ );
8239
+ }
8240
+ const existing = spec.source ? path20.resolve(loaded.root, spec.source) : null;
8241
+ const editPath = existing ?? handEditPath(loaded, spec);
8242
+ let created = false;
8243
+ if (!existsSync16(editPath)) {
8244
+ const base = handEditBase(spec, lock);
8245
+ if (!base) {
8246
+ throw new ManifestEditError(
8247
+ `${spec.styleId}/${spec.assetId} has no generated PNG on disk to start from; generate or restore it first`
8248
+ );
8249
+ }
8250
+ await mkdir6(path20.dirname(editPath), { recursive: true });
8251
+ await copyFile(base, editPath);
8252
+ created = true;
8253
+ }
8254
+ const source = portableOutputPath(editPath, loaded.root);
8255
+ let declared = false;
8256
+ let manifestSha256 = opts.expectedSha256 ?? await sha256File(loaded.path);
8257
+ if (spec.source !== source) {
8258
+ const result = await applyManifestEdit(loaded.path, {
8259
+ action: "set-source",
8260
+ assetId: spec.assetId,
8261
+ styleId: spec.styleId,
8262
+ source,
8263
+ expectedSha256: manifestSha256
8264
+ });
8265
+ declared = result.changed;
8266
+ manifestSha256 = result.sha256;
8267
+ }
8268
+ return { editPath, source, created, declared, manifestSha256 };
8269
+ }
8270
+ async function detachHandEdit(loaded, spec, opts = {}) {
8271
+ if (!spec.source) {
8272
+ return { manifestSha256: opts.expectedSha256 ?? await sha256File(loaded.path), changed: false };
8273
+ }
8274
+ const result = await applyManifestEdit(loaded.path, {
8275
+ action: "clear-source",
8276
+ assetId: spec.assetId,
8277
+ styleId: spec.styleId,
8278
+ expectedSha256: opts.expectedSha256 ?? await sha256File(loaded.path)
8279
+ });
8280
+ return { manifestSha256: result.sha256, changed: result.changed };
8281
+ }
8282
+ function openInEditor(file, editor = process.env.PIXELKILN_EDITOR) {
8283
+ const command = editor?.trim() ? editor.trim().split(/\s+/) : process.platform === "darwin" ? ["open"] : process.platform === "win32" ? ["cmd", "/c", "start", ""] : ["xdg-open"];
8284
+ const [program, ...args] = command;
8285
+ const child = spawn2(program, [...args, file], { stdio: "ignore", detached: true });
8286
+ child.on("error", () => {
8287
+ });
8288
+ child.unref();
8289
+ return command.join(" ");
8290
+ }
8291
+
8292
+ // src/gallery/edit.ts
8293
+ var HandEditRequestSchema = z6.discriminatedUnion("action", [
8294
+ z6.object({
8295
+ action: z6.literal("start-edit"),
8296
+ project: z6.string().min(1).optional(),
8297
+ assetId: z6.string().min(1),
8298
+ styleId: z6.string().min(1),
8299
+ expectedSha256: z6.string().regex(/^[0-9a-f]{64}$/),
8300
+ /** Also launch the author's editor on the file. */
8301
+ open: z6.boolean().optional()
8302
+ }).strict(),
8303
+ z6.object({
8304
+ action: z6.literal("detach-edit"),
8305
+ project: z6.string().min(1).optional(),
8306
+ assetId: z6.string().min(1),
8307
+ styleId: z6.string().min(1),
8308
+ expectedSha256: z6.string().regex(/^[0-9a-f]{64}$/)
8309
+ }).strict()
8310
+ ]);
8131
8311
  function createGalleryEditHandler(opts) {
8132
8312
  const log2 = opts.onProgress ?? (() => {
8133
8313
  });
8134
8314
  return async (body) => {
8315
+ const handEdit = HandEditRequestSchema.safeParse(body);
8316
+ if (handEdit.success) {
8317
+ if (!opts.loadProject) throw new ManifestEditError("hand edits are not available in this gallery");
8318
+ const request = handEdit.data;
8319
+ const ctx = await opts.loadProject(request.project);
8320
+ const spec = ctx.specs.find((candidate) => lockKey(candidate.styleId, candidate.assetId) === lockKey(request.styleId, request.assetId));
8321
+ if (!spec) throw new ManifestEditError(`"${request.styleId}/${request.assetId}" is not declared by the manifest`);
8322
+ if (request.action === "start-edit") {
8323
+ const started = await startHandEdit(ctx.loaded, ctx.lock, spec, { expectedSha256: request.expectedSha256 });
8324
+ log2(` hand edit ${started.created ? "created" : "found"}: ${started.source}${started.declared ? " (declared in the manifest)" : ""}`);
8325
+ if (request.open) {
8326
+ const command = (opts.openEditor ?? openInEditor)(started.editPath);
8327
+ log2(` opened with: ${command}`);
8328
+ }
8329
+ } else {
8330
+ const result2 = await detachHandEdit(ctx.loaded, spec, { expectedSha256: request.expectedSha256 });
8331
+ if (result2.changed) log2(` hand edit detached: ${request.styleId}/${request.assetId} (file kept)`);
8332
+ }
8333
+ return opts.reload();
8334
+ }
8135
8335
  const parsed = ManifestEditSchema.safeParse(body);
8136
8336
  if (!parsed.success) {
8137
8337
  throw new ManifestEditError(
@@ -8146,9 +8346,9 @@ function createGalleryEditHandler(opts) {
8146
8346
  }
8147
8347
  const result = await applyManifestEdit(manifestPath, parsed.data);
8148
8348
  if (result.changed) {
8149
- const relative = path19.relative(process.cwd(), result.manifestPath);
8349
+ const relative = path21.relative(process.cwd(), result.manifestPath);
8150
8350
  const shown = !relative || relative.startsWith("..") ? result.manifestPath : relative;
8151
- const subject = parsed.data.action === "patch-style" ? `style ${parsed.data.styleId}` : parsed.data.assetId;
8351
+ const subject = parsed.data.action === "patch-style" ? `style ${parsed.data.styleId}` : parsed.data.action === "set-source" || parsed.data.action === "clear-source" ? `${parsed.data.styleId}/${parsed.data.assetId} source` : parsed.data.assetId;
8152
8352
  log2(` manifest edited: ${parsed.data.action === "add-asset" ? "added" : "changed"} ${subject} in ${shown}`);
8153
8353
  }
8154
8354
  return opts.reload();
@@ -8157,7 +8357,7 @@ function createGalleryEditHandler(opts) {
8157
8357
 
8158
8358
  // src/gallery/snapshot.ts
8159
8359
  function galleryMediaId(absolutePath) {
8160
- return createHash3("sha256").update(path20.resolve(absolutePath)).digest("hex").slice(0, 24);
8360
+ return createHash3("sha256").update(path22.resolve(absolutePath)).digest("hex").slice(0, 24);
8161
8361
  }
8162
8362
  function galleryMediaRoute(id) {
8163
8363
  return `/media/${id}`;
@@ -8182,23 +8382,24 @@ function metadataFps(entry) {
8182
8382
  }
8183
8383
  return null;
8184
8384
  }
8185
- async function fileBytes(absolutePath) {
8385
+ async function fileInfo(absolutePath) {
8186
8386
  try {
8187
8387
  const info = await stat3(absolutePath);
8188
- return info.isFile() ? info.size : null;
8388
+ return info.isFile() ? { bytes: info.size, modifiedAt: info.mtime.toISOString() } : null;
8189
8389
  } catch {
8190
8390
  return null;
8191
8391
  }
8192
8392
  }
8193
8393
  async function describeOutput(media, root, absolutePath, recorded = {}) {
8194
- const bytes = await fileBytes(absolutePath);
8195
- const exists = bytes !== null;
8394
+ const info = await fileInfo(absolutePath);
8395
+ const exists = info !== null;
8196
8396
  const mediaType = recorded.mediaType ?? mediaTypeFromExtension(absolutePath);
8197
8397
  let url = null;
8198
8398
  if (exists && mediaType) {
8199
8399
  const id = galleryMediaId(absolutePath);
8200
8400
  media.set(id, { path: absolutePath, contentType: mediaType });
8201
- url = `${galleryMediaRoute(id)}?v=${(recorded.sha256 ?? String(bytes)).slice(0, 16)}`;
8401
+ const version = recorded.sha256 ?? `${info.bytes}-${Date.parse(info.modifiedAt).toString(36)}`;
8402
+ url = `${galleryMediaRoute(id)}?v=${version.slice(0, 24)}`;
8202
8403
  }
8203
8404
  return {
8204
8405
  path: portableOutputPath(absolutePath, root),
@@ -8207,17 +8408,18 @@ async function describeOutput(media, root, absolutePath, recorded = {}) {
8207
8408
  role: recorded.role ?? null,
8208
8409
  mediaType,
8209
8410
  exists,
8210
- bytes,
8411
+ bytes: info?.bytes ?? null,
8412
+ modifiedAt: info?.modifiedAt ?? null,
8211
8413
  url
8212
8414
  };
8213
8415
  }
8214
8416
  async function describeQuality(media, root, inspection) {
8215
8417
  const outputPaths = inspection.outputs ?? [inspection.output];
8216
8418
  const outputs = await Promise.all(
8217
- outputPaths.map((file) => describeOutput(media, root, path20.resolve(root, file)))
8419
+ outputPaths.map((file) => describeOutput(media, root, path22.resolve(root, file)))
8218
8420
  );
8219
- const recordPath = path20.resolve(root, inspection.record);
8220
- const recordExists = existsSync16(recordPath);
8421
+ const recordPath = path22.resolve(root, inspection.record);
8422
+ const recordExists = existsSync17(recordPath);
8221
8423
  let options = null;
8222
8424
  let check = null;
8223
8425
  if (recordExists) {
@@ -8236,7 +8438,7 @@ async function describeQuality(media, root, inspection) {
8236
8438
  return {
8237
8439
  state: inspection.state,
8238
8440
  reason: inspection.reason,
8239
- output: portableOutputPath(path20.resolve(root, inspection.output), root),
8441
+ output: portableOutputPath(path22.resolve(root, inspection.output), root),
8240
8442
  record: portableOutputPath(recordPath, root),
8241
8443
  recordExists,
8242
8444
  outputs,
@@ -8286,15 +8488,24 @@ async function buildGallerySnapshot(opts) {
8286
8488
  const entry = lock.entries[key];
8287
8489
  const asset = loaded.manifest.assets[spec.assetId] ?? null;
8288
8490
  let outputs;
8491
+ let edit = null;
8492
+ let editStatus = null;
8289
8493
  if (entry && entry.outputs.length) {
8290
8494
  outputs = await Promise.all(
8291
8495
  entry.outputs.map(
8292
8496
  (output, index) => describeOutput(media, root, currentEntryOutputPath(entry, spec, index), output)
8293
8497
  )
8294
8498
  );
8499
+ if (spec.source) {
8500
+ const editPath = path22.resolve(root, spec.source);
8501
+ const editSha = existsSync17(editPath) ? await sha256File(editPath) : null;
8502
+ edit = await describeOutput(media, root, editPath, { sha256: editSha });
8503
+ const generated = outputs[0];
8504
+ editStatus = !edit.exists ? "missing" : generated && editSha === generated.sha256 ? "same" : generated?.modifiedAt && edit.modifiedAt && generated.modifiedAt > edit.modifiedAt && generated.exists ? "regenerated-since" : "edited";
8505
+ }
8295
8506
  } else if (spec.source) {
8296
- outputs = [await describeOutput(media, root, path20.resolve(root, spec.source))];
8297
- } else if (!entry && existsSync16(spec.outFile)) {
8507
+ outputs = [await describeOutput(media, root, path22.resolve(root, spec.source))];
8508
+ } else if (!entry && existsSync17(spec.outFile)) {
8298
8509
  outputs = [await describeOutput(media, root, spec.outFile)];
8299
8510
  } else {
8300
8511
  outputs = [];
@@ -8338,6 +8549,8 @@ async function buildGallerySnapshot(opts) {
8338
8549
  providerMetadata: entry?.providerMetadata ?? {},
8339
8550
  asset,
8340
8551
  source: spec.source ?? null,
8552
+ edit,
8553
+ editStatus,
8341
8554
  tags: spec.tags,
8342
8555
  category: asset?.category ?? null
8343
8556
  });
@@ -8390,6 +8603,8 @@ async function buildGallerySnapshot(opts) {
8390
8603
  providerMetadata: entry.providerMetadata ?? {},
8391
8604
  asset: null,
8392
8605
  source: null,
8606
+ edit: null,
8607
+ editStatus: null,
8393
8608
  tags: [],
8394
8609
  category: null
8395
8610
  });
@@ -8457,7 +8672,7 @@ async function buildGallerySnapshot(opts) {
8457
8672
  id: loaded.manifest.name,
8458
8673
  name: loaded.manifest.name,
8459
8674
  manifest: loaded.path,
8460
- lock: path20.resolve(opts.lockPath),
8675
+ lock: path22.resolve(opts.lockPath),
8461
8676
  root,
8462
8677
  provider: loaded.manifest.provider,
8463
8678
  account: null,
@@ -8482,7 +8697,7 @@ async function buildGallerySnapshot(opts) {
8482
8697
  return { snapshot, media };
8483
8698
  }
8484
8699
  async function buildWorkspaceGallerySnapshot(opts) {
8485
- const dir = path20.dirname(path20.resolve(opts.workspacePath));
8700
+ const dir = path22.dirname(path22.resolve(opts.workspacePath));
8486
8701
  const media = /* @__PURE__ */ new Map();
8487
8702
  const projects = [];
8488
8703
  const items = [];
@@ -8525,7 +8740,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
8525
8740
  id: project.id,
8526
8741
  name: loaded.manifest.name,
8527
8742
  manifest: manifestPath,
8528
- lock: path20.resolve(lockPath),
8743
+ lock: path22.resolve(lockPath),
8529
8744
  root: loaded.root,
8530
8745
  provider: loaded.manifest.provider,
8531
8746
  account: project.account ?? null,
@@ -8540,8 +8755,8 @@ async function buildWorkspaceGallerySnapshot(opts) {
8540
8755
  id: project.id,
8541
8756
  name: project.id,
8542
8757
  manifest: manifestPath,
8543
- lock: path20.resolve(lockPath),
8544
- root: path20.dirname(manifestPath),
8758
+ lock: path22.resolve(lockPath),
8759
+ root: path22.dirname(manifestPath),
8545
8760
  provider: project.provider,
8546
8761
  account: project.account ?? null,
8547
8762
  manifestSha256: null,
@@ -8567,7 +8782,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
8567
8782
  version: 1,
8568
8783
  generatedAt: (opts.now ?? (() => /* @__PURE__ */ new Date()))().toISOString(),
8569
8784
  project: null,
8570
- workspace: { path: path20.resolve(opts.workspacePath), dir, projects },
8785
+ workspace: { path: path22.resolve(opts.workspacePath), dir, projects },
8571
8786
  filter,
8572
8787
  totals: {
8573
8788
  entries: projects.reduce((sum, project) => sum + project.entries, 0),
@@ -8584,7 +8799,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
8584
8799
  }
8585
8800
 
8586
8801
  // src/gallery/server.ts
8587
- import { spawn as spawn2 } from "child_process";
8802
+ import { spawn as spawn3 } from "child_process";
8588
8803
  import { randomBytes } from "crypto";
8589
8804
  import { createServer as createServer2 } from "http";
8590
8805
  import { readFile as readFile16 } from "fs/promises";
@@ -8860,6 +9075,14 @@ function renderGallery(snapshot, opts = {}) {
8860
9075
  .tray > button { padding:5px 11px; font-size:12.5px; }
8861
9076
  @media (max-width: 900px) { .sheet.review-host, .sheet.compare-host { width:100vw; } }
8862
9077
  .drawer .gen { display:flex; gap:8px; flex-wrap:wrap; margin-top:12px; }
9078
+ .pair { display:grid; grid-template-columns:1fr 1fr; gap:8px; margin:8px 0 10px; }
9079
+ .pair figure { margin:0; border:1px solid var(--line); background:var(--panel-deep); background-image:var(--checker);
9080
+ background-size:12px 12px; background-position:0 0,6px 6px; display:grid; place-items:center; padding:8px; min-height:96px; }
9081
+ .pair figure img { image-rendering:pixelated; display:block; max-width:100%; }
9082
+ .pair figcaption { font:11px/1.4 var(--mono); color:var(--dim); background:var(--panel); padding:2px 6px; margin-top:6px; }
9083
+ .card .pen { position:absolute; right:6px; top:6px; background:var(--panel); color:var(--accent-soft); border:1px solid var(--accent);
9084
+ font:700 11px/1 var(--mono); padding:3px 5px; }
9085
+ .hand-actions { display:flex; gap:8px; flex-wrap:wrap; margin-top:8px; }
8863
9086
  .drawer .gen button.primary { padding:7px 14px; }
8864
9087
  @media (max-width: 720px) {
8865
9088
  .bar, .totals, .chips, .style, footer { padding-inline:14px; }
@@ -9306,7 +9529,8 @@ function clearFilters() {
9306
9529
 
9307
9530
  function thumb(item) {
9308
9531
  const cell = el('div', 'cell');
9309
- const shown = item.outputs.filter((o) => o.url);
9532
+ // A hand edit is what ships, so it is what the card shows.
9533
+ const shown = item.edit && item.edit.url && item.editStatus === 'edited' ? [item.edit] : item.outputs.filter((o) => o.url);
9310
9534
  if (!shown.length) {
9311
9535
  cell.classList.add('none');
9312
9536
  cell.append(el('span', null, item.state === 'missing' ? 'not generated yet'
@@ -9357,6 +9581,9 @@ function card(item) {
9357
9581
  body.append(meta);
9358
9582
  const cell = thumb(item);
9359
9583
  if (slot >= 0) cell.append(el('span', 'slot', String(slot + 1)));
9584
+ if (item.editStatus === 'edited' || item.editStatus === 'regenerated-since') {
9585
+ const pen = el('span', 'pen', '\u270E'); pen.title = 'hand-edited'; cell.append(pen);
9586
+ }
9360
9587
  c.append(cell, body);
9361
9588
  c.onclick = (e) => { if (e.shiftKey) toggleCompare(item.id); else openItem(item.id); };
9362
9589
  return c;
@@ -9683,6 +9910,78 @@ function openCompare() {
9683
9910
  close.focus({ preventScroll: true });
9684
9911
  }
9685
9912
 
9913
+ // ---- hand edits -------------------------------------------------------------
9914
+
9915
+ const EDIT_STATUS_TEXT = {
9916
+ same: 'A copy of the generated art, not changed yet. Open it in your editor and save.',
9917
+ edited: 'Differs from the generated art. mount and pack place this file; the generated file stays as the record.',
9918
+ 'regenerated-since': 'The generated art changed after this edit was saved \u2014 the edit is based on an older generation.',
9919
+ missing: 'The declared file is not on disk.',
9920
+ };
9921
+ async function postHandEdit(item, action, extra) {
9922
+ const pr = projectOf(item);
9923
+ const body = { action, assetId: item.assetId, styleId: item.styleId, expectedSha256: pr.manifestSha256, ...extra };
9924
+ if (item.project) body.project = item.project;
9925
+ snap = await postEdit(body);
9926
+ }
9927
+ function handEditSection(item) {
9928
+ const canStart = EDITABLE && item.declared && item.asset && projectOf(item)?.manifestSha256 &&
9929
+ item.outputs.length === 1 && item.outputs[0].exists && item.outputs[0].mediaType === 'image/png';
9930
+ if (!item.edit && !canStart) return null;
9931
+ const s = el('section', 'meta');
9932
+ s.append(el('h3', null, 'Hand edit'));
9933
+ if (item.edit) {
9934
+ const pair = el('div', 'pair');
9935
+ const shared = displayScale(item.width, item.height, 200, 160);
9936
+ const fig = (out, label) => {
9937
+ const f = el('figure');
9938
+ if (out && out.url) {
9939
+ const img = el('img'); img.src = out.url; img.alt = label;
9940
+ if (shared >= 1) { img.width = item.width * shared; img.height = item.height * shared; }
9941
+ f.append(img);
9942
+ } else f.append(el('span', 'state-dim', 'not on disk'));
9943
+ f.append(el('figcaption', null, label));
9944
+ return f;
9945
+ };
9946
+ pair.append(fig(item.outputs[0], 'generated'), fig(item.edit, 'edit'));
9947
+ s.append(pair);
9948
+ const dl = el('dl');
9949
+ const tone = item.editStatus === 'edited' ? 'ok' : item.editStatus === 'same' ? 'dim' : 'warn';
9950
+ const st = el('span', 'state-' + tone); st.append(el('i', 'dot ' + tone), document.createTextNode(' ' + item.editStatus));
9951
+ row(dl, 'status', st);
9952
+ row(dl, 'why', EDIT_STATUS_TEXT[item.editStatus] || '');
9953
+ row(dl, 'file', item.edit.path, { mono: true, copy: item.edit.absolutePath });
9954
+ if (item.edit.sha256) row(dl, 'sha256', item.edit.sha256.slice(0, 16) + '\u2026', { mono: true, copy: item.edit.sha256 });
9955
+ row(dl, 'saved', fmtWhen(item.edit.modifiedAt));
9956
+ s.append(dl);
9957
+ } else {
9958
+ s.append(el('div', 'state-dim', 'Touch it up in your own editor. The generated file stays untouched; the edit is a sibling file the manifest points at, and mount and pack place that instead.'));
9959
+ }
9960
+ if (EDITABLE && item.declared && projectOf(item)?.manifestSha256) {
9961
+ const acts = el('div', 'hand-actions');
9962
+ const msg = el('span', 'msg');
9963
+ const go = async (label, action, extra, done) => {
9964
+ const b = el('button', label === 'Detach edit' ? null : 'primary', label); b.type = 'button';
9965
+ b.onclick = async () => {
9966
+ b.disabled = true; msg.className = 'msg'; msg.textContent = '\u2026';
9967
+ try { await postHandEdit(item, action, extra); ui.notice = { id: item.id, text: done }; render(); }
9968
+ catch (err) { b.disabled = false; msg.className = 'msg bad'; msg.textContent = err.message + (err.status === 409 ? ' \u2014 press Refresh.' : ''); }
9969
+ };
9970
+ acts.append(b);
9971
+ };
9972
+ if (item.edit) {
9973
+ go('Open in editor', 'start-edit', { open: true }, 'Opened ' + item.edit.path + ' in your editor. Save there, then Refresh.');
9974
+ go('Detach edit', 'detach-edit', {}, 'Detached. The file is still at ' + item.edit.path + '; the generated art is placed again.');
9975
+ } else if (canStart) {
9976
+ go('Edit by hand', 'start-edit', { open: true }, 'Created the edit file and opened it in your editor. Save there, then Refresh to see it here.');
9977
+ go('Create edit file only', 'start-edit', { open: false }, 'Created the edit file and declared it in the manifest.');
9978
+ }
9979
+ acts.append(msg);
9980
+ s.append(acts);
9981
+ }
9982
+ return s;
9983
+ }
9984
+
9686
9985
  // ---- editing (only when the server minted a session) ---------------------
9687
9986
 
9688
9987
  // Styles a parent edit reaches: every descendant (via extends) that does not
@@ -10174,6 +10473,9 @@ function renderDrawer() {
10174
10473
  body.append(s);
10175
10474
  }
10176
10475
 
10476
+ const hand = handEditSection(item);
10477
+ if (hand) body.append(hand);
10478
+
10177
10479
  if (item.revision || item.revisionParentKey) {
10178
10480
  const { s, dl } = section('Lineage');
10179
10481
  row(dl, 'mode', item.revision ? item.revision.mode : (item.asset && item.asset.revision && item.asset.revision.mode));
@@ -10641,7 +10943,7 @@ async function serveGallery(opts) {
10641
10943
  });
10642
10944
  opts.onReady?.(url);
10643
10945
  if (opts.open !== false && process.platform === "darwin") {
10644
- spawn2("open", [url], { stdio: "ignore", detached: true }).unref();
10946
+ spawn3("open", [url], { stdio: "ignore", detached: true }).unref();
10645
10947
  }
10646
10948
  return {
10647
10949
  url,
@@ -10655,14 +10957,14 @@ async function serveGallery(opts) {
10655
10957
 
10656
10958
  // src/gallery/generate.ts
10657
10959
  import { randomBytes as randomBytes2 } from "crypto";
10658
- import { z as z6 } from "zod";
10659
- var GenerateRequestSchema = z6.object({
10660
- project: z6.string().min(1).optional(),
10661
- keys: z6.array(z6.string().min(1)).min(1).max(500),
10960
+ import { z as z7 } from "zod";
10961
+ var GenerateRequestSchema = z7.object({
10962
+ project: z7.string().min(1).optional(),
10963
+ keys: z7.array(z7.string().min(1)).min(1).max(500),
10662
10964
  /** Regenerate work that is up to date, as `gen --force` does. */
10663
- force: z6.boolean().optional(),
10965
+ force: z7.boolean().optional(),
10664
10966
  /** Advance in-flight, review, or selected work without submitting anything. */
10665
- resume: z6.boolean().optional()
10967
+ resume: z7.boolean().optional()
10666
10968
  }).strict();
10667
10969
  var GenerateRequestError = class extends Error {
10668
10970
  constructor(message6, status = 400) {
@@ -10900,15 +11202,15 @@ function createGenerateHandlers(opts) {
10900
11202
 
10901
11203
  // src/pipeline/salvage.ts
10902
11204
  import { readFile as readFile17 } from "fs/promises";
10903
- import { existsSync as existsSync17 } from "fs";
10904
- import path21 from "path";
11205
+ import { existsSync as existsSync18 } from "fs";
11206
+ import path23 from "path";
10905
11207
  function providerClaimId(provider, objectId) {
10906
11208
  return `${encodeURIComponent(provider)}:${encodeURIComponent(objectId)}`;
10907
11209
  }
10908
11210
  async function loadClaims(lockPaths, opts = {}) {
10909
11211
  const claimed = /* @__PURE__ */ new Set();
10910
11212
  for (const p of lockPaths) {
10911
- if (!existsSync17(p)) throw new Error(`Claim lockfile not found: ${p}`);
11213
+ if (!existsSync18(p)) throw new Error(`Claim lockfile not found: ${p}`);
10912
11214
  let parsed;
10913
11215
  try {
10914
11216
  parsed = parseLock(JSON.parse(await readFile17(p, "utf8")));
@@ -10959,20 +11261,20 @@ function matchStyleByPattern(prompt, manifest) {
10959
11261
  return null;
10960
11262
  }
10961
11263
  async function loadSiblingManifests(ownManifestPath, workspaceManifestPaths, claimPaths) {
10962
- const own = path21.resolve(ownManifestPath);
11264
+ const own = path23.resolve(ownManifestPath);
10963
11265
  const siblingManifestPaths = [
10964
11266
  .../* @__PURE__ */ new Set([
10965
11267
  ...workspaceManifestPaths,
10966
- ...claimPaths.map((c) => path21.join(path21.dirname(path21.resolve(c)), "pixelkiln.manifest.json"))
11268
+ ...claimPaths.map((c) => path23.join(path23.dirname(path23.resolve(c)), "pixelkiln.manifest.json"))
10967
11269
  ])
10968
11270
  ];
10969
11271
  const siblings = [];
10970
11272
  for (const siblingManifestPath of siblingManifestPaths) {
10971
- if (path21.resolve(siblingManifestPath) === own) continue;
10972
- if (!existsSync17(siblingManifestPath)) continue;
11273
+ if (path23.resolve(siblingManifestPath) === own) continue;
11274
+ if (!existsSync18(siblingManifestPath)) continue;
10973
11275
  try {
10974
11276
  const { manifest } = await loadManifest(siblingManifestPath);
10975
- siblings.push({ label: path21.basename(path21.dirname(siblingManifestPath)), manifest });
11277
+ siblings.push({ label: path23.basename(path23.dirname(siblingManifestPath)), manifest });
10976
11278
  } catch {
10977
11279
  }
10978
11280
  }
@@ -11191,15 +11493,15 @@ async function workspaceStatus(ws, dir) {
11191
11493
  }
11192
11494
 
11193
11495
  // src/pipeline/cache-health.ts
11194
- import { existsSync as existsSync18 } from "fs";
11496
+ import { existsSync as existsSync19 } from "fs";
11195
11497
  import { readFile as readFile18, readdir as readdir2, rm as rm8 } from "fs/promises";
11196
- import path22 from "path";
11498
+ import path24 from "path";
11197
11499
  async function inspectCaches(lock, lockPath, options = {}) {
11198
- if (options.prune && !existsSync18(lockPath)) {
11199
- throw new Error(`Refusing to prune without an existing lockfile at ${path22.resolve(lockPath)}`);
11500
+ if (options.prune && !existsSync19(lockPath)) {
11501
+ throw new Error(`Refusing to prune without an existing lockfile at ${path24.resolve(lockPath)}`);
11200
11502
  }
11201
- const contentDir = path22.resolve(path22.dirname(lockPath), ".pixelkiln", "cache");
11202
- const remotePath = path22.resolve(cachePathFor(lockPath));
11503
+ const contentDir = path24.resolve(path24.dirname(lockPath), ".pixelkiln", "cache");
11504
+ const remotePath = path24.resolve(cachePathFor(lockPath));
11203
11505
  const referenced = new Set(
11204
11506
  Object.values(lock.entries).flatMap((entry) => entry.outputs.map((output) => output.sha256))
11205
11507
  );
@@ -11213,7 +11515,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
11213
11515
  ]);
11214
11516
  for (const name of names) {
11215
11517
  try {
11216
- await rm8(path22.join(contentDir, name), { force: true });
11518
+ await rm8(path24.join(contentDir, name), { force: true });
11217
11519
  removed.contentFiles++;
11218
11520
  } catch {
11219
11521
  }
@@ -11240,7 +11542,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
11240
11542
  async function inspectContentCache(contentDir, referenced) {
11241
11543
  const report = {
11242
11544
  path: contentDir,
11243
- exists: existsSync18(contentDir),
11545
+ exists: existsSync19(contentDir),
11244
11546
  files: 0,
11245
11547
  bytes: 0,
11246
11548
  valid: 0,
@@ -11261,7 +11563,7 @@ async function inspectContentCache(contentDir, referenced) {
11261
11563
  continue;
11262
11564
  }
11263
11565
  report.files++;
11264
- const file = path22.join(contentDir, entry.name);
11566
+ const file = path24.join(contentDir, entry.name);
11265
11567
  const mediaType = mediaTypeFromExtension(entry.name);
11266
11568
  const expected = mediaType ? entry.name.slice(0, -4) : "";
11267
11569
  let bytes;
@@ -11304,7 +11606,7 @@ async function inspectContentCache(contentDir, referenced) {
11304
11606
  async function inspectRemoteHashCache(remotePath) {
11305
11607
  const report = {
11306
11608
  path: remotePath,
11307
- exists: existsSync18(remotePath),
11609
+ exists: existsSync19(remotePath),
11308
11610
  entries: 0,
11309
11611
  valid: 0,
11310
11612
  invalidIds: [],
@@ -11584,8 +11886,8 @@ function isRecord2(value) {
11584
11886
  }
11585
11887
 
11586
11888
  // src/pick/salvage-server.ts
11587
- import { mkdir as mkdir6, writeFile as writeFile9, readFile as readFile19 } from "fs/promises";
11588
- import path23 from "path";
11889
+ import { mkdir as mkdir7, writeFile as writeFile9, readFile as readFile19 } from "fs/promises";
11890
+ import path25 from "path";
11589
11891
 
11590
11892
  // src/pick/salvage-sheet.ts
11591
11893
  var escapeHtml3 = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
@@ -11750,7 +12052,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
11750
12052
  });
11751
12053
  const html = renderSalvageSheet(orphans, {
11752
12054
  styleId: ctx.styleId,
11753
- importDir: path23.relative(process.cwd(), ctx.importDir) || "."
12055
+ importDir: path25.relative(process.cwd(), ctx.importDir) || "."
11754
12056
  });
11755
12057
  const byId = new Map(orphans.map((o) => [o.id, o]));
11756
12058
  const existingTags = new Map(orphans.map((o) => [o.id, o.tags]));
@@ -11779,13 +12081,14 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
11779
12081
  if (!buf.subarray(0, 8).equals(PNG_SIGNATURE2)) throw new Error("not a PNG");
11780
12082
  decodePng(buf);
11781
12083
  const assetId = idFromPrompt(orphan.prompt, taken);
11782
- const rel = path23.join("_salvaged", `${assetId}.png`);
11783
- const outFile = path23.resolve(ctx.importDir, rel);
11784
- await mkdir6(path23.dirname(outFile), { recursive: true });
12084
+ const rel = path25.join("_salvaged", `${assetId}.png`);
12085
+ const outFile = path25.resolve(ctx.importDir, rel);
12086
+ await mkdir7(path25.dirname(outFile), { recursive: true });
11785
12087
  await writeFile9(outFile, buf);
11786
12088
  ctx.manifest.assets[assetId] = {
11787
12089
  prompt: orphan.prompt,
11788
12090
  promptByStyle: {},
12091
+ sourceByStyle: {},
11789
12092
  category: "_salvaged",
11790
12093
  file: rel,
11791
12094
  tags: ["salvaged"],
@@ -11813,7 +12116,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
11813
12116
  sourceUrl: durableSource,
11814
12117
  sourceUrls: durableSource ? [{ url: durableSource }] : [],
11815
12118
  outputs: [{
11816
- path: portableOutputPath(outFile, path23.dirname(ctx.manifestPath)),
12119
+ path: portableOutputPath(outFile, path25.dirname(ctx.manifestPath)),
11817
12120
  sha256: sha256(buf)
11818
12121
  }],
11819
12122
  submittedAt: orphan.createdAt,
@@ -11847,51 +12150,51 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
11847
12150
  }
11848
12151
 
11849
12152
  // src/recipes.ts
11850
- import path24 from "path";
11851
- import { existsSync as existsSync19 } from "fs";
12153
+ import path26 from "path";
12154
+ import { existsSync as existsSync20 } from "fs";
11852
12155
  import { readdir as readdir3, readFile as readFile20 } from "fs/promises";
11853
12156
  import { fileURLToPath } from "url";
11854
- import { z as z7 } from "zod";
12157
+ import { z as z8 } from "zod";
11855
12158
  var SHA256_RE = /^[0-9a-f]{64}$/;
11856
12159
  var RECIPE_ID_RE = /^[a-z0-9][a-z0-9-]*\/[a-z0-9][a-z0-9-]*$/;
11857
12160
  var VERSION_RE = /^\d+\.\d+\.\d+$/;
11858
12161
  var RECIPE_FILE = "pixelkiln.recipe.json";
11859
12162
  var RECIPE_DIR_TOKEN = "{{recipeDir}}";
11860
- var PortablePathSchema = z7.string().min(1).refine((value) => {
11861
- if (value.includes("\\") || path24.posix.isAbsolute(value)) return false;
12163
+ var PortablePathSchema = z8.string().min(1).refine((value) => {
12164
+ if (value.includes("\\") || path26.posix.isAbsolute(value)) return false;
11862
12165
  const parts = value.split("/");
11863
12166
  return !parts.includes("") && !parts.includes(".") && !parts.includes("..");
11864
12167
  }, "expected a portable relative path without '.', '..', or backslashes");
11865
- var RecipeBindingSchema = z7.object({
11866
- nodeId: z7.string().min(1),
11867
- input: z7.string().min(1)
12168
+ var RecipeBindingSchema = z8.object({
12169
+ nodeId: z8.string().min(1),
12170
+ input: z8.string().min(1)
11868
12171
  }).strict();
11869
- var RecipeSchema = z7.object({
11870
- $schema: z7.string().url().optional(),
11871
- format: z7.literal("pixelkiln-recipe"),
11872
- schemaVersion: z7.literal(1),
11873
- id: z7.string().regex(RECIPE_ID_RE, "expected provider/name"),
11874
- version: z7.string().regex(VERSION_RE, "expected x.y.z"),
11875
- provider: z7.string().regex(/^[a-z0-9][a-z0-9-]*$/),
11876
- summary: z7.string().min(1).max(240),
11877
- files: z7.array(z7.object({
12172
+ var RecipeSchema = z8.object({
12173
+ $schema: z8.string().url().optional(),
12174
+ format: z8.literal("pixelkiln-recipe"),
12175
+ schemaVersion: z8.literal(1),
12176
+ id: z8.string().regex(RECIPE_ID_RE, "expected provider/name"),
12177
+ version: z8.string().regex(VERSION_RE, "expected x.y.z"),
12178
+ provider: z8.string().regex(/^[a-z0-9][a-z0-9-]*$/),
12179
+ summary: z8.string().min(1).max(240),
12180
+ files: z8.array(z8.object({
11878
12181
  path: PortablePathSchema,
11879
- role: z7.enum(["workflow", "reference"]),
11880
- sha256: z7.string().regex(SHA256_RE)
12182
+ role: z8.enum(["workflow", "reference"]),
12183
+ sha256: z8.string().regex(SHA256_RE)
11881
12184
  }).strict()).default([]),
11882
- models: z7.array(z7.object({
12185
+ models: z8.array(z8.object({
11883
12186
  path: PortablePathSchema,
11884
- sha256: z7.string().regex(SHA256_RE),
11885
- source: z7.string().url(),
11886
- license: z7.string().min(1)
12187
+ sha256: z8.string().regex(SHA256_RE),
12188
+ source: z8.string().url(),
12189
+ license: z8.string().min(1)
11887
12190
  }).strict()).default([]),
11888
- styleId: z7.string().regex(/^[a-z0-9][a-z0-9-]*$/),
12191
+ styleId: z8.string().regex(/^[a-z0-9][a-z0-9-]*$/),
11889
12192
  style: StyleSchema,
11890
- workflow: z7.object({
12193
+ workflow: z8.object({
11891
12194
  path: PortablePathSchema,
11892
- outputNodeId: z7.string().min(1),
11893
- numImages: z7.number().int().min(1).max(16),
11894
- bindings: z7.object({
12195
+ outputNodeId: z8.string().min(1),
12196
+ numImages: z8.number().int().min(1).max(16),
12197
+ bindings: z8.object({
11895
12198
  prompt: RecipeBindingSchema,
11896
12199
  width: RecipeBindingSchema.optional(),
11897
12200
  height: RecipeBindingSchema.optional(),
@@ -11902,67 +12205,67 @@ var RecipeSchema = z7.object({
11902
12205
  strength: RecipeBindingSchema.optional()
11903
12206
  }).strict()
11904
12207
  }).strict().optional(),
11905
- quality: z7.object({
11906
- stage: z7.enum(["composition-source", "production-candidate"]),
11907
- workingCanvas: z7.object({
11908
- width: z7.number().int().min(16),
11909
- height: z7.number().int().min(16)
12208
+ quality: z8.object({
12209
+ stage: z8.enum(["composition-source", "production-candidate"]),
12210
+ workingCanvas: z8.object({
12211
+ width: z8.number().int().min(16),
12212
+ height: z8.number().int().min(16)
11910
12213
  }).strict(),
11911
- recommendedNativeSize: z7.object({
11912
- min: z7.number().int().min(1),
11913
- max: z7.number().int().min(1)
12214
+ recommendedNativeSize: z8.object({
12215
+ min: z8.number().int().min(1),
12216
+ max: z8.number().int().min(1)
11914
12217
  }).strict(),
11915
- paletteColors: z7.object({
11916
- min: z7.number().int().min(1),
11917
- max: z7.number().int().min(1)
12218
+ paletteColors: z8.object({
12219
+ min: z8.number().int().min(1),
12220
+ max: z8.number().int().min(1)
11918
12221
  }).strict(),
11919
- background: z7.enum(["opaque", "transparent", "full-bleed"]),
11920
- checks: z7.array(z7.enum([
12222
+ background: z8.enum(["opaque", "transparent", "full-bleed"]),
12223
+ checks: z8.array(z8.enum([
11921
12224
  "prompt-coverage",
11922
12225
  "native-grid",
11923
12226
  "final-palette",
11924
12227
  "alpha-edge",
11925
12228
  "human-1x"
11926
12229
  ])).min(1),
11927
- notes: z7.array(z7.string().min(1)).default([])
12230
+ notes: z8.array(z8.string().min(1)).default([])
11928
12231
  }).strict(),
11929
- integrity: z7.object({
11930
- algorithm: z7.literal("sha256"),
11931
- digest: z7.string().regex(SHA256_RE)
12232
+ integrity: z8.object({
12233
+ algorithm: z8.literal("sha256"),
12234
+ digest: z8.string().regex(SHA256_RE)
11932
12235
  }).strict()
11933
12236
  }).strict().superRefine((recipe, context) => {
11934
12237
  if (recipe.style.provider && recipe.style.provider !== recipe.provider) {
11935
12238
  context.addIssue({
11936
- code: z7.ZodIssueCode.custom,
12239
+ code: z8.ZodIssueCode.custom,
11937
12240
  path: ["style", "provider"],
11938
12241
  message: "must match the recipe provider"
11939
12242
  });
11940
12243
  }
11941
12244
  const filePaths = recipe.files.map((file) => file.path);
11942
12245
  if (new Set(filePaths).size !== filePaths.length) {
11943
- context.addIssue({ code: z7.ZodIssueCode.custom, path: ["files"], message: "duplicate file path" });
12246
+ context.addIssue({ code: z8.ZodIssueCode.custom, path: ["files"], message: "duplicate file path" });
11944
12247
  }
11945
12248
  const modelPaths = recipe.models.map((model) => model.path);
11946
12249
  if (new Set(modelPaths).size !== modelPaths.length) {
11947
- context.addIssue({ code: z7.ZodIssueCode.custom, path: ["models"], message: "duplicate model path" });
12250
+ context.addIssue({ code: z8.ZodIssueCode.custom, path: ["models"], message: "duplicate model path" });
11948
12251
  }
11949
12252
  if (filePaths.includes(RECIPE_FILE)) {
11950
12253
  context.addIssue({
11951
- code: z7.ZodIssueCode.custom,
12254
+ code: z8.ZodIssueCode.custom,
11952
12255
  path: ["files"],
11953
12256
  message: `${RECIPE_FILE} cannot hash itself`
11954
12257
  });
11955
12258
  }
11956
12259
  if (recipe.quality.recommendedNativeSize.min > recipe.quality.recommendedNativeSize.max) {
11957
12260
  context.addIssue({
11958
- code: z7.ZodIssueCode.custom,
12261
+ code: z8.ZodIssueCode.custom,
11959
12262
  path: ["quality", "recommendedNativeSize"],
11960
12263
  message: "min cannot exceed max"
11961
12264
  });
11962
12265
  }
11963
12266
  if (recipe.quality.paletteColors.min > recipe.quality.paletteColors.max) {
11964
12267
  context.addIssue({
11965
- code: z7.ZodIssueCode.custom,
12268
+ code: z8.ZodIssueCode.custom,
11966
12269
  path: ["quality", "paletteColors"],
11967
12270
  message: "min cannot exceed max"
11968
12271
  });
@@ -11971,7 +12274,7 @@ var RecipeSchema = z7.object({
11971
12274
  (file) => file.path === recipe.workflow.path && file.role === "workflow"
11972
12275
  )) {
11973
12276
  context.addIssue({
11974
- code: z7.ZodIssueCode.custom,
12277
+ code: z8.ZodIssueCode.custom,
11975
12278
  path: ["workflow", "path"],
11976
12279
  message: "must name a file whose role is workflow"
11977
12280
  });
@@ -11979,7 +12282,7 @@ var RecipeSchema = z7.object({
11979
12282
  if (recipe.provider === "comfyui") {
11980
12283
  if (!recipe.workflow) {
11981
12284
  context.addIssue({
11982
- code: z7.ZodIssueCode.custom,
12285
+ code: z8.ZodIssueCode.custom,
11983
12286
  path: ["workflow"],
11984
12287
  message: "ComfyUI recipes require workflow metadata"
11985
12288
  });
@@ -11988,7 +12291,7 @@ var RecipeSchema = z7.object({
11988
12291
  const options = recipe.style.providerOptions.comfyui;
11989
12292
  if (!options || typeof options !== "object" || Array.isArray(options)) {
11990
12293
  context.addIssue({
11991
- code: z7.ZodIssueCode.custom,
12294
+ code: z8.ZodIssueCode.custom,
11992
12295
  path: ["style", "providerOptions", "comfyui"],
11993
12296
  message: "ComfyUI recipes require ComfyUI provider options"
11994
12297
  });
@@ -12002,7 +12305,7 @@ var RecipeSchema = z7.object({
12002
12305
  };
12003
12306
  if (JSON.stringify(canonical3(options)) !== JSON.stringify(canonical3(expected))) {
12004
12307
  context.addIssue({
12005
- code: z7.ZodIssueCode.custom,
12308
+ code: z8.ZodIssueCode.custom,
12006
12309
  path: ["style", "providerOptions", "comfyui"],
12007
12310
  message: "must match workflow metadata and use {{recipeDir}} for workflowFile"
12008
12311
  });
@@ -12034,7 +12337,7 @@ function message4(error) {
12034
12337
  return error instanceof Error ? error.message : String(error);
12035
12338
  }
12036
12339
  async function readRecipeFile(recipePath, bundled) {
12037
- const absolute = path24.resolve(recipePath);
12340
+ const absolute = path26.resolve(recipePath);
12038
12341
  let raw;
12039
12342
  try {
12040
12343
  raw = JSON.parse(await readFile20(absolute, "utf8"));
@@ -12048,13 +12351,13 @@ async function readRecipeFile(recipePath, bundled) {
12048
12351
  ` + parsed.error.issues.map((issue) => ` ${issue.path.join(".") || "$"}: ${issue.message}`).join("\n")
12049
12352
  );
12050
12353
  }
12051
- return { recipe: parsed.data, path: absolute, dir: path24.dirname(absolute), bundled };
12354
+ return { recipe: parsed.data, path: absolute, dir: path26.dirname(absolute), bundled };
12052
12355
  }
12053
12356
  async function walkRecipeFiles(root) {
12054
- if (!existsSync19(root)) return [];
12357
+ if (!existsSync20(root)) return [];
12055
12358
  const out = [];
12056
12359
  for (const entry of await readdir3(root, { withFileTypes: true })) {
12057
- const resolved = path24.join(root, entry.name);
12360
+ const resolved = path26.join(root, entry.name);
12058
12361
  if (entry.isDirectory()) out.push(...await walkRecipeFiles(resolved));
12059
12362
  else if (entry.isFile() && entry.name === RECIPE_FILE) out.push(resolved);
12060
12363
  }
@@ -12065,7 +12368,7 @@ function bundledRecipeRoot() {
12065
12368
  }
12066
12369
  async function listBundledRecipes(root = bundledRecipeRoot()) {
12067
12370
  const recipes = await Promise.all(
12068
- (await walkRecipeFiles(path24.resolve(root))).map((file) => readRecipeFile(file, true))
12371
+ (await walkRecipeFiles(path26.resolve(root))).map((file) => readRecipeFile(file, true))
12069
12372
  );
12070
12373
  return recipes.sort(
12071
12374
  (a, b) => a.recipe.id.localeCompare(b.recipe.id) || compareVersions(b.recipe.version, a.recipe.version)
@@ -12088,9 +12391,9 @@ function parseSelector(selector) {
12088
12391
  return { id: match[1], version: match[2] };
12089
12392
  }
12090
12393
  async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
12091
- const local = path24.resolve(target);
12092
- if (existsSync19(local)) {
12093
- const recipePath = path24.basename(local) === RECIPE_FILE ? local : path24.join(local, RECIPE_FILE);
12394
+ const local = path26.resolve(target);
12395
+ if (existsSync20(local)) {
12396
+ const recipePath = path26.basename(local) === RECIPE_FILE ? local : path26.join(local, RECIPE_FILE);
12094
12397
  return readRecipeFile(recipePath, false);
12095
12398
  }
12096
12399
  if (target.startsWith(".") || target.startsWith("/") || target.includes("\\")) {
@@ -12104,8 +12407,8 @@ async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
12104
12407
  return matches[0];
12105
12408
  }
12106
12409
  async function verifyFile(root, file) {
12107
- const absolute = path24.resolve(root, ...file.path.split("/"));
12108
- if (!existsSync19(absolute)) {
12410
+ const absolute = path26.resolve(root, ...file.path.split("/"));
12411
+ if (!existsSync20(absolute)) {
12109
12412
  return { path: file.path, expectedSha256: file.sha256, actualSha256: null, status: "missing" };
12110
12413
  }
12111
12414
  const actualSha256 = await sha256File(absolute);
@@ -12120,7 +12423,7 @@ async function verifyRecipe(target, options = {}) {
12120
12423
  const loaded = await resolveRecipe(target, options.bundledRoot);
12121
12424
  const actualIntegrity = recipeDigest(loaded.recipe);
12122
12425
  const files = await Promise.all(loaded.recipe.files.map((file) => verifyFile(loaded.dir, file)));
12123
- const modelRoot = options.modelRoot ? path24.resolve(options.modelRoot) : null;
12426
+ const modelRoot = options.modelRoot ? path26.resolve(options.modelRoot) : null;
12124
12427
  const models = modelRoot ? await Promise.all(loaded.recipe.models.map(async (model) => ({
12125
12428
  ...await verifyFile(modelRoot, model),
12126
12429
  source: model.source,
@@ -12154,7 +12457,7 @@ async function verifyRecipe(target, options = {}) {
12154
12457
  };
12155
12458
  }
12156
12459
  function renderStyle(style, destination, cwd) {
12157
- const portableDir = path24.relative(cwd, destination).split(path24.sep).join("/") || ".";
12460
+ const portableDir = path26.relative(cwd, destination).split(path26.sep).join("/") || ".";
12158
12461
  const replace = (value) => {
12159
12462
  if (typeof value === "string") return value.replaceAll(RECIPE_DIR_TOKEN, portableDir);
12160
12463
  if (Array.isArray(value)) return value.map(replace);
@@ -12166,24 +12469,24 @@ function renderStyle(style, destination, cwd) {
12166
12469
  return replace(style);
12167
12470
  }
12168
12471
  async function installRecipe(target, options = {}) {
12169
- const cwd = path24.resolve(options.cwd ?? process.cwd());
12472
+ const cwd = path26.resolve(options.cwd ?? process.cwd());
12170
12473
  const loaded = await resolveRecipe(target, options.bundledRoot);
12171
12474
  const verification = await verifyRecipe(loaded.path);
12172
12475
  if (!verification.ok) {
12173
12476
  throw new Error(`Recipe ${loaded.recipe.id}@${loaded.recipe.version} failed integrity verification.`);
12174
12477
  }
12175
- const destination = options.out ? path24.resolve(cwd, options.out) : path24.join(cwd, "pixelkiln-recipes", ...loaded.recipe.id.split("/"), loaded.recipe.version);
12478
+ const destination = options.out ? path26.resolve(cwd, options.out) : path26.join(cwd, "pixelkiln-recipes", ...loaded.recipe.id.split("/"), loaded.recipe.version);
12176
12479
  const sources = [
12177
12480
  { path: RECIPE_FILE, source: loaded.path },
12178
- ...loaded.recipe.files.map((file) => ({ path: file.path, source: path24.join(loaded.dir, ...file.path.split("/")) }))
12481
+ ...loaded.recipe.files.map((file) => ({ path: file.path, source: path26.join(loaded.dir, ...file.path.split("/")) }))
12179
12482
  ];
12180
12483
  const files = await Promise.all(sources.map(async (file) => ({
12181
- path: path24.join(destination, ...file.path.split("/")),
12484
+ path: path26.join(destination, ...file.path.split("/")),
12182
12485
  data: await readFile20(file.source)
12183
12486
  })));
12184
12487
  if (!options.force) {
12185
12488
  for (const file of files) {
12186
- if (!existsSync19(file.path)) continue;
12489
+ if (!existsSync20(file.path)) continue;
12187
12490
  const current = await readFile20(file.path);
12188
12491
  if (!current.equals(file.data)) {
12189
12492
  throw new Error(
@@ -12207,15 +12510,15 @@ async function installRecipe(target, options = {}) {
12207
12510
  }
12208
12511
 
12209
12512
  // src/pipeline/quality-regression.ts
12210
- import path25 from "path";
12211
- import { existsSync as existsSync20 } from "fs";
12513
+ import path27 from "path";
12514
+ import { existsSync as existsSync21 } from "fs";
12212
12515
  import { readFile as readFile21 } from "fs/promises";
12213
- import { z as z8 } from "zod";
12516
+ import { z as z9 } from "zod";
12214
12517
  var SHA256_RE2 = /^[0-9a-f]{64}$/;
12215
12518
  var HEX_RE = /^#[0-9a-f]{6}$/;
12216
12519
  var MAX_REDMEAN_DISTANCE = 765;
12217
- var QualityPathSchema = z8.string().min(1).refine(
12218
- (value) => !path25.posix.isAbsolute(value) && !value.includes("\\"),
12520
+ var QualityPathSchema = z9.string().min(1).refine(
12521
+ (value) => !path27.posix.isAbsolute(value) && !value.includes("\\"),
12219
12522
  "expected a portable relative path"
12220
12523
  );
12221
12524
  var DEFAULT_QUALITY_TOLERANCES = {
@@ -12228,53 +12531,53 @@ var DEFAULT_QUALITY_TOLERANCES = {
12228
12531
  maxEdgeContrastDrop: 0.03,
12229
12532
  maxIsolatedPixelIncrease: 5e-3
12230
12533
  };
12231
- var QualityToleranceSchema = z8.object({
12232
- requireExactHash: z8.boolean(),
12233
- maxColorCountIncrease: z8.number().int().min(0),
12234
- maxNewColors: z8.number().int().min(0),
12235
- maxTransparencyDelta: z8.number().min(0).max(1),
12236
- maxPartialAlphaIncrease: z8.number().min(0).max(1),
12237
- maxEdgeDensityDelta: z8.number().min(0).max(1),
12238
- maxEdgeContrastDrop: z8.number().min(0).max(1),
12239
- maxIsolatedPixelIncrease: z8.number().min(0).max(1)
12534
+ var QualityToleranceSchema = z9.object({
12535
+ requireExactHash: z9.boolean(),
12536
+ maxColorCountIncrease: z9.number().int().min(0),
12537
+ maxNewColors: z9.number().int().min(0),
12538
+ maxTransparencyDelta: z9.number().min(0).max(1),
12539
+ maxPartialAlphaIncrease: z9.number().min(0).max(1),
12540
+ maxEdgeDensityDelta: z9.number().min(0).max(1),
12541
+ maxEdgeContrastDrop: z9.number().min(0).max(1),
12542
+ maxIsolatedPixelIncrease: z9.number().min(0).max(1)
12240
12543
  }).strict();
12241
- var ImageQualityMetricsSchema = z8.object({
12242
- sha256: z8.string().regex(SHA256_RE2),
12243
- width: z8.number().int().min(1),
12244
- height: z8.number().int().min(1),
12245
- colorCount: z8.number().int().min(0),
12246
- colors: z8.array(z8.string().regex(HEX_RE)),
12247
- transparency: z8.number().min(0).max(1),
12248
- partialAlpha: z8.number().min(0).max(1),
12249
- edgeDensity: z8.number().min(0).max(1),
12250
- meanEdgeContrast: z8.number().min(0).max(1),
12251
- isolatedPixelRatio: z8.number().min(0).max(1)
12544
+ var ImageQualityMetricsSchema = z9.object({
12545
+ sha256: z9.string().regex(SHA256_RE2),
12546
+ width: z9.number().int().min(1),
12547
+ height: z9.number().int().min(1),
12548
+ colorCount: z9.number().int().min(0),
12549
+ colors: z9.array(z9.string().regex(HEX_RE)),
12550
+ transparency: z9.number().min(0).max(1),
12551
+ partialAlpha: z9.number().min(0).max(1),
12552
+ edgeDensity: z9.number().min(0).max(1),
12553
+ meanEdgeContrast: z9.number().min(0).max(1),
12554
+ isolatedPixelRatio: z9.number().min(0).max(1)
12252
12555
  }).strict();
12253
- var QualityBaselineCaseSchema = z8.object({
12254
- id: z8.string().regex(/^[a-z0-9][a-z0-9/_-]*$/),
12556
+ var QualityBaselineCaseSchema = z9.object({
12557
+ id: z9.string().regex(/^[a-z0-9][a-z0-9/_-]*$/),
12255
12558
  file: QualityPathSchema,
12256
- record: z8.object({
12559
+ record: z9.object({
12257
12560
  path: QualityPathSchema,
12258
- sha256: z8.string().regex(SHA256_RE2)
12561
+ sha256: z9.string().regex(SHA256_RE2)
12259
12562
  }).strict().optional(),
12260
12563
  expected: ImageQualityMetricsSchema,
12261
12564
  tolerances: QualityToleranceSchema
12262
12565
  }).strict();
12263
- var QualityBaselineSchema = z8.object({
12264
- $schema: z8.string().url().optional(),
12265
- format: z8.literal("pixelkiln-quality-baseline"),
12266
- schemaVersion: z8.literal(1),
12267
- cases: z8.array(QualityBaselineCaseSchema).min(1)
12566
+ var QualityBaselineSchema = z9.object({
12567
+ $schema: z9.string().url().optional(),
12568
+ format: z9.literal("pixelkiln-quality-baseline"),
12569
+ schemaVersion: z9.literal(1),
12570
+ cases: z9.array(QualityBaselineCaseSchema).min(1)
12268
12571
  }).strict().superRefine((baseline, context) => {
12269
12572
  const ids = baseline.cases.map((entry) => entry.id);
12270
12573
  if (new Set(ids).size !== ids.length) {
12271
- context.addIssue({ code: z8.ZodIssueCode.custom, path: ["cases"], message: "duplicate case id" });
12574
+ context.addIssue({ code: z9.ZodIssueCode.custom, path: ["cases"], message: "duplicate case id" });
12272
12575
  }
12273
12576
  });
12274
- var QualityInputSchema = z8.object({
12275
- id: z8.string().regex(/^[a-z0-9][a-z0-9/_-]*$/),
12276
- path: z8.string().min(1),
12277
- record: z8.string().min(1).optional(),
12577
+ var QualityInputSchema = z9.object({
12578
+ id: z9.string().regex(/^[a-z0-9][a-z0-9/_-]*$/),
12579
+ path: z9.string().min(1),
12580
+ record: z9.string().min(1).optional(),
12278
12581
  tolerances: QualityToleranceSchema.partial().optional()
12279
12582
  }).strict();
12280
12583
  function message5(error) {
@@ -12362,7 +12665,7 @@ function measureDecodedImageQuality(png, digest2) {
12362
12665
  };
12363
12666
  }
12364
12667
  async function measureImageQuality(file) {
12365
- const absolute = path25.resolve(file);
12668
+ const absolute = path27.resolve(file);
12366
12669
  let bytes;
12367
12670
  try {
12368
12671
  bytes = await readFile21(absolute);
@@ -12376,7 +12679,7 @@ async function measureImageQuality(file) {
12376
12679
  }
12377
12680
  }
12378
12681
  function resolveQualityInputs(raw, inputsFilePath) {
12379
- const parsed = z8.array(QualityInputSchema).min(1).safeParse(raw);
12682
+ const parsed = z9.array(QualityInputSchema).min(1).safeParse(raw);
12380
12683
  if (!parsed.success) {
12381
12684
  throw new Error(
12382
12685
  "--inputs must be a non-empty JSON array of quality cases:\n" + parsed.error.issues.map((issue) => ` ${issue.path.join(".") || "$"}: ${issue.message}`).join("\n")
@@ -12384,11 +12687,11 @@ function resolveQualityInputs(raw, inputsFilePath) {
12384
12687
  }
12385
12688
  const ids = parsed.data.map((entry) => entry.id);
12386
12689
  if (new Set(ids).size !== ids.length) throw new Error("--inputs contains a duplicate quality case id.");
12387
- const root = path25.dirname(path25.resolve(inputsFilePath));
12690
+ const root = path27.dirname(path27.resolve(inputsFilePath));
12388
12691
  return parsed.data.map((entry) => ({
12389
12692
  id: entry.id,
12390
- file: path25.resolve(root, entry.path),
12391
- ...entry.record ? { record: path25.resolve(root, entry.record) } : {},
12693
+ file: path27.resolve(root, entry.path),
12694
+ ...entry.record ? { record: path27.resolve(root, entry.record) } : {},
12392
12695
  tolerances: QualityToleranceSchema.parse({
12393
12696
  ...DEFAULT_QUALITY_TOLERANCES,
12394
12697
  ...entry.tolerances
@@ -12396,7 +12699,7 @@ function resolveQualityInputs(raw, inputsFilePath) {
12396
12699
  }));
12397
12700
  }
12398
12701
  function portableRelative2(from, to) {
12399
- return path25.relative(from, path25.resolve(to)).split(path25.sep).join("/") || ".";
12702
+ return path27.relative(from, path27.resolve(to)).split(path27.sep).join("/") || ".";
12400
12703
  }
12401
12704
  async function verifyRecordForImage(recordPath, imagePath) {
12402
12705
  const verification = await verifyArtifactBundle(recordPath);
@@ -12411,20 +12714,20 @@ async function verifyRecordForImage(recordPath, imagePath) {
12411
12714
  if (record.kind !== "refine" || options === null || typeof options !== "object" || options.schema !== "pixelkiln-quality") {
12412
12715
  throw new Error(`Quality record ${recordPath} is not a PixelKiln refinement record.`);
12413
12716
  }
12414
- const outputs = record.outputs.map((output) => path25.resolve(path25.dirname(recordPath), output.path));
12415
- if (!outputs.includes(path25.resolve(imagePath))) {
12717
+ const outputs = record.outputs.map((output) => path27.resolve(path27.dirname(recordPath), output.path));
12718
+ if (!outputs.includes(path27.resolve(imagePath))) {
12416
12719
  throw new Error(`Quality record ${recordPath} does not own ${imagePath}.`);
12417
12720
  }
12418
12721
  }
12419
12722
  async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
12420
12723
  if (!inputs.length) throw new Error("Cannot snapshot an empty quality baseline.");
12421
- const absolute = path25.resolve(baselinePath);
12724
+ const absolute = path27.resolve(baselinePath);
12422
12725
  for (const input of inputs) {
12423
- if (path25.resolve(input.file) === absolute || input.record && path25.resolve(input.record) === absolute) {
12726
+ if (path27.resolve(input.file) === absolute || input.record && path27.resolve(input.record) === absolute) {
12424
12727
  throw new Error(`Quality baseline output would overwrite an input: ${absolute}.`);
12425
12728
  }
12426
12729
  }
12427
- const root = path25.dirname(absolute);
12730
+ const root = path27.dirname(absolute);
12428
12731
  const cases = [];
12429
12732
  for (const input of [...inputs].sort((a, b) => a.id.localeCompare(b.id))) {
12430
12733
  const expected = await measureImageQuality(input.file);
@@ -12451,7 +12754,7 @@ async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
12451
12754
  cases
12452
12755
  });
12453
12756
  const data = Buffer.from(JSON.stringify(baseline, null, 2) + "\n");
12454
- if (existsSync20(absolute) && !options.force) {
12757
+ if (existsSync21(absolute) && !options.force) {
12455
12758
  const current = await readFile21(absolute);
12456
12759
  if (!current.equals(data)) {
12457
12760
  throw new Error(`Quality baseline already exists with different content: ${absolute}. Pass --force to replace it.`);
@@ -12461,7 +12764,7 @@ async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
12461
12764
  return { baseline, path: absolute, changed: result.changed.length > 0 };
12462
12765
  }
12463
12766
  async function readQualityBaseline(baselinePath) {
12464
- const absolute = path25.resolve(baselinePath);
12767
+ const absolute = path27.resolve(baselinePath);
12465
12768
  let raw;
12466
12769
  try {
12467
12770
  raw = JSON.parse(await readFile21(absolute, "utf8"));
@@ -12534,12 +12837,12 @@ function regressionViolations(expected, actual, tolerances) {
12534
12837
  return { violations, warnings };
12535
12838
  }
12536
12839
  async function checkQualityBaseline(baselinePath) {
12537
- const absolute = path25.resolve(baselinePath);
12840
+ const absolute = path27.resolve(baselinePath);
12538
12841
  const baseline = await readQualityBaseline(absolute);
12539
- const root = path25.dirname(absolute);
12842
+ const root = path27.dirname(absolute);
12540
12843
  const cases = [];
12541
12844
  for (const entry of baseline.cases) {
12542
- const file = path25.resolve(root, entry.file);
12845
+ const file = path27.resolve(root, entry.file);
12543
12846
  let actual = null;
12544
12847
  const violations = [];
12545
12848
  const warnings = [];
@@ -12552,7 +12855,7 @@ async function checkQualityBaseline(baselinePath) {
12552
12855
  violations.push(message5(error));
12553
12856
  }
12554
12857
  if (entry.record) {
12555
- const recordPath = path25.resolve(root, entry.record.path);
12858
+ const recordPath = path27.resolve(root, entry.record.path);
12556
12859
  try {
12557
12860
  const recordHash = await sha256File(recordPath);
12558
12861
  if (recordHash !== entry.record.sha256) violations.push("quality record hash changed");
@@ -12638,13 +12941,13 @@ async function runGallery(initial, reload, args, access2) {
12638
12941
  };
12639
12942
  const loadProject = async (project) => {
12640
12943
  const ctx = await access2.loadProject(project);
12641
- const dir = path26.dirname(ctx.loaded.path);
12944
+ const dir = path28.dirname(ctx.loaded.path);
12642
12945
  const declared = readEnvFiles(dir);
12643
12946
  const credentialNames = new Set(availableProviders().flatMap((id) => providerCredentialEnvs(providerFactory(id))));
12644
12947
  for (const [name, value] of Object.entries(declared)) {
12645
12948
  if (credentialNames.has(name) && name in process.env && process.env[name] !== value) {
12646
12949
  throw new Error(
12647
- `${path26.relative(process.cwd(), dir) || "."} sets ${name} to a different value than the one this gallery already loaded; run a separate gallery for that project so its work uses its own account.`
12950
+ `${path28.relative(process.cwd(), dir) || "."} sets ${name} to a different value than the one this gallery already loaded; run a separate gallery for that project so its work uses its own account.`
12648
12951
  );
12649
12952
  }
12650
12953
  }
@@ -12670,7 +12973,7 @@ async function runGallery(initial, reload, args, access2) {
12670
12973
  args.edit,
12671
12974
  budget ? describeBudget(budget) : null
12672
12975
  ),
12673
- ...args.edit ? { edit: createGalleryEditHandler({ manifestFor: access2.manifestFor, reload, onProgress: log }) } : {},
12976
+ ...args.edit ? { edit: createGalleryEditHandler({ manifestFor: access2.manifestFor, loadProject: access2.loadProject, reload, onProgress: log }) } : {},
12674
12977
  ...budget ? { generate: createGenerateHandlers({ loadProject, providerFor, budget, reload, onProgress: log }) } : {}
12675
12978
  });
12676
12979
  await new Promise((resolve) => {
@@ -12686,11 +12989,11 @@ async function runGallery(initial, reload, args, access2) {
12686
12989
  log("\n gallery closed");
12687
12990
  }
12688
12991
  async function provenanceFile(id, file) {
12689
- const absolute = path26.resolve(file);
12992
+ const absolute = path28.resolve(file);
12690
12993
  return {
12691
12994
  id,
12692
12995
  path: absolute,
12693
- sha256: existsSync21(absolute) ? await sha256File(absolute) : null,
12996
+ sha256: existsSync22(absolute) ? await sha256File(absolute) : null,
12694
12997
  included: true
12695
12998
  };
12696
12999
  }
@@ -12765,6 +13068,7 @@ var COMMANDS = [
12765
13068
  "balance",
12766
13069
  "status",
12767
13070
  "gallery",
13071
+ "edit",
12768
13072
  "quality",
12769
13073
  "refine",
12770
13074
  "recipe",
@@ -12779,6 +13083,7 @@ var WORKSPACE_SUBCOMMANDS = ["add", "remove", "list", "status", "claims"];
12779
13083
  var REFINE_SUBCOMMANDS = ["run", "approve", "check"];
12780
13084
  var RECIPE_SUBCOMMANDS = ["list", "inspect", "install", "verify"];
12781
13085
  var QUALITY_SUBCOMMANDS = ["snapshot", "check"];
13086
+ var EDIT_SUBCOMMANDS = ["start", "detach"];
12782
13087
  function parseArgs(argv) {
12783
13088
  const [command = "help"] = argv;
12784
13089
  if (!COMMANDS.includes(command)) {
@@ -12836,6 +13141,12 @@ function parseArgs(argv) {
12836
13141
  }
12837
13142
  rest = rest.slice(1);
12838
13143
  }
13144
+ } else if (command === "edit") {
13145
+ subcommand = rest[0]?.startsWith("-") || rest[0] === void 0 ? "start" : rest[0];
13146
+ if (!EDIT_SUBCOMMANDS.includes(subcommand)) {
13147
+ throw new Error(`Unknown edit subcommand "${subcommand}". Known: ${EDIT_SUBCOMMANDS.join(", ")}`);
13148
+ }
13149
+ if (rest[0] === subcommand) rest = rest.slice(1);
12839
13150
  } else if (command === "refine") {
12840
13151
  subcommand = rest[0]?.startsWith("-") || rest[0] === void 0 ? "run" : rest[0];
12841
13152
  if (!REFINE_SUBCOMMANDS.includes(subcommand)) {
@@ -12949,7 +13260,7 @@ function parseArgs(argv) {
12949
13260
  return {
12950
13261
  command,
12951
13262
  manifest,
12952
- lock: get("--lock") ?? path26.join(path26.dirname(path26.resolve(manifest)), "pixelkiln.lock.json"),
13263
+ lock: get("--lock") ?? path28.join(path28.dirname(path28.resolve(manifest)), "pixelkiln.lock.json"),
12953
13264
  explicitLock: get("--lock"),
12954
13265
  styles: list("--style"),
12955
13266
  assets: list("--only"),
@@ -13030,6 +13341,9 @@ Commands
13030
13341
  tag Push manifest tags to the objects upstream (free).
13031
13342
  balance Show the provider's remaining balance.
13032
13343
  status Summarise the lockfile.
13344
+ edit Hand-edit an asset in your own editor: copies the generated PNG to
13345
+ <outDir>/edits/, declares it as the asset's source, and opens it.
13346
+ edit detach places the generated art again (the file is kept).
13033
13347
  gallery Open a local read-only gallery of every generation and its
13034
13348
  provenance: prompt, provider, cost, outputs, lineage, quality.
13035
13349
  --workspace <catalog> shows every registered project at once;
@@ -13072,7 +13386,7 @@ Options
13072
13386
  and the gallery snapshot (no server)
13073
13387
  --check plan/audit/cache: exit nonzero when the selected state is unsafe
13074
13388
  --yes, -y Skip the confirmation prompt
13075
- --no-open Do not auto-open the browser during pick, salvage, or gallery
13389
+ --no-open Do not auto-open the browser (pick, salvage, gallery) or editor (edit)
13076
13390
  --edit gallery: allow manifest edits from the page (never spends)
13077
13391
  --tag Also push tags upstream after fetch
13078
13392
  --claims a.json,b Other projects' lockfiles (salvage; required if account is shared)
@@ -13089,6 +13403,8 @@ Examples
13089
13403
  pixelkiln gallery --style heybud-premium
13090
13404
  pixelkiln gallery --workspace pixelkiln.workspace.json
13091
13405
  pixelkiln gallery --edit --budget 80
13406
+ pixelkiln edit --style base --only anvil
13407
+ PIXELKILN_EDITOR="open -a Aseprite" pixelkiln edit --only anvil --style base
13092
13408
  pixelkiln adopt --tag
13093
13409
  pixelkiln pack --style heybud-premium
13094
13410
  pixelkiln pack --inputs sprites.json --out dist/sheet # no manifest needed
@@ -13165,6 +13481,15 @@ function printResumeActions(specs, lock) {
13165
13481
  }
13166
13482
  return actions.length;
13167
13483
  }
13484
+ function manifestSources(manifest, styleId) {
13485
+ const sources = {};
13486
+ for (const [assetId, asset] of Object.entries(manifest.assets)) {
13487
+ if (asset.styles.length && !asset.styles.includes(styleId)) continue;
13488
+ const source = asset.sourceByStyle[styleId] ?? asset.source;
13489
+ if (source) sources[assetId] = source;
13490
+ }
13491
+ return sources;
13492
+ }
13168
13493
  function manifestProviderIds(manifest) {
13169
13494
  return [...new Set(
13170
13495
  Object.values(manifest.styles).map((style) => style.provider ?? manifest.provider)
@@ -13238,10 +13563,10 @@ async function confirm(question, auto) {
13238
13563
  return answer === "y" || answer === "yes";
13239
13564
  }
13240
13565
  async function requireCompleteWorkspaceClaims(workspacePath) {
13241
- if (!existsSync21(workspacePath)) {
13566
+ if (!existsSync22(workspacePath)) {
13242
13567
  throw new Error(`Workspace catalog not found: ${workspacePath}`);
13243
13568
  }
13244
- const dir = path26.dirname(path26.resolve(workspacePath));
13569
+ const dir = path28.dirname(path28.resolve(workspacePath));
13245
13570
  const ws = await loadWorkspace(workspacePath);
13246
13571
  const diagnostics = validateWorkspace(ws, dir);
13247
13572
  const errors = diagnostics.filter((d) => d.level === "error");
@@ -13273,15 +13598,15 @@ async function main() {
13273
13598
  if (!args.out) throw new Error("quality snapshot needs --out <pixelkiln.quality.json>.");
13274
13599
  let raw;
13275
13600
  try {
13276
- raw = JSON.parse(await readFile22(path26.resolve(args.inputs), "utf8"));
13601
+ raw = JSON.parse(await readFile22(path28.resolve(args.inputs), "utf8"));
13277
13602
  } catch (error) {
13278
13603
  throw new Error(
13279
- `Could not read quality inputs ${path26.resolve(args.inputs)}: ${error instanceof Error ? error.message : String(error)}`,
13604
+ `Could not read quality inputs ${path28.resolve(args.inputs)}: ${error instanceof Error ? error.message : String(error)}`,
13280
13605
  { cause: error }
13281
13606
  );
13282
13607
  }
13283
13608
  const inputs = resolveQualityInputs(raw, args.inputs);
13284
- if (path26.resolve(args.inputs) === path26.resolve(args.out)) {
13609
+ if (path28.resolve(args.inputs) === path28.resolve(args.out)) {
13285
13610
  throw new Error("quality snapshot --out must not overwrite its --inputs file.");
13286
13611
  }
13287
13612
  const result = await snapshotQualityBaseline(inputs, args.out, { force: args.force });
@@ -13294,7 +13619,7 @@ async function main() {
13294
13619
  }, null, 2));
13295
13620
  } else {
13296
13621
  log(
13297
- ` ${result.changed ? "wrote" : "unchanged"} ${path26.relative(process.cwd(), result.path)} (${result.baseline.cases.length} case(s))`
13622
+ ` ${result.changed ? "wrote" : "unchanged"} ${path28.relative(process.cwd(), result.path)} (${result.baseline.cases.length} case(s))`
13298
13623
  );
13299
13624
  log(` Review the tolerances, commit the baseline, then run quality check in CI.`);
13300
13625
  }
@@ -13389,14 +13714,14 @@ async function main() {
13389
13714
  }, null, 2));
13390
13715
  } else {
13391
13716
  log(` installed ${result.recipe.id}@${result.recipe.version}`);
13392
- log(` ${path26.relative(process.cwd(), result.destination) || "."}`);
13717
+ log(` ${path28.relative(process.cwd(), result.destination) || "."}`);
13393
13718
  log(`
13394
13719
  Add this entry under your manifest's styles object:`);
13395
13720
  log(JSON.stringify({ [result.styleId]: result.style }, null, 2));
13396
13721
  if (result.recipe.models.length) {
13397
13722
  log(`
13398
13723
  Models are not downloaded automatically. Verify them with:`);
13399
- log(` pixelkiln recipe verify ${path26.relative(process.cwd(), result.destination)} --model-root <ComfyUI/models>`);
13724
+ log(` pixelkiln recipe verify ${path28.relative(process.cwd(), result.destination)} --model-root <ComfyUI/models>`);
13400
13725
  }
13401
13726
  }
13402
13727
  return;
@@ -13430,12 +13755,12 @@ async function main() {
13430
13755
  ` recovered ${result2.detection.columns}x${result2.detection.rows} native grid (${result2.detection.confidence}: ${result2.detection.consensus})`
13431
13756
  );
13432
13757
  log(` enforced ${result2.palette.length}-color palette without dithering`);
13433
- log(` wrote ${path26.relative(process.cwd(), result2.output)}`);
13434
- log(` quality record: ${path26.relative(process.cwd(), result2.record)}`);
13758
+ log(` wrote ${path28.relative(process.cwd(), result2.output)}`);
13759
+ log(` quality record: ${path28.relative(process.cwd(), result2.record)}`);
13435
13760
  log(`
13436
13761
  Automated checks passed. Human 1\xD7 review is still required:`);
13437
13762
  log(
13438
- ` pixelkiln refine approve --from ${path26.relative(process.cwd(), result2.record)} --reviewer "Your Name"`
13763
+ ` pixelkiln refine approve --from ${path28.relative(process.cwd(), result2.record)} --reviewer "Your Name"`
13439
13764
  );
13440
13765
  }
13441
13766
  return;
@@ -13463,7 +13788,7 @@ async function main() {
13463
13788
  if (args.json) log(JSON.stringify(result2, null, 2));
13464
13789
  else {
13465
13790
  log(
13466
- result2.options.frameSet ? ` approved ${result2.outputs.length} frames in ${path26.relative(process.cwd(), result2.record)} by ${args.reviewer.trim()}` : ` approved ${path26.relative(process.cwd(), result2.output)} by ${args.reviewer.trim()}`
13791
+ result2.options.frameSet ? ` approved ${result2.outputs.length} frames in ${path28.relative(process.cwd(), result2.record)} by ${args.reviewer.trim()}` : ` approved ${path28.relative(process.cwd(), result2.output)} by ${args.reviewer.trim()}`
13467
13792
  );
13468
13793
  }
13469
13794
  return;
@@ -13472,14 +13797,14 @@ async function main() {
13472
13797
  if (args.json) {
13473
13798
  log(JSON.stringify(result, null, 2));
13474
13799
  } else {
13475
- log(` ${result.safe ? "release-ready" : "not release-ready"}: ${path26.relative(process.cwd(), result.output || result.record)}`);
13800
+ log(` ${result.safe ? "release-ready" : "not release-ready"}: ${path28.relative(process.cwd(), result.output || result.record)}`);
13476
13801
  for (const reason of result.reasons) log(` ${reason}`);
13477
13802
  }
13478
13803
  if (!result.safe) process.exitCode = 1;
13479
13804
  return;
13480
13805
  }
13481
13806
  if (args.command === "balance") {
13482
- loadEnvFiles(path26.dirname(path26.resolve(args.manifest)));
13807
+ loadEnvFiles(path28.dirname(path28.resolve(args.manifest)));
13483
13808
  loadEnvFiles(process.cwd());
13484
13809
  const loaded2 = await loadManifest(args.manifest);
13485
13810
  const selectedProvider = accountProviderId(loaded2.manifest, args.provider, "balance");
@@ -13492,31 +13817,31 @@ async function main() {
13492
13817
  }
13493
13818
  if (args.command === "init") {
13494
13819
  if (!args.from) throw new Error("init needs --from <dir> pointing at your existing PNGs.");
13495
- const root = path26.resolve(args.from);
13496
- if (!existsSync21(root)) throw new Error(`No directory at ${root}`);
13820
+ const root = path28.resolve(args.from);
13821
+ if (!existsSync22(root)) throw new Error(`No directory at ${root}`);
13497
13822
  const generator = args.generator ?? "map";
13498
13823
  if (generator !== "1dir" && generator !== "map") {
13499
13824
  throw new Error(`--generator must be "1dir" or "map", got "${args.generator}".`);
13500
13825
  }
13501
- const target = path26.resolve(args.out ?? "pixelkiln.manifest.json");
13826
+ const target = path28.resolve(args.out ?? "pixelkiln.manifest.json");
13502
13827
  const { assets, skipped } = await scanAssets(root, { exclude: args.exclude });
13503
13828
  if (!assets.length) throw new Error(`No PNGs found under ${root}`);
13504
13829
  const manifest = buildManifest(
13505
- args.name ?? path26.basename(path26.dirname(target)),
13830
+ args.name ?? path28.basename(path28.dirname(target)),
13506
13831
  args.styles[0] ?? "base",
13507
13832
  generator,
13508
- path26.relative(path26.dirname(target), root) || ".",
13833
+ path28.relative(path28.dirname(target), root) || ".",
13509
13834
  assets
13510
13835
  );
13511
13836
  await writeManifestFile(target, manifest);
13512
- log(` scanned ${assets.length} PNG(s) under ${path26.relative(process.cwd(), root)}`);
13837
+ log(` scanned ${assets.length} PNG(s) under ${path28.relative(process.cwd(), root)}`);
13513
13838
  if (skipped.length) log(` skipped ${skipped.length} unreadable file(s)`);
13514
- log(` wrote ${path26.relative(process.cwd(), target)}`);
13839
+ log(` wrote ${path28.relative(process.cwd(), target)}`);
13515
13840
  log(`
13516
13841
  Prompts are intentionally empty. To recover the real ones from your`);
13517
13842
  log(` PixelLab account instead of inventing them:`);
13518
13843
  log(`
13519
- pixelkiln adopt --manifest ${path26.relative(process.cwd(), target)} --write-prompts
13844
+ pixelkiln adopt --manifest ${path28.relative(process.cwd(), target)} --write-prompts
13520
13845
  `);
13521
13846
  return;
13522
13847
  }
@@ -13564,10 +13889,10 @@ async function main() {
13564
13889
  if (args.primaryOnly || args.outputRoles.length) {
13565
13890
  throw new Error("--primary-only and --output-role require manifest-driven pack");
13566
13891
  }
13567
- const raw = JSON.parse(await readFile22(path26.resolve(args.inputs), "utf8"));
13892
+ const raw = JSON.parse(await readFile22(path28.resolve(args.inputs), "utf8"));
13568
13893
  const inputs = resolvePackInputs(raw, args.inputs);
13569
13894
  const { png, atlas, skipped, sources } = packSprites(inputs, { columns: args.columns });
13570
- const base = path26.resolve(args.out.replace(/\.png$/, ""));
13895
+ const base = path28.resolve(args.out.replace(/\.png$/, ""));
13571
13896
  const outputs = [
13572
13897
  { path: `${base}.png`, data: png },
13573
13898
  { path: `${base}.json`, data: JSON.stringify(atlas, null, 2) + "\n" }
@@ -13580,17 +13905,17 @@ async function main() {
13580
13905
  log(
13581
13906
  ` ${atlas.frames.length} sprite(s), ${atlas.sheet.width}x${atlas.sheet.height} in ${atlas.columns} column(s) \u2014 ${(png.length / 1024).toFixed(1)} KB`
13582
13907
  );
13583
- log(` ${path26.relative(process.cwd(), base)}.png + .json + .pixelkiln.json`);
13908
+ log(` ${path28.relative(process.cwd(), base)}.png + .json + .pixelkiln.json`);
13584
13909
  for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
13585
13910
  return;
13586
13911
  }
13587
13912
  if (args.command === "workspace") {
13588
- const workspacePath = path26.resolve(args.workspace ?? "pixelkiln.workspace.json");
13589
- const dir = path26.dirname(workspacePath);
13913
+ const workspacePath = path28.resolve(args.workspace ?? "pixelkiln.workspace.json");
13914
+ const dir = path28.dirname(workspacePath);
13590
13915
  if (args.subcommand === "add") {
13591
- const manifestPath = path26.resolve(args.target);
13916
+ const manifestPath = path28.resolve(args.target);
13592
13917
  const loadedTarget = await loadManifest(manifestPath);
13593
- const lockPath = args.explicitLock ? path26.resolve(args.explicitLock) : path26.join(path26.dirname(manifestPath), "pixelkiln.lock.json");
13918
+ const lockPath = args.explicitLock ? path28.resolve(args.explicitLock) : path28.join(path28.dirname(manifestPath), "pixelkiln.lock.json");
13594
13919
  const ws = await loadWorkspace(workspacePath);
13595
13920
  const id = args.name ?? loadedTarget.manifest.name;
13596
13921
  if (ws.projects.some((p) => p.id === id)) {
@@ -13610,10 +13935,10 @@ async function main() {
13610
13935
  ...args.account ? { account: args.account } : {}
13611
13936
  };
13612
13937
  await saveWorkspace(workspacePath, { version: 1, projects: [...ws.projects, project] });
13613
- log(` registered "${id}" in ${path26.relative(process.cwd(), workspacePath)}`);
13938
+ log(` registered "${id}" in ${path28.relative(process.cwd(), workspacePath)}`);
13614
13939
  log(` manifest: ${project.manifest}`);
13615
13940
  log(` lock: ${project.lock}`);
13616
- if (!existsSync21(lockPath)) {
13941
+ if (!existsSync22(lockPath)) {
13617
13942
  log(
13618
13943
  ` warning: no lockfile there yet \u2014 this project contributes no claims until one is generated`
13619
13944
  );
@@ -13622,7 +13947,7 @@ async function main() {
13622
13947
  }
13623
13948
  if (args.subcommand === "remove") {
13624
13949
  const ws = await loadWorkspace(workspacePath);
13625
- const resolvedTarget = path26.resolve(args.target);
13950
+ const resolvedTarget = path28.resolve(args.target);
13626
13951
  const match = ws.projects.find(
13627
13952
  (p) => p.id === args.target || resolveProject(dir, p).manifestPath === resolvedTarget
13628
13953
  );
@@ -13633,11 +13958,11 @@ async function main() {
13633
13958
  version: 1,
13634
13959
  projects: ws.projects.filter((p) => p !== match)
13635
13960
  });
13636
- log(` removed "${match.id}" from ${path26.relative(process.cwd(), workspacePath)}`);
13961
+ log(` removed "${match.id}" from ${path28.relative(process.cwd(), workspacePath)}`);
13637
13962
  return;
13638
13963
  }
13639
13964
  if (args.subcommand === "list") {
13640
- if (!existsSync21(workspacePath)) {
13965
+ if (!existsSync22(workspacePath)) {
13641
13966
  throw new Error(`Workspace catalog not found: ${workspacePath}`);
13642
13967
  }
13643
13968
  const ws = await loadWorkspace(workspacePath);
@@ -13645,9 +13970,9 @@ async function main() {
13645
13970
  if (args.json) {
13646
13971
  log(JSON.stringify({ version: 1, workspace: workspacePath, projects: ws.projects, diagnostics }, null, 2));
13647
13972
  } else if (!ws.projects.length) {
13648
- log(` no projects registered in ${path26.relative(process.cwd(), workspacePath)}`);
13973
+ log(` no projects registered in ${path28.relative(process.cwd(), workspacePath)}`);
13649
13974
  } else {
13650
- log(` ${ws.projects.length} project(s) in ${path26.relative(process.cwd(), workspacePath)}:`);
13975
+ log(` ${ws.projects.length} project(s) in ${path28.relative(process.cwd(), workspacePath)}:`);
13651
13976
  for (const p of ws.projects) {
13652
13977
  log(` ${p.id.padEnd(24)} ${p.manifest.padEnd(40)} (${p.provider}${p.account ? `, ${p.account}` : ""})`);
13653
13978
  }
@@ -13657,7 +13982,7 @@ async function main() {
13657
13982
  return;
13658
13983
  }
13659
13984
  if (args.subcommand === "status") {
13660
- if (!existsSync21(workspacePath)) {
13985
+ if (!existsSync22(workspacePath)) {
13661
13986
  throw new Error(`Workspace catalog not found: ${workspacePath}`);
13662
13987
  }
13663
13988
  const ws = await loadWorkspace(workspacePath);
@@ -13665,7 +13990,7 @@ async function main() {
13665
13990
  if (args.json) {
13666
13991
  log(JSON.stringify({ ...report, workspace: workspacePath }, null, 2));
13667
13992
  } else {
13668
- log(` workspace: ${path26.relative(process.cwd(), workspacePath)}`);
13993
+ log(` workspace: ${path28.relative(process.cwd(), workspacePath)}`);
13669
13994
  for (const p of report.projects) {
13670
13995
  if (p.error) {
13671
13996
  log(`
@@ -13712,8 +14037,8 @@ async function main() {
13712
14037
  }
13713
14038
  }
13714
14039
  if (args.command === "gallery" && args.workspace) {
13715
- const workspacePath = path26.resolve(args.workspace);
13716
- if (!existsSync21(workspacePath)) {
14040
+ const workspacePath = path28.resolve(args.workspace);
14041
+ if (!existsSync22(workspacePath)) {
13717
14042
  throw new Error(`Workspace catalog not found: ${workspacePath}`);
13718
14043
  }
13719
14044
  const filter = { styles: args.styles, assets: args.assets };
@@ -13733,7 +14058,7 @@ async function main() {
13733
14058
  const registered = async (projectId) => {
13734
14059
  const project = (await loadWorkspace(workspacePath)).projects.find((candidate) => candidate.id === projectId);
13735
14060
  if (!projectId || !project) throw new Error(`unknown workspace project "${projectId ?? ""}"`);
13736
- return resolveProject(path26.dirname(workspacePath), project);
14061
+ return resolveProject(path28.dirname(workspacePath), project);
13737
14062
  };
13738
14063
  await runGallery(initial, build, args, {
13739
14064
  manifestFor: async (projectId) => (await registered(projectId)).manifestPath,
@@ -13748,14 +14073,14 @@ async function main() {
13748
14073
  });
13749
14074
  return;
13750
14075
  }
13751
- if (!existsSync21(path26.resolve(args.manifest))) {
14076
+ if (!existsSync22(path28.resolve(args.manifest))) {
13752
14077
  throw new Error(
13753
- `No manifest at ${path26.resolve(args.manifest)}. Pass --manifest, or run \`pixelkiln init --from <dir>\`.`
14078
+ `No manifest at ${path28.resolve(args.manifest)}. Pass --manifest, or run \`pixelkiln init --from <dir>\`.`
13754
14079
  );
13755
14080
  }
13756
- const manifestDir = path26.dirname(path26.resolve(args.manifest));
14081
+ const manifestDir = path28.dirname(path28.resolve(args.manifest));
13757
14082
  const envFiles2 = [...loadEnvFiles(manifestDir)];
13758
- if (path26.resolve(process.cwd()) !== manifestDir) envFiles2.push(...loadEnvFiles(process.cwd()));
14083
+ if (path28.resolve(process.cwd()) !== manifestDir) envFiles2.push(...loadEnvFiles(process.cwd()));
13759
14084
  const loaded = await loadManifest(args.manifest);
13760
14085
  let specs = await resolveSpecs(loaded, {
13761
14086
  styles: args.styles,
@@ -13828,7 +14153,7 @@ async function main() {
13828
14153
  });
13829
14154
  };
13830
14155
  await runGallery(await build(), reload, args, {
13831
- manifestFor: () => path26.resolve(args.manifest),
14156
+ manifestFor: () => path28.resolve(args.manifest),
13832
14157
  loadProject: async () => {
13833
14158
  const freshLoaded = await loadManifest(args.manifest);
13834
14159
  const freshSpecs = await resolveSpecs(freshLoaded);
@@ -13839,6 +14164,29 @@ async function main() {
13839
14164
  });
13840
14165
  return;
13841
14166
  }
14167
+ if (args.command === "edit") {
14168
+ if (!args.assets.length) throw new Error("edit needs --only <asset id> (and --style when the asset is in several styles).");
14169
+ if (specs.length !== 1) {
14170
+ throw new Error(
14171
+ specs.length ? `edit works on one asset in one style; --only ${args.assets.join(",")} resolves to ${specs.length}: ` + specs.map((spec2) => `${spec2.styleId}/${spec2.assetId}`).join(", ") + ". Add --style." : "edit matched no asset."
14172
+ );
14173
+ }
14174
+ const spec = specs[0];
14175
+ if (args.subcommand === "detach") {
14176
+ const result = await detachHandEdit(loaded, spec);
14177
+ log(result.changed ? ` detached the hand edit for ${spec.styleId}/${spec.assetId}; the generated art is placed again (the edit file is kept)` : ` ${spec.styleId}/${spec.assetId} has no hand edit`);
14178
+ return;
14179
+ }
14180
+ const started = await startHandEdit(loaded, lock, spec);
14181
+ log(` ${started.created ? "created" : "found"} ${started.source}${started.declared ? " and declared it as the asset's source" : ""}`);
14182
+ if (args.noOpen) {
14183
+ log(` edit it with your own tool, then run pixelkiln plan; mount and pack place it in place of the generated art`);
14184
+ } else {
14185
+ const command = openInEditor(started.editPath);
14186
+ log(` opened with: ${command}${process.env.PIXELKILN_EDITOR ? "" : " (set PIXELKILN_EDITOR to choose the program)"}`);
14187
+ }
14188
+ return;
14189
+ }
13842
14190
  const plan = await buildPlan(specs, lock, { force: args.force });
13843
14191
  if (args.command === "refine") {
13844
14192
  const pathOwnedFlags = [
@@ -13880,7 +14228,7 @@ async function main() {
13880
14228
  log(` ${item.state.padEnd(16)} ${item.key} ${item.reason}`);
13881
14229
  if (item.state === "needs-approval") {
13882
14230
  log(
13883
- ` pixelkiln refine approve --from ${path26.relative(process.cwd(), item.record)} --reviewer "Your Name"`
14231
+ ` pixelkiln refine approve --from ${path28.relative(process.cwd(), item.record)} --reviewer "Your Name"`
13884
14232
  );
13885
14233
  }
13886
14234
  }
@@ -13971,7 +14319,7 @@ async function main() {
13971
14319
  let intact = true;
13972
14320
  for (const output of entry.outputs) {
13973
14321
  const file = resolveOutputPath(output.path, item.spec.root);
13974
- if (!existsSync21(file) || await sha256File(file) !== output.sha256) {
14322
+ if (!existsSync22(file) || await sha256File(file) !== output.sha256) {
13975
14323
  intact = false;
13976
14324
  break;
13977
14325
  }
@@ -14029,7 +14377,7 @@ async function main() {
14029
14377
  if (args.primaryOnly && args.outputRoles.length) {
14030
14378
  throw new Error("pack accepts either --primary-only or --output-role, not both");
14031
14379
  }
14032
- const manifestDir2 = path26.dirname(path26.resolve(args.manifest));
14380
+ const manifestDir2 = path28.dirname(path28.resolve(args.manifest));
14033
14381
  const styleIds = args.styles.length ? args.styles : Object.keys(loaded.manifest.styles);
14034
14382
  for (const styleId of styleIds) {
14035
14383
  const packagingSpecs = await resolveSpecs(loaded, { styles: [styleId] });
@@ -14038,10 +14386,11 @@ async function main() {
14038
14386
  columns: args.columns,
14039
14387
  outputRoles: args.outputRoles,
14040
14388
  primaryOnly: args.primaryOnly,
14041
- sourceOverrides: qualitySources
14389
+ sourceOverrides: qualitySources,
14390
+ sources: manifestSources(loaded.manifest, styleId)
14042
14391
  });
14043
14392
  const style = loaded.manifest.styles[styleId];
14044
- const base = args.out ? path26.resolve(args.out.replace(/\.png$/, "")) : path26.resolve(manifestDir2, style.outDir, `${styleId}-sheet`);
14393
+ const base = args.out ? path28.resolve(args.out.replace(/\.png$/, "")) : path28.resolve(manifestDir2, style.outDir, `${styleId}-sheet`);
14045
14394
  const outputs = [
14046
14395
  { path: `${base}.png`, data: png },
14047
14396
  { path: `${base}.json`, data: JSON.stringify(atlas, null, 2) + "\n" }
@@ -14071,13 +14420,13 @@ async function main() {
14071
14420
  log(
14072
14421
  ` ${styleId} \u2014 ${atlas.frames.length} sprite(s), ${atlas.sheet.width}x${atlas.sheet.height} in ${atlas.columns} column(s)`
14073
14422
  );
14074
- log(` ${path26.relative(process.cwd(), base)}.png + .json + .pixelkiln.json`);
14423
+ log(` ${path28.relative(process.cwd(), base)}.png + .json + .pixelkiln.json`);
14075
14424
  for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
14076
14425
  }
14077
14426
  return;
14078
14427
  }
14079
14428
  if (args.command === "mount") {
14080
- const manifestDir2 = path26.dirname(path26.resolve(args.manifest));
14429
+ const manifestDir2 = path28.dirname(path28.resolve(args.manifest));
14081
14430
  const styleIds = args.styles.length ? args.styles : Object.keys(loaded.manifest.styles);
14082
14431
  for (const styleId of styleIds) {
14083
14432
  const style = loaded.manifest.styles[styleId];
@@ -14092,10 +14441,11 @@ async function main() {
14092
14441
  const cells = {};
14093
14442
  const sources = {};
14094
14443
  const outputRoles = {};
14444
+ const styleSources = manifestSources(loaded.manifest, styleId);
14095
14445
  for (const [assetId, asset] of Object.entries(loaded.manifest.assets)) {
14096
14446
  if (!asset.cell) continue;
14097
14447
  cells[assetId] = asset.cell;
14098
- if (asset.source) sources[assetId] = asset.source;
14448
+ if (styleSources[assetId]) sources[assetId] = styleSources[assetId];
14099
14449
  if (asset.outputRole) outputRoles[assetId] = asset.outputRole;
14100
14450
  }
14101
14451
  const packagingSpecs = await resolveSpecs(loaded, { styles: [styleId] });
@@ -14115,7 +14465,7 @@ async function main() {
14115
14465
  sources,
14116
14466
  outputRoles
14117
14467
  );
14118
- const out = path26.resolve(manifestDir2, style.mount.out);
14468
+ const out = path28.resolve(manifestDir2, style.mount.out);
14119
14469
  const metadata = out.replace(/\.png$/, "") + ".json";
14120
14470
  const companion = out.replace(/\.png$/, "") + ".pixelkiln.json";
14121
14471
  const outputs = [
@@ -14135,7 +14485,7 @@ async function main() {
14135
14485
  await provenanceFile("$lock", args.lock),
14136
14486
  ...qualityRecords,
14137
14487
  ...artifactSources.filter(
14138
- (source) => source.id !== "$base" || path26.resolve(source.path) !== out
14488
+ (source) => source.id !== "$base" || path28.resolve(source.path) !== out
14139
14489
  )
14140
14490
  ],
14141
14491
  options: {
@@ -14148,7 +14498,7 @@ async function main() {
14148
14498
  log(
14149
14499
  ` ${styleId} \u2014 ${atlas.frames.length} cell(s) into ${atlas.sheet.width}x${atlas.sheet.height}` + (overBase ? ` over ${style.mount.base}` : " (new sheet)")
14150
14500
  );
14151
- log(` ${path26.relative(process.cwd(), out)} + atlas/provenance JSON`);
14501
+ log(` ${path28.relative(process.cwd(), out)} + atlas/provenance JSON`);
14152
14502
  for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
14153
14503
  }
14154
14504
  return;
@@ -14204,7 +14554,7 @@ async function main() {
14204
14554
  }
14205
14555
  if (args.command === "export") {
14206
14556
  const format = args.format ?? "generic";
14207
- const manifestDir2 = path26.dirname(path26.resolve(args.manifest));
14557
+ const manifestDir2 = path28.dirname(path28.resolve(args.manifest));
14208
14558
  const selected = specs.filter((spec) => {
14209
14559
  if (spec.generator !== "tiles") return false;
14210
14560
  if (args.styles.length && !args.styles.includes(spec.styleId)) return false;
@@ -14220,12 +14570,12 @@ async function main() {
14220
14570
  for (const spec of selected) {
14221
14571
  const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
14222
14572
  const style = loaded.manifest.styles[spec.styleId];
14223
- const defaultBase = path26.resolve(manifestDir2, style.outDir, `${spec.assetId}-tileset`);
14224
- const base = args.out ? path26.resolve(args.out.replace(/\.(?:png|json|tsj|tres)$/i, "")) : defaultBase;
14573
+ const defaultBase = path28.resolve(manifestDir2, style.outDir, `${spec.assetId}-tileset`);
14574
+ const base = args.out ? path28.resolve(args.out.replace(/\.(?:png|json|tsj|tres)$/i, "")) : defaultBase;
14225
14575
  const result = exportTileset(entry, spec, {
14226
14576
  format,
14227
14577
  manifestDir: manifestDir2,
14228
- imageName: path26.basename(`${base}.png`),
14578
+ imageName: path28.basename(`${base}.png`),
14229
14579
  columns: args.columns
14230
14580
  });
14231
14581
  const outputs = [
@@ -14243,7 +14593,7 @@ async function main() {
14243
14593
  asset: spec.assetId,
14244
14594
  columns: args.columns ?? null,
14245
14595
  format,
14246
- image: path26.basename(`${base}.png`),
14596
+ image: path28.basename(`${base}.png`),
14247
14597
  providerRules: result.generic.providerRules,
14248
14598
  style: spec.styleId,
14249
14599
  tileType: spec.tileType ?? null
@@ -14253,7 +14603,7 @@ async function main() {
14253
14603
  ` ${spec.styleId}/${spec.assetId} \u2014 ${result.generic.tiles.length} tile(s), ${result.generic.sheet.width}x${result.generic.sheet.height} (${format})`
14254
14604
  );
14255
14605
  log(
14256
- ` ${path26.relative(process.cwd(), base)}.png + ${path26.basename(base)}${result.extension} + .pixelkiln.json`
14606
+ ` ${path28.relative(process.cwd(), base)}.png + ${path28.basename(base)}${result.extension} + .pixelkiln.json`
14257
14607
  );
14258
14608
  }
14259
14609
  return;
@@ -14303,12 +14653,12 @@ async function main() {
14303
14653
  tagged ${n} object(s) upstream`);
14304
14654
  }
14305
14655
  if (args.writePrompts) {
14306
- const { filled, stillEmpty } = await writePromptsBack(path26.resolve(args.manifest), lock, {
14656
+ const { filled, stillEmpty } = await writePromptsBack(path28.resolve(args.manifest), lock, {
14307
14657
  onProgress: log,
14308
14658
  provider: provider.id,
14309
14659
  assetIds: specs.map((spec) => spec.assetId)
14310
14660
  });
14311
- log(` recovered ${filled} prompt(s) into ${path26.relative(process.cwd(), args.manifest)}`);
14661
+ log(` recovered ${filled} prompt(s) into ${path28.relative(process.cwd(), args.manifest)}`);
14312
14662
  const reloaded = await loadManifest(args.manifest);
14313
14663
  const rebased = (await resolveSpecs(reloaded, {
14314
14664
  styles: args.styles,
@@ -14337,12 +14687,12 @@ async function main() {
14337
14687
  const provider = providerFor(selectedAccountProvider);
14338
14688
  const jsonMode = args.dryRun && args.json;
14339
14689
  const diag = jsonMode ? (msg = "") => console.error(msg) : log;
14340
- const ownLock = path26.resolve(args.lock);
14690
+ const ownLock = path28.resolve(args.lock);
14341
14691
  let workspaceProjects = [];
14342
14692
  let workspaceDir = "";
14343
14693
  if (args.workspace) {
14344
- const workspacePath = path26.resolve(args.workspace);
14345
- workspaceDir = path26.dirname(workspacePath);
14694
+ const workspacePath = path28.resolve(args.workspace);
14695
+ workspaceDir = path28.dirname(workspacePath);
14346
14696
  const complete = await requireCompleteWorkspaceClaims(workspacePath);
14347
14697
  workspaceProjects = complete.ws.projects;
14348
14698
  for (const d of complete.diagnostics) diag(` WARN ${d.id}: ${d.message}`);
@@ -14351,12 +14701,12 @@ async function main() {
14351
14701
  const lockPaths = [
14352
14702
  .../* @__PURE__ */ new Set([
14353
14703
  ...workspaceLockPaths,
14354
- ...existsSync21(ownLock) ? [ownLock] : [],
14355
- ...args.claims.map((c) => path26.resolve(c))
14704
+ ...existsSync22(ownLock) ? [ownLock] : [],
14705
+ ...args.claims.map((c) => path28.resolve(c))
14356
14706
  ])
14357
14707
  ];
14358
14708
  diag(` claim set (${lockPaths.length} lockfile(s)):`);
14359
- for (const p of lockPaths) diag(` ${path26.relative(process.cwd(), p)}`);
14709
+ for (const p of lockPaths) diag(` ${path28.relative(process.cwd(), p)}`);
14360
14710
  if (!args.claims.length && !args.workspace) {
14361
14711
  diag(
14362
14712
  `
@@ -14364,7 +14714,7 @@ async function main() {
14364
14714
  pass every other project's lockfile via --claims a.json,b.json, or
14365
14715
  register every project in a workspace catalog and pass --workspace.`
14366
14716
  );
14367
- } else if (args.workspace && !workspaceProjects.some((p) => resolveProject(workspaceDir, p).manifestPath === path26.resolve(args.manifest))) {
14717
+ } else if (args.workspace && !workspaceProjects.some((p) => resolveProject(workspaceDir, p).manifestPath === path28.resolve(args.manifest))) {
14368
14718
  diag(
14369
14719
  `
14370
14720
  This project's manifest is not registered in the workspace catalog. Its own
@@ -14448,7 +14798,7 @@ async function main() {
14448
14798
  manifestPath: loaded.path,
14449
14799
  manifest: accountManifest,
14450
14800
  styleId,
14451
- importDir: path26.resolve(loaded.root, style.outDir),
14801
+ importDir: path28.resolve(loaded.root, style.outDir),
14452
14802
  lock,
14453
14803
  lockPath: args.lock
14454
14804
  },