pixelkiln 0.22.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(),
@@ -7985,7 +8031,11 @@ var ManifestEditSchema = z5.discriminatedUnion("action", [
7985
8031
  promptPrefix: z5.string().optional(),
7986
8032
  promptSuffix: z5.string().optional(),
7987
8033
  /** `#rrggbb` values; null or empty clears the style's own palette. */
7988
- palette: z5.array(z5.string().regex(/^#?[0-9a-f]{6}$/i, "expected a six-digit hex colour")).max(256).nullable().optional()
8034
+ palette: z5.array(z5.string().regex(/^#?[0-9a-f]{6}$/i, "expected a six-digit hex colour")).max(256).nullable().optional(),
8035
+ /** Provider view name (PixelLab: low top-down, high top-down, side); empty clears. */
8036
+ view: z5.string().max(64).nullable().optional(),
8037
+ /** pixflux only; null clears the style's own value. */
8038
+ noBackground: z5.boolean().nullable().optional()
7989
8039
  }).strict().refine((patch) => Object.keys(patch).length > 0, { message: "nothing to change" })
7990
8040
  }).strict()
7991
8041
  ]);
@@ -8019,8 +8069,38 @@ function setOrDelete(target, key, value) {
8019
8069
  if (value === null || value === void 0) delete target[key];
8020
8070
  else target[key] = value;
8021
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
+ }
8022
8076
  function applyEdit(raw, edit) {
8023
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
+ }
8024
8104
  if (edit.action === "patch-style") {
8025
8105
  const style = raw.styles && Object.hasOwn(raw.styles, edit.styleId) ? raw.styles[edit.styleId] : void 0;
8026
8106
  if (!style) throw new ManifestEditError(`style "${edit.styleId}" is not declared by the manifest`);
@@ -8043,6 +8123,8 @@ function applyEdit(raw, edit) {
8043
8123
  const colors = (patch2.palette ?? []).map((color) => "#" + color.replace(/^#/, "").toLowerCase());
8044
8124
  setOrDelete(style, "palette", colors.length ? colors : null);
8045
8125
  }
8126
+ if (patch2.view !== void 0) setOrDelete(style, "view", patch2.view?.trim() || null);
8127
+ if (patch2.noBackground !== void 0) setOrDelete(style, "noBackground", patch2.noBackground);
8046
8128
  return;
8047
8129
  }
8048
8130
  if (edit.action === "add-asset") {
@@ -8122,10 +8204,134 @@ async function applyManifestEdit(manifestPath, edit) {
8122
8204
  }
8123
8205
  return { manifestPath: absolute, sha256: sha256(next), changed: true };
8124
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
+ ]);
8125
8311
  function createGalleryEditHandler(opts) {
8126
8312
  const log2 = opts.onProgress ?? (() => {
8127
8313
  });
8128
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
+ }
8129
8335
  const parsed = ManifestEditSchema.safeParse(body);
8130
8336
  if (!parsed.success) {
8131
8337
  throw new ManifestEditError(
@@ -8140,9 +8346,9 @@ function createGalleryEditHandler(opts) {
8140
8346
  }
8141
8347
  const result = await applyManifestEdit(manifestPath, parsed.data);
8142
8348
  if (result.changed) {
8143
- const relative = path19.relative(process.cwd(), result.manifestPath);
8349
+ const relative = path21.relative(process.cwd(), result.manifestPath);
8144
8350
  const shown = !relative || relative.startsWith("..") ? result.manifestPath : relative;
8145
- 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;
8146
8352
  log2(` manifest edited: ${parsed.data.action === "add-asset" ? "added" : "changed"} ${subject} in ${shown}`);
8147
8353
  }
8148
8354
  return opts.reload();
@@ -8151,7 +8357,7 @@ function createGalleryEditHandler(opts) {
8151
8357
 
8152
8358
  // src/gallery/snapshot.ts
8153
8359
  function galleryMediaId(absolutePath) {
8154
- 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);
8155
8361
  }
8156
8362
  function galleryMediaRoute(id) {
8157
8363
  return `/media/${id}`;
@@ -8176,23 +8382,24 @@ function metadataFps(entry) {
8176
8382
  }
8177
8383
  return null;
8178
8384
  }
8179
- async function fileBytes(absolutePath) {
8385
+ async function fileInfo(absolutePath) {
8180
8386
  try {
8181
8387
  const info = await stat3(absolutePath);
8182
- return info.isFile() ? info.size : null;
8388
+ return info.isFile() ? { bytes: info.size, modifiedAt: info.mtime.toISOString() } : null;
8183
8389
  } catch {
8184
8390
  return null;
8185
8391
  }
8186
8392
  }
8187
8393
  async function describeOutput(media, root, absolutePath, recorded = {}) {
8188
- const bytes = await fileBytes(absolutePath);
8189
- const exists = bytes !== null;
8394
+ const info = await fileInfo(absolutePath);
8395
+ const exists = info !== null;
8190
8396
  const mediaType = recorded.mediaType ?? mediaTypeFromExtension(absolutePath);
8191
8397
  let url = null;
8192
8398
  if (exists && mediaType) {
8193
8399
  const id = galleryMediaId(absolutePath);
8194
8400
  media.set(id, { path: absolutePath, contentType: mediaType });
8195
- 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)}`;
8196
8403
  }
8197
8404
  return {
8198
8405
  path: portableOutputPath(absolutePath, root),
@@ -8201,17 +8408,18 @@ async function describeOutput(media, root, absolutePath, recorded = {}) {
8201
8408
  role: recorded.role ?? null,
8202
8409
  mediaType,
8203
8410
  exists,
8204
- bytes,
8411
+ bytes: info?.bytes ?? null,
8412
+ modifiedAt: info?.modifiedAt ?? null,
8205
8413
  url
8206
8414
  };
8207
8415
  }
8208
8416
  async function describeQuality(media, root, inspection) {
8209
8417
  const outputPaths = inspection.outputs ?? [inspection.output];
8210
8418
  const outputs = await Promise.all(
8211
- outputPaths.map((file) => describeOutput(media, root, path20.resolve(root, file)))
8419
+ outputPaths.map((file) => describeOutput(media, root, path22.resolve(root, file)))
8212
8420
  );
8213
- const recordPath = path20.resolve(root, inspection.record);
8214
- const recordExists = existsSync16(recordPath);
8421
+ const recordPath = path22.resolve(root, inspection.record);
8422
+ const recordExists = existsSync17(recordPath);
8215
8423
  let options = null;
8216
8424
  let check = null;
8217
8425
  if (recordExists) {
@@ -8230,7 +8438,7 @@ async function describeQuality(media, root, inspection) {
8230
8438
  return {
8231
8439
  state: inspection.state,
8232
8440
  reason: inspection.reason,
8233
- output: portableOutputPath(path20.resolve(root, inspection.output), root),
8441
+ output: portableOutputPath(path22.resolve(root, inspection.output), root),
8234
8442
  record: portableOutputPath(recordPath, root),
8235
8443
  recordExists,
8236
8444
  outputs,
@@ -8280,15 +8488,24 @@ async function buildGallerySnapshot(opts) {
8280
8488
  const entry = lock.entries[key];
8281
8489
  const asset = loaded.manifest.assets[spec.assetId] ?? null;
8282
8490
  let outputs;
8491
+ let edit = null;
8492
+ let editStatus = null;
8283
8493
  if (entry && entry.outputs.length) {
8284
8494
  outputs = await Promise.all(
8285
8495
  entry.outputs.map(
8286
8496
  (output, index) => describeOutput(media, root, currentEntryOutputPath(entry, spec, index), output)
8287
8497
  )
8288
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
+ }
8289
8506
  } else if (spec.source) {
8290
- outputs = [await describeOutput(media, root, path20.resolve(root, spec.source))];
8291
- } else if (!entry && existsSync16(spec.outFile)) {
8507
+ outputs = [await describeOutput(media, root, path22.resolve(root, spec.source))];
8508
+ } else if (!entry && existsSync17(spec.outFile)) {
8292
8509
  outputs = [await describeOutput(media, root, spec.outFile)];
8293
8510
  } else {
8294
8511
  outputs = [];
@@ -8332,6 +8549,8 @@ async function buildGallerySnapshot(opts) {
8332
8549
  providerMetadata: entry?.providerMetadata ?? {},
8333
8550
  asset,
8334
8551
  source: spec.source ?? null,
8552
+ edit,
8553
+ editStatus,
8335
8554
  tags: spec.tags,
8336
8555
  category: asset?.category ?? null
8337
8556
  });
@@ -8384,6 +8603,8 @@ async function buildGallerySnapshot(opts) {
8384
8603
  providerMetadata: entry.providerMetadata ?? {},
8385
8604
  asset: null,
8386
8605
  source: null,
8606
+ edit: null,
8607
+ editStatus: null,
8387
8608
  tags: [],
8388
8609
  category: null
8389
8610
  });
@@ -8422,6 +8643,8 @@ async function buildGallerySnapshot(opts) {
8422
8643
  promptPrefix: style?.promptPrefix ?? "",
8423
8644
  promptSuffix: style?.promptSuffix ?? "",
8424
8645
  palette: style?.palette ?? [],
8646
+ view: style?.view ?? null,
8647
+ noBackground: style?.noBackground ?? true,
8425
8648
  quality: Boolean(style?.quality),
8426
8649
  tags: style?.tags ?? [],
8427
8650
  extends: typeof rawStyle?.extends === "string" ? rawStyle.extends : null,
@@ -8449,7 +8672,7 @@ async function buildGallerySnapshot(opts) {
8449
8672
  id: loaded.manifest.name,
8450
8673
  name: loaded.manifest.name,
8451
8674
  manifest: loaded.path,
8452
- lock: path20.resolve(opts.lockPath),
8675
+ lock: path22.resolve(opts.lockPath),
8453
8676
  root,
8454
8677
  provider: loaded.manifest.provider,
8455
8678
  account: null,
@@ -8474,7 +8697,7 @@ async function buildGallerySnapshot(opts) {
8474
8697
  return { snapshot, media };
8475
8698
  }
8476
8699
  async function buildWorkspaceGallerySnapshot(opts) {
8477
- const dir = path20.dirname(path20.resolve(opts.workspacePath));
8700
+ const dir = path22.dirname(path22.resolve(opts.workspacePath));
8478
8701
  const media = /* @__PURE__ */ new Map();
8479
8702
  const projects = [];
8480
8703
  const items = [];
@@ -8517,7 +8740,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
8517
8740
  id: project.id,
8518
8741
  name: loaded.manifest.name,
8519
8742
  manifest: manifestPath,
8520
- lock: path20.resolve(lockPath),
8743
+ lock: path22.resolve(lockPath),
8521
8744
  root: loaded.root,
8522
8745
  provider: loaded.manifest.provider,
8523
8746
  account: project.account ?? null,
@@ -8532,8 +8755,8 @@ async function buildWorkspaceGallerySnapshot(opts) {
8532
8755
  id: project.id,
8533
8756
  name: project.id,
8534
8757
  manifest: manifestPath,
8535
- lock: path20.resolve(lockPath),
8536
- root: path20.dirname(manifestPath),
8758
+ lock: path22.resolve(lockPath),
8759
+ root: path22.dirname(manifestPath),
8537
8760
  provider: project.provider,
8538
8761
  account: project.account ?? null,
8539
8762
  manifestSha256: null,
@@ -8559,7 +8782,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
8559
8782
  version: 1,
8560
8783
  generatedAt: (opts.now ?? (() => /* @__PURE__ */ new Date()))().toISOString(),
8561
8784
  project: null,
8562
- workspace: { path: path20.resolve(opts.workspacePath), dir, projects },
8785
+ workspace: { path: path22.resolve(opts.workspacePath), dir, projects },
8563
8786
  filter,
8564
8787
  totals: {
8565
8788
  entries: projects.reduce((sum, project) => sum + project.entries, 0),
@@ -8576,7 +8799,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
8576
8799
  }
8577
8800
 
8578
8801
  // src/gallery/server.ts
8579
- import { spawn as spawn2 } from "child_process";
8802
+ import { spawn as spawn3 } from "child_process";
8580
8803
  import { randomBytes } from "crypto";
8581
8804
  import { createServer as createServer2 } from "http";
8582
8805
  import { readFile as readFile16 } from "fs/promises";
@@ -8852,6 +9075,14 @@ function renderGallery(snapshot, opts = {}) {
8852
9075
  .tray > button { padding:5px 11px; font-size:12.5px; }
8853
9076
  @media (max-width: 900px) { .sheet.review-host, .sheet.compare-host { width:100vw; } }
8854
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; }
8855
9086
  .drawer .gen button.primary { padding:7px 14px; }
8856
9087
  @media (max-width: 720px) {
8857
9088
  .bar, .totals, .chips, .style, footer { padding-inline:14px; }
@@ -8902,6 +9133,7 @@ function renderGallery(snapshot, opts = {}) {
8902
9133
  </footer>
8903
9134
  <div id="drawer-host"></div>
8904
9135
  <div id="dialog-host"></div>
9136
+ <datalist id="view-options"><option value="low top-down"><option value="high top-down"><option value="side"><option value="sidescroller"></datalist>
8905
9137
  <script>
8906
9138
  const INITIAL = ${data};
8907
9139
  const SESSION = ${session};
@@ -9297,7 +9529,8 @@ function clearFilters() {
9297
9529
 
9298
9530
  function thumb(item) {
9299
9531
  const cell = el('div', 'cell');
9300
- 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);
9301
9534
  if (!shown.length) {
9302
9535
  cell.classList.add('none');
9303
9536
  cell.append(el('span', null, item.state === 'missing' ? 'not generated yet'
@@ -9348,6 +9581,9 @@ function card(item) {
9348
9581
  body.append(meta);
9349
9582
  const cell = thumb(item);
9350
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
+ }
9351
9587
  c.append(cell, body);
9352
9588
  c.onclick = (e) => { if (e.shiftKey) toggleCompare(item.id); else openItem(item.id); };
9353
9589
  return c;
@@ -9674,6 +9910,78 @@ function openCompare() {
9674
9910
  close.focus({ preventScroll: true });
9675
9911
  }
9676
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
+
9677
9985
  // ---- editing (only when the server minted a session) ---------------------
9678
9986
 
9679
9987
  // Styles a parent edit reaches: every descendant (via extends) that does not
@@ -9693,9 +10001,18 @@ function inheritorsOf(style, field) {
9693
10001
  }
9694
10002
  return out;
9695
10003
  }
10004
+ // noBackground reaches the request only for pixflux and non-PixelLab
10005
+ // providers; elsewhere a change is recorded but alters nothing.
10006
+ const fieldReaches = (field, style) =>
10007
+ field !== 'noBackground' || style.generator === 'pixflux' || style.provider !== 'pixellab';
9696
10008
  function blastRadius(style, fields) {
9697
- const styleIds = new Set([style.id]);
9698
- for (const field of fields) for (const id of inheritorsOf(style, field)) styleIds.add(id);
10009
+ const byId = new Map(snap.styles.filter((x) => x.project === style.project).map((x) => [x.id, x]));
10010
+ const styleIds = new Set();
10011
+ for (const field of fields) {
10012
+ for (const id of [style.id, ...inheritorsOf(style, field)]) {
10013
+ if (fieldReaches(field, byId.get(id) || style)) styleIds.add(id);
10014
+ }
10015
+ }
9699
10016
  const affected = snap.items.filter((i) => i.project === style.project && i.declared && styleIds.has(i.styleId));
9700
10017
  const units = new Map();
9701
10018
  for (const i of affected) if (i.estimatedCost !== null) units.set(i.costUnit, (units.get(i.costUnit) || 0) + i.estimatedCost);
@@ -9720,6 +10037,11 @@ function styleForm(style) {
9720
10037
  const prefix = el('input'); prefix.type = 'text'; prefix.value = style.promptPrefix; prefix.placeholder = 'none';
9721
10038
  const suffix = el('input'); suffix.type = 'text'; suffix.value = style.promptSuffix; suffix.placeholder = 'none';
9722
10039
  const palette = el('input'); palette.type = 'text'; palette.value = style.palette.join(', '); palette.placeholder = '#rrggbb, #rrggbb \u2014 leave empty for no forced palette';
10040
+ const view = el('input'); view.type = 'text'; view.value = style.view || ''; view.placeholder = 'provider default';
10041
+ view.setAttribute('list', 'view-options');
10042
+ const noBg = el('select');
10043
+ noBg.append(new Option(style.extends ? 'inherit from ' + style.extends : 'default (on)', 'default'), new Option('on \u2014 strip the generated background', 'on'), new Option('off \u2014 keep the background (scenes, banners)', 'off'));
10044
+ noBg.value = own('noBackground') ? (style.noBackground ? 'on' : 'off') : 'default';
9723
10045
  const swatches = el('div', 'pal');
9724
10046
  const drawSwatches = () => {
9725
10047
  swatches.textContent = '';
@@ -9732,12 +10054,21 @@ function styleForm(style) {
9732
10054
  field('palette', palette, 'Forced colours where the provider supports them (pixflux). ' + provenance('palette')),
9733
10055
  swatches,
9734
10056
  );
10057
+ const row = el('div', 'row');
10058
+ row.append(
10059
+ field('view', view, 'PixelLab accepts low top-down, high top-down, side; Retro Diffusion reads sidescroller. ' + provenance('view')),
10060
+ field('background', noBg, (fieldReaches('noBackground', style) ? 'Sent for this style. ' : 'Not sent for ' + style.provider + ' ' + style.generator + '; recorded only. ') + provenance('noBackground')),
10061
+ );
10062
+ form.append(row);
9735
10063
  const blast = el('div', 'blast');
9736
10064
  const changed = () => {
9737
10065
  const out = [];
9738
10066
  if (prefix.value !== style.promptPrefix) out.push('promptPrefix');
9739
10067
  if (suffix.value !== style.promptSuffix) out.push('promptSuffix');
9740
10068
  if (parsePalette(palette.value).join(',') !== style.palette.map((c) => c.toLowerCase()).join(',')) out.push('palette');
10069
+ if (view.value.trim() !== (style.view || '')) out.push('view');
10070
+ const bgNow = own('noBackground') ? (style.noBackground ? 'on' : 'off') : 'default';
10071
+ if (noBg.value !== bgNow) out.push('noBackground');
9741
10072
  return out;
9742
10073
  };
9743
10074
  const updateBlast = () => {
@@ -9745,6 +10076,11 @@ function styleForm(style) {
9745
10076
  drawSwatches();
9746
10077
  if (!fields.length) { blast.className = 'blast'; blast.textContent = 'No changes yet. A style change alters the request for every asset that uses it.'; return; }
9747
10078
  const r = blastRadius(style, fields);
10079
+ if (!r.assets) {
10080
+ blast.className = 'blast';
10081
+ blast.textContent = 'Recorded in the manifest only: nothing in ' + style.id + ' sends this field, so no request changes.';
10082
+ return;
10083
+ }
9748
10084
  blast.className = 'blast hot';
9749
10085
  blast.textContent = '';
9750
10086
  const others = r.styles.filter((id) => id !== style.id);
@@ -9754,7 +10090,8 @@ function styleForm(style) {
9754
10090
  : ' in ' + style.id + '.'));
9755
10091
  blast.append(document.createTextNode(' Generated ones become stale; regenerating all of them is about ' + (r.cost || 'nothing') + '. Nothing is spent until you generate.'));
9756
10092
  };
9757
- for (const input of [prefix, suffix, palette]) input.addEventListener('input', updateBlast);
10093
+ for (const input of [prefix, suffix, palette, view]) input.addEventListener('input', updateBlast);
10094
+ noBg.addEventListener('change', updateBlast);
9758
10095
  updateBlast();
9759
10096
  form.append(blast);
9760
10097
  const actions = el('div', 'actions');
@@ -9773,6 +10110,8 @@ function styleForm(style) {
9773
10110
  if (fields.includes('promptPrefix')) patch.promptPrefix = prefix.value;
9774
10111
  if (fields.includes('promptSuffix')) patch.promptSuffix = suffix.value;
9775
10112
  if (fields.includes('palette')) patch.palette = colors;
10113
+ if (fields.includes('view')) patch.view = view.value.trim();
10114
+ if (fields.includes('noBackground')) patch.noBackground = noBg.value === 'default' ? null : noBg.value === 'on';
9776
10115
  save.disabled = true; msg.className = 'msg'; msg.textContent = 'saving\u2026';
9777
10116
  const r = blastRadius(style, fields);
9778
10117
  try {
@@ -9781,7 +10120,9 @@ function styleForm(style) {
9781
10120
  snap = await postEdit(body);
9782
10121
  ui.editing = null;
9783
10122
  ui.notice = { id: 'style:' + (style.project || '') + ':' + style.id,
9784
- text: 'Saved. The request for ' + r.assets + (r.assets === 1 ? ' asset' : ' assets') + ' changed' + (r.cost ? ' \u2014 about ' + r.cost + ' to regenerate.' : '.') + ' Nothing is spent until you generate.' };
10123
+ text: r.assets
10124
+ ? 'Saved. The request for ' + r.assets + (r.assets === 1 ? ' asset' : ' assets') + ' changed' + (r.cost ? ' \u2014 about ' + r.cost + ' to regenerate.' : '.') + ' Nothing is spent until you generate.'
10125
+ : 'Saved. Recorded in the manifest; no request changed.' };
9785
10126
  render();
9786
10127
  } catch (err) {
9787
10128
  save.disabled = false;
@@ -10132,6 +10473,9 @@ function renderDrawer() {
10132
10473
  body.append(s);
10133
10474
  }
10134
10475
 
10476
+ const hand = handEditSection(item);
10477
+ if (hand) body.append(hand);
10478
+
10135
10479
  if (item.revision || item.revisionParentKey) {
10136
10480
  const { s, dl } = section('Lineage');
10137
10481
  row(dl, 'mode', item.revision ? item.revision.mode : (item.asset && item.asset.revision && item.asset.revision.mode));
@@ -10599,7 +10943,7 @@ async function serveGallery(opts) {
10599
10943
  });
10600
10944
  opts.onReady?.(url);
10601
10945
  if (opts.open !== false && process.platform === "darwin") {
10602
- spawn2("open", [url], { stdio: "ignore", detached: true }).unref();
10946
+ spawn3("open", [url], { stdio: "ignore", detached: true }).unref();
10603
10947
  }
10604
10948
  return {
10605
10949
  url,
@@ -10613,14 +10957,14 @@ async function serveGallery(opts) {
10613
10957
 
10614
10958
  // src/gallery/generate.ts
10615
10959
  import { randomBytes as randomBytes2 } from "crypto";
10616
- import { z as z6 } from "zod";
10617
- var GenerateRequestSchema = z6.object({
10618
- project: z6.string().min(1).optional(),
10619
- 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),
10620
10964
  /** Regenerate work that is up to date, as `gen --force` does. */
10621
- force: z6.boolean().optional(),
10965
+ force: z7.boolean().optional(),
10622
10966
  /** Advance in-flight, review, or selected work without submitting anything. */
10623
- resume: z6.boolean().optional()
10967
+ resume: z7.boolean().optional()
10624
10968
  }).strict();
10625
10969
  var GenerateRequestError = class extends Error {
10626
10970
  constructor(message6, status = 400) {
@@ -10858,15 +11202,15 @@ function createGenerateHandlers(opts) {
10858
11202
 
10859
11203
  // src/pipeline/salvage.ts
10860
11204
  import { readFile as readFile17 } from "fs/promises";
10861
- import { existsSync as existsSync17 } from "fs";
10862
- import path21 from "path";
11205
+ import { existsSync as existsSync18 } from "fs";
11206
+ import path23 from "path";
10863
11207
  function providerClaimId(provider, objectId) {
10864
11208
  return `${encodeURIComponent(provider)}:${encodeURIComponent(objectId)}`;
10865
11209
  }
10866
11210
  async function loadClaims(lockPaths, opts = {}) {
10867
11211
  const claimed = /* @__PURE__ */ new Set();
10868
11212
  for (const p of lockPaths) {
10869
- if (!existsSync17(p)) throw new Error(`Claim lockfile not found: ${p}`);
11213
+ if (!existsSync18(p)) throw new Error(`Claim lockfile not found: ${p}`);
10870
11214
  let parsed;
10871
11215
  try {
10872
11216
  parsed = parseLock(JSON.parse(await readFile17(p, "utf8")));
@@ -10917,20 +11261,20 @@ function matchStyleByPattern(prompt, manifest) {
10917
11261
  return null;
10918
11262
  }
10919
11263
  async function loadSiblingManifests(ownManifestPath, workspaceManifestPaths, claimPaths) {
10920
- const own = path21.resolve(ownManifestPath);
11264
+ const own = path23.resolve(ownManifestPath);
10921
11265
  const siblingManifestPaths = [
10922
11266
  .../* @__PURE__ */ new Set([
10923
11267
  ...workspaceManifestPaths,
10924
- ...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"))
10925
11269
  ])
10926
11270
  ];
10927
11271
  const siblings = [];
10928
11272
  for (const siblingManifestPath of siblingManifestPaths) {
10929
- if (path21.resolve(siblingManifestPath) === own) continue;
10930
- if (!existsSync17(siblingManifestPath)) continue;
11273
+ if (path23.resolve(siblingManifestPath) === own) continue;
11274
+ if (!existsSync18(siblingManifestPath)) continue;
10931
11275
  try {
10932
11276
  const { manifest } = await loadManifest(siblingManifestPath);
10933
- siblings.push({ label: path21.basename(path21.dirname(siblingManifestPath)), manifest });
11277
+ siblings.push({ label: path23.basename(path23.dirname(siblingManifestPath)), manifest });
10934
11278
  } catch {
10935
11279
  }
10936
11280
  }
@@ -11149,15 +11493,15 @@ async function workspaceStatus(ws, dir) {
11149
11493
  }
11150
11494
 
11151
11495
  // src/pipeline/cache-health.ts
11152
- import { existsSync as existsSync18 } from "fs";
11496
+ import { existsSync as existsSync19 } from "fs";
11153
11497
  import { readFile as readFile18, readdir as readdir2, rm as rm8 } from "fs/promises";
11154
- import path22 from "path";
11498
+ import path24 from "path";
11155
11499
  async function inspectCaches(lock, lockPath, options = {}) {
11156
- if (options.prune && !existsSync18(lockPath)) {
11157
- 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)}`);
11158
11502
  }
11159
- const contentDir = path22.resolve(path22.dirname(lockPath), ".pixelkiln", "cache");
11160
- const remotePath = path22.resolve(cachePathFor(lockPath));
11503
+ const contentDir = path24.resolve(path24.dirname(lockPath), ".pixelkiln", "cache");
11504
+ const remotePath = path24.resolve(cachePathFor(lockPath));
11161
11505
  const referenced = new Set(
11162
11506
  Object.values(lock.entries).flatMap((entry) => entry.outputs.map((output) => output.sha256))
11163
11507
  );
@@ -11171,7 +11515,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
11171
11515
  ]);
11172
11516
  for (const name of names) {
11173
11517
  try {
11174
- await rm8(path22.join(contentDir, name), { force: true });
11518
+ await rm8(path24.join(contentDir, name), { force: true });
11175
11519
  removed.contentFiles++;
11176
11520
  } catch {
11177
11521
  }
@@ -11198,7 +11542,7 @@ async function inspectCaches(lock, lockPath, options = {}) {
11198
11542
  async function inspectContentCache(contentDir, referenced) {
11199
11543
  const report = {
11200
11544
  path: contentDir,
11201
- exists: existsSync18(contentDir),
11545
+ exists: existsSync19(contentDir),
11202
11546
  files: 0,
11203
11547
  bytes: 0,
11204
11548
  valid: 0,
@@ -11219,7 +11563,7 @@ async function inspectContentCache(contentDir, referenced) {
11219
11563
  continue;
11220
11564
  }
11221
11565
  report.files++;
11222
- const file = path22.join(contentDir, entry.name);
11566
+ const file = path24.join(contentDir, entry.name);
11223
11567
  const mediaType = mediaTypeFromExtension(entry.name);
11224
11568
  const expected = mediaType ? entry.name.slice(0, -4) : "";
11225
11569
  let bytes;
@@ -11262,7 +11606,7 @@ async function inspectContentCache(contentDir, referenced) {
11262
11606
  async function inspectRemoteHashCache(remotePath) {
11263
11607
  const report = {
11264
11608
  path: remotePath,
11265
- exists: existsSync18(remotePath),
11609
+ exists: existsSync19(remotePath),
11266
11610
  entries: 0,
11267
11611
  valid: 0,
11268
11612
  invalidIds: [],
@@ -11542,8 +11886,8 @@ function isRecord2(value) {
11542
11886
  }
11543
11887
 
11544
11888
  // src/pick/salvage-server.ts
11545
- import { mkdir as mkdir6, writeFile as writeFile9, readFile as readFile19 } from "fs/promises";
11546
- import path23 from "path";
11889
+ import { mkdir as mkdir7, writeFile as writeFile9, readFile as readFile19 } from "fs/promises";
11890
+ import path25 from "path";
11547
11891
 
11548
11892
  // src/pick/salvage-sheet.ts
11549
11893
  var escapeHtml3 = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
@@ -11708,7 +12052,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
11708
12052
  });
11709
12053
  const html = renderSalvageSheet(orphans, {
11710
12054
  styleId: ctx.styleId,
11711
- importDir: path23.relative(process.cwd(), ctx.importDir) || "."
12055
+ importDir: path25.relative(process.cwd(), ctx.importDir) || "."
11712
12056
  });
11713
12057
  const byId = new Map(orphans.map((o) => [o.id, o]));
11714
12058
  const existingTags = new Map(orphans.map((o) => [o.id, o.tags]));
@@ -11737,13 +12081,14 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
11737
12081
  if (!buf.subarray(0, 8).equals(PNG_SIGNATURE2)) throw new Error("not a PNG");
11738
12082
  decodePng(buf);
11739
12083
  const assetId = idFromPrompt(orphan.prompt, taken);
11740
- const rel = path23.join("_salvaged", `${assetId}.png`);
11741
- const outFile = path23.resolve(ctx.importDir, rel);
11742
- 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 });
11743
12087
  await writeFile9(outFile, buf);
11744
12088
  ctx.manifest.assets[assetId] = {
11745
12089
  prompt: orphan.prompt,
11746
12090
  promptByStyle: {},
12091
+ sourceByStyle: {},
11747
12092
  category: "_salvaged",
11748
12093
  file: rel,
11749
12094
  tags: ["salvaged"],
@@ -11771,7 +12116,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
11771
12116
  sourceUrl: durableSource,
11772
12117
  sourceUrls: durableSource ? [{ url: durableSource }] : [],
11773
12118
  outputs: [{
11774
- path: portableOutputPath(outFile, path23.dirname(ctx.manifestPath)),
12119
+ path: portableOutputPath(outFile, path25.dirname(ctx.manifestPath)),
11775
12120
  sha256: sha256(buf)
11776
12121
  }],
11777
12122
  submittedAt: orphan.createdAt,
@@ -11805,51 +12150,51 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
11805
12150
  }
11806
12151
 
11807
12152
  // src/recipes.ts
11808
- import path24 from "path";
11809
- import { existsSync as existsSync19 } from "fs";
12153
+ import path26 from "path";
12154
+ import { existsSync as existsSync20 } from "fs";
11810
12155
  import { readdir as readdir3, readFile as readFile20 } from "fs/promises";
11811
12156
  import { fileURLToPath } from "url";
11812
- import { z as z7 } from "zod";
12157
+ import { z as z8 } from "zod";
11813
12158
  var SHA256_RE = /^[0-9a-f]{64}$/;
11814
12159
  var RECIPE_ID_RE = /^[a-z0-9][a-z0-9-]*\/[a-z0-9][a-z0-9-]*$/;
11815
12160
  var VERSION_RE = /^\d+\.\d+\.\d+$/;
11816
12161
  var RECIPE_FILE = "pixelkiln.recipe.json";
11817
12162
  var RECIPE_DIR_TOKEN = "{{recipeDir}}";
11818
- var PortablePathSchema = z7.string().min(1).refine((value) => {
11819
- 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;
11820
12165
  const parts = value.split("/");
11821
12166
  return !parts.includes("") && !parts.includes(".") && !parts.includes("..");
11822
12167
  }, "expected a portable relative path without '.', '..', or backslashes");
11823
- var RecipeBindingSchema = z7.object({
11824
- nodeId: z7.string().min(1),
11825
- input: z7.string().min(1)
12168
+ var RecipeBindingSchema = z8.object({
12169
+ nodeId: z8.string().min(1),
12170
+ input: z8.string().min(1)
11826
12171
  }).strict();
11827
- var RecipeSchema = z7.object({
11828
- $schema: z7.string().url().optional(),
11829
- format: z7.literal("pixelkiln-recipe"),
11830
- schemaVersion: z7.literal(1),
11831
- id: z7.string().regex(RECIPE_ID_RE, "expected provider/name"),
11832
- version: z7.string().regex(VERSION_RE, "expected x.y.z"),
11833
- provider: z7.string().regex(/^[a-z0-9][a-z0-9-]*$/),
11834
- summary: z7.string().min(1).max(240),
11835
- 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({
11836
12181
  path: PortablePathSchema,
11837
- role: z7.enum(["workflow", "reference"]),
11838
- sha256: z7.string().regex(SHA256_RE)
12182
+ role: z8.enum(["workflow", "reference"]),
12183
+ sha256: z8.string().regex(SHA256_RE)
11839
12184
  }).strict()).default([]),
11840
- models: z7.array(z7.object({
12185
+ models: z8.array(z8.object({
11841
12186
  path: PortablePathSchema,
11842
- sha256: z7.string().regex(SHA256_RE),
11843
- source: z7.string().url(),
11844
- license: z7.string().min(1)
12187
+ sha256: z8.string().regex(SHA256_RE),
12188
+ source: z8.string().url(),
12189
+ license: z8.string().min(1)
11845
12190
  }).strict()).default([]),
11846
- styleId: z7.string().regex(/^[a-z0-9][a-z0-9-]*$/),
12191
+ styleId: z8.string().regex(/^[a-z0-9][a-z0-9-]*$/),
11847
12192
  style: StyleSchema,
11848
- workflow: z7.object({
12193
+ workflow: z8.object({
11849
12194
  path: PortablePathSchema,
11850
- outputNodeId: z7.string().min(1),
11851
- numImages: z7.number().int().min(1).max(16),
11852
- bindings: z7.object({
12195
+ outputNodeId: z8.string().min(1),
12196
+ numImages: z8.number().int().min(1).max(16),
12197
+ bindings: z8.object({
11853
12198
  prompt: RecipeBindingSchema,
11854
12199
  width: RecipeBindingSchema.optional(),
11855
12200
  height: RecipeBindingSchema.optional(),
@@ -11860,67 +12205,67 @@ var RecipeSchema = z7.object({
11860
12205
  strength: RecipeBindingSchema.optional()
11861
12206
  }).strict()
11862
12207
  }).strict().optional(),
11863
- quality: z7.object({
11864
- stage: z7.enum(["composition-source", "production-candidate"]),
11865
- workingCanvas: z7.object({
11866
- width: z7.number().int().min(16),
11867
- 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)
11868
12213
  }).strict(),
11869
- recommendedNativeSize: z7.object({
11870
- min: z7.number().int().min(1),
11871
- max: z7.number().int().min(1)
12214
+ recommendedNativeSize: z8.object({
12215
+ min: z8.number().int().min(1),
12216
+ max: z8.number().int().min(1)
11872
12217
  }).strict(),
11873
- paletteColors: z7.object({
11874
- min: z7.number().int().min(1),
11875
- max: z7.number().int().min(1)
12218
+ paletteColors: z8.object({
12219
+ min: z8.number().int().min(1),
12220
+ max: z8.number().int().min(1)
11876
12221
  }).strict(),
11877
- background: z7.enum(["opaque", "transparent", "full-bleed"]),
11878
- checks: z7.array(z7.enum([
12222
+ background: z8.enum(["opaque", "transparent", "full-bleed"]),
12223
+ checks: z8.array(z8.enum([
11879
12224
  "prompt-coverage",
11880
12225
  "native-grid",
11881
12226
  "final-palette",
11882
12227
  "alpha-edge",
11883
12228
  "human-1x"
11884
12229
  ])).min(1),
11885
- notes: z7.array(z7.string().min(1)).default([])
12230
+ notes: z8.array(z8.string().min(1)).default([])
11886
12231
  }).strict(),
11887
- integrity: z7.object({
11888
- algorithm: z7.literal("sha256"),
11889
- digest: z7.string().regex(SHA256_RE)
12232
+ integrity: z8.object({
12233
+ algorithm: z8.literal("sha256"),
12234
+ digest: z8.string().regex(SHA256_RE)
11890
12235
  }).strict()
11891
12236
  }).strict().superRefine((recipe, context) => {
11892
12237
  if (recipe.style.provider && recipe.style.provider !== recipe.provider) {
11893
12238
  context.addIssue({
11894
- code: z7.ZodIssueCode.custom,
12239
+ code: z8.ZodIssueCode.custom,
11895
12240
  path: ["style", "provider"],
11896
12241
  message: "must match the recipe provider"
11897
12242
  });
11898
12243
  }
11899
12244
  const filePaths = recipe.files.map((file) => file.path);
11900
12245
  if (new Set(filePaths).size !== filePaths.length) {
11901
- 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" });
11902
12247
  }
11903
12248
  const modelPaths = recipe.models.map((model) => model.path);
11904
12249
  if (new Set(modelPaths).size !== modelPaths.length) {
11905
- 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" });
11906
12251
  }
11907
12252
  if (filePaths.includes(RECIPE_FILE)) {
11908
12253
  context.addIssue({
11909
- code: z7.ZodIssueCode.custom,
12254
+ code: z8.ZodIssueCode.custom,
11910
12255
  path: ["files"],
11911
12256
  message: `${RECIPE_FILE} cannot hash itself`
11912
12257
  });
11913
12258
  }
11914
12259
  if (recipe.quality.recommendedNativeSize.min > recipe.quality.recommendedNativeSize.max) {
11915
12260
  context.addIssue({
11916
- code: z7.ZodIssueCode.custom,
12261
+ code: z8.ZodIssueCode.custom,
11917
12262
  path: ["quality", "recommendedNativeSize"],
11918
12263
  message: "min cannot exceed max"
11919
12264
  });
11920
12265
  }
11921
12266
  if (recipe.quality.paletteColors.min > recipe.quality.paletteColors.max) {
11922
12267
  context.addIssue({
11923
- code: z7.ZodIssueCode.custom,
12268
+ code: z8.ZodIssueCode.custom,
11924
12269
  path: ["quality", "paletteColors"],
11925
12270
  message: "min cannot exceed max"
11926
12271
  });
@@ -11929,7 +12274,7 @@ var RecipeSchema = z7.object({
11929
12274
  (file) => file.path === recipe.workflow.path && file.role === "workflow"
11930
12275
  )) {
11931
12276
  context.addIssue({
11932
- code: z7.ZodIssueCode.custom,
12277
+ code: z8.ZodIssueCode.custom,
11933
12278
  path: ["workflow", "path"],
11934
12279
  message: "must name a file whose role is workflow"
11935
12280
  });
@@ -11937,7 +12282,7 @@ var RecipeSchema = z7.object({
11937
12282
  if (recipe.provider === "comfyui") {
11938
12283
  if (!recipe.workflow) {
11939
12284
  context.addIssue({
11940
- code: z7.ZodIssueCode.custom,
12285
+ code: z8.ZodIssueCode.custom,
11941
12286
  path: ["workflow"],
11942
12287
  message: "ComfyUI recipes require workflow metadata"
11943
12288
  });
@@ -11946,7 +12291,7 @@ var RecipeSchema = z7.object({
11946
12291
  const options = recipe.style.providerOptions.comfyui;
11947
12292
  if (!options || typeof options !== "object" || Array.isArray(options)) {
11948
12293
  context.addIssue({
11949
- code: z7.ZodIssueCode.custom,
12294
+ code: z8.ZodIssueCode.custom,
11950
12295
  path: ["style", "providerOptions", "comfyui"],
11951
12296
  message: "ComfyUI recipes require ComfyUI provider options"
11952
12297
  });
@@ -11960,7 +12305,7 @@ var RecipeSchema = z7.object({
11960
12305
  };
11961
12306
  if (JSON.stringify(canonical3(options)) !== JSON.stringify(canonical3(expected))) {
11962
12307
  context.addIssue({
11963
- code: z7.ZodIssueCode.custom,
12308
+ code: z8.ZodIssueCode.custom,
11964
12309
  path: ["style", "providerOptions", "comfyui"],
11965
12310
  message: "must match workflow metadata and use {{recipeDir}} for workflowFile"
11966
12311
  });
@@ -11992,7 +12337,7 @@ function message4(error) {
11992
12337
  return error instanceof Error ? error.message : String(error);
11993
12338
  }
11994
12339
  async function readRecipeFile(recipePath, bundled) {
11995
- const absolute = path24.resolve(recipePath);
12340
+ const absolute = path26.resolve(recipePath);
11996
12341
  let raw;
11997
12342
  try {
11998
12343
  raw = JSON.parse(await readFile20(absolute, "utf8"));
@@ -12006,13 +12351,13 @@ async function readRecipeFile(recipePath, bundled) {
12006
12351
  ` + parsed.error.issues.map((issue) => ` ${issue.path.join(".") || "$"}: ${issue.message}`).join("\n")
12007
12352
  );
12008
12353
  }
12009
- return { recipe: parsed.data, path: absolute, dir: path24.dirname(absolute), bundled };
12354
+ return { recipe: parsed.data, path: absolute, dir: path26.dirname(absolute), bundled };
12010
12355
  }
12011
12356
  async function walkRecipeFiles(root) {
12012
- if (!existsSync19(root)) return [];
12357
+ if (!existsSync20(root)) return [];
12013
12358
  const out = [];
12014
12359
  for (const entry of await readdir3(root, { withFileTypes: true })) {
12015
- const resolved = path24.join(root, entry.name);
12360
+ const resolved = path26.join(root, entry.name);
12016
12361
  if (entry.isDirectory()) out.push(...await walkRecipeFiles(resolved));
12017
12362
  else if (entry.isFile() && entry.name === RECIPE_FILE) out.push(resolved);
12018
12363
  }
@@ -12023,7 +12368,7 @@ function bundledRecipeRoot() {
12023
12368
  }
12024
12369
  async function listBundledRecipes(root = bundledRecipeRoot()) {
12025
12370
  const recipes = await Promise.all(
12026
- (await walkRecipeFiles(path24.resolve(root))).map((file) => readRecipeFile(file, true))
12371
+ (await walkRecipeFiles(path26.resolve(root))).map((file) => readRecipeFile(file, true))
12027
12372
  );
12028
12373
  return recipes.sort(
12029
12374
  (a, b) => a.recipe.id.localeCompare(b.recipe.id) || compareVersions(b.recipe.version, a.recipe.version)
@@ -12046,9 +12391,9 @@ function parseSelector(selector) {
12046
12391
  return { id: match[1], version: match[2] };
12047
12392
  }
12048
12393
  async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
12049
- const local = path24.resolve(target);
12050
- if (existsSync19(local)) {
12051
- 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);
12052
12397
  return readRecipeFile(recipePath, false);
12053
12398
  }
12054
12399
  if (target.startsWith(".") || target.startsWith("/") || target.includes("\\")) {
@@ -12062,8 +12407,8 @@ async function resolveRecipe(target, bundledRoot = bundledRecipeRoot()) {
12062
12407
  return matches[0];
12063
12408
  }
12064
12409
  async function verifyFile(root, file) {
12065
- const absolute = path24.resolve(root, ...file.path.split("/"));
12066
- if (!existsSync19(absolute)) {
12410
+ const absolute = path26.resolve(root, ...file.path.split("/"));
12411
+ if (!existsSync20(absolute)) {
12067
12412
  return { path: file.path, expectedSha256: file.sha256, actualSha256: null, status: "missing" };
12068
12413
  }
12069
12414
  const actualSha256 = await sha256File(absolute);
@@ -12078,7 +12423,7 @@ async function verifyRecipe(target, options = {}) {
12078
12423
  const loaded = await resolveRecipe(target, options.bundledRoot);
12079
12424
  const actualIntegrity = recipeDigest(loaded.recipe);
12080
12425
  const files = await Promise.all(loaded.recipe.files.map((file) => verifyFile(loaded.dir, file)));
12081
- const modelRoot = options.modelRoot ? path24.resolve(options.modelRoot) : null;
12426
+ const modelRoot = options.modelRoot ? path26.resolve(options.modelRoot) : null;
12082
12427
  const models = modelRoot ? await Promise.all(loaded.recipe.models.map(async (model) => ({
12083
12428
  ...await verifyFile(modelRoot, model),
12084
12429
  source: model.source,
@@ -12112,7 +12457,7 @@ async function verifyRecipe(target, options = {}) {
12112
12457
  };
12113
12458
  }
12114
12459
  function renderStyle(style, destination, cwd) {
12115
- const portableDir = path24.relative(cwd, destination).split(path24.sep).join("/") || ".";
12460
+ const portableDir = path26.relative(cwd, destination).split(path26.sep).join("/") || ".";
12116
12461
  const replace = (value) => {
12117
12462
  if (typeof value === "string") return value.replaceAll(RECIPE_DIR_TOKEN, portableDir);
12118
12463
  if (Array.isArray(value)) return value.map(replace);
@@ -12124,24 +12469,24 @@ function renderStyle(style, destination, cwd) {
12124
12469
  return replace(style);
12125
12470
  }
12126
12471
  async function installRecipe(target, options = {}) {
12127
- const cwd = path24.resolve(options.cwd ?? process.cwd());
12472
+ const cwd = path26.resolve(options.cwd ?? process.cwd());
12128
12473
  const loaded = await resolveRecipe(target, options.bundledRoot);
12129
12474
  const verification = await verifyRecipe(loaded.path);
12130
12475
  if (!verification.ok) {
12131
12476
  throw new Error(`Recipe ${loaded.recipe.id}@${loaded.recipe.version} failed integrity verification.`);
12132
12477
  }
12133
- 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);
12134
12479
  const sources = [
12135
12480
  { path: RECIPE_FILE, source: loaded.path },
12136
- ...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("/")) }))
12137
12482
  ];
12138
12483
  const files = await Promise.all(sources.map(async (file) => ({
12139
- path: path24.join(destination, ...file.path.split("/")),
12484
+ path: path26.join(destination, ...file.path.split("/")),
12140
12485
  data: await readFile20(file.source)
12141
12486
  })));
12142
12487
  if (!options.force) {
12143
12488
  for (const file of files) {
12144
- if (!existsSync19(file.path)) continue;
12489
+ if (!existsSync20(file.path)) continue;
12145
12490
  const current = await readFile20(file.path);
12146
12491
  if (!current.equals(file.data)) {
12147
12492
  throw new Error(
@@ -12165,15 +12510,15 @@ async function installRecipe(target, options = {}) {
12165
12510
  }
12166
12511
 
12167
12512
  // src/pipeline/quality-regression.ts
12168
- import path25 from "path";
12169
- import { existsSync as existsSync20 } from "fs";
12513
+ import path27 from "path";
12514
+ import { existsSync as existsSync21 } from "fs";
12170
12515
  import { readFile as readFile21 } from "fs/promises";
12171
- import { z as z8 } from "zod";
12516
+ import { z as z9 } from "zod";
12172
12517
  var SHA256_RE2 = /^[0-9a-f]{64}$/;
12173
12518
  var HEX_RE = /^#[0-9a-f]{6}$/;
12174
12519
  var MAX_REDMEAN_DISTANCE = 765;
12175
- var QualityPathSchema = z8.string().min(1).refine(
12176
- (value) => !path25.posix.isAbsolute(value) && !value.includes("\\"),
12520
+ var QualityPathSchema = z9.string().min(1).refine(
12521
+ (value) => !path27.posix.isAbsolute(value) && !value.includes("\\"),
12177
12522
  "expected a portable relative path"
12178
12523
  );
12179
12524
  var DEFAULT_QUALITY_TOLERANCES = {
@@ -12186,53 +12531,53 @@ var DEFAULT_QUALITY_TOLERANCES = {
12186
12531
  maxEdgeContrastDrop: 0.03,
12187
12532
  maxIsolatedPixelIncrease: 5e-3
12188
12533
  };
12189
- var QualityToleranceSchema = z8.object({
12190
- requireExactHash: z8.boolean(),
12191
- maxColorCountIncrease: z8.number().int().min(0),
12192
- maxNewColors: z8.number().int().min(0),
12193
- maxTransparencyDelta: z8.number().min(0).max(1),
12194
- maxPartialAlphaIncrease: z8.number().min(0).max(1),
12195
- maxEdgeDensityDelta: z8.number().min(0).max(1),
12196
- maxEdgeContrastDrop: z8.number().min(0).max(1),
12197
- 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)
12198
12543
  }).strict();
12199
- var ImageQualityMetricsSchema = z8.object({
12200
- sha256: z8.string().regex(SHA256_RE2),
12201
- width: z8.number().int().min(1),
12202
- height: z8.number().int().min(1),
12203
- colorCount: z8.number().int().min(0),
12204
- colors: z8.array(z8.string().regex(HEX_RE)),
12205
- transparency: z8.number().min(0).max(1),
12206
- partialAlpha: z8.number().min(0).max(1),
12207
- edgeDensity: z8.number().min(0).max(1),
12208
- meanEdgeContrast: z8.number().min(0).max(1),
12209
- 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)
12210
12555
  }).strict();
12211
- var QualityBaselineCaseSchema = z8.object({
12212
- 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/_-]*$/),
12213
12558
  file: QualityPathSchema,
12214
- record: z8.object({
12559
+ record: z9.object({
12215
12560
  path: QualityPathSchema,
12216
- sha256: z8.string().regex(SHA256_RE2)
12561
+ sha256: z9.string().regex(SHA256_RE2)
12217
12562
  }).strict().optional(),
12218
12563
  expected: ImageQualityMetricsSchema,
12219
12564
  tolerances: QualityToleranceSchema
12220
12565
  }).strict();
12221
- var QualityBaselineSchema = z8.object({
12222
- $schema: z8.string().url().optional(),
12223
- format: z8.literal("pixelkiln-quality-baseline"),
12224
- schemaVersion: z8.literal(1),
12225
- 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)
12226
12571
  }).strict().superRefine((baseline, context) => {
12227
12572
  const ids = baseline.cases.map((entry) => entry.id);
12228
12573
  if (new Set(ids).size !== ids.length) {
12229
- 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" });
12230
12575
  }
12231
12576
  });
12232
- var QualityInputSchema = z8.object({
12233
- id: z8.string().regex(/^[a-z0-9][a-z0-9/_-]*$/),
12234
- path: z8.string().min(1),
12235
- 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(),
12236
12581
  tolerances: QualityToleranceSchema.partial().optional()
12237
12582
  }).strict();
12238
12583
  function message5(error) {
@@ -12320,7 +12665,7 @@ function measureDecodedImageQuality(png, digest2) {
12320
12665
  };
12321
12666
  }
12322
12667
  async function measureImageQuality(file) {
12323
- const absolute = path25.resolve(file);
12668
+ const absolute = path27.resolve(file);
12324
12669
  let bytes;
12325
12670
  try {
12326
12671
  bytes = await readFile21(absolute);
@@ -12334,7 +12679,7 @@ async function measureImageQuality(file) {
12334
12679
  }
12335
12680
  }
12336
12681
  function resolveQualityInputs(raw, inputsFilePath) {
12337
- const parsed = z8.array(QualityInputSchema).min(1).safeParse(raw);
12682
+ const parsed = z9.array(QualityInputSchema).min(1).safeParse(raw);
12338
12683
  if (!parsed.success) {
12339
12684
  throw new Error(
12340
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")
@@ -12342,11 +12687,11 @@ function resolveQualityInputs(raw, inputsFilePath) {
12342
12687
  }
12343
12688
  const ids = parsed.data.map((entry) => entry.id);
12344
12689
  if (new Set(ids).size !== ids.length) throw new Error("--inputs contains a duplicate quality case id.");
12345
- const root = path25.dirname(path25.resolve(inputsFilePath));
12690
+ const root = path27.dirname(path27.resolve(inputsFilePath));
12346
12691
  return parsed.data.map((entry) => ({
12347
12692
  id: entry.id,
12348
- file: path25.resolve(root, entry.path),
12349
- ...entry.record ? { record: path25.resolve(root, entry.record) } : {},
12693
+ file: path27.resolve(root, entry.path),
12694
+ ...entry.record ? { record: path27.resolve(root, entry.record) } : {},
12350
12695
  tolerances: QualityToleranceSchema.parse({
12351
12696
  ...DEFAULT_QUALITY_TOLERANCES,
12352
12697
  ...entry.tolerances
@@ -12354,7 +12699,7 @@ function resolveQualityInputs(raw, inputsFilePath) {
12354
12699
  }));
12355
12700
  }
12356
12701
  function portableRelative2(from, to) {
12357
- return path25.relative(from, path25.resolve(to)).split(path25.sep).join("/") || ".";
12702
+ return path27.relative(from, path27.resolve(to)).split(path27.sep).join("/") || ".";
12358
12703
  }
12359
12704
  async function verifyRecordForImage(recordPath, imagePath) {
12360
12705
  const verification = await verifyArtifactBundle(recordPath);
@@ -12369,20 +12714,20 @@ async function verifyRecordForImage(recordPath, imagePath) {
12369
12714
  if (record.kind !== "refine" || options === null || typeof options !== "object" || options.schema !== "pixelkiln-quality") {
12370
12715
  throw new Error(`Quality record ${recordPath} is not a PixelKiln refinement record.`);
12371
12716
  }
12372
- const outputs = record.outputs.map((output) => path25.resolve(path25.dirname(recordPath), output.path));
12373
- 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))) {
12374
12719
  throw new Error(`Quality record ${recordPath} does not own ${imagePath}.`);
12375
12720
  }
12376
12721
  }
12377
12722
  async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
12378
12723
  if (!inputs.length) throw new Error("Cannot snapshot an empty quality baseline.");
12379
- const absolute = path25.resolve(baselinePath);
12724
+ const absolute = path27.resolve(baselinePath);
12380
12725
  for (const input of inputs) {
12381
- 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) {
12382
12727
  throw new Error(`Quality baseline output would overwrite an input: ${absolute}.`);
12383
12728
  }
12384
12729
  }
12385
- const root = path25.dirname(absolute);
12730
+ const root = path27.dirname(absolute);
12386
12731
  const cases = [];
12387
12732
  for (const input of [...inputs].sort((a, b) => a.id.localeCompare(b.id))) {
12388
12733
  const expected = await measureImageQuality(input.file);
@@ -12409,7 +12754,7 @@ async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
12409
12754
  cases
12410
12755
  });
12411
12756
  const data = Buffer.from(JSON.stringify(baseline, null, 2) + "\n");
12412
- if (existsSync20(absolute) && !options.force) {
12757
+ if (existsSync21(absolute) && !options.force) {
12413
12758
  const current = await readFile21(absolute);
12414
12759
  if (!current.equals(data)) {
12415
12760
  throw new Error(`Quality baseline already exists with different content: ${absolute}. Pass --force to replace it.`);
@@ -12419,7 +12764,7 @@ async function snapshotQualityBaseline(inputs, baselinePath, options = {}) {
12419
12764
  return { baseline, path: absolute, changed: result.changed.length > 0 };
12420
12765
  }
12421
12766
  async function readQualityBaseline(baselinePath) {
12422
- const absolute = path25.resolve(baselinePath);
12767
+ const absolute = path27.resolve(baselinePath);
12423
12768
  let raw;
12424
12769
  try {
12425
12770
  raw = JSON.parse(await readFile21(absolute, "utf8"));
@@ -12492,12 +12837,12 @@ function regressionViolations(expected, actual, tolerances) {
12492
12837
  return { violations, warnings };
12493
12838
  }
12494
12839
  async function checkQualityBaseline(baselinePath) {
12495
- const absolute = path25.resolve(baselinePath);
12840
+ const absolute = path27.resolve(baselinePath);
12496
12841
  const baseline = await readQualityBaseline(absolute);
12497
- const root = path25.dirname(absolute);
12842
+ const root = path27.dirname(absolute);
12498
12843
  const cases = [];
12499
12844
  for (const entry of baseline.cases) {
12500
- const file = path25.resolve(root, entry.file);
12845
+ const file = path27.resolve(root, entry.file);
12501
12846
  let actual = null;
12502
12847
  const violations = [];
12503
12848
  const warnings = [];
@@ -12510,7 +12855,7 @@ async function checkQualityBaseline(baselinePath) {
12510
12855
  violations.push(message5(error));
12511
12856
  }
12512
12857
  if (entry.record) {
12513
- const recordPath = path25.resolve(root, entry.record.path);
12858
+ const recordPath = path27.resolve(root, entry.record.path);
12514
12859
  try {
12515
12860
  const recordHash = await sha256File(recordPath);
12516
12861
  if (recordHash !== entry.record.sha256) violations.push("quality record hash changed");
@@ -12596,13 +12941,13 @@ async function runGallery(initial, reload, args, access2) {
12596
12941
  };
12597
12942
  const loadProject = async (project) => {
12598
12943
  const ctx = await access2.loadProject(project);
12599
- const dir = path26.dirname(ctx.loaded.path);
12944
+ const dir = path28.dirname(ctx.loaded.path);
12600
12945
  const declared = readEnvFiles(dir);
12601
12946
  const credentialNames = new Set(availableProviders().flatMap((id) => providerCredentialEnvs(providerFactory(id))));
12602
12947
  for (const [name, value] of Object.entries(declared)) {
12603
12948
  if (credentialNames.has(name) && name in process.env && process.env[name] !== value) {
12604
12949
  throw new Error(
12605
- `${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.`
12606
12951
  );
12607
12952
  }
12608
12953
  }
@@ -12628,7 +12973,7 @@ async function runGallery(initial, reload, args, access2) {
12628
12973
  args.edit,
12629
12974
  budget ? describeBudget(budget) : null
12630
12975
  ),
12631
- ...args.edit ? { edit: createGalleryEditHandler({ manifestFor: access2.manifestFor, reload, onProgress: log }) } : {},
12976
+ ...args.edit ? { edit: createGalleryEditHandler({ manifestFor: access2.manifestFor, loadProject: access2.loadProject, reload, onProgress: log }) } : {},
12632
12977
  ...budget ? { generate: createGenerateHandlers({ loadProject, providerFor, budget, reload, onProgress: log }) } : {}
12633
12978
  });
12634
12979
  await new Promise((resolve) => {
@@ -12644,11 +12989,11 @@ async function runGallery(initial, reload, args, access2) {
12644
12989
  log("\n gallery closed");
12645
12990
  }
12646
12991
  async function provenanceFile(id, file) {
12647
- const absolute = path26.resolve(file);
12992
+ const absolute = path28.resolve(file);
12648
12993
  return {
12649
12994
  id,
12650
12995
  path: absolute,
12651
- sha256: existsSync21(absolute) ? await sha256File(absolute) : null,
12996
+ sha256: existsSync22(absolute) ? await sha256File(absolute) : null,
12652
12997
  included: true
12653
12998
  };
12654
12999
  }
@@ -12723,6 +13068,7 @@ var COMMANDS = [
12723
13068
  "balance",
12724
13069
  "status",
12725
13070
  "gallery",
13071
+ "edit",
12726
13072
  "quality",
12727
13073
  "refine",
12728
13074
  "recipe",
@@ -12737,6 +13083,7 @@ var WORKSPACE_SUBCOMMANDS = ["add", "remove", "list", "status", "claims"];
12737
13083
  var REFINE_SUBCOMMANDS = ["run", "approve", "check"];
12738
13084
  var RECIPE_SUBCOMMANDS = ["list", "inspect", "install", "verify"];
12739
13085
  var QUALITY_SUBCOMMANDS = ["snapshot", "check"];
13086
+ var EDIT_SUBCOMMANDS = ["start", "detach"];
12740
13087
  function parseArgs(argv) {
12741
13088
  const [command = "help"] = argv;
12742
13089
  if (!COMMANDS.includes(command)) {
@@ -12794,6 +13141,12 @@ function parseArgs(argv) {
12794
13141
  }
12795
13142
  rest = rest.slice(1);
12796
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);
12797
13150
  } else if (command === "refine") {
12798
13151
  subcommand = rest[0]?.startsWith("-") || rest[0] === void 0 ? "run" : rest[0];
12799
13152
  if (!REFINE_SUBCOMMANDS.includes(subcommand)) {
@@ -12907,7 +13260,7 @@ function parseArgs(argv) {
12907
13260
  return {
12908
13261
  command,
12909
13262
  manifest,
12910
- 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"),
12911
13264
  explicitLock: get("--lock"),
12912
13265
  styles: list("--style"),
12913
13266
  assets: list("--only"),
@@ -12988,6 +13341,9 @@ Commands
12988
13341
  tag Push manifest tags to the objects upstream (free).
12989
13342
  balance Show the provider's remaining balance.
12990
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).
12991
13347
  gallery Open a local read-only gallery of every generation and its
12992
13348
  provenance: prompt, provider, cost, outputs, lineage, quality.
12993
13349
  --workspace <catalog> shows every registered project at once;
@@ -13030,7 +13386,7 @@ Options
13030
13386
  and the gallery snapshot (no server)
13031
13387
  --check plan/audit/cache: exit nonzero when the selected state is unsafe
13032
13388
  --yes, -y Skip the confirmation prompt
13033
- --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)
13034
13390
  --edit gallery: allow manifest edits from the page (never spends)
13035
13391
  --tag Also push tags upstream after fetch
13036
13392
  --claims a.json,b Other projects' lockfiles (salvage; required if account is shared)
@@ -13047,6 +13403,8 @@ Examples
13047
13403
  pixelkiln gallery --style heybud-premium
13048
13404
  pixelkiln gallery --workspace pixelkiln.workspace.json
13049
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
13050
13408
  pixelkiln adopt --tag
13051
13409
  pixelkiln pack --style heybud-premium
13052
13410
  pixelkiln pack --inputs sprites.json --out dist/sheet # no manifest needed
@@ -13123,6 +13481,15 @@ function printResumeActions(specs, lock) {
13123
13481
  }
13124
13482
  return actions.length;
13125
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
+ }
13126
13493
  function manifestProviderIds(manifest) {
13127
13494
  return [...new Set(
13128
13495
  Object.values(manifest.styles).map((style) => style.provider ?? manifest.provider)
@@ -13196,10 +13563,10 @@ async function confirm(question, auto) {
13196
13563
  return answer === "y" || answer === "yes";
13197
13564
  }
13198
13565
  async function requireCompleteWorkspaceClaims(workspacePath) {
13199
- if (!existsSync21(workspacePath)) {
13566
+ if (!existsSync22(workspacePath)) {
13200
13567
  throw new Error(`Workspace catalog not found: ${workspacePath}`);
13201
13568
  }
13202
- const dir = path26.dirname(path26.resolve(workspacePath));
13569
+ const dir = path28.dirname(path28.resolve(workspacePath));
13203
13570
  const ws = await loadWorkspace(workspacePath);
13204
13571
  const diagnostics = validateWorkspace(ws, dir);
13205
13572
  const errors = diagnostics.filter((d) => d.level === "error");
@@ -13231,15 +13598,15 @@ async function main() {
13231
13598
  if (!args.out) throw new Error("quality snapshot needs --out <pixelkiln.quality.json>.");
13232
13599
  let raw;
13233
13600
  try {
13234
- raw = JSON.parse(await readFile22(path26.resolve(args.inputs), "utf8"));
13601
+ raw = JSON.parse(await readFile22(path28.resolve(args.inputs), "utf8"));
13235
13602
  } catch (error) {
13236
13603
  throw new Error(
13237
- `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)}`,
13238
13605
  { cause: error }
13239
13606
  );
13240
13607
  }
13241
13608
  const inputs = resolveQualityInputs(raw, args.inputs);
13242
- if (path26.resolve(args.inputs) === path26.resolve(args.out)) {
13609
+ if (path28.resolve(args.inputs) === path28.resolve(args.out)) {
13243
13610
  throw new Error("quality snapshot --out must not overwrite its --inputs file.");
13244
13611
  }
13245
13612
  const result = await snapshotQualityBaseline(inputs, args.out, { force: args.force });
@@ -13252,7 +13619,7 @@ async function main() {
13252
13619
  }, null, 2));
13253
13620
  } else {
13254
13621
  log(
13255
- ` ${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))`
13256
13623
  );
13257
13624
  log(` Review the tolerances, commit the baseline, then run quality check in CI.`);
13258
13625
  }
@@ -13347,14 +13714,14 @@ async function main() {
13347
13714
  }, null, 2));
13348
13715
  } else {
13349
13716
  log(` installed ${result.recipe.id}@${result.recipe.version}`);
13350
- log(` ${path26.relative(process.cwd(), result.destination) || "."}`);
13717
+ log(` ${path28.relative(process.cwd(), result.destination) || "."}`);
13351
13718
  log(`
13352
13719
  Add this entry under your manifest's styles object:`);
13353
13720
  log(JSON.stringify({ [result.styleId]: result.style }, null, 2));
13354
13721
  if (result.recipe.models.length) {
13355
13722
  log(`
13356
13723
  Models are not downloaded automatically. Verify them with:`);
13357
- 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>`);
13358
13725
  }
13359
13726
  }
13360
13727
  return;
@@ -13388,12 +13755,12 @@ async function main() {
13388
13755
  ` recovered ${result2.detection.columns}x${result2.detection.rows} native grid (${result2.detection.confidence}: ${result2.detection.consensus})`
13389
13756
  );
13390
13757
  log(` enforced ${result2.palette.length}-color palette without dithering`);
13391
- log(` wrote ${path26.relative(process.cwd(), result2.output)}`);
13392
- 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)}`);
13393
13760
  log(`
13394
13761
  Automated checks passed. Human 1\xD7 review is still required:`);
13395
13762
  log(
13396
- ` 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"`
13397
13764
  );
13398
13765
  }
13399
13766
  return;
@@ -13421,7 +13788,7 @@ async function main() {
13421
13788
  if (args.json) log(JSON.stringify(result2, null, 2));
13422
13789
  else {
13423
13790
  log(
13424
- 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()}`
13425
13792
  );
13426
13793
  }
13427
13794
  return;
@@ -13430,14 +13797,14 @@ async function main() {
13430
13797
  if (args.json) {
13431
13798
  log(JSON.stringify(result, null, 2));
13432
13799
  } else {
13433
- 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)}`);
13434
13801
  for (const reason of result.reasons) log(` ${reason}`);
13435
13802
  }
13436
13803
  if (!result.safe) process.exitCode = 1;
13437
13804
  return;
13438
13805
  }
13439
13806
  if (args.command === "balance") {
13440
- loadEnvFiles(path26.dirname(path26.resolve(args.manifest)));
13807
+ loadEnvFiles(path28.dirname(path28.resolve(args.manifest)));
13441
13808
  loadEnvFiles(process.cwd());
13442
13809
  const loaded2 = await loadManifest(args.manifest);
13443
13810
  const selectedProvider = accountProviderId(loaded2.manifest, args.provider, "balance");
@@ -13450,31 +13817,31 @@ async function main() {
13450
13817
  }
13451
13818
  if (args.command === "init") {
13452
13819
  if (!args.from) throw new Error("init needs --from <dir> pointing at your existing PNGs.");
13453
- const root = path26.resolve(args.from);
13454
- 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}`);
13455
13822
  const generator = args.generator ?? "map";
13456
13823
  if (generator !== "1dir" && generator !== "map") {
13457
13824
  throw new Error(`--generator must be "1dir" or "map", got "${args.generator}".`);
13458
13825
  }
13459
- const target = path26.resolve(args.out ?? "pixelkiln.manifest.json");
13826
+ const target = path28.resolve(args.out ?? "pixelkiln.manifest.json");
13460
13827
  const { assets, skipped } = await scanAssets(root, { exclude: args.exclude });
13461
13828
  if (!assets.length) throw new Error(`No PNGs found under ${root}`);
13462
13829
  const manifest = buildManifest(
13463
- args.name ?? path26.basename(path26.dirname(target)),
13830
+ args.name ?? path28.basename(path28.dirname(target)),
13464
13831
  args.styles[0] ?? "base",
13465
13832
  generator,
13466
- path26.relative(path26.dirname(target), root) || ".",
13833
+ path28.relative(path28.dirname(target), root) || ".",
13467
13834
  assets
13468
13835
  );
13469
13836
  await writeManifestFile(target, manifest);
13470
- 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)}`);
13471
13838
  if (skipped.length) log(` skipped ${skipped.length} unreadable file(s)`);
13472
- log(` wrote ${path26.relative(process.cwd(), target)}`);
13839
+ log(` wrote ${path28.relative(process.cwd(), target)}`);
13473
13840
  log(`
13474
13841
  Prompts are intentionally empty. To recover the real ones from your`);
13475
13842
  log(` PixelLab account instead of inventing them:`);
13476
13843
  log(`
13477
- pixelkiln adopt --manifest ${path26.relative(process.cwd(), target)} --write-prompts
13844
+ pixelkiln adopt --manifest ${path28.relative(process.cwd(), target)} --write-prompts
13478
13845
  `);
13479
13846
  return;
13480
13847
  }
@@ -13522,10 +13889,10 @@ async function main() {
13522
13889
  if (args.primaryOnly || args.outputRoles.length) {
13523
13890
  throw new Error("--primary-only and --output-role require manifest-driven pack");
13524
13891
  }
13525
- const raw = JSON.parse(await readFile22(path26.resolve(args.inputs), "utf8"));
13892
+ const raw = JSON.parse(await readFile22(path28.resolve(args.inputs), "utf8"));
13526
13893
  const inputs = resolvePackInputs(raw, args.inputs);
13527
13894
  const { png, atlas, skipped, sources } = packSprites(inputs, { columns: args.columns });
13528
- const base = path26.resolve(args.out.replace(/\.png$/, ""));
13895
+ const base = path28.resolve(args.out.replace(/\.png$/, ""));
13529
13896
  const outputs = [
13530
13897
  { path: `${base}.png`, data: png },
13531
13898
  { path: `${base}.json`, data: JSON.stringify(atlas, null, 2) + "\n" }
@@ -13538,17 +13905,17 @@ async function main() {
13538
13905
  log(
13539
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`
13540
13907
  );
13541
- log(` ${path26.relative(process.cwd(), base)}.png + .json + .pixelkiln.json`);
13908
+ log(` ${path28.relative(process.cwd(), base)}.png + .json + .pixelkiln.json`);
13542
13909
  for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
13543
13910
  return;
13544
13911
  }
13545
13912
  if (args.command === "workspace") {
13546
- const workspacePath = path26.resolve(args.workspace ?? "pixelkiln.workspace.json");
13547
- const dir = path26.dirname(workspacePath);
13913
+ const workspacePath = path28.resolve(args.workspace ?? "pixelkiln.workspace.json");
13914
+ const dir = path28.dirname(workspacePath);
13548
13915
  if (args.subcommand === "add") {
13549
- const manifestPath = path26.resolve(args.target);
13916
+ const manifestPath = path28.resolve(args.target);
13550
13917
  const loadedTarget = await loadManifest(manifestPath);
13551
- 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");
13552
13919
  const ws = await loadWorkspace(workspacePath);
13553
13920
  const id = args.name ?? loadedTarget.manifest.name;
13554
13921
  if (ws.projects.some((p) => p.id === id)) {
@@ -13568,10 +13935,10 @@ async function main() {
13568
13935
  ...args.account ? { account: args.account } : {}
13569
13936
  };
13570
13937
  await saveWorkspace(workspacePath, { version: 1, projects: [...ws.projects, project] });
13571
- log(` registered "${id}" in ${path26.relative(process.cwd(), workspacePath)}`);
13938
+ log(` registered "${id}" in ${path28.relative(process.cwd(), workspacePath)}`);
13572
13939
  log(` manifest: ${project.manifest}`);
13573
13940
  log(` lock: ${project.lock}`);
13574
- if (!existsSync21(lockPath)) {
13941
+ if (!existsSync22(lockPath)) {
13575
13942
  log(
13576
13943
  ` warning: no lockfile there yet \u2014 this project contributes no claims until one is generated`
13577
13944
  );
@@ -13580,7 +13947,7 @@ async function main() {
13580
13947
  }
13581
13948
  if (args.subcommand === "remove") {
13582
13949
  const ws = await loadWorkspace(workspacePath);
13583
- const resolvedTarget = path26.resolve(args.target);
13950
+ const resolvedTarget = path28.resolve(args.target);
13584
13951
  const match = ws.projects.find(
13585
13952
  (p) => p.id === args.target || resolveProject(dir, p).manifestPath === resolvedTarget
13586
13953
  );
@@ -13591,11 +13958,11 @@ async function main() {
13591
13958
  version: 1,
13592
13959
  projects: ws.projects.filter((p) => p !== match)
13593
13960
  });
13594
- log(` removed "${match.id}" from ${path26.relative(process.cwd(), workspacePath)}`);
13961
+ log(` removed "${match.id}" from ${path28.relative(process.cwd(), workspacePath)}`);
13595
13962
  return;
13596
13963
  }
13597
13964
  if (args.subcommand === "list") {
13598
- if (!existsSync21(workspacePath)) {
13965
+ if (!existsSync22(workspacePath)) {
13599
13966
  throw new Error(`Workspace catalog not found: ${workspacePath}`);
13600
13967
  }
13601
13968
  const ws = await loadWorkspace(workspacePath);
@@ -13603,9 +13970,9 @@ async function main() {
13603
13970
  if (args.json) {
13604
13971
  log(JSON.stringify({ version: 1, workspace: workspacePath, projects: ws.projects, diagnostics }, null, 2));
13605
13972
  } else if (!ws.projects.length) {
13606
- log(` no projects registered in ${path26.relative(process.cwd(), workspacePath)}`);
13973
+ log(` no projects registered in ${path28.relative(process.cwd(), workspacePath)}`);
13607
13974
  } else {
13608
- 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)}:`);
13609
13976
  for (const p of ws.projects) {
13610
13977
  log(` ${p.id.padEnd(24)} ${p.manifest.padEnd(40)} (${p.provider}${p.account ? `, ${p.account}` : ""})`);
13611
13978
  }
@@ -13615,7 +13982,7 @@ async function main() {
13615
13982
  return;
13616
13983
  }
13617
13984
  if (args.subcommand === "status") {
13618
- if (!existsSync21(workspacePath)) {
13985
+ if (!existsSync22(workspacePath)) {
13619
13986
  throw new Error(`Workspace catalog not found: ${workspacePath}`);
13620
13987
  }
13621
13988
  const ws = await loadWorkspace(workspacePath);
@@ -13623,7 +13990,7 @@ async function main() {
13623
13990
  if (args.json) {
13624
13991
  log(JSON.stringify({ ...report, workspace: workspacePath }, null, 2));
13625
13992
  } else {
13626
- log(` workspace: ${path26.relative(process.cwd(), workspacePath)}`);
13993
+ log(` workspace: ${path28.relative(process.cwd(), workspacePath)}`);
13627
13994
  for (const p of report.projects) {
13628
13995
  if (p.error) {
13629
13996
  log(`
@@ -13670,8 +14037,8 @@ async function main() {
13670
14037
  }
13671
14038
  }
13672
14039
  if (args.command === "gallery" && args.workspace) {
13673
- const workspacePath = path26.resolve(args.workspace);
13674
- if (!existsSync21(workspacePath)) {
14040
+ const workspacePath = path28.resolve(args.workspace);
14041
+ if (!existsSync22(workspacePath)) {
13675
14042
  throw new Error(`Workspace catalog not found: ${workspacePath}`);
13676
14043
  }
13677
14044
  const filter = { styles: args.styles, assets: args.assets };
@@ -13691,7 +14058,7 @@ async function main() {
13691
14058
  const registered = async (projectId) => {
13692
14059
  const project = (await loadWorkspace(workspacePath)).projects.find((candidate) => candidate.id === projectId);
13693
14060
  if (!projectId || !project) throw new Error(`unknown workspace project "${projectId ?? ""}"`);
13694
- return resolveProject(path26.dirname(workspacePath), project);
14061
+ return resolveProject(path28.dirname(workspacePath), project);
13695
14062
  };
13696
14063
  await runGallery(initial, build, args, {
13697
14064
  manifestFor: async (projectId) => (await registered(projectId)).manifestPath,
@@ -13706,14 +14073,14 @@ async function main() {
13706
14073
  });
13707
14074
  return;
13708
14075
  }
13709
- if (!existsSync21(path26.resolve(args.manifest))) {
14076
+ if (!existsSync22(path28.resolve(args.manifest))) {
13710
14077
  throw new Error(
13711
- `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>\`.`
13712
14079
  );
13713
14080
  }
13714
- const manifestDir = path26.dirname(path26.resolve(args.manifest));
14081
+ const manifestDir = path28.dirname(path28.resolve(args.manifest));
13715
14082
  const envFiles2 = [...loadEnvFiles(manifestDir)];
13716
- if (path26.resolve(process.cwd()) !== manifestDir) envFiles2.push(...loadEnvFiles(process.cwd()));
14083
+ if (path28.resolve(process.cwd()) !== manifestDir) envFiles2.push(...loadEnvFiles(process.cwd()));
13717
14084
  const loaded = await loadManifest(args.manifest);
13718
14085
  let specs = await resolveSpecs(loaded, {
13719
14086
  styles: args.styles,
@@ -13786,7 +14153,7 @@ async function main() {
13786
14153
  });
13787
14154
  };
13788
14155
  await runGallery(await build(), reload, args, {
13789
- manifestFor: () => path26.resolve(args.manifest),
14156
+ manifestFor: () => path28.resolve(args.manifest),
13790
14157
  loadProject: async () => {
13791
14158
  const freshLoaded = await loadManifest(args.manifest);
13792
14159
  const freshSpecs = await resolveSpecs(freshLoaded);
@@ -13797,6 +14164,29 @@ async function main() {
13797
14164
  });
13798
14165
  return;
13799
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
+ }
13800
14190
  const plan = await buildPlan(specs, lock, { force: args.force });
13801
14191
  if (args.command === "refine") {
13802
14192
  const pathOwnedFlags = [
@@ -13838,7 +14228,7 @@ async function main() {
13838
14228
  log(` ${item.state.padEnd(16)} ${item.key} ${item.reason}`);
13839
14229
  if (item.state === "needs-approval") {
13840
14230
  log(
13841
- ` 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"`
13842
14232
  );
13843
14233
  }
13844
14234
  }
@@ -13929,7 +14319,7 @@ async function main() {
13929
14319
  let intact = true;
13930
14320
  for (const output of entry.outputs) {
13931
14321
  const file = resolveOutputPath(output.path, item.spec.root);
13932
- if (!existsSync21(file) || await sha256File(file) !== output.sha256) {
14322
+ if (!existsSync22(file) || await sha256File(file) !== output.sha256) {
13933
14323
  intact = false;
13934
14324
  break;
13935
14325
  }
@@ -13987,7 +14377,7 @@ async function main() {
13987
14377
  if (args.primaryOnly && args.outputRoles.length) {
13988
14378
  throw new Error("pack accepts either --primary-only or --output-role, not both");
13989
14379
  }
13990
- const manifestDir2 = path26.dirname(path26.resolve(args.manifest));
14380
+ const manifestDir2 = path28.dirname(path28.resolve(args.manifest));
13991
14381
  const styleIds = args.styles.length ? args.styles : Object.keys(loaded.manifest.styles);
13992
14382
  for (const styleId of styleIds) {
13993
14383
  const packagingSpecs = await resolveSpecs(loaded, { styles: [styleId] });
@@ -13996,10 +14386,11 @@ async function main() {
13996
14386
  columns: args.columns,
13997
14387
  outputRoles: args.outputRoles,
13998
14388
  primaryOnly: args.primaryOnly,
13999
- sourceOverrides: qualitySources
14389
+ sourceOverrides: qualitySources,
14390
+ sources: manifestSources(loaded.manifest, styleId)
14000
14391
  });
14001
14392
  const style = loaded.manifest.styles[styleId];
14002
- 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`);
14003
14394
  const outputs = [
14004
14395
  { path: `${base}.png`, data: png },
14005
14396
  { path: `${base}.json`, data: JSON.stringify(atlas, null, 2) + "\n" }
@@ -14029,13 +14420,13 @@ async function main() {
14029
14420
  log(
14030
14421
  ` ${styleId} \u2014 ${atlas.frames.length} sprite(s), ${atlas.sheet.width}x${atlas.sheet.height} in ${atlas.columns} column(s)`
14031
14422
  );
14032
- log(` ${path26.relative(process.cwd(), base)}.png + .json + .pixelkiln.json`);
14423
+ log(` ${path28.relative(process.cwd(), base)}.png + .json + .pixelkiln.json`);
14033
14424
  for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
14034
14425
  }
14035
14426
  return;
14036
14427
  }
14037
14428
  if (args.command === "mount") {
14038
- const manifestDir2 = path26.dirname(path26.resolve(args.manifest));
14429
+ const manifestDir2 = path28.dirname(path28.resolve(args.manifest));
14039
14430
  const styleIds = args.styles.length ? args.styles : Object.keys(loaded.manifest.styles);
14040
14431
  for (const styleId of styleIds) {
14041
14432
  const style = loaded.manifest.styles[styleId];
@@ -14050,10 +14441,11 @@ async function main() {
14050
14441
  const cells = {};
14051
14442
  const sources = {};
14052
14443
  const outputRoles = {};
14444
+ const styleSources = manifestSources(loaded.manifest, styleId);
14053
14445
  for (const [assetId, asset] of Object.entries(loaded.manifest.assets)) {
14054
14446
  if (!asset.cell) continue;
14055
14447
  cells[assetId] = asset.cell;
14056
- if (asset.source) sources[assetId] = asset.source;
14448
+ if (styleSources[assetId]) sources[assetId] = styleSources[assetId];
14057
14449
  if (asset.outputRole) outputRoles[assetId] = asset.outputRole;
14058
14450
  }
14059
14451
  const packagingSpecs = await resolveSpecs(loaded, { styles: [styleId] });
@@ -14073,7 +14465,7 @@ async function main() {
14073
14465
  sources,
14074
14466
  outputRoles
14075
14467
  );
14076
- const out = path26.resolve(manifestDir2, style.mount.out);
14468
+ const out = path28.resolve(manifestDir2, style.mount.out);
14077
14469
  const metadata = out.replace(/\.png$/, "") + ".json";
14078
14470
  const companion = out.replace(/\.png$/, "") + ".pixelkiln.json";
14079
14471
  const outputs = [
@@ -14093,7 +14485,7 @@ async function main() {
14093
14485
  await provenanceFile("$lock", args.lock),
14094
14486
  ...qualityRecords,
14095
14487
  ...artifactSources.filter(
14096
- (source) => source.id !== "$base" || path26.resolve(source.path) !== out
14488
+ (source) => source.id !== "$base" || path28.resolve(source.path) !== out
14097
14489
  )
14098
14490
  ],
14099
14491
  options: {
@@ -14106,7 +14498,7 @@ async function main() {
14106
14498
  log(
14107
14499
  ` ${styleId} \u2014 ${atlas.frames.length} cell(s) into ${atlas.sheet.width}x${atlas.sheet.height}` + (overBase ? ` over ${style.mount.base}` : " (new sheet)")
14108
14500
  );
14109
- log(` ${path26.relative(process.cwd(), out)} + atlas/provenance JSON`);
14501
+ log(` ${path28.relative(process.cwd(), out)} + atlas/provenance JSON`);
14110
14502
  for (const s of skipped) log(` skipped ${s.id}: ${s.reason}`);
14111
14503
  }
14112
14504
  return;
@@ -14162,7 +14554,7 @@ async function main() {
14162
14554
  }
14163
14555
  if (args.command === "export") {
14164
14556
  const format = args.format ?? "generic";
14165
- const manifestDir2 = path26.dirname(path26.resolve(args.manifest));
14557
+ const manifestDir2 = path28.dirname(path28.resolve(args.manifest));
14166
14558
  const selected = specs.filter((spec) => {
14167
14559
  if (spec.generator !== "tiles") return false;
14168
14560
  if (args.styles.length && !args.styles.includes(spec.styleId)) return false;
@@ -14178,12 +14570,12 @@ async function main() {
14178
14570
  for (const spec of selected) {
14179
14571
  const entry = lock.entries[lockKey(spec.styleId, spec.assetId)];
14180
14572
  const style = loaded.manifest.styles[spec.styleId];
14181
- const defaultBase = path26.resolve(manifestDir2, style.outDir, `${spec.assetId}-tileset`);
14182
- 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;
14183
14575
  const result = exportTileset(entry, spec, {
14184
14576
  format,
14185
14577
  manifestDir: manifestDir2,
14186
- imageName: path26.basename(`${base}.png`),
14578
+ imageName: path28.basename(`${base}.png`),
14187
14579
  columns: args.columns
14188
14580
  });
14189
14581
  const outputs = [
@@ -14201,7 +14593,7 @@ async function main() {
14201
14593
  asset: spec.assetId,
14202
14594
  columns: args.columns ?? null,
14203
14595
  format,
14204
- image: path26.basename(`${base}.png`),
14596
+ image: path28.basename(`${base}.png`),
14205
14597
  providerRules: result.generic.providerRules,
14206
14598
  style: spec.styleId,
14207
14599
  tileType: spec.tileType ?? null
@@ -14211,7 +14603,7 @@ async function main() {
14211
14603
  ` ${spec.styleId}/${spec.assetId} \u2014 ${result.generic.tiles.length} tile(s), ${result.generic.sheet.width}x${result.generic.sheet.height} (${format})`
14212
14604
  );
14213
14605
  log(
14214
- ` ${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`
14215
14607
  );
14216
14608
  }
14217
14609
  return;
@@ -14261,12 +14653,12 @@ async function main() {
14261
14653
  tagged ${n} object(s) upstream`);
14262
14654
  }
14263
14655
  if (args.writePrompts) {
14264
- const { filled, stillEmpty } = await writePromptsBack(path26.resolve(args.manifest), lock, {
14656
+ const { filled, stillEmpty } = await writePromptsBack(path28.resolve(args.manifest), lock, {
14265
14657
  onProgress: log,
14266
14658
  provider: provider.id,
14267
14659
  assetIds: specs.map((spec) => spec.assetId)
14268
14660
  });
14269
- 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)}`);
14270
14662
  const reloaded = await loadManifest(args.manifest);
14271
14663
  const rebased = (await resolveSpecs(reloaded, {
14272
14664
  styles: args.styles,
@@ -14295,12 +14687,12 @@ async function main() {
14295
14687
  const provider = providerFor(selectedAccountProvider);
14296
14688
  const jsonMode = args.dryRun && args.json;
14297
14689
  const diag = jsonMode ? (msg = "") => console.error(msg) : log;
14298
- const ownLock = path26.resolve(args.lock);
14690
+ const ownLock = path28.resolve(args.lock);
14299
14691
  let workspaceProjects = [];
14300
14692
  let workspaceDir = "";
14301
14693
  if (args.workspace) {
14302
- const workspacePath = path26.resolve(args.workspace);
14303
- workspaceDir = path26.dirname(workspacePath);
14694
+ const workspacePath = path28.resolve(args.workspace);
14695
+ workspaceDir = path28.dirname(workspacePath);
14304
14696
  const complete = await requireCompleteWorkspaceClaims(workspacePath);
14305
14697
  workspaceProjects = complete.ws.projects;
14306
14698
  for (const d of complete.diagnostics) diag(` WARN ${d.id}: ${d.message}`);
@@ -14309,12 +14701,12 @@ async function main() {
14309
14701
  const lockPaths = [
14310
14702
  .../* @__PURE__ */ new Set([
14311
14703
  ...workspaceLockPaths,
14312
- ...existsSync21(ownLock) ? [ownLock] : [],
14313
- ...args.claims.map((c) => path26.resolve(c))
14704
+ ...existsSync22(ownLock) ? [ownLock] : [],
14705
+ ...args.claims.map((c) => path28.resolve(c))
14314
14706
  ])
14315
14707
  ];
14316
14708
  diag(` claim set (${lockPaths.length} lockfile(s)):`);
14317
- for (const p of lockPaths) diag(` ${path26.relative(process.cwd(), p)}`);
14709
+ for (const p of lockPaths) diag(` ${path28.relative(process.cwd(), p)}`);
14318
14710
  if (!args.claims.length && !args.workspace) {
14319
14711
  diag(
14320
14712
  `
@@ -14322,7 +14714,7 @@ async function main() {
14322
14714
  pass every other project's lockfile via --claims a.json,b.json, or
14323
14715
  register every project in a workspace catalog and pass --workspace.`
14324
14716
  );
14325
- } 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))) {
14326
14718
  diag(
14327
14719
  `
14328
14720
  This project's manifest is not registered in the workspace catalog. Its own
@@ -14406,7 +14798,7 @@ async function main() {
14406
14798
  manifestPath: loaded.path,
14407
14799
  manifest: accountManifest,
14408
14800
  styleId,
14409
- importDir: path26.resolve(loaded.root, style.outDir),
14801
+ importDir: path28.resolve(loaded.root, style.outDir),
14410
14802
  lock,
14411
14803
  lockPath: args.lock
14412
14804
  },