makeables 0.7.0 → 0.9.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 +54 -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-QJHEOZGN.js → chunk-RFIQNWNF.js} +256 -154
  34. package/dist/cli.js +700 -109
  35. package/dist/{mcp-AOJ6UJSC.js → mcp-HS2XZXSO.js} +1 -1
  36. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -2,6 +2,7 @@
2
2
  import {
3
3
  CloudError,
4
4
  InputError,
5
+ PACKAGE_LIMIT,
5
6
  addBadgeSticker,
6
7
  badgeDesignObjectSchema,
7
8
  badgeDesignSchema,
@@ -29,6 +30,8 @@ import {
29
30
  prepareSheetPrint,
30
31
  readBadge,
31
32
  readPackageJson,
33
+ readPackageZip,
34
+ readSubmission,
32
35
  removeBadgeLayer,
33
36
  renderBadge,
34
37
  setBadgeLayer,
@@ -52,13 +55,13 @@ import {
52
55
  svgNodeXml,
53
56
  version,
54
57
  writeAtomic
55
- } from "./chunk-QJHEOZGN.js";
58
+ } from "./chunk-RFIQNWNF.js";
56
59
 
57
60
  // src/cli.ts
58
61
  import { spawnSync as spawnSync2 } from "child_process";
59
- import { constants as constants2 } from "fs";
60
- import { access, lstat as lstat2, open as open2, readFile as readFile7 } from "fs/promises";
61
- import { dirname as dirname3, extname as extname4, resolve as resolve6 } from "path";
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";
62
65
  import { parseArgs } from "util";
63
66
  import sharp8 from "sharp";
64
67
  import { zodToJsonSchema } from "zod-to-json-schema";
@@ -222,12 +225,13 @@ async function readToolResult(path) {
222
225
  if (!info.isFile() || info.size > 12e6)
223
226
  throw new InputError("Expected a result file below 12 MB.");
224
227
  let value = JSON.parse(await readFile2(path, "utf8"));
225
- if ("success" in value) {
228
+ if (typeof value === "string") value = JSON.parse(value);
229
+ if (value && typeof value === "object" && "success" in value) {
226
230
  if (!value.success) throw new InputError("The browser command failed.");
227
231
  value = value.data.output;
228
232
  }
229
233
  if (typeof value === "string") value = JSON.parse(value);
230
- if (!value?.ok)
234
+ if (!value?.ok || !value.data || typeof value.data !== "object")
231
235
  throw new InputError(value?.error?.message || "The tool returned an invalid result.");
232
236
  return value.data;
233
237
  }
@@ -328,8 +332,12 @@ async function saveBundle(url, out) {
328
332
  } finally {
329
333
  await file.close();
330
334
  }
335
+ const saved = await readFile3(path);
331
336
  return {
332
337
  path,
338
+ bytes: saved.length,
339
+ sha256: await hashBytes(saved),
340
+ downloadVerified: true,
333
341
  snapshotHash: await hashBytes(jsonBytes(pkg.manifest)),
334
342
  name: pkg.manifest.title,
335
343
  complete: true,
@@ -1146,28 +1154,244 @@ async function bindStickerMap(design, layerId, path) {
1146
1154
  import { spawn } from "child_process";
1147
1155
  import { createHash as createHash3 } from "crypto";
1148
1156
  import { existsSync as existsSync2 } from "fs";
1149
- import { chmod, lstat, mkdir as mkdir2, mkdtemp, readFile as readFile6, rename as rename2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
1150
- import { homedir as homedir2, tmpdir as tmpdir2 } from "os";
1151
- import { dirname as dirname2, extname as extname3, join as join3, resolve as resolve5 } from "path";
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";
1158
+ 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";
1152
1160
  import sharp7 from "sharp";
1153
1161
 
1154
1162
  // src/wallet-release.json
1155
1163
  var wallet_release_default = {
1156
- version: "0.2.0",
1164
+ version: "0.3.1",
1157
1165
  protocol: 1,
1158
1166
  repository: "crafter-station/makeables-community",
1159
1167
  platforms: {
1160
1168
  "darwin-arm64": {
1161
- asset: "makeables-wallet-0.2.0-darwin-arm64.tar.gz",
1162
- sha256: "5c51f0bd17c80875c12d5913845f368d22501e0a37b03ce8cbcbdc17a0433c37"
1169
+ asset: "makeables-wallet-0.3.1-darwin-arm64.tar.gz",
1170
+ sha256: "4ab38b2f451492f5365c4d8800c375d4c0112c246b7f6bb8e46ca85772298afc"
1163
1171
  },
1164
1172
  "darwin-x64": {
1165
- asset: "makeables-wallet-0.2.0-darwin-x64.tar.gz",
1166
- sha256: "39705b13b59034fa04872433ab4fa7cd04f6c5d1714df2a41698b316afcf95d8"
1173
+ asset: "makeables-wallet-0.3.1-darwin-x64.tar.gz",
1174
+ sha256: "f301d7be01bdeda2976b182cdf10b3eb30611f2bb88ef3edb520bfa29b814048"
1167
1175
  }
1168
1176
  }
1169
1177
  };
1170
1178
 
1179
+ // src/wallet-storage.ts
1180
+ 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";
1183
+ import { homedir as homedir2 } from "os";
1184
+ import { dirname as dirname2, join as join3, resolve as resolve5 } from "path";
1185
+ function walletHome() {
1186
+ return resolve5(
1187
+ process.env.MAKEABLES_WALLET_HOME ?? join3(homedir2(), "Library", "Application Support", "Makeables", "Wallet")
1188
+ );
1189
+ }
1190
+ async function privateDirectory(path) {
1191
+ const absolute = resolve5(path);
1192
+ const parent = dirname2(absolute);
1193
+ if (parent !== absolute) {
1194
+ try {
1195
+ const info = await lstat(parent);
1196
+ if (info.isSymbolicLink()) throw new InputError("Wallet storage cannot use symbolic links.");
1197
+ if (!info.isDirectory()) throw new InputError("Wallet storage must be a directory.");
1198
+ } catch (error) {
1199
+ if (error.code !== "ENOENT") throw error;
1200
+ await privateDirectory(parent);
1201
+ }
1202
+ }
1203
+ await mkdir2(absolute, { recursive: true, mode: 448 });
1204
+ if (!(await lstat(absolute)).isDirectory())
1205
+ throw new InputError("Wallet storage must be a directory, not a symbolic link.");
1206
+ await chmod(absolute, 448);
1207
+ return absolute;
1208
+ }
1209
+ async function readPrivateJson(path) {
1210
+ const handle = await open2(path, constants2.O_RDONLY | constants2.O_NOFOLLOW);
1211
+ try {
1212
+ const info = await handle.stat();
1213
+ if (!info.isFile() || info.size > 1e6)
1214
+ throw new InputError("Invalid Wallet target registry.");
1215
+ return JSON.parse(await handle.readFile("utf8"));
1216
+ } finally {
1217
+ await handle.close();
1218
+ }
1219
+ }
1220
+ async function writePrivateJson(path, value) {
1221
+ const temporary = join3(dirname2(path), `.${randomUUID2()}.json`);
1222
+ try {
1223
+ const handle = await open2(temporary, "wx", 384);
1224
+ try {
1225
+ await handle.writeFile(`${JSON.stringify(value)}
1226
+ `);
1227
+ await handle.sync();
1228
+ } finally {
1229
+ await handle.close();
1230
+ }
1231
+ await rename2(temporary, path);
1232
+ } finally {
1233
+ await rm2(temporary, { force: true });
1234
+ }
1235
+ }
1236
+
1237
+ // 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";
1240
+ var uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
1241
+ function submissionId(input) {
1242
+ if (uuid.test(input)) return input.toLowerCase();
1243
+ let url;
1244
+ try {
1245
+ url = new URL(input);
1246
+ } catch {
1247
+ throw new InputError("Supply a submission UUID or official Makeables card URL.");
1248
+ }
1249
+ if (url.origin !== "https://makeables.dev" || url.username || url.password || url.hash)
1250
+ throw new InputError("Submission URLs must use https://makeables.dev.");
1251
+ const id = url.pathname === "/make/card" ? url.searchParams.get("submission") : /^\/submissions\/([^/]+)$/.exec(url.pathname)?.[1];
1252
+ if (!id || !uuid.test(id))
1253
+ throw new InputError("This URL does not identify a Makeables card submission.");
1254
+ return id.toLowerCase();
1255
+ }
1256
+ async function downloadWalletSubmission(input) {
1257
+ const started = performance.now();
1258
+ const id = submissionId(input);
1259
+ const response = await fetch(`https://makeables.dev/api/submissions/${id}/files?download=1`, {
1260
+ redirect: "error",
1261
+ signal: AbortSignal.timeout(3e4)
1262
+ });
1263
+ if (!response.ok || !response.body)
1264
+ throw new InputError(
1265
+ `Submission download failed (${response.status}). Use an approved public card or a local exported package.`
1266
+ );
1267
+ const reader = response.body.getReader();
1268
+ const chunks = [];
1269
+ let size = 0;
1270
+ try {
1271
+ if (Number(response.headers.get("content-length")) > PACKAGE_LIMIT)
1272
+ throw new InputError("Submission package exceeds 24 MB.");
1273
+ while (true) {
1274
+ const { done, value } = await reader.read();
1275
+ if (done) break;
1276
+ size += value.length;
1277
+ if (size > PACKAGE_LIMIT) throw new InputError("Submission package exceeds 24 MB.");
1278
+ chunks.push(value);
1279
+ }
1280
+ } finally {
1281
+ await reader.cancel();
1282
+ }
1283
+ const bytes = Buffer.concat(chunks);
1284
+ const pkg = await readPackageZip(bytes);
1285
+ if (pkg.manifest.product !== "card")
1286
+ throw new InputError("Wallet artwork requires a card submission.");
1287
+ const directory = await mkdtemp(
1288
+ join4(
1289
+ await privateDirectory(join4(await privateDirectory(walletHome()), "submissions")),
1290
+ "download-"
1291
+ )
1292
+ );
1293
+ try {
1294
+ const path = join4(directory, `${id}.makeables.zip`);
1295
+ await writeFile2(path, bytes, { mode: 384, flag: "wx" });
1296
+ const saved = await readFile6(path);
1297
+ const sha256 = await hashBytes(saved);
1298
+ if (saved.length !== bytes.length || sha256 !== await hashBytes(bytes))
1299
+ throw new Error("Saved submission checksum does not match.");
1300
+ return {
1301
+ path,
1302
+ bytes: saved.length,
1303
+ sha256,
1304
+ submissionId: id,
1305
+ title: pkg.manifest.title,
1306
+ downloadVerified: true,
1307
+ elapsedMs: Math.round(performance.now() - started)
1308
+ };
1309
+ } catch (error) {
1310
+ await rm3(directory, { recursive: true, force: true });
1311
+ throw error;
1312
+ }
1313
+ }
1314
+
1315
+ // src/wallet-targets.ts
1316
+ import { lstat as lstat2 } from "fs/promises";
1317
+ import { join as join5 } from "path";
1318
+ 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(
1322
+ (value) => !/\d{5}/.test(value) && [...value].every(
1323
+ (character) => character.charCodeAt(0) >= 32 && character.charCodeAt(0) !== 127
1324
+ )
1325
+ );
1326
+ var targetSchema = z3.object({
1327
+ target: alias,
1328
+ 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()
1333
+ }).strict();
1334
+ var registrySchema = z3.object({ version: z3.literal(1), targets: z3.array(targetSchema).max(100) }).strict();
1335
+ function targetIdentity(values, key = "remember") {
1336
+ const result = targetSchema.pick({ target: true, label: true, last4: true }).safeParse({
1337
+ target: values[key],
1338
+ label: values.label,
1339
+ last4: values.last4
1340
+ });
1341
+ if (!result.success)
1342
+ throw new InputError(
1343
+ `Supply --${key} with a lowercase alias and --label with the user-confirmed card name. Optional --last4 must be exactly four digits; never supply a full card number.`
1344
+ );
1345
+ return result.data;
1346
+ }
1347
+ function registryPath() {
1348
+ return join5(walletHome(), "targets", "registry.json");
1349
+ }
1350
+ async function walletTargets() {
1351
+ const path = registryPath();
1352
+ try {
1353
+ for (const parent of [walletHome(), join5(walletHome(), "targets")]) {
1354
+ if (!(await lstat2(parent)).isDirectory())
1355
+ throw new InputError("Invalid Wallet target storage.");
1356
+ }
1357
+ const result = registrySchema.safeParse(await readPrivateJson(path));
1358
+ if (!result.success || new Set(result.data.targets.map((entry) => entry.target)).size !== result.data.targets.length)
1359
+ throw new InputError("Invalid Wallet target registry. Inspect local Wallet storage.");
1360
+ return result.data.targets;
1361
+ } catch (error) {
1362
+ if (error.code === "ENOENT") return [];
1363
+ throw error;
1364
+ }
1365
+ }
1366
+ function targetChoices(targets) {
1367
+ return targets.map(({ target, label: label2, last4, identitySource }) => ({
1368
+ target,
1369
+ label: label2,
1370
+ ...last4 ? { last4 } : {},
1371
+ displayName: `${label2}${last4 ? ` \u2022\u2022\u2022\u2022${last4}` : ""}`,
1372
+ identitySource
1373
+ }));
1374
+ }
1375
+ async function withWalletTargets(action) {
1376
+ const directory = await privateDirectory(join5(await privateDirectory(walletHome()), "targets"));
1377
+ const release = await lockfile.lock(directory, { realpath: false, retries: 0 });
1378
+ try {
1379
+ return await action(await walletTargets(), async (targets) => {
1380
+ const registry = registrySchema.parse({ version: 1, targets });
1381
+ await writePrivateJson(registryPath(), registry);
1382
+ });
1383
+ } finally {
1384
+ await release();
1385
+ }
1386
+ }
1387
+ function requireNewTarget(targets, target) {
1388
+ if (targets.some((entry) => entry.target === target))
1389
+ throw new InputError(
1390
+ `Target "${target}" already exists. Choose a new alias; existing bindings are never replaced.`
1391
+ );
1392
+ if (targets.length >= 100) throw new InputError("Remove an unused target before adding another.");
1393
+ }
1394
+
1171
1395
  // src/wallet.ts
1172
1396
  var WalletError = class extends Error {
1173
1397
  constructor(details) {
@@ -1178,9 +1402,9 @@ var WalletError = class extends Error {
1178
1402
  };
1179
1403
  function installation() {
1180
1404
  const platform = `${process.platform}-${process.arch}`;
1181
- const cache = process.env.MAKEABLES_WALLET_CACHE ?? join3(homedir2(), "Library", "Caches", "Makeables", "wallet");
1182
- const root = resolve5(cache, `${wallet_release_default.version}-${platform}`);
1183
- return { platform, root, executable: join3(root, "makeables-wallet", "makeables-wallet") };
1405
+ const cache = process.env.MAKEABLES_WALLET_CACHE ?? join6(homedir3(), "Library", "Caches", "Makeables", "wallet");
1406
+ const root = resolve6(cache, `${wallet_release_default.version}-${platform}`);
1407
+ return { platform, root, executable: join6(root, "makeables-wallet", "makeables-wallet") };
1184
1408
  }
1185
1409
  async function execute(command, args, progress) {
1186
1410
  return new Promise((accept, reject) => {
@@ -1219,13 +1443,24 @@ async function execute(command, args, progress) {
1219
1443
  });
1220
1444
  }
1221
1445
  async function invoke(executable, args, progress) {
1222
- const result = JSON.parse(await execute(executable, args, progress));
1446
+ let result;
1447
+ try {
1448
+ result = JSON.parse(await execute(executable, args, progress));
1449
+ if (!result || typeof result.ok !== "boolean") throw new Error("Missing companion result.");
1450
+ } catch {
1451
+ throw new WalletError({
1452
+ code: "WALLET_COMPANION_FAILED",
1453
+ message: "The Wallet companion ended without a valid receipt. Inspect wallet backups and doctor before another device operation."
1454
+ });
1455
+ }
1223
1456
  if (!result.ok) {
1224
- if (result.error?.code === "INVALID_INPUT")
1225
- throw new InputError(result.error.message ?? "Invalid Wallet input.");
1226
1457
  throw new WalletError(result.error ?? {});
1227
1458
  }
1228
- if (!result.data) throw new Error("Wallet companion returned no receipt.");
1459
+ if (!result.data || typeof result.data !== "object" || Array.isArray(result.data))
1460
+ throw new WalletError({
1461
+ code: "WALLET_COMPANION_FAILED",
1462
+ message: "The Wallet companion returned no receipt. Inspect wallet backups before another device operation."
1463
+ });
1229
1464
  return result.data;
1230
1465
  }
1231
1466
  async function walletDoctor() {
@@ -1278,6 +1513,7 @@ async function setupWallet(progress) {
1278
1513
  const url = `https://github.com/${wallet_release_default.repository}/releases/download/wallet-v${wallet_release_default.version}/${artifact.asset}`;
1279
1514
  progress?.({
1280
1515
  event: "wallet_progress",
1516
+ phase: "setup",
1281
1517
  message: `Downloading the Wallet companion ${wallet_release_default.version} for ${platform}.`
1282
1518
  });
1283
1519
  const response = await fetch(url, { signal: AbortSignal.timeout(18e4) });
@@ -1299,48 +1535,43 @@ async function setupWallet(progress) {
1299
1535
  const bytes = Buffer.concat(chunks);
1300
1536
  if (createHash3("sha256").update(bytes).digest("hex") !== artifact.sha256)
1301
1537
  throw new Error("Wallet companion checksum did not match. Nothing was installed.");
1302
- await mkdir2(dirname2(root), { recursive: true, mode: 448 });
1303
- const stage = await mkdtemp(join3(dirname2(root), ".install-"));
1538
+ await mkdir3(dirname3(root), { recursive: true, mode: 448 });
1539
+ const stage = await mkdtemp2(join6(dirname3(root), ".install-"));
1304
1540
  try {
1305
- const archive = join3(stage, "companion.tar.gz");
1306
- await writeFile2(archive, bytes, { mode: 384, flag: "wx" });
1541
+ const archive = join6(stage, "companion.tar.gz");
1542
+ await writeFile3(archive, bytes, { mode: 384, flag: "wx" });
1307
1543
  const members = (await execute("tar", ["-tzf", archive])).trim().split("\n");
1308
1544
  if (members.some(
1309
1545
  (path) => !path.startsWith("makeables-wallet/") || path.split("/").includes("..")
1310
1546
  ))
1311
1547
  throw new Error("Unexpected companion archive contents.");
1312
1548
  await execute("tar", ["-xzf", archive, "-C", stage]);
1313
- await rm2(archive);
1314
- await chmod(join3(stage, "makeables-wallet", "makeables-wallet"), 448);
1315
- const info = await invoke(join3(stage, "makeables-wallet", "makeables-wallet"), ["version"]);
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"]);
1316
1552
  if (info.version !== wallet_release_default.version || info.protocol !== wallet_release_default.protocol)
1317
1553
  throw new Error("Companion version does not match the installed CLI.");
1318
- await rename2(stage, root);
1554
+ await rename3(stage, root);
1319
1555
  } finally {
1320
- await rm2(stage, { recursive: true, force: true });
1556
+ await rm4(stage, { recursive: true, force: true });
1321
1557
  }
1322
1558
  return walletDoctor();
1323
1559
  }
1324
- async function companion() {
1325
- const info = await walletDoctor();
1326
- if (!info.ready || !("installed" in info) || typeof info.installed !== "string")
1327
- throw new InputError(
1328
- "Run makeables wallet setup first. It installs the macOS companion without a source checkout."
1329
- );
1330
- return info.installed;
1331
- }
1332
1560
  async function walletArtwork(file, side2 = "front") {
1333
- const input = resolve5(file);
1334
- const info = await lstat(input);
1335
- if (!info.isFile() || info.size > 12e6)
1336
- throw new InputError("Expected a local design or artwork file below 12 MB.");
1561
+ const input = resolve6(file);
1562
+ const info = await lstat3(input);
1563
+ if (!info.isFile() || info.size > 336e5)
1564
+ throw new InputError("Expected artwork or a complete Makeables package below 32 MB.");
1337
1565
  let source;
1338
- if (extname3(input).toLowerCase() === ".json") {
1339
- const design = await readBadge(input);
1340
- if (design.product !== "card") throw new InputError("Wallet artwork requires a card document.");
1341
- source = (await renderBadge(design, { face: side2, person: {} })).png;
1566
+ if ([".json", ".zip"].includes(extname3(input).toLowerCase())) {
1567
+ const pkg = await readSubmission(input);
1568
+ if (pkg.manifest.product !== "card")
1569
+ throw new InputError("Wallet artwork requires a card document.");
1570
+ const path = side2 === "front" ? pkg.manifest.front : pkg.manifest.back;
1571
+ if (!path) throw new InputError("This package has no back artwork.");
1572
+ source = Buffer.from(pkg.files[path]);
1342
1573
  } else {
1343
- source = await readFile6(input);
1574
+ source = await readFile7(input);
1344
1575
  if (extname3(input).toLowerCase() === ".svg") {
1345
1576
  const result = checkSubmittedSvg(source.toString("utf8"));
1346
1577
  if (!result.ok) throw new InputError(result.error);
@@ -1348,73 +1579,392 @@ async function walletArtwork(file, side2 = "front") {
1348
1579
  }
1349
1580
  return sharp7(source, { limitInputPixels: 24e6, density: 144 }).rotate().resize(1536, 969, { fit: "contain", background: "#000000" }).flatten({ background: "#000000" }).toColourspace("srgb").png().toBuffer();
1350
1581
  }
1582
+ async function preparedArtwork(directory) {
1583
+ const path = resolve6(directory);
1584
+ if (!(await lstat3(path)).isDirectory())
1585
+ throw new InputError("--assets must be a prepared artwork directory.");
1586
+ for (const name of [
1587
+ "cardBackgroundCombined@3x.png",
1588
+ "cardBackgroundCombined@2x.png",
1589
+ "cardBackgroundCombined.pdf"
1590
+ ]) {
1591
+ const file = join6(path, name);
1592
+ const info = await lstat3(file);
1593
+ if (!info.isFile() || info.size === 0 || info.size > 16 * 1024 * 1024)
1594
+ throw new InputError(`Invalid prepared asset: ${name}. Run wallet prep again.`);
1595
+ const signature = Buffer.from(name.endsWith(".pdf") ? "%PDF-" : "\x89PNG\r\n\n", "latin1");
1596
+ if (!(await readFile7(file)).subarray(0, signature.length).equals(signature))
1597
+ throw new InputError(`Invalid prepared asset: ${name}. Run wallet prep again.`);
1598
+ }
1599
+ return { path, preview: join6(path, "cardBackgroundCombined@3x.png") };
1600
+ }
1351
1601
  async function walletCommand(command, values, progress) {
1602
+ if (values.remember) {
1603
+ const identity = targetIdentity(values);
1604
+ return withWalletTargets(async (targets, save) => {
1605
+ requireNewTarget(targets, identity.target);
1606
+ const result = await runWalletCommand(command, values, progress);
1607
+ if (result.status !== "complete") return result;
1608
+ try {
1609
+ if (!result.backupVerified || typeof result.backupId !== "string")
1610
+ throw new Error("The companion did not provide a verified backup binding.");
1611
+ await save([
1612
+ ...targets,
1613
+ {
1614
+ ...identity,
1615
+ backup: result.backupId,
1616
+ identitySource: "user-confirmed",
1617
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
1618
+ }
1619
+ ]);
1620
+ return { ...result, targetSaved: true, target: identity.target };
1621
+ } catch {
1622
+ return {
1623
+ ...result,
1624
+ targetSaved: false,
1625
+ warning: "Artwork installed, but the card label was not saved. Use wallet targets add with the verified backup; do not repeat the installation."
1626
+ };
1627
+ }
1628
+ });
1629
+ }
1630
+ return runWalletCommand(command, values, progress);
1631
+ }
1632
+ async function runWalletCommand(command, values, progress) {
1633
+ const started = performance.now();
1634
+ const elapsed = () => Math.round(performance.now() - started);
1635
+ let latestPhase;
1636
+ const report = (phase, message) => {
1637
+ latestPhase = phase;
1638
+ progress({
1639
+ event: "wallet_progress",
1640
+ phase,
1641
+ message,
1642
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1643
+ elapsedMs: elapsed()
1644
+ });
1645
+ };
1646
+ const forward = (data) => {
1647
+ if (data && typeof data === "object") {
1648
+ if ("phase" in data && typeof data.phase === "string") latestPhase = data.phase;
1649
+ const ready = latestPhase === "selection-ready" || "message" in data && data.message === "Connected to iPhone activity stream.";
1650
+ if (ready) latestPhase = "selection-ready";
1651
+ progress({
1652
+ ...data,
1653
+ phase: latestPhase,
1654
+ ...ready ? { ready: true, message: "Listening. Open Wallet and tap the exact card to customize." } : {},
1655
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1656
+ elapsedMs: elapsed()
1657
+ });
1658
+ }
1659
+ };
1352
1660
  const need = (flag) => {
1353
1661
  const value = values[flag];
1354
1662
  if (typeof value !== "string" || !value) throw new InputError(`Supply --${flag}.`);
1355
1663
  return value;
1356
1664
  };
1357
1665
  if (command === "wallet doctor") return walletDoctor();
1358
- if (command === "wallet setup") return setupWallet(progress);
1359
- if (command === "wallet apply" || command === "wallet restore" || command === "wallet recover") {
1666
+ if (command === "wallet setup") return setupWallet(forward);
1667
+ if (command === "wallet targets list") return { targets: targetChoices(await walletTargets()) };
1668
+ if (command === "wallet targets remove") {
1669
+ const name = need("target");
1670
+ return withWalletTargets(async (targets, save) => {
1671
+ if (!targets.some((entry) => entry.target === name))
1672
+ throw new InputError("Unknown saved target.");
1673
+ await save(targets.filter((entry) => entry.target !== name));
1674
+ return { removed: name, backupPreserved: true };
1675
+ });
1676
+ }
1677
+ if (command === "wallet targets add") {
1678
+ const identity = targetIdentity(values, "target");
1679
+ const backup = need("backup");
1680
+ if (!values.yes)
1681
+ throw new InputError("Use --yes after the user confirms which card this backup belongs to.");
1682
+ return withWalletTargets(async (targets, save) => {
1683
+ requireNewTarget(targets, identity.target);
1684
+ const info2 = await walletDoctor();
1685
+ if (!info2.ready || !("installed" in info2) || typeof info2.installed !== "string")
1686
+ throw new InputError("Run makeables wallet setup first.");
1687
+ const verified = await invoke(info2.installed, ["backups", "--backup", backup], forward);
1688
+ if (verified.verified !== true || typeof verified.path !== "string" || !["complete", "restored"].includes(String(verified.status)))
1689
+ throw new InputError("Choose a verified original backup from a completed operation.");
1690
+ const entry = {
1691
+ ...identity,
1692
+ backup: verified.path,
1693
+ identitySource: "user-confirmed",
1694
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
1695
+ };
1696
+ await save([...targets, entry]);
1697
+ return { targetSaved: true, ...targetChoices([entry])[0] };
1698
+ });
1699
+ }
1700
+ const targetCount = [values.target, values.card, values.backup].filter(Boolean).length;
1701
+ if (targetCount > 1)
1702
+ throw new InputError("Choose exactly one --target, --card selection or --backup original.");
1703
+ if (values.target) {
1704
+ const target = (await walletTargets()).find((entry) => entry.target === values.target);
1705
+ if (!target) throw new InputError("Unknown saved target. Run makeables wallet targets list.");
1706
+ values = { ...values, backup: target.backup };
1707
+ }
1708
+ const installing = command === "wallet install";
1709
+ const applying = command === "wallet apply" || installing && Boolean(values.card || values.backup);
1710
+ if (applying || command === "wallet restore" || command === "wallet recover") {
1360
1711
  if (!values.yes)
1361
1712
  throw new InputError(
1362
1713
  "Use --yes only after the user requests this operation on the selected card."
1363
1714
  );
1364
1715
  }
1365
- if (command === "wallet apply" && Boolean(values.card) === Boolean(values.backup))
1716
+ if (applying && Boolean(values.card) === Boolean(values.backup))
1366
1717
  throw new InputError("Choose exactly one --card selection or --backup original.");
1367
- const executable = await companion();
1718
+ if (installing && values.yes && !applying)
1719
+ throw new InputError("Select an explicit --target, --card or --backup before using --yes.");
1720
+ if (values.scan && (applying || values.yes))
1721
+ throw new InputError("--scan discovers a new card; omit --target, --card, --backup and --yes.");
1722
+ if ((values.label || values.last4) && !values.remember)
1723
+ throw new InputError("--label and --last4 require --remember.");
1724
+ if (values.remember && (!installing || values.target || values.backup))
1725
+ throw new InputError(
1726
+ "--remember is for a newly selected card. Use wallet targets add for an existing backup."
1727
+ );
1728
+ if (installing && applying && (values.device || values.seconds))
1729
+ throw new InputError(
1730
+ "--device and --seconds are for selection; --target, --card or --backup already binds the target."
1731
+ );
1732
+ if (values.seconds && (!/^\d+$/.test(String(values.seconds)) || Number(values.seconds) < 1 || Number(values.seconds) > 300))
1733
+ throw new InputError("--seconds must be between 1 and 300.");
1734
+ if (installing || applying || command === "wallet prep") {
1735
+ if ([values.file, values.assets, values.submission].filter(Boolean).length !== 1)
1736
+ throw new InputError(
1737
+ "Supply exactly one --file artwork, --submission URL or UUID, or --assets prepared directory."
1738
+ );
1739
+ if (values.assets && values.side)
1740
+ throw new InputError("--side applies to --file or --submission, not prepared --assets.");
1741
+ }
1742
+ if (values.side && !["front", "back"].includes(String(values.side)))
1743
+ throw new InputError("--side must be front or back.");
1744
+ if (command === "wallet prep") {
1745
+ need("out");
1746
+ }
1747
+ if (command === "wallet restore") need("backup");
1748
+ const existing = values.assets ? await preparedArtwork(need("assets")) : void 0;
1749
+ let source;
1750
+ if (values.submission) {
1751
+ report("download", "Downloading and verifying the complete card package.");
1752
+ source = await downloadWalletSubmission(need("submission"));
1753
+ values = { ...values, file: String(source.path) };
1754
+ }
1755
+ if (installing) report("setup", "Checking the Wallet companion.");
1756
+ const info = installing || command === "wallet prep" ? await setupWallet(forward) : await walletDoctor();
1757
+ if (!info.ready || !("installed" in info) || typeof info.installed !== "string")
1758
+ throw new InputError("Run makeables wallet setup first.");
1759
+ const executable = info.installed;
1760
+ const capabilities = "capabilities" in info && Array.isArray(info.capabilities) ? info.capabilities : [];
1368
1761
  if (command === "wallet list") {
1369
1762
  const args = ["list"];
1370
1763
  for (const flag of ["device", "seconds"])
1371
1764
  if (typeof values[flag] === "string") args.push(`--${flag}`, String(values[flag]));
1372
- return invoke(executable, args, progress);
1765
+ if (values.first) {
1766
+ if (!capabilities.includes("first-selection"))
1767
+ throw new InputError(
1768
+ "This companion needs an update for --first. Omit --first for a full scan."
1769
+ );
1770
+ args.push("--first");
1771
+ }
1772
+ return invoke(executable, args, forward);
1373
1773
  }
1374
1774
  if (command === "wallet backups") {
1375
1775
  return invoke(
1376
1776
  executable,
1377
1777
  ["backups", ...values.backup ? ["--backup", need("backup")] : []],
1378
- progress
1778
+ forward
1379
1779
  );
1380
1780
  }
1381
1781
  if (command === "wallet recover") {
1382
1782
  return invoke(
1383
1783
  executable,
1384
1784
  ["recover", "--backup", need("backup"), "--device", need("device"), "--yes"],
1385
- progress
1785
+ forward
1386
1786
  );
1387
1787
  }
1388
1788
  if (command === "wallet restore") {
1389
- return invoke(executable, ["restore", "--backup", need("backup"), "--yes"], progress);
1390
- }
1391
- if (command === "wallet prep" || command === "wallet apply") {
1392
- const file = need("file");
1393
- if (values.side && !["front", "back"].includes(String(values.side)))
1394
- throw new InputError("--side must be front or back.");
1395
- const png = await walletArtwork(file, values.side === "back" ? "back" : "front");
1396
- const work = await mkdtemp(join3(tmpdir2(), "makeables-wallet-"));
1789
+ report("preflight", "Checking the original artwork backup and connected iPhone.");
1397
1790
  try {
1398
- const raster = join3(work, "artwork.png");
1399
- await writeFile2(raster, png, { mode: 384 });
1400
- const target = command === "wallet prep" ? resolve5(need("out")) : join3(work, "assets");
1401
- const prepared = await invoke(
1791
+ const result = await invoke(
1402
1792
  executable,
1403
- ["prepare", "--file", raster, "--out", target],
1404
- progress
1793
+ ["restore", "--backup", need("backup"), "--yes"],
1794
+ forward
1405
1795
  );
1406
- if (command === "wallet prep") return prepared;
1407
- progress({ event: "wallet_progress", message: "Full artwork rendered and prepared." });
1796
+ return { ...result, elapsedMs: elapsed() };
1797
+ } catch (error) {
1798
+ if (error instanceof WalletError)
1799
+ throw new WalletError({
1800
+ ...error.details,
1801
+ phase: error.details.phase ?? latestPhase ?? "preflight",
1802
+ deviceWriteAttempted: error.details.deviceWriteAttempted ?? true,
1803
+ nextAction: error.details.nextAction ?? "inspect-backups"
1804
+ });
1805
+ throw error;
1806
+ }
1807
+ }
1808
+ if (command === "wallet prep" || applying || installing) {
1809
+ const work = await mkdtemp2(join6(tmpdir2(), "makeables-wallet-"));
1810
+ let phase = "prepare";
1811
+ let deviceWriteAttempted = false;
1812
+ let persistent;
1813
+ let prepared;
1814
+ try {
1815
+ let target;
1816
+ if (existing) {
1817
+ target = resolve6(need("assets"));
1818
+ prepared = existing;
1819
+ } else {
1820
+ report("prepare", "Preparing the complete card artwork.");
1821
+ 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 });
1824
+ if (command === "wallet prep") {
1825
+ target = resolve6(need("out"));
1826
+ } else if (installing && !applying) {
1827
+ const root = await privateDirectory(
1828
+ join6(await privateDirectory(walletHome()), "prepared")
1829
+ );
1830
+ persistent = await mkdtemp2(join6(root, "install-"));
1831
+ target = join6(persistent, "assets");
1832
+ } else {
1833
+ target = join6(work, "assets");
1834
+ }
1835
+ prepared = await invoke(
1836
+ executable,
1837
+ ["prepare", "--file", raster, "--out", target],
1838
+ forward
1839
+ );
1840
+ await preparedArtwork(target);
1841
+ }
1842
+ if (command === "wallet prep") return { ...prepared, source, elapsedMs: elapsed() };
1843
+ if (installing && !applying) {
1844
+ phase = "selection";
1845
+ const continuation = ["--assets", target];
1846
+ for (const flag of ["remember", "label", "last4"])
1847
+ if (typeof values[flag] === "string")
1848
+ continuation.push(`--${flag}`, String(values[flag]));
1849
+ const saved = await walletTargets();
1850
+ if (saved.length && !values.scan && !values.device)
1851
+ return {
1852
+ status: "needs-target",
1853
+ targets: targetChoices(saved),
1854
+ source,
1855
+ assets: target,
1856
+ preview: prepared.preview,
1857
+ deviceWriteAttempted: false,
1858
+ nextAction: "choose-saved-target-or-scan",
1859
+ next: {
1860
+ command: "wallet install",
1861
+ args: ["--assets", target],
1862
+ required: ["target", "yes"],
1863
+ alternative: { args: [...continuation, "--scan"], required: [] }
1864
+ },
1865
+ elapsedMs: elapsed()
1866
+ };
1867
+ report("selection", "Artwork is ready. Connecting to the iPhone activity stream.");
1868
+ const args2 = ["list"];
1869
+ for (const flag of ["device", "seconds"])
1870
+ if (typeof values[flag] === "string") args2.push(`--${flag}`, String(values[flag]));
1871
+ if (capabilities.includes("first-selection")) args2.push("--first");
1872
+ const selection = await invoke(executable, args2, forward);
1873
+ const hasCards = Array.isArray(selection.cards) && selection.cards.length > 0;
1874
+ const status = selection.needsDevice ? "needs-device" : hasCards ? "needs-selection" : "needs-card";
1875
+ return {
1876
+ ...selection,
1877
+ status,
1878
+ assets: target,
1879
+ preview: prepared.preview,
1880
+ source,
1881
+ deviceWriteAttempted: false,
1882
+ nextAction: selection.needsDevice ? "select-device" : hasCards ? "confirm-card" : "tap-card",
1883
+ next: {
1884
+ command: "wallet install",
1885
+ args: !hasCards && (values.scan || saved.length) ? [...continuation, "--scan"] : continuation,
1886
+ required: selection.needsDevice ? ["device"] : hasCards ? ["card", "yes"] : []
1887
+ },
1888
+ elapsedMs: elapsed()
1889
+ };
1890
+ }
1891
+ report("preflight", "Artwork is ready. Keep the selected iPhone connected.");
1408
1892
  const args = ["apply", "--assets", target, "--yes"];
1409
1893
  if (values.card) args.push("--card", need("card"));
1410
1894
  if (values.backup) args.push("--backup", need("backup"));
1411
- return invoke(executable, args, progress);
1895
+ phase = "apply";
1896
+ deviceWriteAttempted = true;
1897
+ const result = await invoke(executable, args, forward);
1898
+ return {
1899
+ ...result,
1900
+ ...values.target ? { target: values.target } : {},
1901
+ source,
1902
+ elapsedMs: elapsed()
1903
+ };
1904
+ } catch (error) {
1905
+ if (error instanceof WalletError) {
1906
+ throw new WalletError({
1907
+ ...error.details,
1908
+ phase: error.details.phase ?? latestPhase ?? phase,
1909
+ deviceWriteAttempted: error.details.deviceWriteAttempted ?? deviceWriteAttempted,
1910
+ ...phase === "selection" && prepared ? {
1911
+ assets: String(prepared.path),
1912
+ preview: String(prepared.preview),
1913
+ nextAction: "retry-selection"
1914
+ } : {}
1915
+ });
1916
+ }
1917
+ throw error;
1412
1918
  } finally {
1413
- await rm2(work, { recursive: true, force: true });
1919
+ await rm4(work, { recursive: true, force: true });
1920
+ if (persistent && phase === "prepare") await rm4(persistent, { recursive: true, force: true });
1414
1921
  }
1415
1922
  }
1416
1923
  throw new InputError("Unknown Wallet command.");
1417
1924
  }
1925
+ function walletSummary(result) {
1926
+ if (result.status === "restored") {
1927
+ const seconds = typeof result.elapsedMs === "number" ? ` in ${(result.elapsedMs / 1e3).toFixed(1)}s` : "";
1928
+ return [
1929
+ `Original Wallet artwork restored${seconds}.`,
1930
+ ...result.backupVerified ? ["Original backup preserved and verified."] : [],
1931
+ ...typeof result.backup === "string" ? [`Backup: ${result.backup}`] : [],
1932
+ "Force-close Wallet, reopen it and check the original design."
1933
+ ].join("\n");
1934
+ }
1935
+ if (result.status === "complete") {
1936
+ const seconds = typeof result.elapsedMs === "number" ? ` in ${(result.elapsedMs / 1e3).toFixed(1)}s` : "";
1937
+ return [
1938
+ `Wallet artwork installed${seconds}.`,
1939
+ result.backupVerified ? "Original artwork backup saved and verified." : "Original artwork backup saved.",
1940
+ ...typeof result.backup === "string" ? [`Backup: ${result.backup}`] : [],
1941
+ typeof result.refresh === "string" ? result.refresh : "Close and reopen Wallet to see the design.",
1942
+ ...typeof result.warning === "string" ? [result.warning] : []
1943
+ ].join("\n");
1944
+ }
1945
+ if (result.status === "needs-target")
1946
+ return [
1947
+ "Artwork prepared. No card has been changed.",
1948
+ ...(result.targets ?? []).map(
1949
+ (entry) => `${entry.target}: ${entry.displayName} (user-confirmed)`
1950
+ ),
1951
+ "Choose a saved --target with --yes, or use --scan and tap a different card after selection-ready.",
1952
+ `Prepared assets: ${String(result.assets)}`
1953
+ ].join("\n");
1954
+ if (["needs-device", "needs-selection", "needs-card"].includes(String(result.status)))
1955
+ return [
1956
+ "Artwork prepared. No card has been changed.",
1957
+ `Preview: ${String(result.preview)}`,
1958
+ 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.",
1959
+ JSON.stringify(
1960
+ { assets: result.assets, devices: result.devices, cards: result.cards },
1961
+ null,
1962
+ 2
1963
+ ),
1964
+ `Continue: makeables wallet install --assets '${String(result.assets).replaceAll("'", "'\\''")}'${result.status === "needs-selection" ? " --card <selection> --yes" : result.status === "needs-device" ? " --device <selection>" : ""}`
1965
+ ].join("\n");
1966
+ return JSON.stringify(result, null, 2);
1967
+ }
1418
1968
 
1419
1969
  // src/cli.ts
1420
1970
  var commands = [
@@ -1442,9 +1992,13 @@ var commands = [
1442
1992
  ),
1443
1993
  "wallet setup",
1444
1994
  "wallet doctor",
1445
- "wallet list [--device <selection>] [--seconds 45]",
1446
- "wallet prep --file <card.json|artwork.svg|image.png> --out <new-directory> [--side front|back]",
1447
- "wallet apply --file <artwork> --card <selection> | --backup <id|path> --yes",
1995
+ "wallet install (--submission <UUID|URL> | --file <artwork|package> | --assets <directory>) [(--target <alias> | --card <selection> | --backup <id|path>) --yes] [--scan] [--remember <alias> --label <name> --last4 <digits>]",
1996
+ "wallet targets list",
1997
+ "wallet targets add --target <alias> --backup <id|path> --label <name> [--last4 <digits>] --yes",
1998
+ "wallet targets remove --target <alias>",
1999
+ "wallet list [--device <selection>] [--seconds 45] [--first]",
2000
+ "wallet prep (--submission <UUID|URL> | --file <card.json|package.zip|artwork.svg|image.png>) --out <new-directory> [--side front|back]",
2001
+ "wallet apply (--file <artwork> | --assets <directory>) (--card <selection> | --backup <id|path>) --yes",
1448
2002
  "wallet restore --backup <id|path> --yes",
1449
2003
  "wallet backups [--backup <id|path>]",
1450
2004
  "wallet recover --backup <transaction-path> --device <selection> --yes",
@@ -1481,14 +2035,14 @@ function emit(data, next, human, json) {
1481
2035
  );
1482
2036
  }
1483
2037
  async function newPath(file) {
1484
- const path = resolve6(file);
2038
+ const path = resolve7(file);
1485
2039
  try {
1486
- await lstat2(path);
2040
+ await lstat4(path);
1487
2041
  throw new InputError(`File already exists: ${path}. Choose a new --out path.`);
1488
2042
  } catch (error) {
1489
2043
  if (error.code !== "ENOENT") throw error;
1490
2044
  }
1491
- await access(dirname3(path), constants2.W_OK);
2045
+ await access(dirname4(path), constants3.W_OK);
1492
2046
  return path;
1493
2047
  }
1494
2048
  async function run() {
@@ -1536,6 +2090,13 @@ async function run() {
1536
2090
  backup: { type: "string" },
1537
2091
  device: { type: "string" },
1538
2092
  seconds: { type: "string" },
2093
+ assets: { type: "string" },
2094
+ first: { type: "boolean" },
2095
+ submission: { type: "string" },
2096
+ scan: { type: "boolean" },
2097
+ remember: { type: "string" },
2098
+ label: { type: "string" },
2099
+ last4: { type: "string" },
1539
2100
  type: { type: "string" },
1540
2101
  size: { type: "string" },
1541
2102
  "width-mm": { type: "string" },
@@ -1548,16 +2109,19 @@ async function run() {
1548
2109
  return;
1549
2110
  }
1550
2111
  if (values.help || !positionals.length) {
2112
+ const prefix = positionals.join(" ");
2113
+ const helpCommands = prefix ? commands.filter((entry) => entry === prefix || entry.startsWith(`${prefix} `)) : commands;
2114
+ if (!helpCommands.length) throw new InputError(`Unknown command: ${prefix}`);
1551
2115
  if (!json) banner(version);
1552
2116
  emit(
1553
2117
  {
1554
- commands,
2118
+ commands: helpCommands,
1555
2119
  flags: ["--json", "--help", "--version"],
1556
2120
  exitCodes: { success: 0, invalidInput: 2, systemFailure: 1 }
1557
2121
  },
1558
2122
  nextSteps,
1559
2123
  `${style("Commands", "1", !json)}
1560
- ${commands.map((command2) => ` npx makeables ${command2}`).join("\n")}
2124
+ ${helpCommands.map((command2) => ` npx makeables ${command2}`).join("\n")}
1561
2125
 
1562
2126
  JSON is automatic when piped. --out creates new files only.
1563
2127
  Install globally: npm install --global makeables
@@ -1573,9 +2137,28 @@ From a built checkout: npm run studio -- <command>`,
1573
2137
  ),
1574
2138
  "wallet setup": [],
1575
2139
  "wallet doctor": [],
1576
- "wallet list": ["device", "seconds"],
1577
- "wallet prep": ["file", "out", "side"],
1578
- "wallet apply": ["file", "card", "backup", "side", "yes"],
2140
+ "wallet install": [
2141
+ "submission",
2142
+ "file",
2143
+ "assets",
2144
+ "target",
2145
+ "card",
2146
+ "backup",
2147
+ "side",
2148
+ "device",
2149
+ "seconds",
2150
+ "yes",
2151
+ "scan",
2152
+ "remember",
2153
+ "label",
2154
+ "last4"
2155
+ ],
2156
+ "wallet targets list": [],
2157
+ "wallet targets add": ["target", "backup", "label", "last4", "yes"],
2158
+ "wallet targets remove": ["target"],
2159
+ "wallet list": ["device", "seconds", "first"],
2160
+ "wallet prep": ["submission", "file", "out", "side"],
2161
+ "wallet apply": ["file", "assets", "card", "backup", "side", "yes"],
1579
2162
  "wallet restore": ["backup", "yes"],
1580
2163
  "wallet backups": ["backup"],
1581
2164
  "wallet recover": ["backup", "device", "yes"],
@@ -1648,8 +2231,16 @@ From a built checkout: npm run studio -- <command>`,
1648
2231
  if (values.product && !["card", "badge", "sticker"].includes(values.product))
1649
2232
  throw new InputError("--product must be card, badge or sticker.");
1650
2233
  if (command.startsWith("wallet ")) {
1651
- const result2 = await walletCommand(command, values, progress);
1652
- emit(result2, ["makeables skills get wallet"], JSON.stringify(result2, null, 2), json);
2234
+ const walletProgress = (data) => {
2235
+ if (json) return progress(data);
2236
+ if (data && typeof data === "object" && "message" in data) {
2237
+ const phase = "phase" in data ? `[${String(data.phase)}] ` : "";
2238
+ process.stderr.write(`${phase}${String(data.message)}
2239
+ `);
2240
+ }
2241
+ };
2242
+ const result2 = await walletCommand(command, values, walletProgress);
2243
+ emit(result2, ["makeables skills get wallet"], walletSummary(result2), json);
1653
2244
  return;
1654
2245
  }
1655
2246
  if (command.startsWith("image ") && imageTools.some((tool) => command === `image ${tool}`)) {
@@ -1804,7 +2395,7 @@ Run makeables skills get core for setup.`,
1804
2395
  }
1805
2396
  if (command === "mcp") {
1806
2397
  try {
1807
- const { runMcp } = await import("./mcp-AOJ6UJSC.js");
2398
+ const { runMcp } = await import("./mcp-HS2XZXSO.js");
1808
2399
  await runMcp(values.root ?? process.cwd());
1809
2400
  } catch (error) {
1810
2401
  process.stderr.write(
@@ -1860,10 +2451,10 @@ Keep this process running while you design.`,
1860
2451
  if (positionals.length !== 3) throw new InputError("Supply one discovered tool name.");
1861
2452
  if (values.params?.startsWith("@")) {
1862
2453
  const path2 = values.params.slice(1);
1863
- const info = await lstat2(path2);
2454
+ const info = await lstat4(path2);
1864
2455
  if (!info.isFile() || info.size > 12e6)
1865
2456
  throw new InputError("Expected a params file below 12 MB.");
1866
- params = JSON.parse(await readFile7(path2, "utf8"));
2457
+ params = JSON.parse(await readFile8(path2, "utf8"));
1867
2458
  } else if (values.params) params = JSON.parse(values.params);
1868
2459
  }
1869
2460
  const result2 = await studioRequest(
@@ -1946,7 +2537,7 @@ ${styles.map((entry) => `${column(entry.id ?? "", 25)}${entry.name}`).join("\n")
1946
2537
  const path2 = values.out ? await newPath(values.out) : null;
1947
2538
  const dryRun = Boolean(values["dry-run"]);
1948
2539
  if (path2 && !dryRun) {
1949
- const handle2 = await open2(path2, "wx", 384);
2540
+ const handle2 = await open3(path2, "wx", 384);
1950
2541
  try {
1951
2542
  await handle2.writeFile(`${JSON.stringify(design, null, 2)}
1952
2543
  `);
@@ -1969,14 +2560,14 @@ ${dryRun ? "Dry run. No files changed." : "2 faces \xB7 editable layers"}`,
1969
2560
  return;
1970
2561
  }
1971
2562
  if (!values.file) throw new InputError("Supply --file to validate a document.");
1972
- const path = resolve6(values.file);
1973
- const handle = await open2(path, "r");
2563
+ const path = resolve7(values.file);
2564
+ const handle = await open3(path, "r");
1974
2565
  let input;
1975
2566
  try {
1976
2567
  const info = await handle.stat();
1977
2568
  if (!info.isFile() || info.size > 2 * 1024 * 1024)
1978
2569
  throw new InputError("Expected a JSON file smaller than 2 MB.");
1979
- input = JSON.parse(await readFile7(handle, "utf8"));
2570
+ input = JSON.parse(await readFile8(handle, "utf8"));
1980
2571
  } finally {
1981
2572
  await handle.close();
1982
2573
  }
@@ -1995,7 +2586,7 @@ Schema and layout constraints passed.`,
1995
2586
  }
1996
2587
  async function readParams(value) {
1997
2588
  return JSON.parse(
1998
- value.startsWith("@") ? await readFile7(resolve6(value.slice(1)), "utf8") : value
2589
+ value.startsWith("@") ? await readFile8(resolve7(value.slice(1)), "utf8") : value
1999
2590
  );
2000
2591
  }
2001
2592
  async function cardCommand(command, values, json) {
@@ -2057,7 +2648,7 @@ ${path ?? `No file written. Add --out ${kind}.json.`}`,
2057
2648
  return true;
2058
2649
  }
2059
2650
  if (command === "material map") {
2060
- const path = resolve6(need(file, "file"));
2651
+ const path = resolve7(need(file, "file"));
2061
2652
  const next = await bindStickerMap(
2062
2653
  await readBadge(path),
2063
2654
  need(values.id, "id"),
@@ -2073,10 +2664,10 @@ ${path ?? `No file written. Add --out ${kind}.json.`}`,
2073
2664
  return true;
2074
2665
  }
2075
2666
  if (command === "material set") {
2076
- const path = resolve6(need(file, "file"));
2667
+ const path = resolve7(need(file, "file"));
2077
2668
  const design = await readBadge(path);
2078
2669
  const raw = need(values.params, "params");
2079
- const input = JSON.parse(raw.startsWith("@") ? await readFile7(raw.slice(1), "utf8") : raw);
2670
+ const input = JSON.parse(raw.startsWith("@") ? await readFile8(raw.slice(1), "utf8") : raw);
2080
2671
  if (design.product === "sticker") {
2081
2672
  const patch2 = stickerMaterialSchema.partial().strict().safeParse(input);
2082
2673
  if (!patch2.success)
@@ -2160,7 +2751,7 @@ ${path ?? `No file written. Add --out ${kind}.json.`}`,
2160
2751
  await readBadge(path),
2161
2752
  face,
2162
2753
  need(values.sticker, "sticker"),
2163
- dirname3(resolve6(path))
2754
+ dirname4(resolve7(path))
2164
2755
  );
2165
2756
  await writeAtomic(path, `${JSON.stringify(design, null, 2)}
2166
2757
  `);
@@ -2175,7 +2766,7 @@ ${path ?? `No file written. Add --out ${kind}.json.`}`,
2175
2766
  }
2176
2767
  if (command.startsWith("sheet ")) {
2177
2768
  const write = async (target, bytes) => {
2178
- const handle = await open2(target, values.overwrite ? "w" : "wx", 420);
2769
+ const handle = await open3(target, values.overwrite ? "w" : "wx", 420);
2179
2770
  try {
2180
2771
  await handle.writeFile(bytes);
2181
2772
  } finally {
@@ -2254,9 +2845,9 @@ ${path ?? `No file written. Add --out ${kind}.json.`}`,
2254
2845
  throw new InputError(
2255
2846
  `The sheet is not printable. Run makeables sheet check --file ${JSON.stringify(path)}.`
2256
2847
  );
2257
- const out = resolve6(need(values.out, "out"));
2848
+ const out = resolve7(need(values.out, "out"));
2258
2849
  if (!out.endsWith(".pdf")) throw new InputError("--out must end in .pdf.");
2259
- const preview = typeof values.preview === "string" ? resolve6(values.preview) : void 0;
2850
+ const preview = typeof values.preview === "string" ? resolve7(values.preview) : void 0;
2260
2851
  if (preview && !preview.endsWith(".png")) throw new InputError("--preview must end in .png.");
2261
2852
  const targets = [out, ...preview ? [preview] : []];
2262
2853
  if (!values.overwrite) for (const target of targets) await newPath(target);
@@ -2274,12 +2865,12 @@ ${path ?? `No file written. Add --out ${kind}.json.`}`,
2274
2865
  if (command === "cut") {
2275
2866
  const design = await readBadge(need(file, "file"));
2276
2867
  if (design.product !== "sticker") throw new InputError("cut applies to sticker designs.");
2277
- const out = typeof values.out === "string" ? resolve6(values.out) : void 0;
2868
+ const out = typeof values.out === "string" ? resolve7(values.out) : void 0;
2278
2869
  if (out && !out.endsWith(".svg")) throw new InputError("--out must end in .svg.");
2279
2870
  const target = out && (values.overwrite ? out : await newPath(out));
2280
2871
  const { cut, svg } = await stickerPrintCut(design);
2281
2872
  if (target) {
2282
- const handle = await open2(target, values.overwrite ? "w" : "wx", 420);
2873
+ const handle = await open3(target, values.overwrite ? "w" : "wx", 420);
2283
2874
  try {
2284
2875
  await handle.writeFile(svg);
2285
2876
  } finally {
@@ -2312,14 +2903,14 @@ ${path ?? `No file written. Add --out ${kind}.json.`}`,
2312
2903
  }
2313
2904
  if (command === "render") {
2314
2905
  const path = need(file, "file");
2315
- const out = resolve6(need(values.out, "out"));
2906
+ const out = resolve7(need(values.out, "out"));
2316
2907
  if (!out.endsWith(".png")) throw new InputError("--out must end in .png.");
2317
2908
  const target = values.overwrite ? out : await newPath(out);
2318
2909
  const number = values.number === void 0 ? void 0 : Number(values.number);
2319
2910
  if (number !== void 0 && !Number.isInteger(number))
2320
2911
  throw new InputError("--number must be an integer.");
2321
2912
  const face = side(values.side);
2322
- const value = JSON.parse(await readFile7(resolve6(path), "utf8"));
2913
+ const value = JSON.parse(await readFile8(resolve7(path), "utf8"));
2323
2914
  const pkg = value?.format === "makeables-package" ? await readPackageJson(value) : void 0;
2324
2915
  if (pkg && ["photo", "name", "role", "number"].some((key) => values[key] !== void 0))
2325
2916
  throw new InputError(
@@ -2343,7 +2934,7 @@ ${path ?? `No file written. Add --out ${kind}.json.`}`,
2343
2934
  ...number === void 0 ? {} : { number }
2344
2935
  }
2345
2936
  });
2346
- const handle = await open2(target, values.overwrite ? "w" : "wx", 420);
2937
+ const handle = await open3(target, values.overwrite ? "w" : "wx", 420);
2347
2938
  try {
2348
2939
  await handle.writeFile(png);
2349
2940
  } finally {
@@ -2381,9 +2972,9 @@ ${path ?? `No file written. Add --out ${kind}.json.`}`,
2381
2972
  return true;
2382
2973
  }
2383
2974
  if (command === "validate") {
2384
- const path = resolve6(need(file, "file"));
2975
+ const path = resolve7(need(file, "file"));
2385
2976
  if (extname4(path).toLowerCase() === ".svg") {
2386
- const source = await readFile7(path, "utf8");
2977
+ const source = await readFile8(path, "utf8");
2387
2978
  const result2 = checkSubmittedSvg(source);
2388
2979
  if (!result2.ok) throw new InputError(result2.error);
2389
2980
  const preview = values.out ? await newPath(need(values.out, "out")) : void 0;
@@ -2404,7 +2995,7 @@ ${path ?? `No file written. Add --out ${kind}.json.`}`,
2404
2995
  return true;
2405
2996
  }
2406
2997
  if (values.out) throw new InputError("--out is only used to preview SVG validation.");
2407
- const input = JSON.parse(await readFile7(path, "utf8"));
2998
+ const input = JSON.parse(await readFile8(path, "utf8"));
2408
2999
  const result = badgeDesignSchema.safeParse(input);
2409
3000
  if (!result.success)
2410
3001
  throw new InputError(
@@ -2422,7 +3013,7 @@ ${path ?? `No file written. Add --out ${kind}.json.`}`,
2422
3013
  return false;
2423
3014
  }
2424
3015
  run().catch((error) => {
2425
- const userError = error instanceof InputError || error instanceof SyntaxError || [
3016
+ const userError = error instanceof InputError || error instanceof WalletError && error.details.code === "INVALID_INPUT" || error instanceof SyntaxError || [
2426
3017
  "ERR_PARSE_ARGS_UNKNOWN_OPTION",
2427
3018
  "ERR_PARSE_ARGS_INVALID_OPTION_VALUE",
2428
3019
  "ERR_PARSE_ARGS_UNEXPECTED_POSITIONAL",