makeables 0.9.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,4 +1,24 @@
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,
@@ -9,61 +29,53 @@ import {
9
29
  cardStyles,
10
30
  checkSubmittedSvg,
11
31
  connectAccount,
12
- createStickerSheet,
32
+ createStickerDesign,
13
33
  defaultStickerMaterial,
14
34
  designCatalog,
15
35
  designProfile,
16
36
  draftFromPackage,
17
37
  findCardStyle,
18
38
  findDesign,
19
- getSkill,
20
39
  hashBytes,
21
40
  jsonBytes,
41
+ kitDocumentSchema,
22
42
  listCardDesigns,
23
43
  logout,
24
44
  newBadge,
25
45
  newCard,
26
46
  openBrowser,
27
- packSheet,
28
47
  packageJson,
29
- placeNear,
30
- prepareSheetPrint,
31
48
  readBadge,
32
49
  readPackageJson,
33
50
  readPackageZip,
34
51
  readSubmission,
35
52
  removeBadgeLayer,
36
53
  renderBadge,
54
+ renderDraftFace,
37
55
  setBadgeLayer,
38
- sheetPrintPdf,
39
- sheetSizes,
40
- sheetSpec,
41
- sheetTypeIds,
42
- sheetTypes,
43
56
  side,
44
- skills,
45
57
  startStudio,
46
58
  stickerCut,
47
59
  stickerCutSvg,
48
60
  stickerMaterialSchema,
49
- stickerSheetSchema,
50
61
  stickerStyles,
51
62
  studioRequest,
52
63
  submitDesign,
53
64
  summarizeBadgeLayers,
54
65
  svgNodeSchema,
55
66
  svgNodeXml,
56
- version,
57
67
  writeAtomic
58
- } from "./chunk-RFIQNWNF.js";
68
+ } from "./chunk-G6OTYITI.js";
59
69
 
60
70
  // src/cli.ts
61
71
  import { spawnSync as spawnSync2 } from "child_process";
62
- import { constants as constants3 } from "fs";
63
- import { access, lstat as lstat4, open as open3, readFile as readFile8 } from "fs/promises";
64
- import { dirname as dirname4, extname as extname4, resolve as resolve7 } 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";
65
76
  import { parseArgs } from "util";
66
77
  import sharp8 from "sharp";
78
+ import { ZodError } from "zod";
67
79
  import { zodToJsonSchema } from "zod-to-json-schema";
68
80
 
69
81
  // src/card-import.ts
@@ -274,6 +286,199 @@ async function extractImage(file, out) {
274
286
  return { path, bytes: bytes.length };
275
287
  }
276
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
+
277
482
  // src/presentation.ts
278
483
  import { stripVTControlCharacters } from "util";
279
484
  function machineOutput(json) {
@@ -298,11 +503,11 @@ function banner(version2) {
298
503
  // src/publish.ts
299
504
  import { createHash as createHash2, randomUUID } from "crypto";
300
505
  import { constants } from "fs";
301
- 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";
302
507
  import { homedir } from "os";
303
- import { dirname, join, resolve as resolve2 } from "path";
304
- import sharp2 from "sharp";
305
- 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";
306
511
  async function saveBundle(url, out) {
307
512
  const state = await studioRequest(url, "/call", {
308
513
  name: "badge_inspect",
@@ -323,7 +528,7 @@ async function saveBundle(url, out) {
323
528
  result.error?.message ?? "The editor could not export a bundle. Refresh it after saving the current draft."
324
529
  );
325
530
  const pkg = await readPackageJson(result.data);
326
- const path = resolve2(out);
531
+ const path = resolve3(out);
327
532
  const file = await open(path, "wx", 384);
328
533
  try {
329
534
  await file.writeFile(`${JSON.stringify(packageJson(pkg))}
@@ -344,48 +549,48 @@ async function saveBundle(url, out) {
344
549
  published: false
345
550
  };
346
551
  }
347
- var receiptSchema = z.object({
348
- id: z.string().uuid(),
349
- version: z.number().int().positive(),
350
- state: z.enum(["published", "withdrawn"]),
351
- 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()
352
557
  });
353
558
 
354
559
  // src/reference-image.ts
355
560
  import { spawnSync } from "child_process";
356
561
  import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "fs";
357
562
  import { tmpdir } from "os";
358
- import { join as join2, resolve as resolve3 } from "path";
563
+ import { join as join3, resolve as resolve4 } from "path";
359
564
  import { DOMParser as DOMParser2, XMLSerializer } from "@xmldom/xmldom";
360
- import sharp3 from "sharp";
361
- import { z as z2 } from "zod";
565
+ import sharp4 from "sharp";
566
+ import { z as z3 } from "zod";
362
567
  var W = 1200;
363
568
  var H = 756;
364
569
  var N = W * H;
365
- var optionSchema = z2.object({
366
- crop: z2.string().optional(),
367
- patch: z2.array(z2.string()).default([]),
368
- "edge-band": z2.coerce.number().min(0).max(1e3).optional(),
369
- "bg-tol": z2.coerce.number().min(0).max(765).optional(),
370
- title: z2.string().min(1).max(200).optional(),
371
- desc: z2.string().max(5e3).optional(),
372
- credit: z2.string().max(5e3).optional(),
373
- tones: z2.coerce.number().int().min(2).max(12).optional(),
374
- ink: z2.coerce.number().min(0).max(1).optional(),
375
- raster: z2.boolean().optional(),
376
- points: z2.array(z2.string()).max(100).optional(),
377
- box: z2.string().optional(),
378
- rows: z2.string().optional(),
379
- cols: z2.string().optional(),
380
- dark: z2.coerce.number().min(0).max(765).optional(),
381
- colors: z2.coerce.number().int().min(1).max(12).optional(),
382
- at: z2.string().optional(),
383
- up: z2.coerce.number().int().min(1).max(8).optional(),
384
- k: z2.coerce.number().min(0).max(1).optional(),
385
- fill: z2.string().regex(/^#[a-fA-F0-9]{6}$/).optional(),
386
- top: z2.enum(["left", "right"]).optional(),
387
- upright: z2.enum(["left", "right"]).optional(),
388
- "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()
389
594
  }).strict();
390
595
  function fail(message) {
391
596
  throw new InputError(message);
@@ -404,7 +609,7 @@ function validBox(values, width, height) {
404
609
  }
405
610
  var lumOf = (r, g, b) => 0.299 * r + 0.587 * g + 0.114 * b;
406
611
  async function detect(file) {
407
- 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 });
408
613
  const { width: w, height: h } = info;
409
614
  const px = (x, y) => {
410
615
  const i = (y * w + x) * 3;
@@ -480,7 +685,7 @@ async function detect(file) {
480
685
  async function prepare(file, o) {
481
686
  if (!o.crop || !o.out) fail('prepare needs --out <dir> and --params with {"crop":"x,y,w,h"}.');
482
687
  const [cx, cy, cw, ch] = nums(o.crop, 4, "crop");
483
- const meta = await sharp3(file, { limitInputPixels: 24e6 }).metadata();
688
+ const meta = await sharp4(file, { limitInputPixels: 24e6 }).metadata();
484
689
  validBox([cx, cy, cw, ch], meta.width ?? 0, meta.height ?? 0);
485
690
  if (Math.abs(cw / ch / (W / H) - 1) > 0.03)
486
691
  fail(
@@ -493,12 +698,12 @@ async function prepare(file, o) {
493
698
  if (feather < 0 || ![0, 1].includes(flop))
494
699
  fail("Patch feather must be nonnegative and flop must be 0 or 1.");
495
700
  }
496
- const out = resolve3(o.out);
701
+ const out = resolve4(o.out);
497
702
  mkdirSync(out);
498
703
  const layers = [];
499
704
  for (const spec of o.patch) {
500
705
  const [dx, dy, pw, ph, sx, sy, feather = 6, flop = 0] = nums(spec, 6, "patch");
501
- 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();
502
707
  const rgba = Buffer.alloc(pw * ph * 4);
503
708
  for (let y = 0; y < ph; y++)
504
709
  for (let x = 0; x < pw; x++) {
@@ -510,10 +715,10 @@ async function prepare(file, o) {
510
715
  }
511
716
  layers.push({ input: rgba, raw: { width: pw, height: ph, channels: 4 }, left: dx, top: dy });
512
717
  }
513
- const patched = await sharp3(file).removeAlpha().composite(layers).png().toBuffer();
514
- 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 });
515
720
  const px = card.data, CW = card.info.width, CH = card.info.height;
516
- 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 });
517
722
  const sp = (x, y) => {
518
723
  const i = (y * src.info.width + x) * 3;
519
724
  return [src.data[i], src.data[i + 1], src.data[i + 2]];
@@ -553,14 +758,14 @@ async function prepare(file, o) {
553
758
  if (y > 0) queue.push(i - CW);
554
759
  if (y < CH - 1) queue.push(i + CW);
555
760
  }
556
- const cardPng = await sharp3(px, { raw: card.info }).png().toBuffer();
557
- const resized = sharp3(cardPng).resize(W, H, { fit: "fill", kernel: "lanczos3" });
558
- await resized.clone().png().toFile(join2(out, "card.png"));
559
- 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"));
560
765
  const rgb = await resized.clone().toColourspace("srgb").raw().toBuffer();
561
766
  const g = await resized.clone().toColourspace("b-w").raw().toBuffer();
562
- writeFileSync(join2(out, "rgb.raw"), rgb);
563
- writeFileSync(join2(out, "lum.raw"), g);
767
+ writeFileSync(join3(out, "rgb.raw"), rgb);
768
+ writeFileSync(join3(out, "lum.raw"), g);
564
769
  const GW = W * 2, GH = H * 2, big = Buffer.alloc(GW * GH);
565
770
  for (let y = 0; y < GH; y++) {
566
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;
@@ -572,14 +777,14 @@ async function prepare(file, o) {
572
777
  );
573
778
  }
574
779
  }
575
- writeFileSync(join2(out, "ink.pgm"), Buffer.concat([Buffer.from(`P5
780
+ writeFileSync(join3(out, "ink.pgm"), Buffer.concat([Buffer.from(`P5
576
781
  ${GW} ${GH}
577
782
  255
578
783
  `), big]));
579
784
  const hex = paper.map((v) => Math.round(v).toString(16).padStart(2, "0")).join("");
580
785
  return {
581
786
  out,
582
- preview: join2(out, "card.png"),
787
+ preview: join3(out, "card.png"),
583
788
  paper: `#${hex}`,
584
789
  patches: o.patch.length,
585
790
  filledEdgePixels: filled,
@@ -604,10 +809,10 @@ function potrace(input, extra) {
604
809
  }
605
810
  var esc = (s) => String(s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
606
811
  function vectorize(dir, o) {
607
- dir = resolve3(dir);
812
+ dir = resolve4(dir);
608
813
  if (!o.out || !o.title) fail("vectorize needs --out <file.svg> and --title");
609
814
  const K = Number(o.tones ?? 7);
610
- 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"));
611
816
  if (lum.length !== N || rgb.length !== N * 3)
612
817
  fail("Expected an intact directory created by image prepare.");
613
818
  let c = [...Array(K)].map((_, i) => 30 + i * 200 / (K - 1));
@@ -657,11 +862,11 @@ function vectorize(dir, o) {
657
862
  const L = used.length;
658
863
  const inkK = o.ink !== void 0 ? Number(o.ink) : L > 1 ? (c[used[0]] + c[used[1]]) / 2 / 255 : 0.25;
659
864
  const groups = [];
660
- const maskFile = join2(dir, "mask.pbm");
865
+ const maskFile = join3(dir, "mask.pbm");
661
866
  for (let j = o.raster ? 0 : L - 2; j >= 0; j--) {
662
867
  let traced;
663
868
  if (j === 0) {
664
- traced = potrace(join2(dir, "ink.pgm"), [
869
+ traced = potrace(join3(dir, "ink.pgm"), [
665
870
  "-k",
666
871
  inkK.toFixed(4),
667
872
  "-t",
@@ -684,7 +889,7 @@ ${W} ${H}
684
889
  if (traced) groups.push(`<g transform="${traced.tr}" fill="${colors[j]}">${traced.paths}</g>`);
685
890
  }
686
891
  const base = o.raster ? `<rect width="${W}" height="${H}" fill="${colors[L - 1]}"/>
687
- <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]}"/>`;
688
893
  const title = esc(o.title), desc = esc(o.desc ?? `${o.title} card artwork.`);
689
894
  const meta = esc(o.credit ?? "Original artwork traced from a reference image.");
690
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}">
@@ -697,9 +902,9 @@ ${groups.join("\n")}
697
902
  `;
698
903
  const checked = checkSubmittedSvg(svg);
699
904
  if (!checked.ok) fail(checked.error);
700
- writeFileSync(resolve3(o.out), svg, { flag: "wx" });
905
+ writeFileSync(resolve4(o.out), svg, { flag: "wx" });
701
906
  return {
702
- out: resolve3(o.out),
907
+ out: resolve4(o.out),
703
908
  bytes: Buffer.byteLength(svg),
704
909
  mode: o.raster ? "raster background + vector ink" : "pure vector",
705
910
  tones: colors,
@@ -707,7 +912,7 @@ ${groups.join("\n")}
707
912
  };
708
913
  }
709
914
  async function rawImage(file) {
710
- 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 });
711
916
  const px = (x, y) => {
712
917
  if (!Number.isInteger(x) || !Number.isInteger(y) || x < 0 || y < 0 || x >= info.width || y >= info.height)
713
918
  fail("Pixel coordinates are outside the image.");
@@ -806,14 +1011,14 @@ async function glyphs(file, o) {
806
1011
  if (!o.box || !o.at) fail("glyphs needs --box x,y,w,h and --at X,Y");
807
1012
  const [bx, by, bw, bh] = nums(o.box, 4, "box");
808
1013
  const [ax, ay] = nums(o.at, 2, "at");
809
- const meta = await sharp3(file, { limitInputPixels: 24e6 }).metadata();
1014
+ const meta = await sharp4(file, { limitInputPixels: 24e6 }).metadata();
810
1015
  validBox([bx, by, bw, bh], meta.width ?? 0, meta.height ?? 0);
811
1016
  const up = Number(o.up ?? 4);
812
1017
  if (bw * bh * up * up > 24e6)
813
1018
  fail("Trace region is too large; lower up or choose a smaller box.");
814
- 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 });
815
- const temp = mkdtempSync(join2(tmpdir(), "makeables-glyphs-"));
816
- 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");
817
1022
  writeFileSync(pgm, Buffer.concat([Buffer.from(`P5
818
1023
  ${info.width} ${info.height}
819
1024
  255
@@ -832,12 +1037,12 @@ ${info.width} ${info.height}
832
1037
  const paths = [...svg.matchAll(/ d="([^"]+)"/g)].map((m) => m[1].replace(/\s+/g, " ")).join(" ");
833
1038
  if (!tr || !paths) fail("nothing dark enough to trace in that box; lower --k or check the box");
834
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>`;
835
- if (o.out) writeFileSync(resolve3(o.out), snippet, { flag: "wx" });
1040
+ if (o.out) writeFileSync(resolve4(o.out), snippet, { flag: "wx" });
836
1041
  return {
837
1042
  box: { x: bx, y: by, w: bw, h: bh },
838
1043
  at: { x: ax, y: ay },
839
1044
  bytes: snippet.length,
840
- out: o.out ? resolve3(o.out) : null,
1045
+ out: o.out ? resolve4(o.out) : null,
841
1046
  snippet: o.out ? void 0 : snippet
842
1047
  };
843
1048
  }
@@ -859,28 +1064,28 @@ function rotate(file, o) {
859
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>`;
860
1065
  const checked = checkSubmittedSvg(svg);
861
1066
  if (!checked.ok) fail(checked.error);
862
- writeFileSync(resolve3(o.out), svg, { flag: "wx" });
863
- 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) };
864
1069
  }
865
1070
  async function compare(file, ref, o) {
866
1071
  if (!o.out) fail("compare needs --out <file.png>");
867
- let card = sharp3(readFileSync(file));
1072
+ let card = sharp4(readFileSync(file));
868
1073
  if (o.upright) card = card.rotate(o.upright === "left" ? 90 : -90);
869
1074
  const cardPng = await card.png().toBuffer();
870
- let reference = sharp3(ref);
1075
+ let reference = sharp4(ref);
871
1076
  if (o["ref-box"]) {
872
1077
  const [x, y, w, h] = nums(o["ref-box"], 4, "ref-box");
873
1078
  reference = reference.extract({ left: x, top: y, width: w, height: h });
874
1079
  }
875
1080
  const height = 900;
876
- const a = await sharp3(cardPng).resize({ height }).png().toBuffer({ resolveWithObject: true });
877
- 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 });
878
1083
  const gap = 24, width = a.info.width + b.info.width + gap * 3;
879
- 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([
880
1085
  { input: a.data, left: gap, top: gap },
881
1086
  { input: b.data, left: a.info.width + gap * 2, top: gap }
882
- ]).png().toFile(resolve3(o.out));
883
- 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" };
884
1089
  }
885
1090
  var imageTools = [
886
1091
  "detect",
@@ -900,7 +1105,7 @@ async function referenceImage(command, file, options, out, reference) {
900
1105
  parsed.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; ")
901
1106
  );
902
1107
  const o = { ...parsed.data, out };
903
- 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.");
904
1109
  if (!existsSync(file)) fail("Input file does not exist.");
905
1110
  switch (command) {
906
1111
  case "detect":
@@ -928,175 +1133,6 @@ async function referenceImage(command, file, options, out, reference) {
928
1133
  }
929
1134
  }
930
1135
 
931
- // src/sheet.ts
932
- import { readFile as readFile4 } from "fs/promises";
933
- import { resolve as resolve4 } from "path";
934
- import { deflateSync } from "zlib";
935
- import { createCanvas } from "@napi-rs/canvas";
936
- import sharp4 from "sharp";
937
- async function readSheet(file) {
938
- const value = JSON.parse(await readFile4(resolve4(file), "utf8"));
939
- const parsed = stickerSheetSchema.safeParse(value);
940
- if (!parsed.success)
941
- throw new InputError(
942
- `Invalid sheet: ${parsed.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; ")}`
943
- );
944
- return parsed.data;
945
- }
946
- function newSheet(type, size, name) {
947
- if (typeof type !== "string" || !sheetTypeIds.includes(type))
948
- throw new InputError(`--type must be one of ${sheetTypeIds.join(", ")}.`);
949
- const sheet = createStickerSheet(
950
- type,
951
- typeof name === "string" ? name : void 0
952
- );
953
- if (size !== void 0) {
954
- if (typeof size !== "string" || !(size in sheetSizes))
955
- throw new InputError(`--size must be one of ${Object.keys(sheetSizes).join(", ")}.`);
956
- sheet.size = size;
957
- }
958
- return parse(sheet);
959
- }
960
- function parse(sheet) {
961
- const parsed = stickerSheetSchema.safeParse(sheet);
962
- if (!parsed.success)
963
- throw new InputError(parsed.error.issues.map((issue) => issue.message).join("; "));
964
- return parsed.data;
965
- }
966
- async function readSticker(source) {
967
- const style2 = stickerStyles.find((entry) => entry.source === source);
968
- if (style2) return badgeDesignSchema.parse(structuredClone(style2));
969
- const design = badgeDesignSchema.parse(JSON.parse(await readFile4(resolve4(source), "utf8")));
970
- if (design.product !== "sticker") throw new InputError(`${source} is not a sticker design.`);
971
- return design;
972
- }
973
- var artCache = /* @__PURE__ */ new Map();
974
- function flatArt(design) {
975
- const key = JSON.stringify(design);
976
- let art = artCache.get(key);
977
- if (!art) {
978
- art = renderBadge(design, { face: "front", person: {} }).then(async ({ png }) => {
979
- const { data, info } = await sharp4(png).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
980
- return {
981
- width: info.width,
982
- height: info.height,
983
- rgba: new Uint8ClampedArray(data.buffer, data.byteOffset, data.length)
984
- };
985
- });
986
- artCache.set(key, art);
987
- }
988
- return art;
989
- }
990
- async function sheetArt(sheet) {
991
- return new Map(
992
- await Promise.all(
993
- sheet.items.map(async (item) => [item.id, await flatArt(item.design)])
994
- )
995
- );
996
- }
997
- function bounds(sheet) {
998
- const spec = sheetSpec(sheet);
999
- return { widthMm: spec.widthMm ?? 0, heightMm: spec.heightMm ?? 0 };
1000
- }
1001
- async function addToSheet(sheet, source, widthMm) {
1002
- const type = sheetTypes[sheet.type];
1003
- if (type.cut === "die-cut" && sheet.items.length)
1004
- throw new InputError("A single sticker file holds one sticker. Start another sheet.");
1005
- const width = widthMm === void 0 ? type.stickerWidthMm : Number(widthMm);
1006
- if (!Number.isFinite(width) || width < 5 || width > 300)
1007
- throw new InputError("--width-mm must be a number from 5 to 300.");
1008
- const design = await readSticker(source);
1009
- const base = (design.source ?? "sticker").replace(/[^a-z0-9-]/g, "").slice(0, 40) || "sticker";
1010
- let id = base;
1011
- for (let n = 2; sheet.items.some((item2) => item2.id === id); n++) id = `${base}-${n}`;
1012
- const spec = sheetSpec(sheet);
1013
- const item = {
1014
- id,
1015
- design,
1016
- x: (spec.widthMm ?? width) / 2,
1017
- y: (spec.heightMm ?? width) / 2,
1018
- rotation: 0,
1019
- widthMm: width
1020
- };
1021
- const next = parse({ ...sheet, items: [...sheet.items, item] });
1022
- if (type.cut === "die-cut") return { sheet: next, item, placed: true };
1023
- const print = prepareSheetPrint(next, await sheetArt(next));
1024
- const moved = placeNear(
1025
- spec,
1026
- bounds(next),
1027
- next.items.map((entry) => ({ item: entry, shape: print.shapes.get(entry.id) })),
1028
- id,
1029
- [item.x, item.y]
1030
- );
1031
- if (moved) next.items[next.items.length - 1] = moved;
1032
- return { sheet: next, item: moved ?? item, placed: Boolean(moved) };
1033
- }
1034
- async function packStickerSheet(sheet) {
1035
- if (sheetTypes[sheet.type].cut === "die-cut") return { sheet, missing: [] };
1036
- const print = prepareSheetPrint(sheet, await sheetArt(sheet));
1037
- const packed = packSheet(
1038
- sheetSpec(sheet),
1039
- bounds(sheet),
1040
- sheet.items.map((item) => ({ item, shape: print.shapes.get(item.id) })),
1041
- sheet.hero
1042
- );
1043
- return { sheet: parse({ ...sheet, items: packed.items }), missing: packed.missing };
1044
- }
1045
- async function checkSheet(sheet) {
1046
- return prepareSheetPrint(sheet, await sheetArt(sheet));
1047
- }
1048
- function sheetReport(print) {
1049
- return {
1050
- printable: print.printable,
1051
- widthMm: Number(print.widthMm.toFixed(2)),
1052
- heightMm: Number(print.heightMm.toFixed(2)),
1053
- stickers: print.items.length,
1054
- violations: print.violations,
1055
- issues: print.issues.map(({ id, issue }) => ({ id, ...issue }))
1056
- };
1057
- }
1058
- async function exportSheetPdf(sheet, print) {
1059
- return sheetPrintPdf(sheet, print, async (bytes) => new Uint8Array(deflateSync(bytes)));
1060
- }
1061
- function sheetPreviewPng(print, dpi = 150) {
1062
- const scale = dpi / 25.4;
1063
- const canvas = createCanvas(
1064
- Math.round(print.widthMm * scale),
1065
- Math.round(print.heightMm * scale)
1066
- );
1067
- const context = canvas.getContext("2d");
1068
- context.fillStyle = "#f4f3ef";
1069
- context.fillRect(0, 0, canvas.width, canvas.height);
1070
- for (const item of print.pdfItems) {
1071
- const art = createCanvas(item.art.width, item.art.height);
1072
- const data = art.getContext("2d").createImageData(item.art.width, item.art.height);
1073
- data.data.set(item.art.rgba);
1074
- art.getContext("2d").putImageData(data, 0, 0);
1075
- context.save();
1076
- context.translate(item.x * scale, item.y * scale);
1077
- context.rotate(item.rotation * Math.PI / 180);
1078
- context.drawImage(
1079
- art,
1080
- -item.widthMm / 2 * scale,
1081
- -item.heightMm / 2 * scale,
1082
- item.widthMm * scale,
1083
- item.heightMm * scale
1084
- );
1085
- context.restore();
1086
- context.strokeStyle = "#ff00ff";
1087
- context.lineWidth = Math.max(1, 0.2 * scale);
1088
- context.beginPath();
1089
- for (const loop of item.cut) {
1090
- for (const [i, [x, y]] of loop.entries())
1091
- if (i) context.lineTo(x * scale, y * scale);
1092
- else context.moveTo(x * scale, y * scale);
1093
- context.closePath();
1094
- }
1095
- context.stroke();
1096
- }
1097
- return canvas.toBuffer("image/png");
1098
- }
1099
-
1100
1136
  // src/sticker-cut.ts
1101
1137
  import sharp5 from "sharp";
1102
1138
  async function stickerPrintCut(design) {
@@ -1114,7 +1150,7 @@ async function stickerPrintCut(design) {
1114
1150
  }
1115
1151
 
1116
1152
  // src/sticker-material.ts
1117
- import { readFile as readFile5, stat as stat3 } from "fs/promises";
1153
+ import { readFile as readFile4, stat as stat3 } from "fs/promises";
1118
1154
  import { basename as basename2 } from "path";
1119
1155
  import sharp6 from "sharp";
1120
1156
  async function bindStickerMap(design, layerId, path) {
@@ -1124,7 +1160,7 @@ async function bindStickerMap(design, layerId, path) {
1124
1160
  const info = await stat3(path);
1125
1161
  if (!info.isFile() || info.size > 4e6)
1126
1162
  throw new InputError("Use an optical image below 4 MB.");
1127
- const bytes = await readFile5(path);
1163
+ const bytes = await readFile4(path);
1128
1164
  const meta = await sharp6(bytes, { limitInputPixels: 16777216 }).metadata();
1129
1165
  if (!meta.width || !meta.height || meta.width > 4096 || meta.height > 4096 || !["png", "webp"].includes(meta.format ?? ""))
1130
1166
  throw new InputError("Use an RGB PNG or WebP optical map up to 4096 \xD7 4096.");
@@ -1150,13 +1186,48 @@ async function bindStickerMap(design, layerId, path) {
1150
1186
  });
1151
1187
  }
1152
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
+
1153
1224
  // src/wallet.ts
1154
1225
  import { spawn } from "child_process";
1155
1226
  import { createHash as createHash3 } from "crypto";
1156
1227
  import { existsSync as existsSync2 } from "fs";
1157
- import { chmod as chmod2, lstat as lstat3, mkdir as mkdir3, mkdtemp as mkdtemp2, readFile as readFile7, rename as rename3, rm as rm4, writeFile as writeFile3 } from "fs/promises";
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";
1158
1229
  import { homedir as homedir3, tmpdir as tmpdir2 } from "os";
1159
- import { dirname as dirname3, extname as extname3, join as join6, resolve as resolve6 } from "path";
1230
+ import { dirname as dirname4, extname as extname3, join as join7, resolve as resolve6 } from "path";
1160
1231
  import sharp7 from "sharp";
1161
1232
 
1162
1233
  // src/wallet-release.json
@@ -1178,18 +1249,18 @@ var wallet_release_default = {
1178
1249
 
1179
1250
  // src/wallet-storage.ts
1180
1251
  import { randomUUID as randomUUID2 } from "crypto";
1181
- import { constants as constants2 } from "fs";
1182
- import { chmod, lstat, mkdir as mkdir2, open as open2, rename as rename2, rm as rm2 } from "fs/promises";
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";
1183
1254
  import { homedir as homedir2 } from "os";
1184
- import { dirname as dirname2, join as join3, resolve as resolve5 } from "path";
1255
+ import { dirname as dirname3, join as join4, resolve as resolve5 } from "path";
1185
1256
  function walletHome() {
1186
1257
  return resolve5(
1187
- process.env.MAKEABLES_WALLET_HOME ?? join3(homedir2(), "Library", "Application Support", "Makeables", "Wallet")
1258
+ process.env.MAKEABLES_WALLET_HOME ?? join4(homedir2(), "Library", "Application Support", "Makeables", "Wallet")
1188
1259
  );
1189
1260
  }
1190
1261
  async function privateDirectory(path) {
1191
1262
  const absolute = resolve5(path);
1192
- const parent = dirname2(absolute);
1263
+ const parent = dirname3(absolute);
1193
1264
  if (parent !== absolute) {
1194
1265
  try {
1195
1266
  const info = await lstat(parent);
@@ -1200,14 +1271,14 @@ async function privateDirectory(path) {
1200
1271
  await privateDirectory(parent);
1201
1272
  }
1202
1273
  }
1203
- await mkdir2(absolute, { recursive: true, mode: 448 });
1274
+ await mkdir3(absolute, { recursive: true, mode: 448 });
1204
1275
  if (!(await lstat(absolute)).isDirectory())
1205
1276
  throw new InputError("Wallet storage must be a directory, not a symbolic link.");
1206
1277
  await chmod(absolute, 448);
1207
1278
  return absolute;
1208
1279
  }
1209
1280
  async function readPrivateJson(path) {
1210
- const handle = await open2(path, constants2.O_RDONLY | constants2.O_NOFOLLOW);
1281
+ const handle = await open3(path, constants3.O_RDONLY | constants3.O_NOFOLLOW);
1211
1282
  try {
1212
1283
  const info = await handle.stat();
1213
1284
  if (!info.isFile() || info.size > 1e6)
@@ -1218,9 +1289,9 @@ async function readPrivateJson(path) {
1218
1289
  }
1219
1290
  }
1220
1291
  async function writePrivateJson(path, value) {
1221
- const temporary = join3(dirname2(path), `.${randomUUID2()}.json`);
1292
+ const temporary = join4(dirname3(path), `.${randomUUID2()}.json`);
1222
1293
  try {
1223
- const handle = await open2(temporary, "wx", 384);
1294
+ const handle = await open3(temporary, "wx", 384);
1224
1295
  try {
1225
1296
  await handle.writeFile(`${JSON.stringify(value)}
1226
1297
  `);
@@ -1228,15 +1299,15 @@ async function writePrivateJson(path, value) {
1228
1299
  } finally {
1229
1300
  await handle.close();
1230
1301
  }
1231
- await rename2(temporary, path);
1302
+ await rename3(temporary, path);
1232
1303
  } finally {
1233
- await rm2(temporary, { force: true });
1304
+ await rm3(temporary, { force: true });
1234
1305
  }
1235
1306
  }
1236
1307
 
1237
1308
  // src/wallet-submission.ts
1238
- import { mkdtemp, readFile as readFile6, rm as rm3, writeFile as writeFile2 } from "fs/promises";
1239
- import { join as join4 } from "path";
1309
+ import { mkdtemp as mkdtemp2, readFile as readFile5, rm as rm4, writeFile as writeFile3 } from "fs/promises";
1310
+ import { join as join5 } from "path";
1240
1311
  var uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
1241
1312
  function submissionId(input) {
1242
1313
  if (uuid.test(input)) return input.toLowerCase();
@@ -1254,7 +1325,7 @@ function submissionId(input) {
1254
1325
  return id.toLowerCase();
1255
1326
  }
1256
1327
  async function downloadWalletSubmission(input) {
1257
- const started = performance.now();
1328
+ const started2 = performance.now();
1258
1329
  const id = submissionId(input);
1259
1330
  const response = await fetch(`https://makeables.dev/api/submissions/${id}/files?download=1`, {
1260
1331
  redirect: "error",
@@ -1284,16 +1355,16 @@ async function downloadWalletSubmission(input) {
1284
1355
  const pkg = await readPackageZip(bytes);
1285
1356
  if (pkg.manifest.product !== "card")
1286
1357
  throw new InputError("Wallet artwork requires a card submission.");
1287
- const directory = await mkdtemp(
1288
- join4(
1289
- await privateDirectory(join4(await privateDirectory(walletHome()), "submissions")),
1358
+ const directory = await mkdtemp2(
1359
+ join5(
1360
+ await privateDirectory(join5(await privateDirectory(walletHome()), "submissions")),
1290
1361
  "download-"
1291
1362
  )
1292
1363
  );
1293
1364
  try {
1294
- const path = join4(directory, `${id}.makeables.zip`);
1295
- await writeFile2(path, bytes, { mode: 384, flag: "wx" });
1296
- const saved = await readFile6(path);
1365
+ const path = join5(directory, `${id}.makeables.zip`);
1366
+ await writeFile3(path, bytes, { mode: 384, flag: "wx" });
1367
+ const saved = await readFile5(path);
1297
1368
  const sha256 = await hashBytes(saved);
1298
1369
  if (saved.length !== bytes.length || sha256 !== await hashBytes(bytes))
1299
1370
  throw new Error("Saved submission checksum does not match.");
@@ -1304,34 +1375,34 @@ async function downloadWalletSubmission(input) {
1304
1375
  submissionId: id,
1305
1376
  title: pkg.manifest.title,
1306
1377
  downloadVerified: true,
1307
- elapsedMs: Math.round(performance.now() - started)
1378
+ elapsedMs: Math.round(performance.now() - started2)
1308
1379
  };
1309
1380
  } catch (error) {
1310
- await rm3(directory, { recursive: true, force: true });
1381
+ await rm4(directory, { recursive: true, force: true });
1311
1382
  throw error;
1312
1383
  }
1313
1384
  }
1314
1385
 
1315
1386
  // src/wallet-targets.ts
1316
1387
  import { lstat as lstat2 } from "fs/promises";
1317
- import { join as join5 } from "path";
1388
+ import { join as join6 } from "path";
1318
1389
  import lockfile from "proper-lockfile";
1319
- import { z as z3 } from "zod";
1320
- var alias = z3.string().regex(/^[a-z][a-z0-9-]{0,47}$/).refine((value) => !/\d{5}/.test(value));
1321
- var label = z3.string().trim().min(1).max(60).refine(
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(
1322
1393
  (value) => !/\d{5}/.test(value) && [...value].every(
1323
1394
  (character) => character.charCodeAt(0) >= 32 && character.charCodeAt(0) !== 127
1324
1395
  )
1325
1396
  );
1326
- var targetSchema = z3.object({
1397
+ var targetSchema = z4.object({
1327
1398
  target: alias,
1328
1399
  label,
1329
- last4: z3.string().regex(/^\d{4}$/).optional(),
1330
- identitySource: z3.literal("user-confirmed"),
1331
- backup: z3.string().min(1),
1332
- createdAt: z3.string().datetime()
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()
1333
1404
  }).strict();
1334
- var registrySchema = z3.object({ version: z3.literal(1), targets: z3.array(targetSchema).max(100) }).strict();
1405
+ var registrySchema = z4.object({ version: z4.literal(1), targets: z4.array(targetSchema).max(100) }).strict();
1335
1406
  function targetIdentity(values, key = "remember") {
1336
1407
  const result = targetSchema.pick({ target: true, label: true, last4: true }).safeParse({
1337
1408
  target: values[key],
@@ -1345,12 +1416,12 @@ function targetIdentity(values, key = "remember") {
1345
1416
  return result.data;
1346
1417
  }
1347
1418
  function registryPath() {
1348
- return join5(walletHome(), "targets", "registry.json");
1419
+ return join6(walletHome(), "targets", "registry.json");
1349
1420
  }
1350
1421
  async function walletTargets() {
1351
1422
  const path = registryPath();
1352
1423
  try {
1353
- for (const parent of [walletHome(), join5(walletHome(), "targets")]) {
1424
+ for (const parent of [walletHome(), join6(walletHome(), "targets")]) {
1354
1425
  if (!(await lstat2(parent)).isDirectory())
1355
1426
  throw new InputError("Invalid Wallet target storage.");
1356
1427
  }
@@ -1373,7 +1444,7 @@ function targetChoices(targets) {
1373
1444
  }));
1374
1445
  }
1375
1446
  async function withWalletTargets(action) {
1376
- const directory = await privateDirectory(join5(await privateDirectory(walletHome()), "targets"));
1447
+ const directory = await privateDirectory(join6(await privateDirectory(walletHome()), "targets"));
1377
1448
  const release = await lockfile.lock(directory, { realpath: false, retries: 0 });
1378
1449
  try {
1379
1450
  return await action(await walletTargets(), async (targets) => {
@@ -1402,9 +1473,9 @@ var WalletError = class extends Error {
1402
1473
  };
1403
1474
  function installation() {
1404
1475
  const platform = `${process.platform}-${process.arch}`;
1405
- const cache = process.env.MAKEABLES_WALLET_CACHE ?? join6(homedir3(), "Library", "Caches", "Makeables", "wallet");
1476
+ const cache = process.env.MAKEABLES_WALLET_CACHE ?? join7(homedir3(), "Library", "Caches", "Makeables", "wallet");
1406
1477
  const root = resolve6(cache, `${wallet_release_default.version}-${platform}`);
1407
- return { platform, root, executable: join6(root, "makeables-wallet", "makeables-wallet") };
1478
+ return { platform, root, executable: join7(root, "makeables-wallet", "makeables-wallet") };
1408
1479
  }
1409
1480
  async function execute(command, args, progress) {
1410
1481
  return new Promise((accept, reject) => {
@@ -1535,25 +1606,25 @@ async function setupWallet(progress) {
1535
1606
  const bytes = Buffer.concat(chunks);
1536
1607
  if (createHash3("sha256").update(bytes).digest("hex") !== artifact.sha256)
1537
1608
  throw new Error("Wallet companion checksum did not match. Nothing was installed.");
1538
- await mkdir3(dirname3(root), { recursive: true, mode: 448 });
1539
- const stage = await mkdtemp2(join6(dirname3(root), ".install-"));
1609
+ await mkdir4(dirname4(root), { recursive: true, mode: 448 });
1610
+ const stage = await mkdtemp3(join7(dirname4(root), ".install-"));
1540
1611
  try {
1541
- const archive = join6(stage, "companion.tar.gz");
1542
- await writeFile3(archive, bytes, { mode: 384, flag: "wx" });
1612
+ const archive = join7(stage, "companion.tar.gz");
1613
+ await writeFile4(archive, bytes, { mode: 384, flag: "wx" });
1543
1614
  const members = (await execute("tar", ["-tzf", archive])).trim().split("\n");
1544
1615
  if (members.some(
1545
1616
  (path) => !path.startsWith("makeables-wallet/") || path.split("/").includes("..")
1546
1617
  ))
1547
1618
  throw new Error("Unexpected companion archive contents.");
1548
1619
  await execute("tar", ["-xzf", archive, "-C", stage]);
1549
- await rm4(archive);
1550
- await chmod2(join6(stage, "makeables-wallet", "makeables-wallet"), 448);
1551
- const info = await invoke(join6(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"]);
1552
1623
  if (info.version !== wallet_release_default.version || info.protocol !== wallet_release_default.protocol)
1553
1624
  throw new Error("Companion version does not match the installed CLI.");
1554
- await rename3(stage, root);
1625
+ await rename4(stage, root);
1555
1626
  } finally {
1556
- await rm4(stage, { recursive: true, force: true });
1627
+ await rm5(stage, { recursive: true, force: true });
1557
1628
  }
1558
1629
  return walletDoctor();
1559
1630
  }
@@ -1571,7 +1642,7 @@ async function walletArtwork(file, side2 = "front") {
1571
1642
  if (!path) throw new InputError("This package has no back artwork.");
1572
1643
  source = Buffer.from(pkg.files[path]);
1573
1644
  } else {
1574
- source = await readFile7(input);
1645
+ source = await readFile6(input);
1575
1646
  if (extname3(input).toLowerCase() === ".svg") {
1576
1647
  const result = checkSubmittedSvg(source.toString("utf8"));
1577
1648
  if (!result.ok) throw new InputError(result.error);
@@ -1588,15 +1659,15 @@ async function preparedArtwork(directory) {
1588
1659
  "cardBackgroundCombined@2x.png",
1589
1660
  "cardBackgroundCombined.pdf"
1590
1661
  ]) {
1591
- const file = join6(path, name);
1662
+ const file = join7(path, name);
1592
1663
  const info = await lstat3(file);
1593
1664
  if (!info.isFile() || info.size === 0 || info.size > 16 * 1024 * 1024)
1594
1665
  throw new InputError(`Invalid prepared asset: ${name}. Run wallet prep again.`);
1595
1666
  const signature = Buffer.from(name.endsWith(".pdf") ? "%PDF-" : "\x89PNG\r\n\n", "latin1");
1596
- if (!(await readFile7(file)).subarray(0, signature.length).equals(signature))
1667
+ if (!(await readFile6(file)).subarray(0, signature.length).equals(signature))
1597
1668
  throw new InputError(`Invalid prepared asset: ${name}. Run wallet prep again.`);
1598
1669
  }
1599
- return { path, preview: join6(path, "cardBackgroundCombined@3x.png") };
1670
+ return { path, preview: join7(path, "cardBackgroundCombined@3x.png") };
1600
1671
  }
1601
1672
  async function walletCommand(command, values, progress) {
1602
1673
  if (values.remember) {
@@ -1630,8 +1701,8 @@ async function walletCommand(command, values, progress) {
1630
1701
  return runWalletCommand(command, values, progress);
1631
1702
  }
1632
1703
  async function runWalletCommand(command, values, progress) {
1633
- const started = performance.now();
1634
- const elapsed = () => Math.round(performance.now() - started);
1704
+ const started2 = performance.now();
1705
+ const elapsed = () => Math.round(performance.now() - started2);
1635
1706
  let latestPhase;
1636
1707
  const report = (phase, message) => {
1637
1708
  latestPhase = phase;
@@ -1806,7 +1877,7 @@ async function runWalletCommand(command, values, progress) {
1806
1877
  }
1807
1878
  }
1808
1879
  if (command === "wallet prep" || applying || installing) {
1809
- const work = await mkdtemp2(join6(tmpdir2(), "makeables-wallet-"));
1880
+ const work = await mkdtemp3(join7(tmpdir2(), "makeables-wallet-"));
1810
1881
  let phase = "prepare";
1811
1882
  let deviceWriteAttempted = false;
1812
1883
  let persistent;
@@ -1819,18 +1890,18 @@ async function runWalletCommand(command, values, progress) {
1819
1890
  } else {
1820
1891
  report("prepare", "Preparing the complete card artwork.");
1821
1892
  const png = await walletArtwork(need("file"), values.side === "back" ? "back" : "front");
1822
- const raster = join6(work, "artwork.png");
1823
- await writeFile3(raster, png, { mode: 384 });
1893
+ const raster = join7(work, "artwork.png");
1894
+ await writeFile4(raster, png, { mode: 384 });
1824
1895
  if (command === "wallet prep") {
1825
1896
  target = resolve6(need("out"));
1826
1897
  } else if (installing && !applying) {
1827
1898
  const root = await privateDirectory(
1828
- join6(await privateDirectory(walletHome()), "prepared")
1899
+ join7(await privateDirectory(walletHome()), "prepared")
1829
1900
  );
1830
- persistent = await mkdtemp2(join6(root, "install-"));
1831
- target = join6(persistent, "assets");
1901
+ persistent = await mkdtemp3(join7(root, "install-"));
1902
+ target = join7(persistent, "assets");
1832
1903
  } else {
1833
- target = join6(work, "assets");
1904
+ target = join7(work, "assets");
1834
1905
  }
1835
1906
  prepared = await invoke(
1836
1907
  executable,
@@ -1916,8 +1987,8 @@ async function runWalletCommand(command, values, progress) {
1916
1987
  }
1917
1988
  throw error;
1918
1989
  } finally {
1919
- await rm4(work, { recursive: true, force: true });
1920
- if (persistent && phase === "prepare") await rm4(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 });
1921
1992
  }
1922
1993
  }
1923
1994
  throw new InputError("Unknown Wallet command.");
@@ -1968,6 +2039,14 @@ function walletSummary(result) {
1968
2039
 
1969
2040
  // src/cli.ts
1970
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>",
1971
2050
  "designs list",
1972
2051
  "new card --design <slug> | --style <id> --out <new-file> [--dry-run]",
1973
2052
  "material set --file <design.json> --params <json|@file>",
@@ -1975,7 +2054,7 @@ var commands = [
1975
2054
  "new badge --style <id> --out <new-file> [--dry-run]",
1976
2055
  "new sticker --style <id> --out <new-file> [--dry-run]",
1977
2056
  "layer list --file <design.json>",
1978
- "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]",
1979
2058
  "layer set --file <design.json> --id <layer> [--side front|back] [--x --y --rotation] [--params <json|@file>]",
1980
2059
  "layer remove --file <design.json> --id <layer> [--side front|back]",
1981
2060
  "cut --file <sticker.json> [--out <cut.svg>] [--overwrite]",
@@ -1984,7 +2063,7 @@ var commands = [
1984
2063
  "sheet pack --file <sheet.json>",
1985
2064
  "sheet check --file <sheet.json>",
1986
2065
  "sheet export --file <sheet.json> --out <sheet.pdf> [--preview <proof.png>] [--overwrite]",
1987
- "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]",
1988
2067
  "validate --file <design.json|artwork.svg> [--out <preview.png>]",
1989
2068
  "new card --file <artwork.svg|image.png> --out <card.json> [--name <title>]",
1990
2069
  ...imageTools.map(
@@ -2027,9 +2106,22 @@ var commands = [
2027
2106
  "schema"
2028
2107
  ];
2029
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
+ });
2030
2121
  function emit(data, next, human, json) {
2122
+ if (stdoutClosed) return;
2031
2123
  process.stdout.write(
2032
- json ? `${JSON.stringify({ ok: true, version, data, nextSteps: next })}
2124
+ json ? `${JSON.stringify({ ok: true, version, data: compact ? compactReceipt(data) : data, nextSteps: next })}
2033
2125
  ` : `${human}
2034
2126
  `
2035
2127
  );
@@ -2042,7 +2134,7 @@ async function newPath(file) {
2042
2134
  } catch (error) {
2043
2135
  if (error.code !== "ENOENT") throw error;
2044
2136
  }
2045
- await access(dirname4(path), constants3.W_OK);
2137
+ await access(dirname5(path), constants4.W_OK);
2046
2138
  return path;
2047
2139
  }
2048
2140
  async function run() {
@@ -2100,10 +2192,16 @@ async function run() {
2100
2192
  type: { type: "string" },
2101
2193
  size: { type: "string" },
2102
2194
  "width-mm": { type: "string" },
2103
- preview: { type: "string" }
2195
+ preview: { type: "string" },
2196
+ appearance: { type: "string" },
2197
+ "relative-to": { type: "string" },
2198
+ compact: { type: "boolean" },
2199
+ trace: { type: "string" }
2104
2200
  }
2105
2201
  });
2106
2202
  const json = machineOutput(Boolean(values.json));
2203
+ compact = Boolean(values.compact);
2204
+ traceFile = values.trace;
2107
2205
  if (values.version) {
2108
2206
  emit({ version }, nextSteps, version, json);
2109
2207
  return;
@@ -2116,7 +2214,7 @@ async function run() {
2116
2214
  emit(
2117
2215
  {
2118
2216
  commands: helpCommands,
2119
- flags: ["--json", "--help", "--version"],
2217
+ flags: ["--json", "--help", "--version", "--compact", "--trace <local.jsonl>"],
2120
2218
  exitCodes: { success: 0, invalidInput: 2, systemFailure: 1 }
2121
2219
  },
2122
2220
  nextSteps,
@@ -2132,6 +2230,14 @@ From a built checkout: npm run studio -- <command>`,
2132
2230
  }
2133
2231
  const command = ["skills get", "studio call"].includes(positionals.slice(0, 2).join(" ")) ? positionals.slice(0, 2).join(" ") : positionals.join(" ");
2134
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"],
2135
2241
  ...Object.fromEntries(
2136
2242
  imageTools.map((tool) => [`image ${tool}`, ["file", "out", "params", "ref"]])
2137
2243
  ),
@@ -2210,7 +2316,7 @@ From a built checkout: npm run studio -- <command>`,
2210
2316
  "material map": ["file", "id", "ref"],
2211
2317
  "new badge": ["style", "out", "dry-run"],
2212
2318
  "layer list": ["file"],
2213
- "layer add": ["file", "sticker", "side"],
2319
+ "layer add": ["file", "sticker", "side", "relative-to"],
2214
2320
  "layer set": ["file", "id", "x", "y", "rotation", "side", "params"],
2215
2321
  "layer remove": ["file", "id", "side"],
2216
2322
  cut: ["file", "out", "overwrite"],
@@ -2219,17 +2325,90 @@ From a built checkout: npm run studio -- <command>`,
2219
2325
  "sheet pack": ["file"],
2220
2326
  "sheet check": ["file"],
2221
2327
  "sheet export": ["file", "out", "preview", "overwrite"],
2222
- render: ["file", "out", "overwrite", "side", "photo", "name", "role", "number"],
2328
+ render: ["file", "out", "overwrite", "side", "photo", "name", "role", "number", "appearance"],
2223
2329
  validate: ["file", "out"]
2224
2330
  };
2225
2331
  if (!(command in allowed)) throw new InputError(`Unknown command: ${command}`);
2332
+ traceCommand = command;
2226
2333
  for (const flag of Object.keys(values))
2227
- if (!["json", "help", "version", ...allowed[command]].includes(flag))
2334
+ if (!["json", "help", "version", "compact", "trace", ...allowed[command]].includes(flag))
2228
2335
  throw new InputError(`--${flag} is not supported by ${command}`);
2229
2336
  const progress = (data) => process.stderr.write(`${JSON.stringify(data)}
2230
2337
  `);
2231
2338
  if (values.product && !["card", "badge", "sticker"].includes(values.product))
2232
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
+ }
2233
2412
  if (command.startsWith("wallet ")) {
2234
2413
  const walletProgress = (data) => {
2235
2414
  if (json) return progress(data);
@@ -2395,7 +2574,7 @@ Run makeables skills get core for setup.`,
2395
2574
  }
2396
2575
  if (command === "mcp") {
2397
2576
  try {
2398
- const { runMcp } = await import("./mcp-HS2XZXSO.js");
2577
+ const { runMcp } = await import("./mcp-VS5XYXVM.js");
2399
2578
  await runMcp(values.root ?? process.cwd());
2400
2579
  } catch (error) {
2401
2580
  process.stderr.write(
@@ -2454,7 +2633,7 @@ Keep this process running while you design.`,
2454
2633
  const info = await lstat4(path2);
2455
2634
  if (!info.isFile() || info.size > 12e6)
2456
2635
  throw new InputError("Expected a params file below 12 MB.");
2457
- params = JSON.parse(await readFile8(path2, "utf8"));
2636
+ params = JSON.parse(await readFile7(path2, "utf8"));
2458
2637
  } else if (values.params) params = JSON.parse(values.params);
2459
2638
  }
2460
2639
  const result2 = await studioRequest(
@@ -2537,7 +2716,7 @@ ${styles.map((entry) => `${column(entry.id ?? "", 25)}${entry.name}`).join("\n")
2537
2716
  const path2 = values.out ? await newPath(values.out) : null;
2538
2717
  const dryRun = Boolean(values["dry-run"]);
2539
2718
  if (path2 && !dryRun) {
2540
- const handle2 = await open3(path2, "wx", 384);
2719
+ const handle2 = await open4(path2, "wx", 384);
2541
2720
  try {
2542
2721
  await handle2.writeFile(`${JSON.stringify(design, null, 2)}
2543
2722
  `);
@@ -2561,13 +2740,13 @@ ${dryRun ? "Dry run. No files changed." : "2 faces \xB7 editable layers"}`,
2561
2740
  }
2562
2741
  if (!values.file) throw new InputError("Supply --file to validate a document.");
2563
2742
  const path = resolve7(values.file);
2564
- const handle = await open3(path, "r");
2743
+ const handle = await open4(path, "r");
2565
2744
  let input;
2566
2745
  try {
2567
2746
  const info = await handle.stat();
2568
2747
  if (!info.isFile() || info.size > 2 * 1024 * 1024)
2569
2748
  throw new InputError("Expected a JSON file smaller than 2 MB.");
2570
- input = JSON.parse(await readFile8(handle, "utf8"));
2749
+ input = JSON.parse(await readFile7(handle, "utf8"));
2571
2750
  } finally {
2572
2751
  await handle.close();
2573
2752
  }
@@ -2586,7 +2765,7 @@ Schema and layout constraints passed.`,
2586
2765
  }
2587
2766
  async function readParams(value) {
2588
2767
  return JSON.parse(
2589
- value.startsWith("@") ? await readFile8(resolve7(value.slice(1)), "utf8") : value
2768
+ value.startsWith("@") ? await readFile7(resolve7(value.slice(1)), "utf8") : value
2590
2769
  );
2591
2770
  }
2592
2771
  async function cardCommand(command, values, json) {
@@ -2667,7 +2846,7 @@ ${path ?? `No file written. Add --out ${kind}.json.`}`,
2667
2846
  const path = resolve7(need(file, "file"));
2668
2847
  const design = await readBadge(path);
2669
2848
  const raw = need(values.params, "params");
2670
- const input = JSON.parse(raw.startsWith("@") ? await readFile8(raw.slice(1), "utf8") : raw);
2849
+ const input = JSON.parse(raw.startsWith("@") ? await readFile7(raw.slice(1), "utf8") : raw);
2671
2850
  if (design.product === "sticker") {
2672
2851
  const patch2 = stickerMaterialSchema.partial().strict().safeParse(input);
2673
2852
  if (!patch2.success)
@@ -2747,11 +2926,13 @@ ${path ?? `No file written. Add --out ${kind}.json.`}`,
2747
2926
  if (command === "layer add") {
2748
2927
  const path = need(file, "file");
2749
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.");
2750
2931
  const design = await addBadgeSticker(
2751
2932
  await readBadge(path),
2752
2933
  face,
2753
2934
  need(values.sticker, "sticker"),
2754
- dirname4(resolve7(path))
2935
+ values["relative-to"] === "cwd" ? process.cwd() : dirname5(resolve7(path))
2755
2936
  );
2756
2937
  await writeAtomic(path, `${JSON.stringify(design, null, 2)}
2757
2938
  `);
@@ -2766,7 +2947,7 @@ ${path ?? `No file written. Add --out ${kind}.json.`}`,
2766
2947
  }
2767
2948
  if (command.startsWith("sheet ")) {
2768
2949
  const write = async (target, bytes) => {
2769
- const handle = await open3(target, values.overwrite ? "w" : "wx", 420);
2950
+ const handle = await open4(target, values.overwrite ? "w" : "wx", 420);
2770
2951
  try {
2771
2952
  await handle.writeFile(bytes);
2772
2953
  } finally {
@@ -2870,7 +3051,7 @@ ${path ?? `No file written. Add --out ${kind}.json.`}`,
2870
3051
  const target = out && (values.overwrite ? out : await newPath(out));
2871
3052
  const { cut, svg } = await stickerPrintCut(design);
2872
3053
  if (target) {
2873
- const handle = await open3(target, values.overwrite ? "w" : "wx", 420);
3054
+ const handle = await open4(target, values.overwrite ? "w" : "wx", 420);
2874
3055
  try {
2875
3056
  await handle.writeFile(svg);
2876
3057
  } finally {
@@ -2902,6 +3083,9 @@ ${path ?? `No file written. Add --out ${kind}.json.`}`,
2902
3083
  return true;
2903
3084
  }
2904
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;
2905
3089
  const path = need(file, "file");
2906
3090
  const out = resolve7(need(values.out, "out"));
2907
3091
  if (!out.endsWith(".png")) throw new InputError("--out must end in .png.");
@@ -2910,7 +3094,7 @@ ${path ?? `No file written. Add --out ${kind}.json.`}`,
2910
3094
  if (number !== void 0 && !Number.isInteger(number))
2911
3095
  throw new InputError("--number must be an integer.");
2912
3096
  const face = side(values.side);
2913
- const value = JSON.parse(await readFile8(resolve7(path), "utf8"));
3097
+ const value = JSON.parse(await readFile7(resolve7(path), "utf8"));
2914
3098
  const pkg = value?.format === "makeables-package" ? await readPackageJson(value) : void 0;
2915
3099
  if (pkg && ["photo", "name", "role", "number"].some((key) => values[key] !== void 0))
2916
3100
  throw new InputError(
@@ -2922,11 +3106,12 @@ ${path ?? `No file written. Add --out ${kind}.json.`}`,
2922
3106
  if (pkg && !previewPath) throw new InputError("This package has no back preview.");
2923
3107
  const card = design.product === "card" || design.product === "sticker";
2924
3108
  const { png, person, samplePortrait } = pkg && previewPath ? {
2925
- png: Buffer.from(pkg.files[previewPath]),
3109
+ png: appearance && draft ? await renderDraftFace(draft, face, appearance) : Buffer.from(pkg.files[previewPath]),
2926
3110
  person: draft?.participant,
2927
3111
  samplePortrait: false
2928
3112
  } : await renderBadge(design, {
2929
3113
  face,
3114
+ appearance,
2930
3115
  photo: typeof values.photo === "string" ? values.photo : void 0,
2931
3116
  person: {
2932
3117
  ...typeof values.name === "string" ? { name: values.name } : {},
@@ -2934,7 +3119,7 @@ ${path ?? `No file written. Add --out ${kind}.json.`}`,
2934
3119
  ...number === void 0 ? {} : { number }
2935
3120
  }
2936
3121
  });
2937
- const handle = await open3(target, values.overwrite ? "w" : "wx", 420);
3122
+ const handle = await open4(target, values.overwrite ? "w" : "wx", 420);
2938
3123
  try {
2939
3124
  await handle.writeFile(png);
2940
3125
  } finally {
@@ -2949,12 +3134,13 @@ ${path ?? `No file written. Add --out ${kind}.json.`}`,
2949
3134
  side: face,
2950
3135
  width,
2951
3136
  height,
3137
+ appearance: appearance ?? (pkg && design.product === "sticker" ? "material" : "flat"),
2952
3138
  ...card ? {} : { person, samplePortrait }
2953
3139
  },
2954
3140
  [
2955
3141
  `Open ${target} to review.`,
2956
3142
  ...portrait ? ["Pass --photo <image> to use the person's portrait."] : [],
2957
- ...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."]
2958
3144
  ],
2959
3145
  `${style("Rendered", "1", !json)} ${face} ${target}${portrait ? "\n note: sample portrait; pass --photo" : ""}`,
2960
3146
  json
@@ -2974,7 +3160,7 @@ ${path ?? `No file written. Add --out ${kind}.json.`}`,
2974
3160
  if (command === "validate") {
2975
3161
  const path = resolve7(need(file, "file"));
2976
3162
  if (extname4(path).toLowerCase() === ".svg") {
2977
- const source = await readFile8(path, "utf8");
3163
+ const source = await readFile7(path, "utf8");
2978
3164
  const result2 = checkSubmittedSvg(source);
2979
3165
  if (!result2.ok) throw new InputError(result2.error);
2980
3166
  const preview = values.out ? await newPath(need(values.out, "out")) : void 0;
@@ -2995,7 +3181,7 @@ ${path ?? `No file written. Add --out ${kind}.json.`}`,
2995
3181
  return true;
2996
3182
  }
2997
3183
  if (values.out) throw new InputError("--out is only used to preview SVG validation.");
2998
- const input = JSON.parse(await readFile8(path, "utf8"));
3184
+ const input = JSON.parse(await readFile7(path, "utf8"));
2999
3185
  const result = badgeDesignSchema.safeParse(input);
3000
3186
  if (!result.success)
3001
3187
  throw new InputError(
@@ -3013,7 +3199,7 @@ ${path ?? `No file written. Add --out ${kind}.json.`}`,
3013
3199
  return false;
3014
3200
  }
3015
3201
  run().catch((error) => {
3016
- 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 || [
3017
3203
  "ERR_PARSE_ARGS_UNKNOWN_OPTION",
3018
3204
  "ERR_PARSE_ARGS_INVALID_OPTION_VALUE",
3019
3205
  "ERR_PARSE_ARGS_UNEXPECTED_POSITIONAL",
@@ -3032,10 +3218,28 @@ run().catch((error) => {
3032
3218
  },
3033
3219
  nextSteps
3034
3220
  };
3035
- if (machineOutput(process.argv.includes("--json")))
3221
+ traceCode = payload.error.code;
3222
+ if (!stdoutClosed && machineOutput(process.argv.includes("--json")))
3036
3223
  process.stdout.write(`${JSON.stringify(payload)}
3037
3224
  `);
3038
3225
  process.stderr.write(`makeables: ${error.message}
3039
3226
  `);
3040
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
+ }
3041
3245
  });