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