makeables 0.8.0 → 0.10.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,66 +1,81 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ addKitMember,
4
+ addToSheet,
5
+ checkSheet,
6
+ exportKit,
7
+ exportSheetPdf,
8
+ importKit,
9
+ kitCover,
10
+ newSheet,
11
+ packStickerSheet,
12
+ readKit,
13
+ readSheet,
14
+ sheetPreviewPng,
15
+ sheetReport
16
+ } from "./chunk-TRIRNJSG.js";
17
+ import {
18
+ getSkill,
19
+ skills,
20
+ version
21
+ } from "./chunk-SZTWKHNA.js";
2
22
  import {
3
23
  CloudError,
4
24
  InputError,
25
+ PACKAGE_LIMIT,
5
26
  addBadgeSticker,
6
27
  badgeDesignObjectSchema,
7
28
  badgeDesignSchema,
8
29
  cardStyles,
9
30
  checkSubmittedSvg,
10
31
  connectAccount,
11
- createStickerSheet,
32
+ createStickerDesign,
12
33
  defaultStickerMaterial,
13
34
  designCatalog,
14
35
  designProfile,
15
36
  draftFromPackage,
16
37
  findCardStyle,
17
38
  findDesign,
18
- getSkill,
19
39
  hashBytes,
20
40
  jsonBytes,
41
+ kitDocumentSchema,
21
42
  listCardDesigns,
22
43
  logout,
23
44
  newBadge,
24
45
  newCard,
25
46
  openBrowser,
26
- packSheet,
27
47
  packageJson,
28
- placeNear,
29
- prepareSheetPrint,
30
48
  readBadge,
31
49
  readPackageJson,
50
+ readPackageZip,
51
+ readSubmission,
32
52
  removeBadgeLayer,
33
53
  renderBadge,
54
+ renderDraftFace,
34
55
  setBadgeLayer,
35
- sheetPrintPdf,
36
- sheetSizes,
37
- sheetSpec,
38
- sheetTypeIds,
39
- sheetTypes,
40
56
  side,
41
- skills,
42
57
  startStudio,
43
58
  stickerCut,
44
59
  stickerCutSvg,
45
60
  stickerMaterialSchema,
46
- stickerSheetSchema,
47
61
  stickerStyles,
48
62
  studioRequest,
49
63
  submitDesign,
50
64
  summarizeBadgeLayers,
51
65
  svgNodeSchema,
52
66
  svgNodeXml,
53
- version,
54
67
  writeAtomic
55
- } from "./chunk-NOES67KG.js";
68
+ } from "./chunk-G6OTYITI.js";
56
69
 
57
70
  // src/cli.ts
58
71
  import { spawnSync as spawnSync2 } from "child_process";
59
- import { constants as constants2 } from "fs";
60
- import { access, lstat as lstat2, open as open2, readFile as readFile7 } from "fs/promises";
61
- import { dirname as dirname3, extname as extname4, resolve as resolve6 } from "path";
72
+ import { randomUUID as randomUUID3 } from "crypto";
73
+ import { constants as constants4 } from "fs";
74
+ import { access, lstat as lstat4, open as open4, readFile as readFile7 } from "fs/promises";
75
+ import { dirname as dirname5, extname as extname4, resolve as resolve7 } from "path";
62
76
  import { parseArgs } from "util";
63
77
  import sharp8 from "sharp";
78
+ import { ZodError } from "zod";
64
79
  import { zodToJsonSchema } from "zod-to-json-schema";
65
80
 
66
81
  // src/card-import.ts
@@ -222,12 +237,13 @@ async function readToolResult(path) {
222
237
  if (!info.isFile() || info.size > 12e6)
223
238
  throw new InputError("Expected a result file below 12 MB.");
224
239
  let value = JSON.parse(await readFile2(path, "utf8"));
225
- if ("success" in value) {
240
+ if (typeof value === "string") value = JSON.parse(value);
241
+ if (value && typeof value === "object" && "success" in value) {
226
242
  if (!value.success) throw new InputError("The browser command failed.");
227
243
  value = value.data.output;
228
244
  }
229
245
  if (typeof value === "string") value = JSON.parse(value);
230
- if (!value?.ok)
246
+ if (!value?.ok || !value.data || typeof value.data !== "object")
231
247
  throw new InputError(value?.error?.message || "The tool returned an invalid result.");
232
248
  return value.data;
233
249
  }
@@ -270,6 +286,199 @@ async function extractImage(file, out) {
270
286
  return { path, bytes: bytes.length };
271
287
  }
272
288
 
289
+ // src/kit-extract.ts
290
+ import { mkdir, mkdtemp, rename, rm, writeFile as writeFile2 } from "fs/promises";
291
+ import { dirname, join, resolve as resolve2 } from "path";
292
+ import sharp2 from "sharp";
293
+ import { z } from "zod";
294
+ var regionSchema = z.object({
295
+ x: z.number().int().nonnegative(),
296
+ y: z.number().int().nonnegative(),
297
+ w: z.number().int().positive(),
298
+ h: z.number().int().positive(),
299
+ name: z.string().trim().min(1).max(60).optional()
300
+ }).strict();
301
+ var optionsSchema = z.object({
302
+ name: z.string().trim().min(1).max(60).default("Artwork kit"),
303
+ gap: z.number().int().min(0).max(40).default(12),
304
+ tolerance: z.number().int().min(0).max(150).default(40),
305
+ minArea: z.number().int().min(1).max(1e5).default(64),
306
+ regions: z.array(regionSchema).min(1).max(24).optional()
307
+ }).strict();
308
+ function dilate(mask, w, h, radius) {
309
+ const horizontal = new Uint8Array(mask.length), out = new Uint8Array(mask.length);
310
+ for (let y = 0; y < h; y++) {
311
+ let count = 0;
312
+ for (let x = -radius; x < w; x++) {
313
+ if (x + radius < w) count += mask[y * w + x + radius];
314
+ if (x - radius - 1 >= 0) count -= mask[y * w + x - radius - 1];
315
+ if (x >= 0) horizontal[y * w + x] = count > 0 ? 1 : 0;
316
+ }
317
+ }
318
+ for (let x = 0; x < w; x++) {
319
+ let count = 0;
320
+ for (let y = -radius; y < h; y++) {
321
+ if (y + radius < h) count += horizontal[(y + radius) * w + x];
322
+ if (y - radius - 1 >= 0) count -= horizontal[(y - radius - 1) * w + x];
323
+ if (y >= 0) out[y * w + x] = count > 0 ? 1 : 0;
324
+ }
325
+ }
326
+ return out;
327
+ }
328
+ function artworkRegions(foreground, w, h, gap, minArea) {
329
+ const mask = dilate(foreground, w, h, gap), queue = new Int32Array(mask.length);
330
+ const regions = [];
331
+ for (let start = 0; start < mask.length; start++) {
332
+ if (!mask[start]) continue;
333
+ let head = 0, tail = 1, x0 = w, y0 = h, x1 = -1, y1 = -1, area = 0;
334
+ queue[0] = start;
335
+ mask[start] = 0;
336
+ while (head < tail) {
337
+ const p = queue[head++], x = p % w, y = Math.floor(p / w);
338
+ if (foreground[p]) {
339
+ x0 = Math.min(x0, x);
340
+ x1 = Math.max(x1, x);
341
+ y0 = Math.min(y0, y);
342
+ y1 = Math.max(y1, y);
343
+ area++;
344
+ }
345
+ for (const n of [
346
+ x > 0 ? p - 1 : -1,
347
+ x < w - 1 ? p + 1 : -1,
348
+ y > 0 ? p - w : -1,
349
+ y < h - 1 ? p + w : -1
350
+ ])
351
+ if (n >= 0 && mask[n]) {
352
+ mask[n] = 0;
353
+ queue[tail++] = n;
354
+ }
355
+ }
356
+ if (area >= minArea)
357
+ regions.push({
358
+ x: Math.max(0, x0 - 2),
359
+ y: Math.max(0, y0 - 2),
360
+ w: Math.min(w - 1, x1 + 2) - Math.max(0, x0 - 2) + 1,
361
+ h: Math.min(h - 1, y1 + 2) - Math.max(0, y0 - 2) + 1
362
+ });
363
+ }
364
+ return regions.sort((a, b) => a.y - b.y || a.x - b.x);
365
+ }
366
+ function removeOuterBackground(rgba, w, h, background, tolerance) {
367
+ const result = new Uint8Array(rgba), seen = new Uint8Array(w * h), queue = new Int32Array(w * h);
368
+ let head = 0, tail = 0;
369
+ const add = (p) => {
370
+ if (seen[p]) return;
371
+ seen[p] = 1;
372
+ if (rgba[p * 4 + 3] < 16 || [0, 1, 2].reduce((n, c) => n + Math.abs(rgba[p * 4 + c] - background[c]), 0) <= tolerance)
373
+ queue[tail++] = p;
374
+ };
375
+ for (let x = 0; x < w; x++) {
376
+ add(x);
377
+ add((h - 1) * w + x);
378
+ }
379
+ for (let y = 0; y < h; y++) {
380
+ add(y * w);
381
+ add(y * w + w - 1);
382
+ }
383
+ while (head < tail) {
384
+ const p = queue[head++], x = p % w, y = Math.floor(p / w);
385
+ result[p * 4 + 3] = 0;
386
+ if (x) add(p - 1);
387
+ if (x < w - 1) add(p + 1);
388
+ if (y) add(p - w);
389
+ if (y < h - 1) add(p + w);
390
+ }
391
+ return result;
392
+ }
393
+ async function extractKit(file, out, input) {
394
+ const options = optionsSchema.parse(input);
395
+ const { data, info } = await sharp2(resolve2(file), { limitInputPixels: 24e6 }).rotate().ensureAlpha().toColourspace("srgb").raw().toBuffer({ resolveWithObject: true });
396
+ const w = info.width, h = info.height;
397
+ const corners = [0, w - 1, (h - 1) * w, w * h - 1];
398
+ const transparentBackground = corners.every((p) => data[p * 4 + 3] < 16);
399
+ const background = [0, 1, 2].map(
400
+ (c) => Math.round(corners.reduce((n, p) => n + data[p * 4 + c], 0) / 4)
401
+ );
402
+ const foreground = new Uint8Array(w * h);
403
+ for (let p = 0; p < foreground.length; p++)
404
+ foreground[p] = data[p * 4 + 3] >= 16 && (transparentBackground || [0, 1, 2].reduce((n, c) => n + Math.abs(data[p * 4 + c] - background[c]), 0) > options.tolerance) ? 1 : 0;
405
+ const regions = options.regions ?? artworkRegions(foreground, w, h, options.gap, options.minArea);
406
+ if (!regions.length || regions.length > 24)
407
+ throw new InputError("Choose 1\u201324 regions using --params, or adjust gap/minArea.");
408
+ if (regions.some((r) => r.x + r.w > w || r.y + r.h > h))
409
+ throw new InputError("Every region must fit inside the image.");
410
+ const target = resolve2(out), temporary = await mkdtemp(join(dirname(target), ".makeables-extract-"));
411
+ try {
412
+ await mkdir(join(temporary, "pieces"));
413
+ const kit = {
414
+ format: "makeables-kit",
415
+ version: 1,
416
+ name: options.name,
417
+ items: [],
418
+ sheets: []
419
+ };
420
+ const views = [];
421
+ for (const [index, region] of regions.entries()) {
422
+ const id = `piece-${String(index + 1).padStart(2, "0")}`, name = region.name ?? `Piece ${index + 1}`;
423
+ const crop = await sharp2(data, { raw: { width: w, height: h, channels: 4 } }).extract({ left: region.x, top: region.y, width: region.w, height: region.h }).raw().toBuffer();
424
+ const pixels = transparentBackground ? crop : removeOuterBackground(crop, region.w, region.h, background, options.tolerance);
425
+ const png = await sharp2(pixels, { raw: { width: region.w, height: region.h, channels: 4 } }).png().toBuffer();
426
+ const design = createStickerDesign();
427
+ design.sticker = { ...defaultStickerMaterial, finish: "vinyl", sparkle: 0, holography: 0 };
428
+ design.name = name;
429
+ design.source = id;
430
+ design.assets = {
431
+ artwork: {
432
+ source: "image",
433
+ name,
434
+ data: `data:image/png;base64,${png.toString("base64")}`,
435
+ sourcePixels: { width: region.w, height: region.h }
436
+ }
437
+ };
438
+ design.front = {
439
+ background: "#00000000",
440
+ layers: [
441
+ {
442
+ id: "artwork",
443
+ kind: "image",
444
+ asset: "artwork",
445
+ name,
446
+ x: 64,
447
+ y: 64,
448
+ w: 896,
449
+ h: 896,
450
+ fit: "contain",
451
+ opacity: 1,
452
+ radius: 0
453
+ }
454
+ ]
455
+ };
456
+ design.back = { background: "#00000000", layers: [] };
457
+ await writeFile2(
458
+ join(temporary, `pieces/${id}.json`),
459
+ JSON.stringify(badgeDesignSchema.parse(design), null, 2)
460
+ );
461
+ kit.items.push({ id, file: `pieces/${id}.json` });
462
+ views.push({ name, bytes: png });
463
+ }
464
+ await writeFile2(join(temporary, "kit.json"), JSON.stringify(kit, null, 2));
465
+ await writeFile2(join(temporary, "contact.png"), await kitCover(options.name, views));
466
+ await mkdir(target);
467
+ await rename(temporary, target);
468
+ return {
469
+ path: join(target, "kit.json"),
470
+ preview: join(target, "contact.png"),
471
+ items: kit.items.length,
472
+ regions,
473
+ source: { width: w, height: h },
474
+ notice: "Review the contact proof. Raster lettering remains part of each image; preserve new text as native layers."
475
+ };
476
+ } catch (error) {
477
+ await rm(temporary, { recursive: true, force: true });
478
+ throw error;
479
+ }
480
+ }
481
+
273
482
  // src/presentation.ts
274
483
  import { stripVTControlCharacters } from "util";
275
484
  function machineOutput(json) {
@@ -294,11 +503,11 @@ function banner(version2) {
294
503
  // src/publish.ts
295
504
  import { createHash as createHash2, randomUUID } from "crypto";
296
505
  import { constants } from "fs";
297
- import { appendFile, link, mkdir, open, readFile as readFile3, rename, rm } from "fs/promises";
506
+ import { appendFile, link, mkdir as mkdir2, open, readFile as readFile3, rename as rename2, rm as rm2 } from "fs/promises";
298
507
  import { homedir } from "os";
299
- import { dirname, join, resolve as resolve2 } from "path";
300
- import sharp2 from "sharp";
301
- import { z } from "zod";
508
+ import { dirname as dirname2, join as join2, resolve as resolve3 } from "path";
509
+ import sharp3 from "sharp";
510
+ import { z as z2 } from "zod";
302
511
  async function saveBundle(url, out) {
303
512
  const state = await studioRequest(url, "/call", {
304
513
  name: "badge_inspect",
@@ -319,7 +528,7 @@ async function saveBundle(url, out) {
319
528
  result.error?.message ?? "The editor could not export a bundle. Refresh it after saving the current draft."
320
529
  );
321
530
  const pkg = await readPackageJson(result.data);
322
- const path = resolve2(out);
531
+ const path = resolve3(out);
323
532
  const file = await open(path, "wx", 384);
324
533
  try {
325
534
  await file.writeFile(`${JSON.stringify(packageJson(pkg))}
@@ -328,56 +537,60 @@ async function saveBundle(url, out) {
328
537
  } finally {
329
538
  await file.close();
330
539
  }
540
+ const saved = await readFile3(path);
331
541
  return {
332
542
  path,
543
+ bytes: saved.length,
544
+ sha256: await hashBytes(saved),
545
+ downloadVerified: true,
333
546
  snapshotHash: await hashBytes(jsonBytes(pkg.manifest)),
334
547
  name: pkg.manifest.title,
335
548
  complete: true,
336
549
  published: false
337
550
  };
338
551
  }
339
- var receiptSchema = z.object({
340
- id: z.string().uuid(),
341
- version: z.number().int().positive(),
342
- state: z.enum(["published", "withdrawn"]),
343
- url: z.string()
552
+ var receiptSchema = z2.object({
553
+ id: z2.string().uuid(),
554
+ version: z2.number().int().positive(),
555
+ state: z2.enum(["published", "withdrawn"]),
556
+ url: z2.string()
344
557
  });
345
558
 
346
559
  // src/reference-image.ts
347
560
  import { spawnSync } from "child_process";
348
561
  import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "fs";
349
562
  import { tmpdir } from "os";
350
- import { join as join2, resolve as resolve3 } from "path";
563
+ import { join as join3, resolve as resolve4 } from "path";
351
564
  import { DOMParser as DOMParser2, XMLSerializer } from "@xmldom/xmldom";
352
- import sharp3 from "sharp";
353
- import { z as z2 } from "zod";
565
+ import sharp4 from "sharp";
566
+ import { z as z3 } from "zod";
354
567
  var W = 1200;
355
568
  var H = 756;
356
569
  var N = W * H;
357
- var optionSchema = z2.object({
358
- crop: z2.string().optional(),
359
- patch: z2.array(z2.string()).default([]),
360
- "edge-band": z2.coerce.number().min(0).max(1e3).optional(),
361
- "bg-tol": z2.coerce.number().min(0).max(765).optional(),
362
- title: z2.string().min(1).max(200).optional(),
363
- desc: z2.string().max(5e3).optional(),
364
- credit: z2.string().max(5e3).optional(),
365
- tones: z2.coerce.number().int().min(2).max(12).optional(),
366
- ink: z2.coerce.number().min(0).max(1).optional(),
367
- raster: z2.boolean().optional(),
368
- points: z2.array(z2.string()).max(100).optional(),
369
- box: z2.string().optional(),
370
- rows: z2.string().optional(),
371
- cols: z2.string().optional(),
372
- dark: z2.coerce.number().min(0).max(765).optional(),
373
- colors: z2.coerce.number().int().min(1).max(12).optional(),
374
- at: z2.string().optional(),
375
- up: z2.coerce.number().int().min(1).max(8).optional(),
376
- k: z2.coerce.number().min(0).max(1).optional(),
377
- fill: z2.string().regex(/^#[a-fA-F0-9]{6}$/).optional(),
378
- top: z2.enum(["left", "right"]).optional(),
379
- upright: z2.enum(["left", "right"]).optional(),
380
- "ref-box": z2.string().optional()
570
+ var optionSchema = z3.object({
571
+ crop: z3.string().optional(),
572
+ patch: z3.array(z3.string()).default([]),
573
+ "edge-band": z3.coerce.number().min(0).max(1e3).optional(),
574
+ "bg-tol": z3.coerce.number().min(0).max(765).optional(),
575
+ title: z3.string().min(1).max(200).optional(),
576
+ desc: z3.string().max(5e3).optional(),
577
+ credit: z3.string().max(5e3).optional(),
578
+ tones: z3.coerce.number().int().min(2).max(12).optional(),
579
+ ink: z3.coerce.number().min(0).max(1).optional(),
580
+ raster: z3.boolean().optional(),
581
+ points: z3.array(z3.string()).max(100).optional(),
582
+ box: z3.string().optional(),
583
+ rows: z3.string().optional(),
584
+ cols: z3.string().optional(),
585
+ dark: z3.coerce.number().min(0).max(765).optional(),
586
+ colors: z3.coerce.number().int().min(1).max(12).optional(),
587
+ at: z3.string().optional(),
588
+ up: z3.coerce.number().int().min(1).max(8).optional(),
589
+ k: z3.coerce.number().min(0).max(1).optional(),
590
+ fill: z3.string().regex(/^#[a-fA-F0-9]{6}$/).optional(),
591
+ top: z3.enum(["left", "right"]).optional(),
592
+ upright: z3.enum(["left", "right"]).optional(),
593
+ "ref-box": z3.string().optional()
381
594
  }).strict();
382
595
  function fail(message) {
383
596
  throw new InputError(message);
@@ -396,7 +609,7 @@ function validBox(values, width, height) {
396
609
  }
397
610
  var lumOf = (r, g, b) => 0.299 * r + 0.587 * g + 0.114 * b;
398
611
  async function detect(file) {
399
- const { data, info } = await sharp3(file, { limitInputPixels: 24e6 }).toColourspace("srgb").removeAlpha().raw().toBuffer({ resolveWithObject: true });
612
+ const { data, info } = await sharp4(file, { limitInputPixels: 24e6 }).toColourspace("srgb").removeAlpha().raw().toBuffer({ resolveWithObject: true });
400
613
  const { width: w, height: h } = info;
401
614
  const px = (x, y) => {
402
615
  const i = (y * w + x) * 3;
@@ -472,7 +685,7 @@ async function detect(file) {
472
685
  async function prepare(file, o) {
473
686
  if (!o.crop || !o.out) fail('prepare needs --out <dir> and --params with {"crop":"x,y,w,h"}.');
474
687
  const [cx, cy, cw, ch] = nums(o.crop, 4, "crop");
475
- const meta = await sharp3(file, { limitInputPixels: 24e6 }).metadata();
688
+ const meta = await sharp4(file, { limitInputPixels: 24e6 }).metadata();
476
689
  validBox([cx, cy, cw, ch], meta.width ?? 0, meta.height ?? 0);
477
690
  if (Math.abs(cw / ch / (W / H) - 1) > 0.03)
478
691
  fail(
@@ -485,12 +698,12 @@ async function prepare(file, o) {
485
698
  if (feather < 0 || ![0, 1].includes(flop))
486
699
  fail("Patch feather must be nonnegative and flop must be 0 or 1.");
487
700
  }
488
- const out = resolve3(o.out);
701
+ const out = resolve4(o.out);
489
702
  mkdirSync(out);
490
703
  const layers = [];
491
704
  for (const spec of o.patch) {
492
705
  const [dx, dy, pw, ph, sx, sy, feather = 6, flop = 0] = nums(spec, 6, "patch");
493
- const patch = await sharp3(file).extract({ left: sx, top: sy, width: pw, height: ph }).flop(!!flop).toColourspace("srgb").removeAlpha().raw().toBuffer();
706
+ const patch = await sharp4(file).extract({ left: sx, top: sy, width: pw, height: ph }).flop(!!flop).toColourspace("srgb").removeAlpha().raw().toBuffer();
494
707
  const rgba = Buffer.alloc(pw * ph * 4);
495
708
  for (let y = 0; y < ph; y++)
496
709
  for (let x = 0; x < pw; x++) {
@@ -502,10 +715,10 @@ async function prepare(file, o) {
502
715
  }
503
716
  layers.push({ input: rgba, raw: { width: pw, height: ph, channels: 4 }, left: dx, top: dy });
504
717
  }
505
- const patched = await sharp3(file).removeAlpha().composite(layers).png().toBuffer();
506
- const card = await sharp3(patched).extract({ left: cx, top: cy, width: cw, height: ch }).toColourspace("srgb").removeAlpha().raw().toBuffer({ resolveWithObject: true });
718
+ const patched = await sharp4(file).removeAlpha().composite(layers).png().toBuffer();
719
+ const card = await sharp4(patched).extract({ left: cx, top: cy, width: cw, height: ch }).toColourspace("srgb").removeAlpha().raw().toBuffer({ resolveWithObject: true });
507
720
  const px = card.data, CW = card.info.width, CH = card.info.height;
508
- const src = await sharp3(file, { limitInputPixels: 24e6 }).toColourspace("srgb").removeAlpha().raw().toBuffer({ resolveWithObject: true });
721
+ const src = await sharp4(file, { limitInputPixels: 24e6 }).toColourspace("srgb").removeAlpha().raw().toBuffer({ resolveWithObject: true });
509
722
  const sp = (x, y) => {
510
723
  const i = (y * src.info.width + x) * 3;
511
724
  return [src.data[i], src.data[i + 1], src.data[i + 2]];
@@ -545,14 +758,14 @@ async function prepare(file, o) {
545
758
  if (y > 0) queue.push(i - CW);
546
759
  if (y < CH - 1) queue.push(i + CW);
547
760
  }
548
- const cardPng = await sharp3(px, { raw: card.info }).png().toBuffer();
549
- const resized = sharp3(cardPng).resize(W, H, { fit: "fill", kernel: "lanczos3" });
550
- await resized.clone().png().toFile(join2(out, "card.png"));
551
- await resized.clone().jpeg({ quality: 84, mozjpeg: true }).toFile(join2(out, "card.jpg"));
761
+ const cardPng = await sharp4(px, { raw: card.info }).png().toBuffer();
762
+ const resized = sharp4(cardPng).resize(W, H, { fit: "fill", kernel: "lanczos3" });
763
+ await resized.clone().png().toFile(join3(out, "card.png"));
764
+ await resized.clone().jpeg({ quality: 84, mozjpeg: true }).toFile(join3(out, "card.jpg"));
552
765
  const rgb = await resized.clone().toColourspace("srgb").raw().toBuffer();
553
766
  const g = await resized.clone().toColourspace("b-w").raw().toBuffer();
554
- writeFileSync(join2(out, "rgb.raw"), rgb);
555
- writeFileSync(join2(out, "lum.raw"), g);
767
+ writeFileSync(join3(out, "rgb.raw"), rgb);
768
+ writeFileSync(join3(out, "lum.raw"), g);
556
769
  const GW = W * 2, GH = H * 2, big = Buffer.alloc(GW * GH);
557
770
  for (let y = 0; y < GH; y++) {
558
771
  const fy = Math.min(H - 1, Math.max(0, (y + 0.5) / 2 - 0.5)), y0 = Math.floor(fy), y1 = Math.min(H - 1, y0 + 1), ty = fy - y0;
@@ -564,14 +777,14 @@ async function prepare(file, o) {
564
777
  );
565
778
  }
566
779
  }
567
- writeFileSync(join2(out, "ink.pgm"), Buffer.concat([Buffer.from(`P5
780
+ writeFileSync(join3(out, "ink.pgm"), Buffer.concat([Buffer.from(`P5
568
781
  ${GW} ${GH}
569
782
  255
570
783
  `), big]));
571
784
  const hex = paper.map((v) => Math.round(v).toString(16).padStart(2, "0")).join("");
572
785
  return {
573
786
  out,
574
- preview: join2(out, "card.png"),
787
+ preview: join3(out, "card.png"),
575
788
  paper: `#${hex}`,
576
789
  patches: o.patch.length,
577
790
  filledEdgePixels: filled,
@@ -596,10 +809,10 @@ function potrace(input, extra) {
596
809
  }
597
810
  var esc = (s) => String(s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
598
811
  function vectorize(dir, o) {
599
- dir = resolve3(dir);
812
+ dir = resolve4(dir);
600
813
  if (!o.out || !o.title) fail("vectorize needs --out <file.svg> and --title");
601
814
  const K = Number(o.tones ?? 7);
602
- const lum = readFileSync(join2(dir, "lum.raw")), rgb = readFileSync(join2(dir, "rgb.raw"));
815
+ const lum = readFileSync(join3(dir, "lum.raw")), rgb = readFileSync(join3(dir, "rgb.raw"));
603
816
  if (lum.length !== N || rgb.length !== N * 3)
604
817
  fail("Expected an intact directory created by image prepare.");
605
818
  let c = [...Array(K)].map((_, i) => 30 + i * 200 / (K - 1));
@@ -649,11 +862,11 @@ function vectorize(dir, o) {
649
862
  const L = used.length;
650
863
  const inkK = o.ink !== void 0 ? Number(o.ink) : L > 1 ? (c[used[0]] + c[used[1]]) / 2 / 255 : 0.25;
651
864
  const groups = [];
652
- const maskFile = join2(dir, "mask.pbm");
865
+ const maskFile = join3(dir, "mask.pbm");
653
866
  for (let j = o.raster ? 0 : L - 2; j >= 0; j--) {
654
867
  let traced;
655
868
  if (j === 0) {
656
- traced = potrace(join2(dir, "ink.pgm"), [
869
+ traced = potrace(join3(dir, "ink.pgm"), [
657
870
  "-k",
658
871
  inkK.toFixed(4),
659
872
  "-t",
@@ -676,7 +889,7 @@ ${W} ${H}
676
889
  if (traced) groups.push(`<g transform="${traced.tr}" fill="${colors[j]}">${traced.paths}</g>`);
677
890
  }
678
891
  const base = o.raster ? `<rect width="${W}" height="${H}" fill="${colors[L - 1]}"/>
679
- <image width="${W}" height="${H}" preserveAspectRatio="none" href="data:image/jpeg;base64,${readFileSync(join2(dir, "card.jpg")).toString("base64")}"/>` : `<rect width="${W}" height="${H}" fill="${colors[L - 1]}"/>`;
892
+ <image width="${W}" height="${H}" preserveAspectRatio="none" href="data:image/jpeg;base64,${readFileSync(join3(dir, "card.jpg")).toString("base64")}"/>` : `<rect width="${W}" height="${H}" fill="${colors[L - 1]}"/>`;
680
893
  const title = esc(o.title), desc = esc(o.desc ?? `${o.title} card artwork.`);
681
894
  const meta = esc(o.credit ?? "Original artwork traced from a reference image.");
682
895
  const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}" role="img" aria-label="${desc}">
@@ -689,9 +902,9 @@ ${groups.join("\n")}
689
902
  `;
690
903
  const checked = checkSubmittedSvg(svg);
691
904
  if (!checked.ok) fail(checked.error);
692
- writeFileSync(resolve3(o.out), svg, { flag: "wx" });
905
+ writeFileSync(resolve4(o.out), svg, { flag: "wx" });
693
906
  return {
694
- out: resolve3(o.out),
907
+ out: resolve4(o.out),
695
908
  bytes: Buffer.byteLength(svg),
696
909
  mode: o.raster ? "raster background + vector ink" : "pure vector",
697
910
  tones: colors,
@@ -699,7 +912,7 @@ ${groups.join("\n")}
699
912
  };
700
913
  }
701
914
  async function rawImage(file) {
702
- const { data, info } = await sharp3(file, { limitInputPixels: 24e6 }).toColourspace("srgb").removeAlpha().raw().toBuffer({ resolveWithObject: true });
915
+ const { data, info } = await sharp4(file, { limitInputPixels: 24e6 }).toColourspace("srgb").removeAlpha().raw().toBuffer({ resolveWithObject: true });
703
916
  const px = (x, y) => {
704
917
  if (!Number.isInteger(x) || !Number.isInteger(y) || x < 0 || y < 0 || x >= info.width || y >= info.height)
705
918
  fail("Pixel coordinates are outside the image.");
@@ -798,14 +1011,14 @@ async function glyphs(file, o) {
798
1011
  if (!o.box || !o.at) fail("glyphs needs --box x,y,w,h and --at X,Y");
799
1012
  const [bx, by, bw, bh] = nums(o.box, 4, "box");
800
1013
  const [ax, ay] = nums(o.at, 2, "at");
801
- const meta = await sharp3(file, { limitInputPixels: 24e6 }).metadata();
1014
+ const meta = await sharp4(file, { limitInputPixels: 24e6 }).metadata();
802
1015
  validBox([bx, by, bw, bh], meta.width ?? 0, meta.height ?? 0);
803
1016
  const up = Number(o.up ?? 4);
804
1017
  if (bw * bh * up * up > 24e6)
805
1018
  fail("Trace region is too large; lower up or choose a smaller box.");
806
- const { data, info } = await sharp3(file).extract({ left: bx, top: by, width: bw, height: bh }).greyscale().removeAlpha().resize({ width: bw * up, kernel: "lanczos3" }).raw().toBuffer({ resolveWithObject: true });
807
- const temp = mkdtempSync(join2(tmpdir(), "makeables-glyphs-"));
808
- const pgm = join2(temp, "glyphs.pgm");
1019
+ const { data, info } = await sharp4(file).extract({ left: bx, top: by, width: bw, height: bh }).greyscale().removeAlpha().resize({ width: bw * up, kernel: "lanczos3" }).raw().toBuffer({ resolveWithObject: true });
1020
+ const temp = mkdtempSync(join3(tmpdir(), "makeables-glyphs-"));
1021
+ const pgm = join3(temp, "glyphs.pgm");
809
1022
  writeFileSync(pgm, Buffer.concat([Buffer.from(`P5
810
1023
  ${info.width} ${info.height}
811
1024
  255
@@ -824,12 +1037,12 @@ ${info.width} ${info.height}
824
1037
  const paths = [...svg.matchAll(/ d="([^"]+)"/g)].map((m) => m[1].replace(/\s+/g, " ")).join(" ");
825
1038
  if (!tr || !paths) fail("nothing dark enough to trace in that box; lower --k or check the box");
826
1039
  const snippet = `<g transform="translate(${ax} ${ay}) scale(${+(1 / up).toFixed(6)})"><path transform="${tr}" fill="${o.fill ?? "#0a0a0a"}" fill-rule="evenodd" d="${paths}"/></g>`;
827
- if (o.out) writeFileSync(resolve3(o.out), snippet, { flag: "wx" });
1040
+ if (o.out) writeFileSync(resolve4(o.out), snippet, { flag: "wx" });
828
1041
  return {
829
1042
  box: { x: bx, y: by, w: bw, h: bh },
830
1043
  at: { x: ax, y: ay },
831
1044
  bytes: snippet.length,
832
- out: o.out ? resolve3(o.out) : null,
1045
+ out: o.out ? resolve4(o.out) : null,
833
1046
  snippet: o.out ? void 0 : snippet
834
1047
  };
835
1048
  }
@@ -851,28 +1064,28 @@ function rotate(file, o) {
851
1064
  const svg = `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}"><g transform="${transform}"><g${inherited}>${body}</g></g></svg>`;
852
1065
  const checked = checkSubmittedSvg(svg);
853
1066
  if (!checked.ok) fail(checked.error);
854
- writeFileSync(resolve3(o.out), svg, { flag: "wx" });
855
- return { out: resolve3(o.out), transform, bytes: Buffer.byteLength(svg) };
1067
+ writeFileSync(resolve4(o.out), svg, { flag: "wx" });
1068
+ return { out: resolve4(o.out), transform, bytes: Buffer.byteLength(svg) };
856
1069
  }
857
1070
  async function compare(file, ref, o) {
858
1071
  if (!o.out) fail("compare needs --out <file.png>");
859
- let card = sharp3(readFileSync(file));
1072
+ let card = sharp4(readFileSync(file));
860
1073
  if (o.upright) card = card.rotate(o.upright === "left" ? 90 : -90);
861
1074
  const cardPng = await card.png().toBuffer();
862
- let reference = sharp3(ref);
1075
+ let reference = sharp4(ref);
863
1076
  if (o["ref-box"]) {
864
1077
  const [x, y, w, h] = nums(o["ref-box"], 4, "ref-box");
865
1078
  reference = reference.extract({ left: x, top: y, width: w, height: h });
866
1079
  }
867
1080
  const height = 900;
868
- const a = await sharp3(cardPng).resize({ height }).png().toBuffer({ resolveWithObject: true });
869
- const b = await sharp3(await reference.png().toBuffer()).resize({ height }).png().toBuffer({ resolveWithObject: true });
1081
+ const a = await sharp4(cardPng).resize({ height }).png().toBuffer({ resolveWithObject: true });
1082
+ const b = await sharp4(await reference.png().toBuffer()).resize({ height }).png().toBuffer({ resolveWithObject: true });
870
1083
  const gap = 24, width = a.info.width + b.info.width + gap * 3;
871
- await sharp3({ create: { width, height: height + gap * 2, channels: 3, background: "#ffffff" } }).composite([
1084
+ await sharp4({ create: { width, height: height + gap * 2, channels: 3, background: "#ffffff" } }).composite([
872
1085
  { input: a.data, left: gap, top: gap },
873
1086
  { input: b.data, left: a.info.width + gap * 2, top: gap }
874
- ]).png().toFile(resolve3(o.out));
875
- return { out: resolve3(o.out), left: "card", right: "reference" };
1087
+ ]).png().toFile(resolve4(o.out));
1088
+ return { out: resolve4(o.out), left: "card", right: "reference" };
876
1089
  }
877
1090
  var imageTools = [
878
1091
  "detect",
@@ -892,7 +1105,7 @@ async function referenceImage(command, file, options, out, reference) {
892
1105
  parsed.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; ")
893
1106
  );
894
1107
  const o = { ...parsed.data, out };
895
- if (out && existsSync(resolve3(out))) fail("Output already exists. Choose a new --out path.");
1108
+ if (out && existsSync(resolve4(out))) fail("Output already exists. Choose a new --out path.");
896
1109
  if (!existsSync(file)) fail("Input file does not exist.");
897
1110
  switch (command) {
898
1111
  case "detect":
@@ -920,175 +1133,6 @@ async function referenceImage(command, file, options, out, reference) {
920
1133
  }
921
1134
  }
922
1135
 
923
- // src/sheet.ts
924
- import { readFile as readFile4 } from "fs/promises";
925
- import { resolve as resolve4 } from "path";
926
- import { deflateSync } from "zlib";
927
- import { createCanvas } from "@napi-rs/canvas";
928
- import sharp4 from "sharp";
929
- async function readSheet(file) {
930
- const value = JSON.parse(await readFile4(resolve4(file), "utf8"));
931
- const parsed = stickerSheetSchema.safeParse(value);
932
- if (!parsed.success)
933
- throw new InputError(
934
- `Invalid sheet: ${parsed.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; ")}`
935
- );
936
- return parsed.data;
937
- }
938
- function newSheet(type, size, name) {
939
- if (typeof type !== "string" || !sheetTypeIds.includes(type))
940
- throw new InputError(`--type must be one of ${sheetTypeIds.join(", ")}.`);
941
- const sheet = createStickerSheet(
942
- type,
943
- typeof name === "string" ? name : void 0
944
- );
945
- if (size !== void 0) {
946
- if (typeof size !== "string" || !(size in sheetSizes))
947
- throw new InputError(`--size must be one of ${Object.keys(sheetSizes).join(", ")}.`);
948
- sheet.size = size;
949
- }
950
- return parse(sheet);
951
- }
952
- function parse(sheet) {
953
- const parsed = stickerSheetSchema.safeParse(sheet);
954
- if (!parsed.success)
955
- throw new InputError(parsed.error.issues.map((issue) => issue.message).join("; "));
956
- return parsed.data;
957
- }
958
- async function readSticker(source) {
959
- const style2 = stickerStyles.find((entry) => entry.source === source);
960
- if (style2) return badgeDesignSchema.parse(structuredClone(style2));
961
- const design = badgeDesignSchema.parse(JSON.parse(await readFile4(resolve4(source), "utf8")));
962
- if (design.product !== "sticker") throw new InputError(`${source} is not a sticker design.`);
963
- return design;
964
- }
965
- var artCache = /* @__PURE__ */ new Map();
966
- function flatArt(design) {
967
- const key = JSON.stringify(design);
968
- let art = artCache.get(key);
969
- if (!art) {
970
- art = renderBadge(design, { face: "front", person: {} }).then(async ({ png }) => {
971
- const { data, info } = await sharp4(png).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
972
- return {
973
- width: info.width,
974
- height: info.height,
975
- rgba: new Uint8ClampedArray(data.buffer, data.byteOffset, data.length)
976
- };
977
- });
978
- artCache.set(key, art);
979
- }
980
- return art;
981
- }
982
- async function sheetArt(sheet) {
983
- return new Map(
984
- await Promise.all(
985
- sheet.items.map(async (item) => [item.id, await flatArt(item.design)])
986
- )
987
- );
988
- }
989
- function bounds(sheet) {
990
- const spec = sheetSpec(sheet);
991
- return { widthMm: spec.widthMm ?? 0, heightMm: spec.heightMm ?? 0 };
992
- }
993
- async function addToSheet(sheet, source, widthMm) {
994
- const type = sheetTypes[sheet.type];
995
- if (type.cut === "die-cut" && sheet.items.length)
996
- throw new InputError("A single sticker file holds one sticker. Start another sheet.");
997
- const width = widthMm === void 0 ? type.stickerWidthMm : Number(widthMm);
998
- if (!Number.isFinite(width) || width < 5 || width > 300)
999
- throw new InputError("--width-mm must be a number from 5 to 300.");
1000
- const design = await readSticker(source);
1001
- const base = (design.source ?? "sticker").replace(/[^a-z0-9-]/g, "").slice(0, 40) || "sticker";
1002
- let id = base;
1003
- for (let n = 2; sheet.items.some((item2) => item2.id === id); n++) id = `${base}-${n}`;
1004
- const spec = sheetSpec(sheet);
1005
- const item = {
1006
- id,
1007
- design,
1008
- x: (spec.widthMm ?? width) / 2,
1009
- y: (spec.heightMm ?? width) / 2,
1010
- rotation: 0,
1011
- widthMm: width
1012
- };
1013
- const next = parse({ ...sheet, items: [...sheet.items, item] });
1014
- if (type.cut === "die-cut") return { sheet: next, item, placed: true };
1015
- const print = prepareSheetPrint(next, await sheetArt(next));
1016
- const moved = placeNear(
1017
- spec,
1018
- bounds(next),
1019
- next.items.map((entry) => ({ item: entry, shape: print.shapes.get(entry.id) })),
1020
- id,
1021
- [item.x, item.y]
1022
- );
1023
- if (moved) next.items[next.items.length - 1] = moved;
1024
- return { sheet: next, item: moved ?? item, placed: Boolean(moved) };
1025
- }
1026
- async function packStickerSheet(sheet) {
1027
- if (sheetTypes[sheet.type].cut === "die-cut") return { sheet, missing: [] };
1028
- const print = prepareSheetPrint(sheet, await sheetArt(sheet));
1029
- const packed = packSheet(
1030
- sheetSpec(sheet),
1031
- bounds(sheet),
1032
- sheet.items.map((item) => ({ item, shape: print.shapes.get(item.id) })),
1033
- sheet.hero
1034
- );
1035
- return { sheet: parse({ ...sheet, items: packed.items }), missing: packed.missing };
1036
- }
1037
- async function checkSheet(sheet) {
1038
- return prepareSheetPrint(sheet, await sheetArt(sheet));
1039
- }
1040
- function sheetReport(print) {
1041
- return {
1042
- printable: print.printable,
1043
- widthMm: Number(print.widthMm.toFixed(2)),
1044
- heightMm: Number(print.heightMm.toFixed(2)),
1045
- stickers: print.items.length,
1046
- violations: print.violations,
1047
- issues: print.issues.map(({ id, issue }) => ({ id, ...issue }))
1048
- };
1049
- }
1050
- async function exportSheetPdf(sheet, print) {
1051
- return sheetPrintPdf(sheet, print, async (bytes) => new Uint8Array(deflateSync(bytes)));
1052
- }
1053
- function sheetPreviewPng(print, dpi = 150) {
1054
- const scale = dpi / 25.4;
1055
- const canvas = createCanvas(
1056
- Math.round(print.widthMm * scale),
1057
- Math.round(print.heightMm * scale)
1058
- );
1059
- const context = canvas.getContext("2d");
1060
- context.fillStyle = "#f4f3ef";
1061
- context.fillRect(0, 0, canvas.width, canvas.height);
1062
- for (const item of print.pdfItems) {
1063
- const art = createCanvas(item.art.width, item.art.height);
1064
- const data = art.getContext("2d").createImageData(item.art.width, item.art.height);
1065
- data.data.set(item.art.rgba);
1066
- art.getContext("2d").putImageData(data, 0, 0);
1067
- context.save();
1068
- context.translate(item.x * scale, item.y * scale);
1069
- context.rotate(item.rotation * Math.PI / 180);
1070
- context.drawImage(
1071
- art,
1072
- -item.widthMm / 2 * scale,
1073
- -item.heightMm / 2 * scale,
1074
- item.widthMm * scale,
1075
- item.heightMm * scale
1076
- );
1077
- context.restore();
1078
- context.strokeStyle = "#ff00ff";
1079
- context.lineWidth = Math.max(1, 0.2 * scale);
1080
- context.beginPath();
1081
- for (const loop of item.cut) {
1082
- for (const [i, [x, y]] of loop.entries())
1083
- if (i) context.lineTo(x * scale, y * scale);
1084
- else context.moveTo(x * scale, y * scale);
1085
- context.closePath();
1086
- }
1087
- context.stroke();
1088
- }
1089
- return canvas.toBuffer("image/png");
1090
- }
1091
-
1092
1136
  // src/sticker-cut.ts
1093
1137
  import sharp5 from "sharp";
1094
1138
  async function stickerPrintCut(design) {
@@ -1106,7 +1150,7 @@ async function stickerPrintCut(design) {
1106
1150
  }
1107
1151
 
1108
1152
  // src/sticker-material.ts
1109
- import { readFile as readFile5, stat as stat3 } from "fs/promises";
1153
+ import { readFile as readFile4, stat as stat3 } from "fs/promises";
1110
1154
  import { basename as basename2 } from "path";
1111
1155
  import sharp6 from "sharp";
1112
1156
  async function bindStickerMap(design, layerId, path) {
@@ -1116,7 +1160,7 @@ async function bindStickerMap(design, layerId, path) {
1116
1160
  const info = await stat3(path);
1117
1161
  if (!info.isFile() || info.size > 4e6)
1118
1162
  throw new InputError("Use an optical image below 4 MB.");
1119
- const bytes = await readFile5(path);
1163
+ const bytes = await readFile4(path);
1120
1164
  const meta = await sharp6(bytes, { limitInputPixels: 16777216 }).metadata();
1121
1165
  if (!meta.width || !meta.height || meta.width > 4096 || meta.height > 4096 || !["png", "webp"].includes(meta.format ?? ""))
1122
1166
  throw new InputError("Use an RGB PNG or WebP optical map up to 4096 \xD7 4096.");
@@ -1142,32 +1186,283 @@ async function bindStickerMap(design, layerId, path) {
1142
1186
  });
1143
1187
  }
1144
1188
 
1189
+ // src/trace.ts
1190
+ import { constants as constants2 } from "fs";
1191
+ import { open as open2 } from "fs/promises";
1192
+ async function appendTrace(path, event) {
1193
+ const file = await open2(
1194
+ path,
1195
+ constants2.O_APPEND | constants2.O_CREAT | constants2.O_WRONLY | constants2.O_NOFOLLOW,
1196
+ 384
1197
+ );
1198
+ try {
1199
+ if (!(await file.stat()).isFile()) throw new Error("Choose a regular trace file.");
1200
+ await file.chmod(384);
1201
+ await file.writeFile(`${JSON.stringify(event)}
1202
+ `);
1203
+ } finally {
1204
+ await file.close();
1205
+ }
1206
+ }
1207
+ function compactReceipt(value) {
1208
+ if (Array.isArray(value)) return value.map(compactReceipt);
1209
+ if (value && typeof value === "object")
1210
+ return Object.fromEntries(
1211
+ Object.entries(value).map(([key, entry]) => [
1212
+ key,
1213
+ key === "design" && entry && typeof entry === "object" ? {
1214
+ name: "name" in entry ? entry.name : void 0,
1215
+ product: "product" in entry ? entry.product : "badge",
1216
+ omitted: true
1217
+ } : compactReceipt(entry)
1218
+ ])
1219
+ );
1220
+ if (typeof value === "string" && value.length > 2e3) return "[omitted; inspect the saved file]";
1221
+ return value;
1222
+ }
1223
+
1145
1224
  // src/wallet.ts
1146
1225
  import { spawn } from "child_process";
1147
1226
  import { createHash as createHash3 } from "crypto";
1148
1227
  import { existsSync as existsSync2 } from "fs";
1149
- import { chmod, lstat, mkdir as mkdir2, mkdtemp, readFile as readFile6, rename as rename2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
1150
- import { homedir as homedir2, tmpdir as tmpdir2 } from "os";
1151
- import { dirname as dirname2, extname as extname3, join as join3, resolve as resolve5 } from "path";
1228
+ import { chmod as chmod2, lstat as lstat3, mkdir as mkdir4, mkdtemp as mkdtemp3, readFile as readFile6, rename as rename4, rm as rm5, writeFile as writeFile4 } from "fs/promises";
1229
+ import { homedir as homedir3, tmpdir as tmpdir2 } from "os";
1230
+ import { dirname as dirname4, extname as extname3, join as join7, resolve as resolve6 } from "path";
1152
1231
  import sharp7 from "sharp";
1153
1232
 
1154
1233
  // src/wallet-release.json
1155
1234
  var wallet_release_default = {
1156
- version: "0.3.0",
1235
+ version: "0.3.1",
1157
1236
  protocol: 1,
1158
1237
  repository: "crafter-station/makeables-community",
1159
1238
  platforms: {
1160
1239
  "darwin-arm64": {
1161
- asset: "makeables-wallet-0.3.0-darwin-arm64.tar.gz",
1162
- sha256: "ebd275228d84c75836820e12a999bcaa7e586dfbb215ba06882ae161a2e4a09f"
1240
+ asset: "makeables-wallet-0.3.1-darwin-arm64.tar.gz",
1241
+ sha256: "4ab38b2f451492f5365c4d8800c375d4c0112c246b7f6bb8e46ca85772298afc"
1163
1242
  },
1164
1243
  "darwin-x64": {
1165
- asset: "makeables-wallet-0.3.0-darwin-x64.tar.gz",
1166
- sha256: "c5bd51863895e6612be838b5023f7a1322424568ea4249651125930a66ff4ccb"
1244
+ asset: "makeables-wallet-0.3.1-darwin-x64.tar.gz",
1245
+ sha256: "f301d7be01bdeda2976b182cdf10b3eb30611f2bb88ef3edb520bfa29b814048"
1167
1246
  }
1168
1247
  }
1169
1248
  };
1170
1249
 
1250
+ // src/wallet-storage.ts
1251
+ import { randomUUID as randomUUID2 } from "crypto";
1252
+ import { constants as constants3 } from "fs";
1253
+ import { chmod, lstat, mkdir as mkdir3, open as open3, rename as rename3, rm as rm3 } from "fs/promises";
1254
+ import { homedir as homedir2 } from "os";
1255
+ import { dirname as dirname3, join as join4, resolve as resolve5 } from "path";
1256
+ function walletHome() {
1257
+ return resolve5(
1258
+ process.env.MAKEABLES_WALLET_HOME ?? join4(homedir2(), "Library", "Application Support", "Makeables", "Wallet")
1259
+ );
1260
+ }
1261
+ async function privateDirectory(path) {
1262
+ const absolute = resolve5(path);
1263
+ const parent = dirname3(absolute);
1264
+ if (parent !== absolute) {
1265
+ try {
1266
+ const info = await lstat(parent);
1267
+ if (info.isSymbolicLink()) throw new InputError("Wallet storage cannot use symbolic links.");
1268
+ if (!info.isDirectory()) throw new InputError("Wallet storage must be a directory.");
1269
+ } catch (error) {
1270
+ if (error.code !== "ENOENT") throw error;
1271
+ await privateDirectory(parent);
1272
+ }
1273
+ }
1274
+ await mkdir3(absolute, { recursive: true, mode: 448 });
1275
+ if (!(await lstat(absolute)).isDirectory())
1276
+ throw new InputError("Wallet storage must be a directory, not a symbolic link.");
1277
+ await chmod(absolute, 448);
1278
+ return absolute;
1279
+ }
1280
+ async function readPrivateJson(path) {
1281
+ const handle = await open3(path, constants3.O_RDONLY | constants3.O_NOFOLLOW);
1282
+ try {
1283
+ const info = await handle.stat();
1284
+ if (!info.isFile() || info.size > 1e6)
1285
+ throw new InputError("Invalid Wallet target registry.");
1286
+ return JSON.parse(await handle.readFile("utf8"));
1287
+ } finally {
1288
+ await handle.close();
1289
+ }
1290
+ }
1291
+ async function writePrivateJson(path, value) {
1292
+ const temporary = join4(dirname3(path), `.${randomUUID2()}.json`);
1293
+ try {
1294
+ const handle = await open3(temporary, "wx", 384);
1295
+ try {
1296
+ await handle.writeFile(`${JSON.stringify(value)}
1297
+ `);
1298
+ await handle.sync();
1299
+ } finally {
1300
+ await handle.close();
1301
+ }
1302
+ await rename3(temporary, path);
1303
+ } finally {
1304
+ await rm3(temporary, { force: true });
1305
+ }
1306
+ }
1307
+
1308
+ // src/wallet-submission.ts
1309
+ import { mkdtemp as mkdtemp2, readFile as readFile5, rm as rm4, writeFile as writeFile3 } from "fs/promises";
1310
+ import { join as join5 } from "path";
1311
+ var uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
1312
+ function submissionId(input) {
1313
+ if (uuid.test(input)) return input.toLowerCase();
1314
+ let url;
1315
+ try {
1316
+ url = new URL(input);
1317
+ } catch {
1318
+ throw new InputError("Supply a submission UUID or official Makeables card URL.");
1319
+ }
1320
+ if (url.origin !== "https://makeables.dev" || url.username || url.password || url.hash)
1321
+ throw new InputError("Submission URLs must use https://makeables.dev.");
1322
+ const id = url.pathname === "/make/card" ? url.searchParams.get("submission") : /^\/submissions\/([^/]+)$/.exec(url.pathname)?.[1];
1323
+ if (!id || !uuid.test(id))
1324
+ throw new InputError("This URL does not identify a Makeables card submission.");
1325
+ return id.toLowerCase();
1326
+ }
1327
+ async function downloadWalletSubmission(input) {
1328
+ const started2 = performance.now();
1329
+ const id = submissionId(input);
1330
+ const response = await fetch(`https://makeables.dev/api/submissions/${id}/files?download=1`, {
1331
+ redirect: "error",
1332
+ signal: AbortSignal.timeout(3e4)
1333
+ });
1334
+ if (!response.ok || !response.body)
1335
+ throw new InputError(
1336
+ `Submission download failed (${response.status}). Use an approved public card or a local exported package.`
1337
+ );
1338
+ const reader = response.body.getReader();
1339
+ const chunks = [];
1340
+ let size = 0;
1341
+ try {
1342
+ if (Number(response.headers.get("content-length")) > PACKAGE_LIMIT)
1343
+ throw new InputError("Submission package exceeds 24 MB.");
1344
+ while (true) {
1345
+ const { done, value } = await reader.read();
1346
+ if (done) break;
1347
+ size += value.length;
1348
+ if (size > PACKAGE_LIMIT) throw new InputError("Submission package exceeds 24 MB.");
1349
+ chunks.push(value);
1350
+ }
1351
+ } finally {
1352
+ await reader.cancel();
1353
+ }
1354
+ const bytes = Buffer.concat(chunks);
1355
+ const pkg = await readPackageZip(bytes);
1356
+ if (pkg.manifest.product !== "card")
1357
+ throw new InputError("Wallet artwork requires a card submission.");
1358
+ const directory = await mkdtemp2(
1359
+ join5(
1360
+ await privateDirectory(join5(await privateDirectory(walletHome()), "submissions")),
1361
+ "download-"
1362
+ )
1363
+ );
1364
+ try {
1365
+ const path = join5(directory, `${id}.makeables.zip`);
1366
+ await writeFile3(path, bytes, { mode: 384, flag: "wx" });
1367
+ const saved = await readFile5(path);
1368
+ const sha256 = await hashBytes(saved);
1369
+ if (saved.length !== bytes.length || sha256 !== await hashBytes(bytes))
1370
+ throw new Error("Saved submission checksum does not match.");
1371
+ return {
1372
+ path,
1373
+ bytes: saved.length,
1374
+ sha256,
1375
+ submissionId: id,
1376
+ title: pkg.manifest.title,
1377
+ downloadVerified: true,
1378
+ elapsedMs: Math.round(performance.now() - started2)
1379
+ };
1380
+ } catch (error) {
1381
+ await rm4(directory, { recursive: true, force: true });
1382
+ throw error;
1383
+ }
1384
+ }
1385
+
1386
+ // src/wallet-targets.ts
1387
+ import { lstat as lstat2 } from "fs/promises";
1388
+ import { join as join6 } from "path";
1389
+ import lockfile from "proper-lockfile";
1390
+ import { z as z4 } from "zod";
1391
+ var alias = z4.string().regex(/^[a-z][a-z0-9-]{0,47}$/).refine((value) => !/\d{5}/.test(value));
1392
+ var label = z4.string().trim().min(1).max(60).refine(
1393
+ (value) => !/\d{5}/.test(value) && [...value].every(
1394
+ (character) => character.charCodeAt(0) >= 32 && character.charCodeAt(0) !== 127
1395
+ )
1396
+ );
1397
+ var targetSchema = z4.object({
1398
+ target: alias,
1399
+ label,
1400
+ last4: z4.string().regex(/^\d{4}$/).optional(),
1401
+ identitySource: z4.literal("user-confirmed"),
1402
+ backup: z4.string().min(1),
1403
+ createdAt: z4.string().datetime()
1404
+ }).strict();
1405
+ var registrySchema = z4.object({ version: z4.literal(1), targets: z4.array(targetSchema).max(100) }).strict();
1406
+ function targetIdentity(values, key = "remember") {
1407
+ const result = targetSchema.pick({ target: true, label: true, last4: true }).safeParse({
1408
+ target: values[key],
1409
+ label: values.label,
1410
+ last4: values.last4
1411
+ });
1412
+ if (!result.success)
1413
+ throw new InputError(
1414
+ `Supply --${key} with a lowercase alias and --label with the user-confirmed card name. Optional --last4 must be exactly four digits; never supply a full card number.`
1415
+ );
1416
+ return result.data;
1417
+ }
1418
+ function registryPath() {
1419
+ return join6(walletHome(), "targets", "registry.json");
1420
+ }
1421
+ async function walletTargets() {
1422
+ const path = registryPath();
1423
+ try {
1424
+ for (const parent of [walletHome(), join6(walletHome(), "targets")]) {
1425
+ if (!(await lstat2(parent)).isDirectory())
1426
+ throw new InputError("Invalid Wallet target storage.");
1427
+ }
1428
+ const result = registrySchema.safeParse(await readPrivateJson(path));
1429
+ if (!result.success || new Set(result.data.targets.map((entry) => entry.target)).size !== result.data.targets.length)
1430
+ throw new InputError("Invalid Wallet target registry. Inspect local Wallet storage.");
1431
+ return result.data.targets;
1432
+ } catch (error) {
1433
+ if (error.code === "ENOENT") return [];
1434
+ throw error;
1435
+ }
1436
+ }
1437
+ function targetChoices(targets) {
1438
+ return targets.map(({ target, label: label2, last4, identitySource }) => ({
1439
+ target,
1440
+ label: label2,
1441
+ ...last4 ? { last4 } : {},
1442
+ displayName: `${label2}${last4 ? ` \u2022\u2022\u2022\u2022${last4}` : ""}`,
1443
+ identitySource
1444
+ }));
1445
+ }
1446
+ async function withWalletTargets(action) {
1447
+ const directory = await privateDirectory(join6(await privateDirectory(walletHome()), "targets"));
1448
+ const release = await lockfile.lock(directory, { realpath: false, retries: 0 });
1449
+ try {
1450
+ return await action(await walletTargets(), async (targets) => {
1451
+ const registry = registrySchema.parse({ version: 1, targets });
1452
+ await writePrivateJson(registryPath(), registry);
1453
+ });
1454
+ } finally {
1455
+ await release();
1456
+ }
1457
+ }
1458
+ function requireNewTarget(targets, target) {
1459
+ if (targets.some((entry) => entry.target === target))
1460
+ throw new InputError(
1461
+ `Target "${target}" already exists. Choose a new alias; existing bindings are never replaced.`
1462
+ );
1463
+ if (targets.length >= 100) throw new InputError("Remove an unused target before adding another.");
1464
+ }
1465
+
1171
1466
  // src/wallet.ts
1172
1467
  var WalletError = class extends Error {
1173
1468
  constructor(details) {
@@ -1178,9 +1473,9 @@ var WalletError = class extends Error {
1178
1473
  };
1179
1474
  function installation() {
1180
1475
  const platform = `${process.platform}-${process.arch}`;
1181
- const cache = process.env.MAKEABLES_WALLET_CACHE ?? join3(homedir2(), "Library", "Caches", "Makeables", "wallet");
1182
- const root = resolve5(cache, `${wallet_release_default.version}-${platform}`);
1183
- return { platform, root, executable: join3(root, "makeables-wallet", "makeables-wallet") };
1476
+ const cache = process.env.MAKEABLES_WALLET_CACHE ?? join7(homedir3(), "Library", "Caches", "Makeables", "wallet");
1477
+ const root = resolve6(cache, `${wallet_release_default.version}-${platform}`);
1478
+ return { platform, root, executable: join7(root, "makeables-wallet", "makeables-wallet") };
1184
1479
  }
1185
1480
  async function execute(command, args, progress) {
1186
1481
  return new Promise((accept, reject) => {
@@ -1311,38 +1606,41 @@ async function setupWallet(progress) {
1311
1606
  const bytes = Buffer.concat(chunks);
1312
1607
  if (createHash3("sha256").update(bytes).digest("hex") !== artifact.sha256)
1313
1608
  throw new Error("Wallet companion checksum did not match. Nothing was installed.");
1314
- await mkdir2(dirname2(root), { recursive: true, mode: 448 });
1315
- const stage = await mkdtemp(join3(dirname2(root), ".install-"));
1609
+ await mkdir4(dirname4(root), { recursive: true, mode: 448 });
1610
+ const stage = await mkdtemp3(join7(dirname4(root), ".install-"));
1316
1611
  try {
1317
- const archive = join3(stage, "companion.tar.gz");
1318
- await writeFile2(archive, bytes, { mode: 384, flag: "wx" });
1612
+ const archive = join7(stage, "companion.tar.gz");
1613
+ await writeFile4(archive, bytes, { mode: 384, flag: "wx" });
1319
1614
  const members = (await execute("tar", ["-tzf", archive])).trim().split("\n");
1320
1615
  if (members.some(
1321
1616
  (path) => !path.startsWith("makeables-wallet/") || path.split("/").includes("..")
1322
1617
  ))
1323
1618
  throw new Error("Unexpected companion archive contents.");
1324
1619
  await execute("tar", ["-xzf", archive, "-C", stage]);
1325
- await rm2(archive);
1326
- await chmod(join3(stage, "makeables-wallet", "makeables-wallet"), 448);
1327
- const info = await invoke(join3(stage, "makeables-wallet", "makeables-wallet"), ["version"]);
1620
+ await rm5(archive);
1621
+ await chmod2(join7(stage, "makeables-wallet", "makeables-wallet"), 448);
1622
+ const info = await invoke(join7(stage, "makeables-wallet", "makeables-wallet"), ["version"]);
1328
1623
  if (info.version !== wallet_release_default.version || info.protocol !== wallet_release_default.protocol)
1329
1624
  throw new Error("Companion version does not match the installed CLI.");
1330
- await rename2(stage, root);
1625
+ await rename4(stage, root);
1331
1626
  } finally {
1332
- await rm2(stage, { recursive: true, force: true });
1627
+ await rm5(stage, { recursive: true, force: true });
1333
1628
  }
1334
1629
  return walletDoctor();
1335
1630
  }
1336
1631
  async function walletArtwork(file, side2 = "front") {
1337
- const input = resolve5(file);
1338
- const info = await lstat(input);
1339
- if (!info.isFile() || info.size > 12e6)
1340
- throw new InputError("Expected a local design or artwork file below 12 MB.");
1632
+ const input = resolve6(file);
1633
+ const info = await lstat3(input);
1634
+ if (!info.isFile() || info.size > 336e5)
1635
+ throw new InputError("Expected artwork or a complete Makeables package below 32 MB.");
1341
1636
  let source;
1342
- if (extname3(input).toLowerCase() === ".json") {
1343
- const design = await readBadge(input);
1344
- if (design.product !== "card") throw new InputError("Wallet artwork requires a card document.");
1345
- source = (await renderBadge(design, { face: side2, person: {} })).png;
1637
+ if ([".json", ".zip"].includes(extname3(input).toLowerCase())) {
1638
+ const pkg = await readSubmission(input);
1639
+ if (pkg.manifest.product !== "card")
1640
+ throw new InputError("Wallet artwork requires a card document.");
1641
+ const path = side2 === "front" ? pkg.manifest.front : pkg.manifest.back;
1642
+ if (!path) throw new InputError("This package has no back artwork.");
1643
+ source = Buffer.from(pkg.files[path]);
1346
1644
  } else {
1347
1645
  source = await readFile6(input);
1348
1646
  if (extname3(input).toLowerCase() === ".svg") {
@@ -1353,27 +1651,58 @@ async function walletArtwork(file, side2 = "front") {
1353
1651
  return sharp7(source, { limitInputPixels: 24e6, density: 144 }).rotate().resize(1536, 969, { fit: "contain", background: "#000000" }).flatten({ background: "#000000" }).toColourspace("srgb").png().toBuffer();
1354
1652
  }
1355
1653
  async function preparedArtwork(directory) {
1356
- const path = resolve5(directory);
1357
- if (!(await lstat(path)).isDirectory())
1654
+ const path = resolve6(directory);
1655
+ if (!(await lstat3(path)).isDirectory())
1358
1656
  throw new InputError("--assets must be a prepared artwork directory.");
1359
1657
  for (const name of [
1360
1658
  "cardBackgroundCombined@3x.png",
1361
1659
  "cardBackgroundCombined@2x.png",
1362
1660
  "cardBackgroundCombined.pdf"
1363
1661
  ]) {
1364
- const file = join3(path, name);
1365
- const info = await lstat(file);
1662
+ const file = join7(path, name);
1663
+ const info = await lstat3(file);
1366
1664
  if (!info.isFile() || info.size === 0 || info.size > 16 * 1024 * 1024)
1367
1665
  throw new InputError(`Invalid prepared asset: ${name}. Run wallet prep again.`);
1368
1666
  const signature = Buffer.from(name.endsWith(".pdf") ? "%PDF-" : "\x89PNG\r\n\n", "latin1");
1369
1667
  if (!(await readFile6(file)).subarray(0, signature.length).equals(signature))
1370
1668
  throw new InputError(`Invalid prepared asset: ${name}. Run wallet prep again.`);
1371
1669
  }
1372
- return { path, preview: join3(path, "cardBackgroundCombined@3x.png") };
1670
+ return { path, preview: join7(path, "cardBackgroundCombined@3x.png") };
1373
1671
  }
1374
1672
  async function walletCommand(command, values, progress) {
1375
- const started = performance.now();
1376
- const elapsed = () => Math.round(performance.now() - started);
1673
+ if (values.remember) {
1674
+ const identity = targetIdentity(values);
1675
+ return withWalletTargets(async (targets, save) => {
1676
+ requireNewTarget(targets, identity.target);
1677
+ const result = await runWalletCommand(command, values, progress);
1678
+ if (result.status !== "complete") return result;
1679
+ try {
1680
+ if (!result.backupVerified || typeof result.backupId !== "string")
1681
+ throw new Error("The companion did not provide a verified backup binding.");
1682
+ await save([
1683
+ ...targets,
1684
+ {
1685
+ ...identity,
1686
+ backup: result.backupId,
1687
+ identitySource: "user-confirmed",
1688
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
1689
+ }
1690
+ ]);
1691
+ return { ...result, targetSaved: true, target: identity.target };
1692
+ } catch {
1693
+ return {
1694
+ ...result,
1695
+ targetSaved: false,
1696
+ warning: "Artwork installed, but the card label was not saved. Use wallet targets add with the verified backup; do not repeat the installation."
1697
+ };
1698
+ }
1699
+ });
1700
+ }
1701
+ return runWalletCommand(command, values, progress);
1702
+ }
1703
+ async function runWalletCommand(command, values, progress) {
1704
+ const started2 = performance.now();
1705
+ const elapsed = () => Math.round(performance.now() - started2);
1377
1706
  let latestPhase;
1378
1707
  const report = (phase, message) => {
1379
1708
  latestPhase = phase;
@@ -1388,9 +1717,12 @@ async function walletCommand(command, values, progress) {
1388
1717
  const forward = (data) => {
1389
1718
  if (data && typeof data === "object") {
1390
1719
  if ("phase" in data && typeof data.phase === "string") latestPhase = data.phase;
1720
+ const ready = latestPhase === "selection-ready" || "message" in data && data.message === "Connected to iPhone activity stream.";
1721
+ if (ready) latestPhase = "selection-ready";
1391
1722
  progress({
1392
1723
  ...data,
1393
1724
  phase: latestPhase,
1725
+ ...ready ? { ready: true, message: "Listening. Open Wallet and tap the exact card to customize." } : {},
1394
1726
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1395
1727
  elapsedMs: elapsed()
1396
1728
  });
@@ -1403,6 +1735,47 @@ async function walletCommand(command, values, progress) {
1403
1735
  };
1404
1736
  if (command === "wallet doctor") return walletDoctor();
1405
1737
  if (command === "wallet setup") return setupWallet(forward);
1738
+ if (command === "wallet targets list") return { targets: targetChoices(await walletTargets()) };
1739
+ if (command === "wallet targets remove") {
1740
+ const name = need("target");
1741
+ return withWalletTargets(async (targets, save) => {
1742
+ if (!targets.some((entry) => entry.target === name))
1743
+ throw new InputError("Unknown saved target.");
1744
+ await save(targets.filter((entry) => entry.target !== name));
1745
+ return { removed: name, backupPreserved: true };
1746
+ });
1747
+ }
1748
+ if (command === "wallet targets add") {
1749
+ const identity = targetIdentity(values, "target");
1750
+ const backup = need("backup");
1751
+ if (!values.yes)
1752
+ throw new InputError("Use --yes after the user confirms which card this backup belongs to.");
1753
+ return withWalletTargets(async (targets, save) => {
1754
+ requireNewTarget(targets, identity.target);
1755
+ const info2 = await walletDoctor();
1756
+ if (!info2.ready || !("installed" in info2) || typeof info2.installed !== "string")
1757
+ throw new InputError("Run makeables wallet setup first.");
1758
+ const verified = await invoke(info2.installed, ["backups", "--backup", backup], forward);
1759
+ if (verified.verified !== true || typeof verified.path !== "string" || !["complete", "restored"].includes(String(verified.status)))
1760
+ throw new InputError("Choose a verified original backup from a completed operation.");
1761
+ const entry = {
1762
+ ...identity,
1763
+ backup: verified.path,
1764
+ identitySource: "user-confirmed",
1765
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
1766
+ };
1767
+ await save([...targets, entry]);
1768
+ return { targetSaved: true, ...targetChoices([entry])[0] };
1769
+ });
1770
+ }
1771
+ const targetCount = [values.target, values.card, values.backup].filter(Boolean).length;
1772
+ if (targetCount > 1)
1773
+ throw new InputError("Choose exactly one --target, --card selection or --backup original.");
1774
+ if (values.target) {
1775
+ const target = (await walletTargets()).find((entry) => entry.target === values.target);
1776
+ if (!target) throw new InputError("Unknown saved target. Run makeables wallet targets list.");
1777
+ values = { ...values, backup: target.backup };
1778
+ }
1406
1779
  const installing = command === "wallet install";
1407
1780
  const applying = command === "wallet apply" || installing && Boolean(values.card || values.backup);
1408
1781
  if (applying || command === "wallet restore" || command === "wallet recover") {
@@ -1414,29 +1787,44 @@ async function walletCommand(command, values, progress) {
1414
1787
  if (applying && Boolean(values.card) === Boolean(values.backup))
1415
1788
  throw new InputError("Choose exactly one --card selection or --backup original.");
1416
1789
  if (installing && values.yes && !applying)
1417
- throw new InputError("Select an explicit --card or --backup before using --yes.");
1790
+ throw new InputError("Select an explicit --target, --card or --backup before using --yes.");
1791
+ if (values.scan && (applying || values.yes))
1792
+ throw new InputError("--scan discovers a new card; omit --target, --card, --backup and --yes.");
1793
+ if ((values.label || values.last4) && !values.remember)
1794
+ throw new InputError("--label and --last4 require --remember.");
1795
+ if (values.remember && (!installing || values.target || values.backup))
1796
+ throw new InputError(
1797
+ "--remember is for a newly selected card. Use wallet targets add for an existing backup."
1798
+ );
1418
1799
  if (installing && applying && (values.device || values.seconds))
1419
1800
  throw new InputError(
1420
- "--device and --seconds are for selection; --card or --backup already binds the target."
1801
+ "--device and --seconds are for selection; --target, --card or --backup already binds the target."
1421
1802
  );
1422
1803
  if (values.seconds && (!/^\d+$/.test(String(values.seconds)) || Number(values.seconds) < 1 || Number(values.seconds) > 300))
1423
1804
  throw new InputError("--seconds must be between 1 and 300.");
1424
- if (installing || applying) {
1425
- if (Boolean(values.file) === Boolean(values.assets))
1426
- throw new InputError("Supply exactly one --file artwork or --assets prepared directory.");
1805
+ if (installing || applying || command === "wallet prep") {
1806
+ if ([values.file, values.assets, values.submission].filter(Boolean).length !== 1)
1807
+ throw new InputError(
1808
+ "Supply exactly one --file artwork, --submission URL or UUID, or --assets prepared directory."
1809
+ );
1427
1810
  if (values.assets && values.side)
1428
- throw new InputError("--side applies to --file, not already prepared --assets.");
1811
+ throw new InputError("--side applies to --file or --submission, not prepared --assets.");
1429
1812
  }
1430
1813
  if (values.side && !["front", "back"].includes(String(values.side)))
1431
1814
  throw new InputError("--side must be front or back.");
1432
1815
  if (command === "wallet prep") {
1433
- need("file");
1434
1816
  need("out");
1435
1817
  }
1436
1818
  if (command === "wallet restore") need("backup");
1437
1819
  const existing = values.assets ? await preparedArtwork(need("assets")) : void 0;
1820
+ let source;
1821
+ if (values.submission) {
1822
+ report("download", "Downloading and verifying the complete card package.");
1823
+ source = await downloadWalletSubmission(need("submission"));
1824
+ values = { ...values, file: String(source.path) };
1825
+ }
1438
1826
  if (installing) report("setup", "Checking the Wallet companion.");
1439
- const info = installing ? await setupWallet(forward) : await walletDoctor();
1827
+ const info = installing || command === "wallet prep" ? await setupWallet(forward) : await walletDoctor();
1440
1828
  if (!info.ready || !("installed" in info) || typeof info.installed !== "string")
1441
1829
  throw new InputError("Run makeables wallet setup first.");
1442
1830
  const executable = info.installed;
@@ -1489,7 +1877,7 @@ async function walletCommand(command, values, progress) {
1489
1877
  }
1490
1878
  }
1491
1879
  if (command === "wallet prep" || applying || installing) {
1492
- const work = await mkdtemp(join3(tmpdir2(), "makeables-wallet-"));
1880
+ const work = await mkdtemp3(join7(tmpdir2(), "makeables-wallet-"));
1493
1881
  let phase = "prepare";
1494
1882
  let deviceWriteAttempted = false;
1495
1883
  let persistent;
@@ -1497,27 +1885,23 @@ async function walletCommand(command, values, progress) {
1497
1885
  try {
1498
1886
  let target;
1499
1887
  if (existing) {
1500
- target = resolve5(need("assets"));
1888
+ target = resolve6(need("assets"));
1501
1889
  prepared = existing;
1502
1890
  } else {
1503
1891
  report("prepare", "Preparing the complete card artwork.");
1504
1892
  const png = await walletArtwork(need("file"), values.side === "back" ? "back" : "front");
1505
- const raster = join3(work, "artwork.png");
1506
- await writeFile2(raster, png, { mode: 384 });
1893
+ const raster = join7(work, "artwork.png");
1894
+ await writeFile4(raster, png, { mode: 384 });
1507
1895
  if (command === "wallet prep") {
1508
- target = resolve5(need("out"));
1896
+ target = resolve6(need("out"));
1509
1897
  } else if (installing && !applying) {
1510
- const root = join3(
1511
- process.env.MAKEABLES_WALLET_HOME ?? join3(homedir2(), "Library", "Application Support", "Makeables", "Wallet"),
1512
- "prepared"
1898
+ const root = await privateDirectory(
1899
+ join7(await privateDirectory(walletHome()), "prepared")
1513
1900
  );
1514
- await mkdir2(root, { recursive: true, mode: 448 });
1515
- if ((await lstat(root)).isSymbolicLink())
1516
- throw new InputError("Prepared artwork storage cannot be a symbolic link.");
1517
- persistent = await mkdtemp(join3(root, "install-"));
1518
- target = join3(persistent, "assets");
1901
+ persistent = await mkdtemp3(join7(root, "install-"));
1902
+ target = join7(persistent, "assets");
1519
1903
  } else {
1520
- target = join3(work, "assets");
1904
+ target = join7(work, "assets");
1521
1905
  }
1522
1906
  prepared = await invoke(
1523
1907
  executable,
@@ -1526,10 +1910,32 @@ async function walletCommand(command, values, progress) {
1526
1910
  );
1527
1911
  await preparedArtwork(target);
1528
1912
  }
1529
- if (command === "wallet prep") return prepared;
1913
+ if (command === "wallet prep") return { ...prepared, source, elapsedMs: elapsed() };
1530
1914
  if (installing && !applying) {
1531
1915
  phase = "selection";
1532
- report("selection", "Artwork is ready. Open Wallet and tap the card to customize.");
1916
+ const continuation = ["--assets", target];
1917
+ for (const flag of ["remember", "label", "last4"])
1918
+ if (typeof values[flag] === "string")
1919
+ continuation.push(`--${flag}`, String(values[flag]));
1920
+ const saved = await walletTargets();
1921
+ if (saved.length && !values.scan && !values.device)
1922
+ return {
1923
+ status: "needs-target",
1924
+ targets: targetChoices(saved),
1925
+ source,
1926
+ assets: target,
1927
+ preview: prepared.preview,
1928
+ deviceWriteAttempted: false,
1929
+ nextAction: "choose-saved-target-or-scan",
1930
+ next: {
1931
+ command: "wallet install",
1932
+ args: ["--assets", target],
1933
+ required: ["target", "yes"],
1934
+ alternative: { args: [...continuation, "--scan"], required: [] }
1935
+ },
1936
+ elapsedMs: elapsed()
1937
+ };
1938
+ report("selection", "Artwork is ready. Connecting to the iPhone activity stream.");
1533
1939
  const args2 = ["list"];
1534
1940
  for (const flag of ["device", "seconds"])
1535
1941
  if (typeof values[flag] === "string") args2.push(`--${flag}`, String(values[flag]));
@@ -1542,11 +1948,12 @@ async function walletCommand(command, values, progress) {
1542
1948
  status,
1543
1949
  assets: target,
1544
1950
  preview: prepared.preview,
1951
+ source,
1545
1952
  deviceWriteAttempted: false,
1546
1953
  nextAction: selection.needsDevice ? "select-device" : hasCards ? "confirm-card" : "tap-card",
1547
1954
  next: {
1548
1955
  command: "wallet install",
1549
- args: ["--assets", target],
1956
+ args: !hasCards && (values.scan || saved.length) ? [...continuation, "--scan"] : continuation,
1550
1957
  required: selection.needsDevice ? ["device"] : hasCards ? ["card", "yes"] : []
1551
1958
  },
1552
1959
  elapsedMs: elapsed()
@@ -1559,7 +1966,12 @@ async function walletCommand(command, values, progress) {
1559
1966
  phase = "apply";
1560
1967
  deviceWriteAttempted = true;
1561
1968
  const result = await invoke(executable, args, forward);
1562
- return { ...result, elapsedMs: elapsed() };
1969
+ return {
1970
+ ...result,
1971
+ ...values.target ? { target: values.target } : {},
1972
+ source,
1973
+ elapsedMs: elapsed()
1974
+ };
1563
1975
  } catch (error) {
1564
1976
  if (error instanceof WalletError) {
1565
1977
  throw new WalletError({
@@ -1575,8 +1987,8 @@ async function walletCommand(command, values, progress) {
1575
1987
  }
1576
1988
  throw error;
1577
1989
  } finally {
1578
- await rm2(work, { recursive: true, force: true });
1579
- if (persistent && phase === "prepare") await rm2(persistent, { recursive: true, force: true });
1990
+ await rm5(work, { recursive: true, force: true });
1991
+ if (persistent && phase === "prepare") await rm5(persistent, { recursive: true, force: true });
1580
1992
  }
1581
1993
  }
1582
1994
  throw new InputError("Unknown Wallet command.");
@@ -1597,9 +2009,19 @@ function walletSummary(result) {
1597
2009
  `Wallet artwork installed${seconds}.`,
1598
2010
  result.backupVerified ? "Original artwork backup saved and verified." : "Original artwork backup saved.",
1599
2011
  ...typeof result.backup === "string" ? [`Backup: ${result.backup}`] : [],
1600
- typeof result.refresh === "string" ? result.refresh : "Close and reopen Wallet to see the design."
2012
+ typeof result.refresh === "string" ? result.refresh : "Close and reopen Wallet to see the design.",
2013
+ ...typeof result.warning === "string" ? [result.warning] : []
1601
2014
  ].join("\n");
1602
2015
  }
2016
+ if (result.status === "needs-target")
2017
+ return [
2018
+ "Artwork prepared. No card has been changed.",
2019
+ ...(result.targets ?? []).map(
2020
+ (entry) => `${entry.target}: ${entry.displayName} (user-confirmed)`
2021
+ ),
2022
+ "Choose a saved --target with --yes, or use --scan and tap a different card after selection-ready.",
2023
+ `Prepared assets: ${String(result.assets)}`
2024
+ ].join("\n");
1603
2025
  if (["needs-device", "needs-selection", "needs-card"].includes(String(result.status)))
1604
2026
  return [
1605
2027
  "Artwork prepared. No card has been changed.",
@@ -1617,6 +2039,14 @@ function walletSummary(result) {
1617
2039
 
1618
2040
  // src/cli.ts
1619
2041
  var commands = [
2042
+ "kit new --name <title> --out <kit.json>",
2043
+ "kit extract --file <artwork-sheet.png> --out <new-directory> [--params <json|@file>]",
2044
+ "kit add --file <kit.json> --ref <design|package> --id <piece-id>",
2045
+ "kit sheet --file <kit.json> --ref <sheet.json> --id <sheet-id>",
2046
+ "kit inspect --file <kit.json|kit.makeables.zip>",
2047
+ "kit render --file <kit.json|kit.makeables.zip> --out <mockup.png> [--overwrite]",
2048
+ "kit export --file <kit.json|kit.makeables.zip> --out <kit.makeables.zip> [--overwrite]",
2049
+ "kit import --file <kit.makeables.zip> --out <new-directory>",
1620
2050
  "designs list",
1621
2051
  "new card --design <slug> | --style <id> --out <new-file> [--dry-run]",
1622
2052
  "material set --file <design.json> --params <json|@file>",
@@ -1624,7 +2054,7 @@ var commands = [
1624
2054
  "new badge --style <id> --out <new-file> [--dry-run]",
1625
2055
  "new sticker --style <id> --out <new-file> [--dry-run]",
1626
2056
  "layer list --file <design.json>",
1627
- "layer add --file <design.json> --sticker <petdex:slug|path> [--side front|back]",
2057
+ "layer add --file <design.json> --sticker <petdex:slug|path> [--side front|back] [--relative-to document|cwd]",
1628
2058
  "layer set --file <design.json> --id <layer> [--side front|back] [--x --y --rotation] [--params <json|@file>]",
1629
2059
  "layer remove --file <design.json> --id <layer> [--side front|back]",
1630
2060
  "cut --file <sticker.json> [--out <cut.svg>] [--overwrite]",
@@ -1633,7 +2063,7 @@ var commands = [
1633
2063
  "sheet pack --file <sheet.json>",
1634
2064
  "sheet check --file <sheet.json>",
1635
2065
  "sheet export --file <sheet.json> --out <sheet.pdf> [--preview <proof.png>] [--overwrite]",
1636
- "render --file <design.json|design.makeables.json> --out <png> [--side front|back] [--photo <image>] [--name --role --number] [--overwrite]",
2066
+ "render --file <design.json|design.makeables.json> --out <png> [--appearance flat|material] [--side front|back] [--photo <image>] [--name --role --number] [--overwrite]",
1637
2067
  "validate --file <design.json|artwork.svg> [--out <preview.png>]",
1638
2068
  "new card --file <artwork.svg|image.png> --out <card.json> [--name <title>]",
1639
2069
  ...imageTools.map(
@@ -1641,9 +2071,12 @@ var commands = [
1641
2071
  ),
1642
2072
  "wallet setup",
1643
2073
  "wallet doctor",
1644
- "wallet install (--file <artwork> | --assets <directory>) [(--card <selection> | --backup <id|path>) --yes]",
2074
+ "wallet install (--submission <UUID|URL> | --file <artwork|package> | --assets <directory>) [(--target <alias> | --card <selection> | --backup <id|path>) --yes] [--scan] [--remember <alias> --label <name> --last4 <digits>]",
2075
+ "wallet targets list",
2076
+ "wallet targets add --target <alias> --backup <id|path> --label <name> [--last4 <digits>] --yes",
2077
+ "wallet targets remove --target <alias>",
1645
2078
  "wallet list [--device <selection>] [--seconds 45] [--first]",
1646
- "wallet prep --file <card.json|artwork.svg|image.png> --out <new-directory> [--side front|back]",
2079
+ "wallet prep (--submission <UUID|URL> | --file <card.json|package.zip|artwork.svg|image.png>) --out <new-directory> [--side front|back]",
1647
2080
  "wallet apply (--file <artwork> | --assets <directory>) (--card <selection> | --backup <id|path>) --yes",
1648
2081
  "wallet restore --backup <id|path> --yes",
1649
2082
  "wallet backups [--backup <id|path>]",
@@ -1673,22 +2106,35 @@ var commands = [
1673
2106
  "schema"
1674
2107
  ];
1675
2108
  var nextSteps = ["makeables skills get core", "makeables doctor"];
2109
+ var compact = false;
2110
+ var stdoutClosed = false;
2111
+ var traceFile;
2112
+ var traceCommand = "invalid";
2113
+ var traceCode;
2114
+ var elapsedStartup = process.uptime() * 1e3;
2115
+ var startedAt = new Date(Date.now() - elapsedStartup).toISOString();
2116
+ var started = performance.now() - elapsedStartup;
2117
+ process.stdout.on("error", (error) => {
2118
+ if (error.code === "EPIPE") stdoutClosed = true;
2119
+ else throw error;
2120
+ });
1676
2121
  function emit(data, next, human, json) {
2122
+ if (stdoutClosed) return;
1677
2123
  process.stdout.write(
1678
- json ? `${JSON.stringify({ ok: true, version, data, nextSteps: next })}
2124
+ json ? `${JSON.stringify({ ok: true, version, data: compact ? compactReceipt(data) : data, nextSteps: next })}
1679
2125
  ` : `${human}
1680
2126
  `
1681
2127
  );
1682
2128
  }
1683
2129
  async function newPath(file) {
1684
- const path = resolve6(file);
2130
+ const path = resolve7(file);
1685
2131
  try {
1686
- await lstat2(path);
2132
+ await lstat4(path);
1687
2133
  throw new InputError(`File already exists: ${path}. Choose a new --out path.`);
1688
2134
  } catch (error) {
1689
2135
  if (error.code !== "ENOENT") throw error;
1690
2136
  }
1691
- await access(dirname3(path), constants2.W_OK);
2137
+ await access(dirname5(path), constants4.W_OK);
1692
2138
  return path;
1693
2139
  }
1694
2140
  async function run() {
@@ -1738,28 +2184,42 @@ async function run() {
1738
2184
  seconds: { type: "string" },
1739
2185
  assets: { type: "string" },
1740
2186
  first: { type: "boolean" },
2187
+ submission: { type: "string" },
2188
+ scan: { type: "boolean" },
2189
+ remember: { type: "string" },
2190
+ label: { type: "string" },
2191
+ last4: { type: "string" },
1741
2192
  type: { type: "string" },
1742
2193
  size: { type: "string" },
1743
2194
  "width-mm": { type: "string" },
1744
- preview: { type: "string" }
2195
+ preview: { type: "string" },
2196
+ appearance: { type: "string" },
2197
+ "relative-to": { type: "string" },
2198
+ compact: { type: "boolean" },
2199
+ trace: { type: "string" }
1745
2200
  }
1746
2201
  });
1747
2202
  const json = machineOutput(Boolean(values.json));
2203
+ compact = Boolean(values.compact);
2204
+ traceFile = values.trace;
1748
2205
  if (values.version) {
1749
2206
  emit({ version }, nextSteps, version, json);
1750
2207
  return;
1751
2208
  }
1752
2209
  if (values.help || !positionals.length) {
2210
+ const prefix = positionals.join(" ");
2211
+ const helpCommands = prefix ? commands.filter((entry) => entry === prefix || entry.startsWith(`${prefix} `)) : commands;
2212
+ if (!helpCommands.length) throw new InputError(`Unknown command: ${prefix}`);
1753
2213
  if (!json) banner(version);
1754
2214
  emit(
1755
2215
  {
1756
- commands,
1757
- flags: ["--json", "--help", "--version"],
2216
+ commands: helpCommands,
2217
+ flags: ["--json", "--help", "--version", "--compact", "--trace <local.jsonl>"],
1758
2218
  exitCodes: { success: 0, invalidInput: 2, systemFailure: 1 }
1759
2219
  },
1760
2220
  nextSteps,
1761
2221
  `${style("Commands", "1", !json)}
1762
- ${commands.map((command2) => ` npx makeables ${command2}`).join("\n")}
2222
+ ${helpCommands.map((command2) => ` npx makeables ${command2}`).join("\n")}
1763
2223
 
1764
2224
  JSON is automatic when piped. --out creates new files only.
1765
2225
  Install globally: npm install --global makeables
@@ -1770,14 +2230,40 @@ From a built checkout: npm run studio -- <command>`,
1770
2230
  }
1771
2231
  const command = ["skills get", "studio call"].includes(positionals.slice(0, 2).join(" ")) ? positionals.slice(0, 2).join(" ") : positionals.join(" ");
1772
2232
  const allowed = {
2233
+ "kit new": ["name", "out"],
2234
+ "kit extract": ["file", "out", "params"],
2235
+ "kit add": ["file", "ref", "id"],
2236
+ "kit sheet": ["file", "ref", "id"],
2237
+ "kit inspect": ["file"],
2238
+ "kit render": ["file", "out", "overwrite"],
2239
+ "kit export": ["file", "out", "overwrite"],
2240
+ "kit import": ["file", "out"],
1773
2241
  ...Object.fromEntries(
1774
2242
  imageTools.map((tool) => [`image ${tool}`, ["file", "out", "params", "ref"]])
1775
2243
  ),
1776
2244
  "wallet setup": [],
1777
2245
  "wallet doctor": [],
1778
- "wallet install": ["file", "assets", "card", "backup", "side", "device", "seconds", "yes"],
2246
+ "wallet install": [
2247
+ "submission",
2248
+ "file",
2249
+ "assets",
2250
+ "target",
2251
+ "card",
2252
+ "backup",
2253
+ "side",
2254
+ "device",
2255
+ "seconds",
2256
+ "yes",
2257
+ "scan",
2258
+ "remember",
2259
+ "label",
2260
+ "last4"
2261
+ ],
2262
+ "wallet targets list": [],
2263
+ "wallet targets add": ["target", "backup", "label", "last4", "yes"],
2264
+ "wallet targets remove": ["target"],
1779
2265
  "wallet list": ["device", "seconds", "first"],
1780
- "wallet prep": ["file", "out", "side"],
2266
+ "wallet prep": ["submission", "file", "out", "side"],
1781
2267
  "wallet apply": ["file", "assets", "card", "backup", "side", "yes"],
1782
2268
  "wallet restore": ["backup", "yes"],
1783
2269
  "wallet backups": ["backup"],
@@ -1830,7 +2316,7 @@ From a built checkout: npm run studio -- <command>`,
1830
2316
  "material map": ["file", "id", "ref"],
1831
2317
  "new badge": ["style", "out", "dry-run"],
1832
2318
  "layer list": ["file"],
1833
- "layer add": ["file", "sticker", "side"],
2319
+ "layer add": ["file", "sticker", "side", "relative-to"],
1834
2320
  "layer set": ["file", "id", "x", "y", "rotation", "side", "params"],
1835
2321
  "layer remove": ["file", "id", "side"],
1836
2322
  cut: ["file", "out", "overwrite"],
@@ -1839,17 +2325,90 @@ From a built checkout: npm run studio -- <command>`,
1839
2325
  "sheet pack": ["file"],
1840
2326
  "sheet check": ["file"],
1841
2327
  "sheet export": ["file", "out", "preview", "overwrite"],
1842
- render: ["file", "out", "overwrite", "side", "photo", "name", "role", "number"],
2328
+ render: ["file", "out", "overwrite", "side", "photo", "name", "role", "number", "appearance"],
1843
2329
  validate: ["file", "out"]
1844
2330
  };
1845
2331
  if (!(command in allowed)) throw new InputError(`Unknown command: ${command}`);
2332
+ traceCommand = command;
1846
2333
  for (const flag of Object.keys(values))
1847
- if (!["json", "help", "version", ...allowed[command]].includes(flag))
2334
+ if (!["json", "help", "version", "compact", "trace", ...allowed[command]].includes(flag))
1848
2335
  throw new InputError(`--${flag} is not supported by ${command}`);
1849
2336
  const progress = (data) => process.stderr.write(`${JSON.stringify(data)}
1850
2337
  `);
1851
2338
  if (values.product && !["card", "badge", "sticker"].includes(values.product))
1852
2339
  throw new InputError("--product must be card, badge or sticker.");
2340
+ if (command.startsWith("kit ")) {
2341
+ const required = (value, flag) => {
2342
+ if (!value) throw new InputError(`Supply --${flag}.`);
2343
+ return value;
2344
+ };
2345
+ let result2;
2346
+ if (command === "kit extract") {
2347
+ result2 = await extractKit(
2348
+ required(values.file, "file"),
2349
+ required(values.out, "out"),
2350
+ values.params ? await readParams(values.params) : {}
2351
+ );
2352
+ } else if (command === "kit new") {
2353
+ const path2 = await newPath(required(values.out, "out"));
2354
+ const kit = kitDocumentSchema.parse({
2355
+ format: "makeables-kit",
2356
+ version: 1,
2357
+ name: required(values.name, "name"),
2358
+ items: [],
2359
+ sheets: []
2360
+ });
2361
+ await writeAtomic(path2, JSON.stringify(kit, null, 2), true);
2362
+ result2 = { path: path2, kit };
2363
+ } else if (command === "kit add" || command === "kit sheet") {
2364
+ const path2 = required(values.file, "file");
2365
+ const kit = await addKitMember(
2366
+ path2,
2367
+ required(values.ref, "ref"),
2368
+ required(values.id, "id"),
2369
+ command === "kit add" ? "items" : "sheets"
2370
+ );
2371
+ await writeAtomic(path2, JSON.stringify(kit, null, 2));
2372
+ result2 = { path: resolve7(path2), items: kit.items.length, sheets: kit.sheets.length };
2373
+ } else if (command === "kit import") {
2374
+ result2 = await importKit(required(values.file, "file"), required(values.out, "out"));
2375
+ } else if (command === "kit inspect" && !required(values.file, "file").toLowerCase().endsWith(".zip")) {
2376
+ const value = JSON.parse(await readFile7(required(values.file, "file"), "utf8"));
2377
+ if (value.format === "makeables-kit") result2 = await readKit(required(values.file, "file"));
2378
+ else {
2379
+ const pkg = await readSubmission(required(values.file, "file"));
2380
+ if (pkg.manifest.product !== "kit")
2381
+ throw new InputError("Choose a kit document or package.");
2382
+ result2 = pkg.manifest;
2383
+ }
2384
+ } else {
2385
+ const { pkg, bytes } = await exportKit(required(values.file, "file"));
2386
+ if (command === "kit inspect") result2 = pkg.manifest;
2387
+ else {
2388
+ const out = resolve7(required(values.out, "out"));
2389
+ const rendering = command === "kit render";
2390
+ if (!out.endsWith(rendering ? ".png" : ".zip"))
2391
+ throw new InputError(rendering ? "--out must end in .png." : "--out must end in .zip.");
2392
+ const handle2 = await open4(
2393
+ values.overwrite ? out : await newPath(out),
2394
+ values.overwrite ? "w" : "wx"
2395
+ );
2396
+ try {
2397
+ await handle2.writeFile(rendering ? pkg.files[pkg.manifest.front] : bytes);
2398
+ } finally {
2399
+ await handle2.close();
2400
+ }
2401
+ result2 = {
2402
+ path: out,
2403
+ product: "kit",
2404
+ title: pkg.manifest.title,
2405
+ bytes: rendering ? pkg.files[pkg.manifest.front].length : bytes.length
2406
+ };
2407
+ }
2408
+ }
2409
+ emit(result2, ["makeables skills get kits"], "Kit ready.", json);
2410
+ return;
2411
+ }
1853
2412
  if (command.startsWith("wallet ")) {
1854
2413
  const walletProgress = (data) => {
1855
2414
  if (json) return progress(data);
@@ -2015,7 +2574,7 @@ Run makeables skills get core for setup.`,
2015
2574
  }
2016
2575
  if (command === "mcp") {
2017
2576
  try {
2018
- const { runMcp } = await import("./mcp-54KV55QM.js");
2577
+ const { runMcp } = await import("./mcp-VS5XYXVM.js");
2019
2578
  await runMcp(values.root ?? process.cwd());
2020
2579
  } catch (error) {
2021
2580
  process.stderr.write(
@@ -2071,7 +2630,7 @@ Keep this process running while you design.`,
2071
2630
  if (positionals.length !== 3) throw new InputError("Supply one discovered tool name.");
2072
2631
  if (values.params?.startsWith("@")) {
2073
2632
  const path2 = values.params.slice(1);
2074
- const info = await lstat2(path2);
2633
+ const info = await lstat4(path2);
2075
2634
  if (!info.isFile() || info.size > 12e6)
2076
2635
  throw new InputError("Expected a params file below 12 MB.");
2077
2636
  params = JSON.parse(await readFile7(path2, "utf8"));
@@ -2157,7 +2716,7 @@ ${styles.map((entry) => `${column(entry.id ?? "", 25)}${entry.name}`).join("\n")
2157
2716
  const path2 = values.out ? await newPath(values.out) : null;
2158
2717
  const dryRun = Boolean(values["dry-run"]);
2159
2718
  if (path2 && !dryRun) {
2160
- const handle2 = await open2(path2, "wx", 384);
2719
+ const handle2 = await open4(path2, "wx", 384);
2161
2720
  try {
2162
2721
  await handle2.writeFile(`${JSON.stringify(design, null, 2)}
2163
2722
  `);
@@ -2180,8 +2739,8 @@ ${dryRun ? "Dry run. No files changed." : "2 faces \xB7 editable layers"}`,
2180
2739
  return;
2181
2740
  }
2182
2741
  if (!values.file) throw new InputError("Supply --file to validate a document.");
2183
- const path = resolve6(values.file);
2184
- const handle = await open2(path, "r");
2742
+ const path = resolve7(values.file);
2743
+ const handle = await open4(path, "r");
2185
2744
  let input;
2186
2745
  try {
2187
2746
  const info = await handle.stat();
@@ -2206,7 +2765,7 @@ Schema and layout constraints passed.`,
2206
2765
  }
2207
2766
  async function readParams(value) {
2208
2767
  return JSON.parse(
2209
- value.startsWith("@") ? await readFile7(resolve6(value.slice(1)), "utf8") : value
2768
+ value.startsWith("@") ? await readFile7(resolve7(value.slice(1)), "utf8") : value
2210
2769
  );
2211
2770
  }
2212
2771
  async function cardCommand(command, values, json) {
@@ -2268,7 +2827,7 @@ ${path ?? `No file written. Add --out ${kind}.json.`}`,
2268
2827
  return true;
2269
2828
  }
2270
2829
  if (command === "material map") {
2271
- const path = resolve6(need(file, "file"));
2830
+ const path = resolve7(need(file, "file"));
2272
2831
  const next = await bindStickerMap(
2273
2832
  await readBadge(path),
2274
2833
  need(values.id, "id"),
@@ -2284,7 +2843,7 @@ ${path ?? `No file written. Add --out ${kind}.json.`}`,
2284
2843
  return true;
2285
2844
  }
2286
2845
  if (command === "material set") {
2287
- const path = resolve6(need(file, "file"));
2846
+ const path = resolve7(need(file, "file"));
2288
2847
  const design = await readBadge(path);
2289
2848
  const raw = need(values.params, "params");
2290
2849
  const input = JSON.parse(raw.startsWith("@") ? await readFile7(raw.slice(1), "utf8") : raw);
@@ -2367,11 +2926,13 @@ ${path ?? `No file written. Add --out ${kind}.json.`}`,
2367
2926
  if (command === "layer add") {
2368
2927
  const path = need(file, "file");
2369
2928
  const face = side(values.side);
2929
+ if (values["relative-to"] && !["cwd", "document"].includes(String(values["relative-to"])))
2930
+ throw new InputError("--relative-to must be document or cwd.");
2370
2931
  const design = await addBadgeSticker(
2371
2932
  await readBadge(path),
2372
2933
  face,
2373
2934
  need(values.sticker, "sticker"),
2374
- dirname3(resolve6(path))
2935
+ values["relative-to"] === "cwd" ? process.cwd() : dirname5(resolve7(path))
2375
2936
  );
2376
2937
  await writeAtomic(path, `${JSON.stringify(design, null, 2)}
2377
2938
  `);
@@ -2386,7 +2947,7 @@ ${path ?? `No file written. Add --out ${kind}.json.`}`,
2386
2947
  }
2387
2948
  if (command.startsWith("sheet ")) {
2388
2949
  const write = async (target, bytes) => {
2389
- const handle = await open2(target, values.overwrite ? "w" : "wx", 420);
2950
+ const handle = await open4(target, values.overwrite ? "w" : "wx", 420);
2390
2951
  try {
2391
2952
  await handle.writeFile(bytes);
2392
2953
  } finally {
@@ -2465,9 +3026,9 @@ ${path ?? `No file written. Add --out ${kind}.json.`}`,
2465
3026
  throw new InputError(
2466
3027
  `The sheet is not printable. Run makeables sheet check --file ${JSON.stringify(path)}.`
2467
3028
  );
2468
- const out = resolve6(need(values.out, "out"));
3029
+ const out = resolve7(need(values.out, "out"));
2469
3030
  if (!out.endsWith(".pdf")) throw new InputError("--out must end in .pdf.");
2470
- const preview = typeof values.preview === "string" ? resolve6(values.preview) : void 0;
3031
+ const preview = typeof values.preview === "string" ? resolve7(values.preview) : void 0;
2471
3032
  if (preview && !preview.endsWith(".png")) throw new InputError("--preview must end in .png.");
2472
3033
  const targets = [out, ...preview ? [preview] : []];
2473
3034
  if (!values.overwrite) for (const target of targets) await newPath(target);
@@ -2485,12 +3046,12 @@ ${path ?? `No file written. Add --out ${kind}.json.`}`,
2485
3046
  if (command === "cut") {
2486
3047
  const design = await readBadge(need(file, "file"));
2487
3048
  if (design.product !== "sticker") throw new InputError("cut applies to sticker designs.");
2488
- const out = typeof values.out === "string" ? resolve6(values.out) : void 0;
3049
+ const out = typeof values.out === "string" ? resolve7(values.out) : void 0;
2489
3050
  if (out && !out.endsWith(".svg")) throw new InputError("--out must end in .svg.");
2490
3051
  const target = out && (values.overwrite ? out : await newPath(out));
2491
3052
  const { cut, svg } = await stickerPrintCut(design);
2492
3053
  if (target) {
2493
- const handle = await open2(target, values.overwrite ? "w" : "wx", 420);
3054
+ const handle = await open4(target, values.overwrite ? "w" : "wx", 420);
2494
3055
  try {
2495
3056
  await handle.writeFile(svg);
2496
3057
  } finally {
@@ -2522,15 +3083,18 @@ ${path ?? `No file written. Add --out ${kind}.json.`}`,
2522
3083
  return true;
2523
3084
  }
2524
3085
  if (command === "render") {
3086
+ if (values.appearance && !["flat", "material"].includes(String(values.appearance)))
3087
+ throw new InputError("--appearance must be flat or material.");
3088
+ const appearance = values.appearance;
2525
3089
  const path = need(file, "file");
2526
- const out = resolve6(need(values.out, "out"));
3090
+ const out = resolve7(need(values.out, "out"));
2527
3091
  if (!out.endsWith(".png")) throw new InputError("--out must end in .png.");
2528
3092
  const target = values.overwrite ? out : await newPath(out);
2529
3093
  const number = values.number === void 0 ? void 0 : Number(values.number);
2530
3094
  if (number !== void 0 && !Number.isInteger(number))
2531
3095
  throw new InputError("--number must be an integer.");
2532
3096
  const face = side(values.side);
2533
- const value = JSON.parse(await readFile7(resolve6(path), "utf8"));
3097
+ const value = JSON.parse(await readFile7(resolve7(path), "utf8"));
2534
3098
  const pkg = value?.format === "makeables-package" ? await readPackageJson(value) : void 0;
2535
3099
  if (pkg && ["photo", "name", "role", "number"].some((key) => values[key] !== void 0))
2536
3100
  throw new InputError(
@@ -2542,11 +3106,12 @@ ${path ?? `No file written. Add --out ${kind}.json.`}`,
2542
3106
  if (pkg && !previewPath) throw new InputError("This package has no back preview.");
2543
3107
  const card = design.product === "card" || design.product === "sticker";
2544
3108
  const { png, person, samplePortrait } = pkg && previewPath ? {
2545
- png: Buffer.from(pkg.files[previewPath]),
3109
+ png: appearance && draft ? await renderDraftFace(draft, face, appearance) : Buffer.from(pkg.files[previewPath]),
2546
3110
  person: draft?.participant,
2547
3111
  samplePortrait: false
2548
3112
  } : await renderBadge(design, {
2549
3113
  face,
3114
+ appearance,
2550
3115
  photo: typeof values.photo === "string" ? values.photo : void 0,
2551
3116
  person: {
2552
3117
  ...typeof values.name === "string" ? { name: values.name } : {},
@@ -2554,7 +3119,7 @@ ${path ?? `No file written. Add --out ${kind}.json.`}`,
2554
3119
  ...number === void 0 ? {} : { number }
2555
3120
  }
2556
3121
  });
2557
- const handle = await open2(target, values.overwrite ? "w" : "wx", 420);
3122
+ const handle = await open4(target, values.overwrite ? "w" : "wx", 420);
2558
3123
  try {
2559
3124
  await handle.writeFile(png);
2560
3125
  } finally {
@@ -2569,12 +3134,13 @@ ${path ?? `No file written. Add --out ${kind}.json.`}`,
2569
3134
  side: face,
2570
3135
  width,
2571
3136
  height,
3137
+ appearance: appearance ?? (pkg && design.product === "sticker" ? "material" : "flat"),
2572
3138
  ...card ? {} : { person, samplePortrait }
2573
3139
  },
2574
3140
  [
2575
3141
  `Open ${target} to review.`,
2576
3142
  ...portrait ? ["Pass --photo <image> to use the person's portrait."] : [],
2577
- ...pkg && design.product === "sticker" ? ["The transparent PNG includes the sticker material."] : ["This is flat artwork. Use an available integrated preview for live materials."]
3143
+ ...(appearance === "material" || pkg && !appearance) && design.product === "sticker" ? ["The transparent PNG includes the sticker material."] : ["This is flat artwork. Use an available integrated preview for live materials."]
2578
3144
  ],
2579
3145
  `${style("Rendered", "1", !json)} ${face} ${target}${portrait ? "\n note: sample portrait; pass --photo" : ""}`,
2580
3146
  json
@@ -2592,7 +3158,7 @@ ${path ?? `No file written. Add --out ${kind}.json.`}`,
2592
3158
  return true;
2593
3159
  }
2594
3160
  if (command === "validate") {
2595
- const path = resolve6(need(file, "file"));
3161
+ const path = resolve7(need(file, "file"));
2596
3162
  if (extname4(path).toLowerCase() === ".svg") {
2597
3163
  const source = await readFile7(path, "utf8");
2598
3164
  const result2 = checkSubmittedSvg(source);
@@ -2633,7 +3199,7 @@ ${path ?? `No file written. Add --out ${kind}.json.`}`,
2633
3199
  return false;
2634
3200
  }
2635
3201
  run().catch((error) => {
2636
- const userError = error instanceof InputError || error instanceof WalletError && error.details.code === "INVALID_INPUT" || error instanceof SyntaxError || [
3202
+ const userError = error instanceof InputError || error instanceof ZodError || error instanceof WalletError && error.details.code === "INVALID_INPUT" || error instanceof SyntaxError || [
2637
3203
  "ERR_PARSE_ARGS_UNKNOWN_OPTION",
2638
3204
  "ERR_PARSE_ARGS_INVALID_OPTION_VALUE",
2639
3205
  "ERR_PARSE_ARGS_UNEXPECTED_POSITIONAL",
@@ -2652,10 +3218,28 @@ run().catch((error) => {
2652
3218
  },
2653
3219
  nextSteps
2654
3220
  };
2655
- if (machineOutput(process.argv.includes("--json")))
3221
+ traceCode = payload.error.code;
3222
+ if (!stdoutClosed && machineOutput(process.argv.includes("--json")))
2656
3223
  process.stdout.write(`${JSON.stringify(payload)}
2657
3224
  `);
2658
3225
  process.stderr.write(`makeables: ${error.message}
2659
3226
  `);
2660
3227
  process.exitCode = exitCode;
3228
+ }).finally(async () => {
3229
+ if (!traceFile) return;
3230
+ try {
3231
+ const supplied = process.env.MAKEABLES_TRACE_SESSION;
3232
+ await appendTrace(resolve7(traceFile), {
3233
+ sessionId: supplied && /^[a-zA-Z0-9_-]{1,80}$/.test(supplied) ? supplied : randomUUID3(),
3234
+ version,
3235
+ command: traceCommand,
3236
+ startedAt,
3237
+ endedAt: (/* @__PURE__ */ new Date()).toISOString(),
3238
+ durationMs: Math.round(performance.now() - started),
3239
+ ok: !traceCode,
3240
+ ...traceCode ? { code: traceCode } : {}
3241
+ });
3242
+ } catch {
3243
+ process.stderr.write("makeables: Could not write the optional local trace.\n");
3244
+ }
2661
3245
  });