makeables 0.6.0 → 0.8.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.
Files changed (36) hide show
  1. package/README.md +28 -9
  2. package/dist/assets/stickers/moraleja/claude-01.webp +0 -0
  3. package/dist/assets/stickers/moraleja/claude-02.webp +0 -0
  4. package/dist/assets/stickers/moraleja/claude-03.webp +0 -0
  5. package/dist/assets/stickers/moraleja/code-brew-pereira.webp +0 -0
  6. package/dist/assets/stickers/moraleja/code-brew-san-01.webp +0 -0
  7. package/dist/assets/stickers/moraleja/code-brew-san-02.webp +0 -0
  8. package/dist/assets/stickers/moraleja/cooking.webp +0 -0
  9. package/dist/assets/stickers/moraleja/crafter-01.webp +0 -0
  10. package/dist/assets/stickers/moraleja/crafter-station.webp +0 -0
  11. package/dist/assets/stickers/moraleja/curious.webp +0 -0
  12. package/dist/assets/stickers/moraleja/jsi.webp +0 -0
  13. package/dist/assets/stickers/moraleja/kebo-01.webp +0 -0
  14. package/dist/assets/stickers/moraleja/kebo-02.webp +0 -0
  15. package/dist/assets/stickers/moraleja/kebo-03.webp +0 -0
  16. package/dist/assets/stickers/moraleja/kebo-04.webp +0 -0
  17. package/dist/assets/stickers/moraleja/kebo-brand.webp +0 -0
  18. package/dist/assets/stickers/moraleja/maca.webp +0 -0
  19. package/dist/assets/stickers/moraleja/moraleja-01.webp +0 -0
  20. package/dist/assets/stickers/moraleja/moraleja-lfg.webp +0 -0
  21. package/dist/assets/stickers/moraleja/pawboard.webp +0 -0
  22. package/dist/assets/stickers/moraleja/provenance.json +223 -0
  23. package/dist/assets/stickers/moraleja/qwstzw.webp +0 -0
  24. package/dist/assets/stickers/moraleja/repo-o-fake.webp +0 -0
  25. package/dist/assets/stickers/moraleja/she-ships.webp +0 -0
  26. package/dist/assets/stickers/moraleja/ship-or-sink.webp +0 -0
  27. package/dist/assets/stickers/moraleja/shippeando.webp +0 -0
  28. package/dist/assets/stickers/moraleja/shippervivbes.webp +0 -0
  29. package/dist/assets/stickers/moraleja/shipping.webp +0 -0
  30. package/dist/assets/stickers/moraleja/taste.webp +0 -0
  31. package/dist/assets/stickers/moraleja/tsb.webp +0 -0
  32. package/dist/assets/stickers/moraleja/vibing.webp +0 -0
  33. package/dist/{chunk-4AJQBAFB.js → chunk-NOES67KG.js} +2259 -442
  34. package/dist/cli.js +640 -79
  35. package/dist/{mcp-LXTO7XOO.js → mcp-54KV55QM.js} +2 -1
  36. package/package.json +2 -2
package/dist/cli.js CHANGED
@@ -8,6 +8,7 @@ import {
8
8
  cardStyles,
9
9
  checkSubmittedSvg,
10
10
  connectAccount,
11
+ createStickerSheet,
11
12
  defaultStickerMaterial,
12
13
  designCatalog,
13
14
  designProfile,
@@ -22,16 +23,27 @@ import {
22
23
  newBadge,
23
24
  newCard,
24
25
  openBrowser,
26
+ packSheet,
25
27
  packageJson,
28
+ placeNear,
29
+ prepareSheetPrint,
26
30
  readBadge,
27
31
  readPackageJson,
28
32
  removeBadgeLayer,
29
33
  renderBadge,
30
34
  setBadgeLayer,
35
+ sheetPrintPdf,
36
+ sheetSizes,
37
+ sheetSpec,
38
+ sheetTypeIds,
39
+ sheetTypes,
31
40
  side,
32
41
  skills,
33
42
  startStudio,
43
+ stickerCut,
44
+ stickerCutSvg,
34
45
  stickerMaterialSchema,
46
+ stickerSheetSchema,
35
47
  stickerStyles,
36
48
  studioRequest,
37
49
  submitDesign,
@@ -40,15 +52,15 @@ import {
40
52
  svgNodeXml,
41
53
  version,
42
54
  writeAtomic
43
- } from "./chunk-4AJQBAFB.js";
55
+ } from "./chunk-NOES67KG.js";
44
56
 
45
57
  // src/cli.ts
46
58
  import { spawnSync as spawnSync2 } from "child_process";
47
59
  import { constants as constants2 } from "fs";
48
- import { access, lstat as lstat2, open as open2, readFile as readFile6 } from "fs/promises";
49
- import { dirname as dirname3, extname as extname4, resolve as resolve5 } from "path";
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";
50
62
  import { parseArgs } from "util";
51
- import sharp6 from "sharp";
63
+ import sharp8 from "sharp";
52
64
  import { zodToJsonSchema } from "zod-to-json-schema";
53
65
 
54
66
  // src/card-import.ts
@@ -908,10 +920,195 @@ async function referenceImage(command, file, options, out, reference) {
908
920
  }
909
921
  }
910
922
 
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
+ // src/sticker-cut.ts
1093
+ import sharp5 from "sharp";
1094
+ async function stickerPrintCut(design) {
1095
+ const { png } = await renderBadge(design, { face: "front", person: {} });
1096
+ const { data, info } = await sharp5(png).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
1097
+ const material = design.sticker ?? defaultStickerMaterial;
1098
+ const cut = stickerCut(
1099
+ new Uint8ClampedArray(data.buffer, data.byteOffset, data.length),
1100
+ info.width,
1101
+ info.height,
1102
+ material.border,
1103
+ material.widthMm
1104
+ );
1105
+ return { cut, svg: stickerCutSvg(cut) };
1106
+ }
1107
+
911
1108
  // src/sticker-material.ts
912
- import { readFile as readFile4, stat as stat3 } from "fs/promises";
1109
+ import { readFile as readFile5, stat as stat3 } from "fs/promises";
913
1110
  import { basename as basename2 } from "path";
914
- import sharp4 from "sharp";
1111
+ import sharp6 from "sharp";
915
1112
  async function bindStickerMap(design, layerId, path) {
916
1113
  if (design.product !== "sticker") throw new InputError("Optical maps require a sticker.");
917
1114
  const layer = design.front.layers.find((layer2) => layer2.id === layerId);
@@ -919,8 +1116,8 @@ async function bindStickerMap(design, layerId, path) {
919
1116
  const info = await stat3(path);
920
1117
  if (!info.isFile() || info.size > 4e6)
921
1118
  throw new InputError("Use an optical image below 4 MB.");
922
- const bytes = await readFile4(path);
923
- const meta = await sharp4(bytes, { limitInputPixels: 16777216 }).metadata();
1119
+ const bytes = await readFile5(path);
1120
+ const meta = await sharp6(bytes, { limitInputPixels: 16777216 }).metadata();
924
1121
  if (!meta.width || !meta.height || meta.width > 4096 || meta.height > 4096 || !["png", "webp"].includes(meta.format ?? ""))
925
1122
  throw new InputError("Use an RGB PNG or WebP optical map up to 4096 \xD7 4096.");
926
1123
  const asset = design.sticker?.effectMap?.asset ?? "optical-map";
@@ -949,24 +1146,24 @@ async function bindStickerMap(design, layerId, path) {
949
1146
  import { spawn } from "child_process";
950
1147
  import { createHash as createHash3 } from "crypto";
951
1148
  import { existsSync as existsSync2 } from "fs";
952
- import { chmod, lstat, mkdir as mkdir2, mkdtemp, readFile as readFile5, rename as rename2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
1149
+ import { chmod, lstat, mkdir as mkdir2, mkdtemp, readFile as readFile6, rename as rename2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
953
1150
  import { homedir as homedir2, tmpdir as tmpdir2 } from "os";
954
- import { dirname as dirname2, extname as extname3, join as join3, resolve as resolve4 } from "path";
955
- import sharp5 from "sharp";
1151
+ import { dirname as dirname2, extname as extname3, join as join3, resolve as resolve5 } from "path";
1152
+ import sharp7 from "sharp";
956
1153
 
957
1154
  // src/wallet-release.json
958
1155
  var wallet_release_default = {
959
- version: "0.2.0",
1156
+ version: "0.3.0",
960
1157
  protocol: 1,
961
1158
  repository: "crafter-station/makeables-community",
962
1159
  platforms: {
963
1160
  "darwin-arm64": {
964
- asset: "makeables-wallet-0.2.0-darwin-arm64.tar.gz",
965
- sha256: "5c51f0bd17c80875c12d5913845f368d22501e0a37b03ce8cbcbdc17a0433c37"
1161
+ asset: "makeables-wallet-0.3.0-darwin-arm64.tar.gz",
1162
+ sha256: "ebd275228d84c75836820e12a999bcaa7e586dfbb215ba06882ae161a2e4a09f"
966
1163
  },
967
1164
  "darwin-x64": {
968
- asset: "makeables-wallet-0.2.0-darwin-x64.tar.gz",
969
- sha256: "39705b13b59034fa04872433ab4fa7cd04f6c5d1714df2a41698b316afcf95d8"
1165
+ asset: "makeables-wallet-0.3.0-darwin-x64.tar.gz",
1166
+ sha256: "c5bd51863895e6612be838b5023f7a1322424568ea4249651125930a66ff4ccb"
970
1167
  }
971
1168
  }
972
1169
  };
@@ -982,7 +1179,7 @@ var WalletError = class extends Error {
982
1179
  function installation() {
983
1180
  const platform = `${process.platform}-${process.arch}`;
984
1181
  const cache = process.env.MAKEABLES_WALLET_CACHE ?? join3(homedir2(), "Library", "Caches", "Makeables", "wallet");
985
- const root = resolve4(cache, `${wallet_release_default.version}-${platform}`);
1182
+ const root = resolve5(cache, `${wallet_release_default.version}-${platform}`);
986
1183
  return { platform, root, executable: join3(root, "makeables-wallet", "makeables-wallet") };
987
1184
  }
988
1185
  async function execute(command, args, progress) {
@@ -1022,13 +1219,24 @@ async function execute(command, args, progress) {
1022
1219
  });
1023
1220
  }
1024
1221
  async function invoke(executable, args, progress) {
1025
- const result = JSON.parse(await execute(executable, args, progress));
1222
+ let result;
1223
+ try {
1224
+ result = JSON.parse(await execute(executable, args, progress));
1225
+ if (!result || typeof result.ok !== "boolean") throw new Error("Missing companion result.");
1226
+ } catch {
1227
+ throw new WalletError({
1228
+ code: "WALLET_COMPANION_FAILED",
1229
+ message: "The Wallet companion ended without a valid receipt. Inspect wallet backups and doctor before another device operation."
1230
+ });
1231
+ }
1026
1232
  if (!result.ok) {
1027
- if (result.error?.code === "INVALID_INPUT")
1028
- throw new InputError(result.error.message ?? "Invalid Wallet input.");
1029
1233
  throw new WalletError(result.error ?? {});
1030
1234
  }
1031
- if (!result.data) throw new Error("Wallet companion returned no receipt.");
1235
+ if (!result.data || typeof result.data !== "object" || Array.isArray(result.data))
1236
+ throw new WalletError({
1237
+ code: "WALLET_COMPANION_FAILED",
1238
+ message: "The Wallet companion returned no receipt. Inspect wallet backups before another device operation."
1239
+ });
1032
1240
  return result.data;
1033
1241
  }
1034
1242
  async function walletDoctor() {
@@ -1081,6 +1289,7 @@ async function setupWallet(progress) {
1081
1289
  const url = `https://github.com/${wallet_release_default.repository}/releases/download/wallet-v${wallet_release_default.version}/${artifact.asset}`;
1082
1290
  progress?.({
1083
1291
  event: "wallet_progress",
1292
+ phase: "setup",
1084
1293
  message: `Downloading the Wallet companion ${wallet_release_default.version} for ${platform}.`
1085
1294
  });
1086
1295
  const response = await fetch(url, { signal: AbortSignal.timeout(18e4) });
@@ -1124,16 +1333,8 @@ async function setupWallet(progress) {
1124
1333
  }
1125
1334
  return walletDoctor();
1126
1335
  }
1127
- async function companion() {
1128
- const info = await walletDoctor();
1129
- if (!info.ready || !("installed" in info) || typeof info.installed !== "string")
1130
- throw new InputError(
1131
- "Run makeables wallet setup first. It installs the macOS companion without a source checkout."
1132
- );
1133
- return info.installed;
1134
- }
1135
1336
  async function walletArtwork(file, side2 = "front") {
1136
- const input = resolve4(file);
1337
+ const input = resolve5(file);
1137
1338
  const info = await lstat(input);
1138
1339
  if (!info.isFile() || info.size > 12e6)
1139
1340
  throw new InputError("Expected a local design or artwork file below 12 MB.");
@@ -1143,81 +1344,276 @@ async function walletArtwork(file, side2 = "front") {
1143
1344
  if (design.product !== "card") throw new InputError("Wallet artwork requires a card document.");
1144
1345
  source = (await renderBadge(design, { face: side2, person: {} })).png;
1145
1346
  } else {
1146
- source = await readFile5(input);
1347
+ source = await readFile6(input);
1147
1348
  if (extname3(input).toLowerCase() === ".svg") {
1148
1349
  const result = checkSubmittedSvg(source.toString("utf8"));
1149
1350
  if (!result.ok) throw new InputError(result.error);
1150
1351
  }
1151
1352
  }
1152
- return sharp5(source, { limitInputPixels: 24e6, density: 144 }).rotate().resize(1536, 969, { fit: "contain", background: "#000000" }).flatten({ background: "#000000" }).toColourspace("srgb").png().toBuffer();
1353
+ return sharp7(source, { limitInputPixels: 24e6, density: 144 }).rotate().resize(1536, 969, { fit: "contain", background: "#000000" }).flatten({ background: "#000000" }).toColourspace("srgb").png().toBuffer();
1354
+ }
1355
+ async function preparedArtwork(directory) {
1356
+ const path = resolve5(directory);
1357
+ if (!(await lstat(path)).isDirectory())
1358
+ throw new InputError("--assets must be a prepared artwork directory.");
1359
+ for (const name of [
1360
+ "cardBackgroundCombined@3x.png",
1361
+ "cardBackgroundCombined@2x.png",
1362
+ "cardBackgroundCombined.pdf"
1363
+ ]) {
1364
+ const file = join3(path, name);
1365
+ const info = await lstat(file);
1366
+ if (!info.isFile() || info.size === 0 || info.size > 16 * 1024 * 1024)
1367
+ throw new InputError(`Invalid prepared asset: ${name}. Run wallet prep again.`);
1368
+ const signature = Buffer.from(name.endsWith(".pdf") ? "%PDF-" : "\x89PNG\r\n\n", "latin1");
1369
+ if (!(await readFile6(file)).subarray(0, signature.length).equals(signature))
1370
+ throw new InputError(`Invalid prepared asset: ${name}. Run wallet prep again.`);
1371
+ }
1372
+ return { path, preview: join3(path, "cardBackgroundCombined@3x.png") };
1153
1373
  }
1154
1374
  async function walletCommand(command, values, progress) {
1375
+ const started = performance.now();
1376
+ const elapsed = () => Math.round(performance.now() - started);
1377
+ let latestPhase;
1378
+ const report = (phase, message) => {
1379
+ latestPhase = phase;
1380
+ progress({
1381
+ event: "wallet_progress",
1382
+ phase,
1383
+ message,
1384
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1385
+ elapsedMs: elapsed()
1386
+ });
1387
+ };
1388
+ const forward = (data) => {
1389
+ if (data && typeof data === "object") {
1390
+ if ("phase" in data && typeof data.phase === "string") latestPhase = data.phase;
1391
+ progress({
1392
+ ...data,
1393
+ phase: latestPhase,
1394
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1395
+ elapsedMs: elapsed()
1396
+ });
1397
+ }
1398
+ };
1155
1399
  const need = (flag) => {
1156
1400
  const value = values[flag];
1157
1401
  if (typeof value !== "string" || !value) throw new InputError(`Supply --${flag}.`);
1158
1402
  return value;
1159
1403
  };
1160
1404
  if (command === "wallet doctor") return walletDoctor();
1161
- if (command === "wallet setup") return setupWallet(progress);
1162
- if (command === "wallet apply" || command === "wallet restore" || command === "wallet recover") {
1405
+ if (command === "wallet setup") return setupWallet(forward);
1406
+ const installing = command === "wallet install";
1407
+ const applying = command === "wallet apply" || installing && Boolean(values.card || values.backup);
1408
+ if (applying || command === "wallet restore" || command === "wallet recover") {
1163
1409
  if (!values.yes)
1164
1410
  throw new InputError(
1165
1411
  "Use --yes only after the user requests this operation on the selected card."
1166
1412
  );
1167
1413
  }
1168
- if (command === "wallet apply" && Boolean(values.card) === Boolean(values.backup))
1414
+ if (applying && Boolean(values.card) === Boolean(values.backup))
1169
1415
  throw new InputError("Choose exactly one --card selection or --backup original.");
1170
- const executable = await companion();
1416
+ if (installing && values.yes && !applying)
1417
+ throw new InputError("Select an explicit --card or --backup before using --yes.");
1418
+ if (installing && applying && (values.device || values.seconds))
1419
+ throw new InputError(
1420
+ "--device and --seconds are for selection; --card or --backup already binds the target."
1421
+ );
1422
+ if (values.seconds && (!/^\d+$/.test(String(values.seconds)) || Number(values.seconds) < 1 || Number(values.seconds) > 300))
1423
+ 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.");
1427
+ if (values.assets && values.side)
1428
+ throw new InputError("--side applies to --file, not already prepared --assets.");
1429
+ }
1430
+ if (values.side && !["front", "back"].includes(String(values.side)))
1431
+ throw new InputError("--side must be front or back.");
1432
+ if (command === "wallet prep") {
1433
+ need("file");
1434
+ need("out");
1435
+ }
1436
+ if (command === "wallet restore") need("backup");
1437
+ const existing = values.assets ? await preparedArtwork(need("assets")) : void 0;
1438
+ if (installing) report("setup", "Checking the Wallet companion.");
1439
+ const info = installing ? await setupWallet(forward) : await walletDoctor();
1440
+ if (!info.ready || !("installed" in info) || typeof info.installed !== "string")
1441
+ throw new InputError("Run makeables wallet setup first.");
1442
+ const executable = info.installed;
1443
+ const capabilities = "capabilities" in info && Array.isArray(info.capabilities) ? info.capabilities : [];
1171
1444
  if (command === "wallet list") {
1172
1445
  const args = ["list"];
1173
1446
  for (const flag of ["device", "seconds"])
1174
1447
  if (typeof values[flag] === "string") args.push(`--${flag}`, String(values[flag]));
1175
- return invoke(executable, args, progress);
1448
+ if (values.first) {
1449
+ if (!capabilities.includes("first-selection"))
1450
+ throw new InputError(
1451
+ "This companion needs an update for --first. Omit --first for a full scan."
1452
+ );
1453
+ args.push("--first");
1454
+ }
1455
+ return invoke(executable, args, forward);
1176
1456
  }
1177
1457
  if (command === "wallet backups") {
1178
1458
  return invoke(
1179
1459
  executable,
1180
1460
  ["backups", ...values.backup ? ["--backup", need("backup")] : []],
1181
- progress
1461
+ forward
1182
1462
  );
1183
1463
  }
1184
1464
  if (command === "wallet recover") {
1185
1465
  return invoke(
1186
1466
  executable,
1187
1467
  ["recover", "--backup", need("backup"), "--device", need("device"), "--yes"],
1188
- progress
1468
+ forward
1189
1469
  );
1190
1470
  }
1191
1471
  if (command === "wallet restore") {
1192
- return invoke(executable, ["restore", "--backup", need("backup"), "--yes"], progress);
1193
- }
1194
- if (command === "wallet prep" || command === "wallet apply") {
1195
- const file = need("file");
1196
- if (values.side && !["front", "back"].includes(String(values.side)))
1197
- throw new InputError("--side must be front or back.");
1198
- const png = await walletArtwork(file, values.side === "back" ? "back" : "front");
1199
- const work = await mkdtemp(join3(tmpdir2(), "makeables-wallet-"));
1472
+ report("preflight", "Checking the original artwork backup and connected iPhone.");
1200
1473
  try {
1201
- const raster = join3(work, "artwork.png");
1202
- await writeFile2(raster, png, { mode: 384 });
1203
- const target = command === "wallet prep" ? resolve4(need("out")) : join3(work, "assets");
1204
- const prepared = await invoke(
1474
+ const result = await invoke(
1205
1475
  executable,
1206
- ["prepare", "--file", raster, "--out", target],
1207
- progress
1476
+ ["restore", "--backup", need("backup"), "--yes"],
1477
+ forward
1208
1478
  );
1479
+ return { ...result, elapsedMs: elapsed() };
1480
+ } catch (error) {
1481
+ if (error instanceof WalletError)
1482
+ throw new WalletError({
1483
+ ...error.details,
1484
+ phase: error.details.phase ?? latestPhase ?? "preflight",
1485
+ deviceWriteAttempted: error.details.deviceWriteAttempted ?? true,
1486
+ nextAction: error.details.nextAction ?? "inspect-backups"
1487
+ });
1488
+ throw error;
1489
+ }
1490
+ }
1491
+ if (command === "wallet prep" || applying || installing) {
1492
+ const work = await mkdtemp(join3(tmpdir2(), "makeables-wallet-"));
1493
+ let phase = "prepare";
1494
+ let deviceWriteAttempted = false;
1495
+ let persistent;
1496
+ let prepared;
1497
+ try {
1498
+ let target;
1499
+ if (existing) {
1500
+ target = resolve5(need("assets"));
1501
+ prepared = existing;
1502
+ } else {
1503
+ report("prepare", "Preparing the complete card artwork.");
1504
+ 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 });
1507
+ if (command === "wallet prep") {
1508
+ target = resolve5(need("out"));
1509
+ } else if (installing && !applying) {
1510
+ const root = join3(
1511
+ process.env.MAKEABLES_WALLET_HOME ?? join3(homedir2(), "Library", "Application Support", "Makeables", "Wallet"),
1512
+ "prepared"
1513
+ );
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");
1519
+ } else {
1520
+ target = join3(work, "assets");
1521
+ }
1522
+ prepared = await invoke(
1523
+ executable,
1524
+ ["prepare", "--file", raster, "--out", target],
1525
+ forward
1526
+ );
1527
+ await preparedArtwork(target);
1528
+ }
1209
1529
  if (command === "wallet prep") return prepared;
1210
- progress({ event: "wallet_progress", message: "Full artwork rendered and prepared." });
1530
+ if (installing && !applying) {
1531
+ phase = "selection";
1532
+ report("selection", "Artwork is ready. Open Wallet and tap the card to customize.");
1533
+ const args2 = ["list"];
1534
+ for (const flag of ["device", "seconds"])
1535
+ if (typeof values[flag] === "string") args2.push(`--${flag}`, String(values[flag]));
1536
+ if (capabilities.includes("first-selection")) args2.push("--first");
1537
+ const selection = await invoke(executable, args2, forward);
1538
+ const hasCards = Array.isArray(selection.cards) && selection.cards.length > 0;
1539
+ const status = selection.needsDevice ? "needs-device" : hasCards ? "needs-selection" : "needs-card";
1540
+ return {
1541
+ ...selection,
1542
+ status,
1543
+ assets: target,
1544
+ preview: prepared.preview,
1545
+ deviceWriteAttempted: false,
1546
+ nextAction: selection.needsDevice ? "select-device" : hasCards ? "confirm-card" : "tap-card",
1547
+ next: {
1548
+ command: "wallet install",
1549
+ args: ["--assets", target],
1550
+ required: selection.needsDevice ? ["device"] : hasCards ? ["card", "yes"] : []
1551
+ },
1552
+ elapsedMs: elapsed()
1553
+ };
1554
+ }
1555
+ report("preflight", "Artwork is ready. Keep the selected iPhone connected.");
1211
1556
  const args = ["apply", "--assets", target, "--yes"];
1212
1557
  if (values.card) args.push("--card", need("card"));
1213
1558
  if (values.backup) args.push("--backup", need("backup"));
1214
- return invoke(executable, args, progress);
1559
+ phase = "apply";
1560
+ deviceWriteAttempted = true;
1561
+ const result = await invoke(executable, args, forward);
1562
+ return { ...result, elapsedMs: elapsed() };
1563
+ } catch (error) {
1564
+ if (error instanceof WalletError) {
1565
+ throw new WalletError({
1566
+ ...error.details,
1567
+ phase: error.details.phase ?? latestPhase ?? phase,
1568
+ deviceWriteAttempted: error.details.deviceWriteAttempted ?? deviceWriteAttempted,
1569
+ ...phase === "selection" && prepared ? {
1570
+ assets: String(prepared.path),
1571
+ preview: String(prepared.preview),
1572
+ nextAction: "retry-selection"
1573
+ } : {}
1574
+ });
1575
+ }
1576
+ throw error;
1215
1577
  } finally {
1216
1578
  await rm2(work, { recursive: true, force: true });
1579
+ if (persistent && phase === "prepare") await rm2(persistent, { recursive: true, force: true });
1217
1580
  }
1218
1581
  }
1219
1582
  throw new InputError("Unknown Wallet command.");
1220
1583
  }
1584
+ function walletSummary(result) {
1585
+ if (result.status === "restored") {
1586
+ const seconds = typeof result.elapsedMs === "number" ? ` in ${(result.elapsedMs / 1e3).toFixed(1)}s` : "";
1587
+ return [
1588
+ `Original Wallet artwork restored${seconds}.`,
1589
+ ...result.backupVerified ? ["Original backup preserved and verified."] : [],
1590
+ ...typeof result.backup === "string" ? [`Backup: ${result.backup}`] : [],
1591
+ "Force-close Wallet, reopen it and check the original design."
1592
+ ].join("\n");
1593
+ }
1594
+ if (result.status === "complete") {
1595
+ const seconds = typeof result.elapsedMs === "number" ? ` in ${(result.elapsedMs / 1e3).toFixed(1)}s` : "";
1596
+ return [
1597
+ `Wallet artwork installed${seconds}.`,
1598
+ result.backupVerified ? "Original artwork backup saved and verified." : "Original artwork backup saved.",
1599
+ ...typeof result.backup === "string" ? [`Backup: ${result.backup}`] : [],
1600
+ typeof result.refresh === "string" ? result.refresh : "Close and reopen Wallet to see the design."
1601
+ ].join("\n");
1602
+ }
1603
+ if (["needs-device", "needs-selection", "needs-card"].includes(String(result.status)))
1604
+ return [
1605
+ "Artwork prepared. No card has been changed.",
1606
+ `Preview: ${String(result.preview)}`,
1607
+ result.status === "needs-device" ? "Connect an unlocked iPhone or choose a listed device." : result.status === "needs-card" ? "No card was detected. Open Wallet, tap the card, and repeat using the prepared assets." : "Confirm which observed card you want to customize, then use its selection handle with --yes.",
1608
+ JSON.stringify(
1609
+ { assets: result.assets, devices: result.devices, cards: result.cards },
1610
+ null,
1611
+ 2
1612
+ ),
1613
+ `Continue: makeables wallet install --assets '${String(result.assets).replaceAll("'", "'\\''")}'${result.status === "needs-selection" ? " --card <selection> --yes" : result.status === "needs-device" ? " --device <selection>" : ""}`
1614
+ ].join("\n");
1615
+ return JSON.stringify(result, null, 2);
1616
+ }
1221
1617
 
1222
1618
  // src/cli.ts
1223
1619
  var commands = [
@@ -1231,6 +1627,12 @@ var commands = [
1231
1627
  "layer add --file <design.json> --sticker <petdex:slug|path> [--side front|back]",
1232
1628
  "layer set --file <design.json> --id <layer> [--side front|back] [--x --y --rotation] [--params <json|@file>]",
1233
1629
  "layer remove --file <design.json> --id <layer> [--side front|back]",
1630
+ "cut --file <sticker.json> [--out <cut.svg>] [--overwrite]",
1631
+ "sheet new --type classic|small|single|clear|holographic --out <new-file> [--size a5|a6|10x15] [--name <title>]",
1632
+ "sheet add --file <sheet.json> --sticker <sticker.json|style-id> [--width-mm <mm>]",
1633
+ "sheet pack --file <sheet.json>",
1634
+ "sheet check --file <sheet.json>",
1635
+ "sheet export --file <sheet.json> --out <sheet.pdf> [--preview <proof.png>] [--overwrite]",
1234
1636
  "render --file <design.json|design.makeables.json> --out <png> [--side front|back] [--photo <image>] [--name --role --number] [--overwrite]",
1235
1637
  "validate --file <design.json|artwork.svg> [--out <preview.png>]",
1236
1638
  "new card --file <artwork.svg|image.png> --out <card.json> [--name <title>]",
@@ -1239,9 +1641,10 @@ var commands = [
1239
1641
  ),
1240
1642
  "wallet setup",
1241
1643
  "wallet doctor",
1242
- "wallet list [--device <selection>] [--seconds 45]",
1644
+ "wallet install (--file <artwork> | --assets <directory>) [(--card <selection> | --backup <id|path>) --yes]",
1645
+ "wallet list [--device <selection>] [--seconds 45] [--first]",
1243
1646
  "wallet prep --file <card.json|artwork.svg|image.png> --out <new-directory> [--side front|back]",
1244
- "wallet apply --file <artwork> --card <selection> | --backup <id|path> --yes",
1647
+ "wallet apply (--file <artwork> | --assets <directory>) (--card <selection> | --backup <id|path>) --yes",
1245
1648
  "wallet restore --backup <id|path> --yes",
1246
1649
  "wallet backups [--backup <id|path>]",
1247
1650
  "wallet recover --backup <transaction-path> --device <selection> --yes",
@@ -1278,7 +1681,7 @@ function emit(data, next, human, json) {
1278
1681
  );
1279
1682
  }
1280
1683
  async function newPath(file) {
1281
- const path = resolve5(file);
1684
+ const path = resolve6(file);
1282
1685
  try {
1283
1686
  await lstat2(path);
1284
1687
  throw new InputError(`File already exists: ${path}. Choose a new --out path.`);
@@ -1332,7 +1735,13 @@ async function run() {
1332
1735
  card: { type: "string" },
1333
1736
  backup: { type: "string" },
1334
1737
  device: { type: "string" },
1335
- seconds: { type: "string" }
1738
+ seconds: { type: "string" },
1739
+ assets: { type: "string" },
1740
+ first: { type: "boolean" },
1741
+ type: { type: "string" },
1742
+ size: { type: "string" },
1743
+ "width-mm": { type: "string" },
1744
+ preview: { type: "string" }
1336
1745
  }
1337
1746
  });
1338
1747
  const json = machineOutput(Boolean(values.json));
@@ -1366,9 +1775,10 @@ From a built checkout: npm run studio -- <command>`,
1366
1775
  ),
1367
1776
  "wallet setup": [],
1368
1777
  "wallet doctor": [],
1369
- "wallet list": ["device", "seconds"],
1778
+ "wallet install": ["file", "assets", "card", "backup", "side", "device", "seconds", "yes"],
1779
+ "wallet list": ["device", "seconds", "first"],
1370
1780
  "wallet prep": ["file", "out", "side"],
1371
- "wallet apply": ["file", "card", "backup", "side", "yes"],
1781
+ "wallet apply": ["file", "assets", "card", "backup", "side", "yes"],
1372
1782
  "wallet restore": ["backup", "yes"],
1373
1783
  "wallet backups": ["backup"],
1374
1784
  "wallet recover": ["backup", "device", "yes"],
@@ -1423,6 +1833,12 @@ From a built checkout: npm run studio -- <command>`,
1423
1833
  "layer add": ["file", "sticker", "side"],
1424
1834
  "layer set": ["file", "id", "x", "y", "rotation", "side", "params"],
1425
1835
  "layer remove": ["file", "id", "side"],
1836
+ cut: ["file", "out", "overwrite"],
1837
+ "sheet new": ["type", "size", "name", "out"],
1838
+ "sheet add": ["file", "sticker", "width-mm"],
1839
+ "sheet pack": ["file"],
1840
+ "sheet check": ["file"],
1841
+ "sheet export": ["file", "out", "preview", "overwrite"],
1426
1842
  render: ["file", "out", "overwrite", "side", "photo", "name", "role", "number"],
1427
1843
  validate: ["file", "out"]
1428
1844
  };
@@ -1435,8 +1851,16 @@ From a built checkout: npm run studio -- <command>`,
1435
1851
  if (values.product && !["card", "badge", "sticker"].includes(values.product))
1436
1852
  throw new InputError("--product must be card, badge or sticker.");
1437
1853
  if (command.startsWith("wallet ")) {
1438
- const result2 = await walletCommand(command, values, progress);
1439
- emit(result2, ["makeables skills get wallet"], JSON.stringify(result2, null, 2), json);
1854
+ const walletProgress = (data) => {
1855
+ if (json) return progress(data);
1856
+ if (data && typeof data === "object" && "message" in data) {
1857
+ const phase = "phase" in data ? `[${String(data.phase)}] ` : "";
1858
+ process.stderr.write(`${phase}${String(data.message)}
1859
+ `);
1860
+ }
1861
+ };
1862
+ const result2 = await walletCommand(command, values, walletProgress);
1863
+ emit(result2, ["makeables skills get wallet"], walletSummary(result2), json);
1440
1864
  return;
1441
1865
  }
1442
1866
  if (command.startsWith("image ") && imageTools.some((tool) => command === `image ${tool}`)) {
@@ -1591,7 +2015,7 @@ Run makeables skills get core for setup.`,
1591
2015
  }
1592
2016
  if (command === "mcp") {
1593
2017
  try {
1594
- const { runMcp } = await import("./mcp-LXTO7XOO.js");
2018
+ const { runMcp } = await import("./mcp-54KV55QM.js");
1595
2019
  await runMcp(values.root ?? process.cwd());
1596
2020
  } catch (error) {
1597
2021
  process.stderr.write(
@@ -1650,7 +2074,7 @@ Keep this process running while you design.`,
1650
2074
  const info = await lstat2(path2);
1651
2075
  if (!info.isFile() || info.size > 12e6)
1652
2076
  throw new InputError("Expected a params file below 12 MB.");
1653
- params = JSON.parse(await readFile6(path2, "utf8"));
2077
+ params = JSON.parse(await readFile7(path2, "utf8"));
1654
2078
  } else if (values.params) params = JSON.parse(values.params);
1655
2079
  }
1656
2080
  const result2 = await studioRequest(
@@ -1756,14 +2180,14 @@ ${dryRun ? "Dry run. No files changed." : "2 faces \xB7 editable layers"}`,
1756
2180
  return;
1757
2181
  }
1758
2182
  if (!values.file) throw new InputError("Supply --file to validate a document.");
1759
- const path = resolve5(values.file);
2183
+ const path = resolve6(values.file);
1760
2184
  const handle = await open2(path, "r");
1761
2185
  let input;
1762
2186
  try {
1763
2187
  const info = await handle.stat();
1764
2188
  if (!info.isFile() || info.size > 2 * 1024 * 1024)
1765
2189
  throw new InputError("Expected a JSON file smaller than 2 MB.");
1766
- input = JSON.parse(await readFile6(handle, "utf8"));
2190
+ input = JSON.parse(await readFile7(handle, "utf8"));
1767
2191
  } finally {
1768
2192
  await handle.close();
1769
2193
  }
@@ -1782,7 +2206,7 @@ Schema and layout constraints passed.`,
1782
2206
  }
1783
2207
  async function readParams(value) {
1784
2208
  return JSON.parse(
1785
- value.startsWith("@") ? await readFile6(resolve5(value.slice(1)), "utf8") : value
2209
+ value.startsWith("@") ? await readFile7(resolve6(value.slice(1)), "utf8") : value
1786
2210
  );
1787
2211
  }
1788
2212
  async function cardCommand(command, values, json) {
@@ -1844,7 +2268,7 @@ ${path ?? `No file written. Add --out ${kind}.json.`}`,
1844
2268
  return true;
1845
2269
  }
1846
2270
  if (command === "material map") {
1847
- const path = resolve5(need(file, "file"));
2271
+ const path = resolve6(need(file, "file"));
1848
2272
  const next = await bindStickerMap(
1849
2273
  await readBadge(path),
1850
2274
  need(values.id, "id"),
@@ -1860,10 +2284,10 @@ ${path ?? `No file written. Add --out ${kind}.json.`}`,
1860
2284
  return true;
1861
2285
  }
1862
2286
  if (command === "material set") {
1863
- const path = resolve5(need(file, "file"));
2287
+ const path = resolve6(need(file, "file"));
1864
2288
  const design = await readBadge(path);
1865
2289
  const raw = need(values.params, "params");
1866
- const input = JSON.parse(raw.startsWith("@") ? await readFile6(raw.slice(1), "utf8") : raw);
2290
+ const input = JSON.parse(raw.startsWith("@") ? await readFile7(raw.slice(1), "utf8") : raw);
1867
2291
  if (design.product === "sticker") {
1868
2292
  const patch2 = stickerMaterialSchema.partial().strict().safeParse(input);
1869
2293
  if (!patch2.success)
@@ -1947,7 +2371,7 @@ ${path ?? `No file written. Add --out ${kind}.json.`}`,
1947
2371
  await readBadge(path),
1948
2372
  face,
1949
2373
  need(values.sticker, "sticker"),
1950
- dirname3(resolve5(path))
2374
+ dirname3(resolve6(path))
1951
2375
  );
1952
2376
  await writeAtomic(path, `${JSON.stringify(design, null, 2)}
1953
2377
  `);
@@ -1960,16 +2384,153 @@ ${path ?? `No file written. Add --out ${kind}.json.`}`,
1960
2384
  );
1961
2385
  return true;
1962
2386
  }
2387
+ if (command.startsWith("sheet ")) {
2388
+ const write = async (target, bytes) => {
2389
+ const handle = await open2(target, values.overwrite ? "w" : "wx", 420);
2390
+ try {
2391
+ await handle.writeFile(bytes);
2392
+ } finally {
2393
+ await handle.close();
2394
+ }
2395
+ };
2396
+ const save = (path2, sheet2) => writeAtomic(path2, `${JSON.stringify(sheet2, null, 2)}
2397
+ `);
2398
+ const checkNext = (path2) => [
2399
+ `makeables sheet check --file ${JSON.stringify(path2)}`,
2400
+ `makeables sheet export --file ${JSON.stringify(path2)} --out sheet.pdf --preview proof.png`
2401
+ ];
2402
+ if (command === "sheet new") {
2403
+ const sheet2 = newSheet(values.type, values.size, values.name);
2404
+ const path2 = await newPath(need(values.out, "out"));
2405
+ await writeAtomic(path2, `${JSON.stringify(sheet2, null, 2)}
2406
+ `, true);
2407
+ emit(
2408
+ { path: path2, sheet: sheet2 },
2409
+ [`makeables sheet add --file ${JSON.stringify(path2)} --sticker <sticker.json|style-id>`],
2410
+ `${style("Sheet ready", "1", !json)} ${sheet2.name} ${path2}`,
2411
+ json
2412
+ );
2413
+ return true;
2414
+ }
2415
+ const path = need(file, "file");
2416
+ const sheet = await readSheet(path);
2417
+ if (command === "sheet add") {
2418
+ const result = await addToSheet(sheet, need(values.sticker, "sticker"), values["width-mm"]);
2419
+ await save(path, result.sheet);
2420
+ emit(
2421
+ {
2422
+ item: { ...result.item, design: void 0 },
2423
+ placed: result.placed,
2424
+ stickers: result.sheet.items.length
2425
+ },
2426
+ result.placed ? checkNext(path) : [
2427
+ `No free spot. Run makeables sheet pack --file ${JSON.stringify(path)} or choose a smaller --width-mm.`
2428
+ ],
2429
+ `${style(result.placed ? "Added" : "Added, overlapping", "1", !json)} ${result.item.id} (${result.item.widthMm} mm)`,
2430
+ json
2431
+ );
2432
+ return true;
2433
+ }
2434
+ if (command === "sheet pack") {
2435
+ const result = await packStickerSheet(sheet);
2436
+ await save(path, result.sheet);
2437
+ emit(
2438
+ { stickers: result.sheet.items.length, missing: result.missing },
2439
+ result.missing.length ? ["Remove stickers or make them smaller, then pack again.", ...checkNext(path)] : checkNext(path),
2440
+ `${style("Packed", "1", !json)} ${result.sheet.items.length - result.missing.length}/${result.sheet.items.length}${result.missing.length ? `, no room for ${result.missing.join(", ")}` : ""}`,
2441
+ json
2442
+ );
2443
+ return true;
2444
+ }
2445
+ const print = await checkSheet(sheet);
2446
+ const report = sheetReport(print);
2447
+ const lines = [
2448
+ ...report.violations.map(
2449
+ (v) => v.code === "too-close" ? ` error: ${v.ids.join(" and ")} are ${v.distanceMm.toFixed(1)} mm apart; the gap is ${v.gapMm} mm.` : ` error: ${v.ids[0]} crosses the ${v.marginMm} mm margin.`
2450
+ ),
2451
+ ...report.issues.map((issue) => ` ${issue.severity}: ${issue.id}: ${issue.message}`)
2452
+ ];
2453
+ const summary = `${style(report.printable ? "Printable" : "Not printable", "1", !json)} ${report.widthMm} \xD7 ${report.heightMm} mm, ${report.stickers} sticker${report.stickers === 1 ? "" : "s"}`;
2454
+ if (command === "sheet check") {
2455
+ emit(
2456
+ report,
2457
+ report.printable ? checkNext(path).slice(1) : ["Fix the errors, then check again."],
2458
+ [summary, ...lines].join("\n"),
2459
+ json
2460
+ );
2461
+ return true;
2462
+ }
2463
+ if (command === "sheet export") {
2464
+ if (!report.printable)
2465
+ throw new InputError(
2466
+ `The sheet is not printable. Run makeables sheet check --file ${JSON.stringify(path)}.`
2467
+ );
2468
+ const out = resolve6(need(values.out, "out"));
2469
+ if (!out.endsWith(".pdf")) throw new InputError("--out must end in .pdf.");
2470
+ const preview = typeof values.preview === "string" ? resolve6(values.preview) : void 0;
2471
+ if (preview && !preview.endsWith(".png")) throw new InputError("--preview must end in .png.");
2472
+ const targets = [out, ...preview ? [preview] : []];
2473
+ if (!values.overwrite) for (const target of targets) await newPath(target);
2474
+ await write(out, await exportSheetPdf(sheet, print));
2475
+ if (preview) await write(preview, sheetPreviewPng(print));
2476
+ emit(
2477
+ { ...report, path: out, ...preview ? { preview } : {} },
2478
+ ["Send the PDF to the printer. Confirm its spot names and template before the first run."],
2479
+ [`${summary} ${out}${preview ? ` ${preview}` : ""}`, ...lines].join("\n"),
2480
+ json
2481
+ );
2482
+ return true;
2483
+ }
2484
+ }
2485
+ if (command === "cut") {
2486
+ const design = await readBadge(need(file, "file"));
2487
+ if (design.product !== "sticker") throw new InputError("cut applies to sticker designs.");
2488
+ const out = typeof values.out === "string" ? resolve6(values.out) : void 0;
2489
+ if (out && !out.endsWith(".svg")) throw new InputError("--out must end in .svg.");
2490
+ const target = out && (values.overwrite ? out : await newPath(out));
2491
+ const { cut, svg } = await stickerPrintCut(design);
2492
+ if (target) {
2493
+ const handle = await open2(target, values.overwrite ? "w" : "wx", 420);
2494
+ try {
2495
+ await handle.writeFile(svg);
2496
+ } finally {
2497
+ await handle.close();
2498
+ }
2499
+ }
2500
+ const printable = !cut.issues.some((issue) => issue.severity === "error");
2501
+ const size = `${cut.widthMm.toFixed(1)} \xD7 ${cut.heightMm.toFixed(1)} mm, border ${cut.borderMm.toFixed(2)} mm`;
2502
+ emit(
2503
+ {
2504
+ ...target ? { path: target } : {},
2505
+ printable,
2506
+ widthMm: cut.widthMm,
2507
+ heightMm: cut.heightMm,
2508
+ borderMm: cut.borderMm,
2509
+ paths: cut.paths.length,
2510
+ issues: cut.issues
2511
+ },
2512
+ [
2513
+ ...cut.issues.map((issue) => issue.message),
2514
+ `Set the printed size with material set --params '{"widthMm":75}'.`
2515
+ ],
2516
+ [
2517
+ `${style(printable ? "Printable" : "Not printable", "1", !json)} ${size}, ${cut.paths.length} cut path${cut.paths.length === 1 ? "" : "s"}${target ? ` ${target}` : ""}`,
2518
+ ...cut.issues.map((issue) => ` ${issue.severity}: ${issue.message}`)
2519
+ ].join("\n"),
2520
+ json
2521
+ );
2522
+ return true;
2523
+ }
1963
2524
  if (command === "render") {
1964
2525
  const path = need(file, "file");
1965
- const out = resolve5(need(values.out, "out"));
2526
+ const out = resolve6(need(values.out, "out"));
1966
2527
  if (!out.endsWith(".png")) throw new InputError("--out must end in .png.");
1967
2528
  const target = values.overwrite ? out : await newPath(out);
1968
2529
  const number = values.number === void 0 ? void 0 : Number(values.number);
1969
2530
  if (number !== void 0 && !Number.isInteger(number))
1970
2531
  throw new InputError("--number must be an integer.");
1971
2532
  const face = side(values.side);
1972
- const value = JSON.parse(await readFile6(resolve5(path), "utf8"));
2533
+ const value = JSON.parse(await readFile7(resolve6(path), "utf8"));
1973
2534
  const pkg = value?.format === "makeables-package" ? await readPackageJson(value) : void 0;
1974
2535
  if (pkg && ["photo", "name", "role", "number"].some((key) => values[key] !== void 0))
1975
2536
  throw new InputError(
@@ -2031,13 +2592,13 @@ ${path ?? `No file written. Add --out ${kind}.json.`}`,
2031
2592
  return true;
2032
2593
  }
2033
2594
  if (command === "validate") {
2034
- const path = resolve5(need(file, "file"));
2595
+ const path = resolve6(need(file, "file"));
2035
2596
  if (extname4(path).toLowerCase() === ".svg") {
2036
- const source = await readFile6(path, "utf8");
2597
+ const source = await readFile7(path, "utf8");
2037
2598
  const result2 = checkSubmittedSvg(source);
2038
2599
  if (!result2.ok) throw new InputError(result2.error);
2039
2600
  const preview = values.out ? await newPath(need(values.out, "out")) : void 0;
2040
- if (preview) await sharp6(Buffer.from(result2.svg)).png().toFile(preview);
2601
+ if (preview) await sharp8(Buffer.from(result2.svg)).png().toFile(preview);
2041
2602
  emit(
2042
2603
  {
2043
2604
  valid: true,
@@ -2054,7 +2615,7 @@ ${path ?? `No file written. Add --out ${kind}.json.`}`,
2054
2615
  return true;
2055
2616
  }
2056
2617
  if (values.out) throw new InputError("--out is only used to preview SVG validation.");
2057
- const input = JSON.parse(await readFile6(path, "utf8"));
2618
+ const input = JSON.parse(await readFile7(path, "utf8"));
2058
2619
  const result = badgeDesignSchema.safeParse(input);
2059
2620
  if (!result.success)
2060
2621
  throw new InputError(
@@ -2072,7 +2633,7 @@ ${path ?? `No file written. Add --out ${kind}.json.`}`,
2072
2633
  return false;
2073
2634
  }
2074
2635
  run().catch((error) => {
2075
- const userError = error instanceof InputError || error instanceof SyntaxError || [
2636
+ const userError = error instanceof InputError || error instanceof WalletError && error.details.code === "INVALID_INPUT" || error instanceof SyntaxError || [
2076
2637
  "ERR_PARSE_ARGS_UNKNOWN_OPTION",
2077
2638
  "ERR_PARSE_ARGS_INVALID_OPTION_VALUE",
2078
2639
  "ERR_PARSE_ARGS_UNEXPECTED_POSITIONAL",