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