@zoralabs/cli 1.1.0 → 1.3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zoralabs/cli",
3
- "version": "1.1.0",
3
+ "version": "1.3.0",
4
4
  "description": "Zora CLI tool",
5
5
  "type": "module",
6
6
  "bin": {
@@ -17,16 +17,22 @@
17
17
  "@inkjs/ui": "^2.0.0",
18
18
  "@inquirer/confirm": "^6.0.8",
19
19
  "@inquirer/core": "^11.1.5",
20
+ "@inquirer/input": "^5.1.2",
20
21
  "@inquirer/password": "^5.0.8",
21
22
  "@inquirer/select": "^5.1.0",
23
+ "@resvg/resvg-wasm": "^2.6.2",
24
+ "@xmtp/content-type-reaction": "^2.0.2",
25
+ "@xmtp/content-type-read-receipt": "^2.0.2",
26
+ "@xmtp/node-sdk": "^6.0.0",
22
27
  "commander": "^13.1.0",
23
28
  "date-fns": "^4.1.0",
24
29
  "ink": "^6.8.0",
25
30
  "ink-spinner": "^5.0.0",
26
31
  "posthog-node": "^5.28.4",
27
32
  "react": "^19.2.4",
33
+ "satori": "^0.26.0",
28
34
  "viem": "2.22.12",
29
- "@zoralabs/coins-sdk": "0.6.0"
35
+ "@zoralabs/coins-sdk": "0.7.0"
30
36
  },
31
37
  "devDependencies": {
32
38
  "@types/node": "^22.13.0",
@@ -44,16 +50,21 @@
44
50
  },
45
51
  "scripts": {
46
52
  "postinstall": "node scripts/postinstall.mjs",
47
- "build": "tsup",
48
- "build:js": "tsup",
53
+ "build": "pnpm run patch:xmtp-binding && tsup",
54
+ "build:js": "pnpm run patch:xmtp-binding && tsup",
55
+ "check": "tsc --noEmit",
49
56
  "zora": "tsx src/index.tsx",
50
57
  "test": "vitest run",
51
58
  "test:js": "pnpm run test",
52
59
  "test:coverage": "vitest run --coverage",
53
- "build:binary": "V=$(node -p \"require('./package.json').version\") && bun build ./src/index.tsx --compile --define \"PKG_VERSION=\\\"$V\\\"\" --outfile ./bin/zora",
60
+ "patch:xmtp-binding": "node scripts/patch-xmtp-binding.mjs",
61
+ "gen:card-fonts": "python3 scripts/gen-card-fonts.py",
62
+ "gen:card-wasm": "node scripts/gen-card-wasm.mjs",
63
+ "gen:card-assets": "pnpm run gen:card-fonts && pnpm run gen:card-wasm",
64
+ "build:binary": "pnpm run patch:xmtp-binding && V=$(node -p \"require('./package.json').version\") && bun build ./src/index.tsx --compile --define \"PKG_VERSION=\\\"$V\\\"\" --outfile ./bin/zora",
54
65
  "build:binary:all": "pnpm run build:binary:mac-arm64 && pnpm run build:binary:mac-x64 && pnpm run build:binary:linux-x64 && pnpm run build:binary:linux-arm64 && pnpm run build:binary:windows-x64",
55
- "build:binary:mac-arm64": "V=$(node -p \"require('./package.json').version\") && bun build ./src/index.tsx --compile --target=bun-darwin-arm64 --define \"PKG_VERSION=\\\"$V\\\"\" --outfile ./bin/zora-darwin-arm64",
56
- "build:binary:mac-x64": "V=$(node -p \"require('./package.json').version\") && bun build ./src/index.tsx --compile --target=bun-darwin-x64 --define \"PKG_VERSION=\\\"$V\\\"\" --outfile ./bin/zora-darwin-x64",
66
+ "build:binary:mac-arm64": "pnpm run patch:xmtp-binding && V=$(node -p \"require('./package.json').version\") && bun build ./src/index.tsx --compile --target=bun-darwin-arm64 --define \"PKG_VERSION=\\\"$V\\\"\" --outfile ./bin/zora-darwin-arm64",
67
+ "build:binary:mac-x64": "pnpm run patch:xmtp-binding && V=$(node -p \"require('./package.json').version\") && bun build ./src/index.tsx --compile --target=bun-darwin-x64 --define \"PKG_VERSION=\\\"$V\\\"\" --outfile ./bin/zora-darwin-x64",
57
68
  "build:binary:linux-x64": "V=$(node -p \"require('./package.json').version\") && bun build ./src/index.tsx --compile --target=bun-linux-x64 --define \"PKG_VERSION=\\\"$V\\\"\" --outfile ./bin/zora-linux-x64",
58
69
  "build:binary:linux-arm64": "V=$(node -p \"require('./package.json').version\") && bun build ./src/index.tsx --compile --target=bun-linux-arm64 --define \"PKG_VERSION=\\\"$V\\\"\" --outfile ./bin/zora-linux-arm64",
59
70
  "build:binary:windows-x64": "V=$(node -p \"require('./package.json').version\") && bun build ./src/index.tsx --compile --target=bun-windows-x64 --define \"PKG_VERSION=\\\"$V\\\"\" --outfile ./bin/zora-windows-x64.exe"
@@ -0,0 +1,76 @@
1
+ #!/usr/bin/env python3
2
+ """Generate the bundled, base64-embedded card fonts for the agent first post.
3
+
4
+ Instantiates static Regular (400) and Medium (500) weights from the ABC Monument
5
+ Grotesk *variable* font and writes them, base64-encoded, into a generated
6
+ TypeScript module. The renderer (Satori) needs static TTF/OTF — it can't read
7
+ woff2 and is unreliable with variable fonts — so we pin the two weights the
8
+ brand template uses and embed them inline (same approach as the greeting cards,
9
+ so the compiled bun binary and the published Node CLI both ship the bytes with
10
+ no external asset files).
11
+
12
+ Run from the package root: python3 scripts/gen-card-fonts.py
13
+ Source font: cli-docs/docs/public/fonts/ABCMonumentGroteskVariableVF.woff
14
+ """
15
+
16
+ import base64
17
+ import io
18
+ import os
19
+
20
+ from fontTools import ttLib
21
+ from fontTools.varLib.instancer import instantiateVariableFont
22
+
23
+ HERE = os.path.dirname(os.path.abspath(__file__))
24
+ PKG_ROOT = os.path.dirname(HERE)
25
+ REPO_ROOT = os.path.dirname(os.path.dirname(PKG_ROOT))
26
+ SRC_FONT = os.path.join(
27
+ REPO_ROOT, "cli-docs", "docs", "public", "fonts", "ABCMonumentGroteskVariableVF.woff"
28
+ )
29
+ OUT_TS = os.path.join(PKG_ROOT, "src", "lib", "agent", "card-fonts.ts")
30
+
31
+ # The two weights the brand meme template uses: caption (Medium) + handle (Regular).
32
+ WEIGHTS = {"regular": 400, "medium": 500}
33
+
34
+
35
+ def instantiate_ttf_base64(weight: int) -> str:
36
+ font = ttLib.TTFont(SRC_FONT)
37
+ # Pin the variable axes to a single static instance (upright, target weight).
38
+ instantiateVariableFont(font, {"wght": weight, "slnt": 0}, inplace=True)
39
+ font.flavor = None # emit a bare TTF (drop woff compression)
40
+ buf = io.BytesIO()
41
+ font.save(buf)
42
+ return base64.b64encode(buf.getvalue()).decode("ascii")
43
+
44
+
45
+ def main() -> None:
46
+ fonts = {name: instantiate_ttf_base64(wght) for name, wght in WEIGHTS.items()}
47
+
48
+ lines = [
49
+ "// AUTO-GENERATED by scripts/gen-card-fonts.py — do not edit by hand.",
50
+ "// Static Regular (400) + Medium (500) instances of ABC Monument Grotesk,",
51
+ "// instantiated from the variable font and inlined as base64 TTF so the",
52
+ "// card renderer ships the bytes in both the bun binary and the Node CLI.",
53
+ "/* eslint-disable */",
54
+ "",
55
+ "import { Buffer } from \"node:buffer\";",
56
+ "",
57
+ ]
58
+ for name in WEIGHTS:
59
+ lines.append(f'const {name}Base64 =')
60
+ lines.append(f' "{fonts[name]}";')
61
+ lines.append("")
62
+ lines.append("/** ABC Monument Grotesk Regular (400) as raw TTF bytes. */")
63
+ lines.append('export const MONUMENT_REGULAR = Buffer.from(regularBase64, "base64");')
64
+ lines.append("/** ABC Monument Grotesk Medium (500) as raw TTF bytes. */")
65
+ lines.append('export const MONUMENT_MEDIUM = Buffer.from(mediumBase64, "base64");')
66
+ lines.append("")
67
+
68
+ with open(OUT_TS, "w") as f:
69
+ f.write("\n".join(lines))
70
+
71
+ sizes = ", ".join(f"{n}={len(b) * 3 // 4 // 1024}KB" for n, b in fonts.items())
72
+ print(f"Wrote {OUT_TS} ({sizes})")
73
+
74
+
75
+ if __name__ == "__main__":
76
+ main()
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env node
2
+ // Generate the bundled, base64-embedded WASM blobs the card renderer needs:
3
+ // - Satori's Yoga layout engine (satori/yoga.wasm)
4
+ // - resvg's SVG rasterizer (@resvg/resvg-wasm/index_bg.wasm)
5
+ //
6
+ // We inline them as base64 (decoded at runtime and handed to satori's `init()`
7
+ // and resvg's `initWasm()`) rather than loading the .wasm files from disk. The
8
+ // default `satori` entry resolves yoga.wasm relative to import.meta.url, which
9
+ // works under Node but not inside a `bun build --compile` single-file binary;
10
+ // embedding the bytes makes the renderer work identically in the published Node
11
+ // CLI and the compiled binary. Same rationale as the inlined fonts/cards.
12
+ //
13
+ // Run from the package root: node scripts/gen-card-wasm.mjs
14
+
15
+ import { readFileSync, writeFileSync } from "node:fs";
16
+ import { createRequire } from "node:module";
17
+ import { dirname, join } from "node:path";
18
+ import { fileURLToPath } from "node:url";
19
+
20
+ const require = createRequire(import.meta.url);
21
+ const here = dirname(fileURLToPath(import.meta.url));
22
+ const outPath = join(here, "..", "src", "lib", "agent", "card-wasm.ts");
23
+
24
+ // Resolve the .wasm files via the package entry points, then sit them next to
25
+ // the resolved module so this keeps working regardless of the hoist layout.
26
+ const yogaWasm = readFileSync(
27
+ join(dirname(require.resolve("satori/package.json")), "yoga.wasm"),
28
+ );
29
+ const resvgWasm = readFileSync(
30
+ require.resolve("@resvg/resvg-wasm/index_bg.wasm"),
31
+ );
32
+
33
+ const ts = `// AUTO-GENERATED by scripts/gen-card-wasm.mjs — do not edit by hand.
34
+ // Inlined WASM for the first-post card renderer: Satori's Yoga layout engine and
35
+ // resvg's SVG rasterizer, base64-encoded so they ship in both the bun binary and
36
+ // the published Node CLI without external asset files.
37
+ /* eslint-disable */
38
+
39
+ import { Buffer } from "node:buffer";
40
+
41
+ const yogaBase64 =
42
+ "${yogaWasm.toString("base64")}";
43
+ const resvgBase64 =
44
+ "${resvgWasm.toString("base64")}";
45
+
46
+ /** Satori's Yoga layout engine (yoga.wasm) as raw bytes. */
47
+ export const YOGA_WASM = Buffer.from(yogaBase64, "base64");
48
+ /** resvg's SVG-to-PNG rasterizer (index_bg.wasm) as raw bytes. */
49
+ export const RESVG_WASM = Buffer.from(resvgBase64, "base64");
50
+ `;
51
+
52
+ writeFileSync(outPath, ts);
53
+ console.log(
54
+ `Wrote ${outPath} (yoga=${Math.round(yogaWasm.length / 1024)}KB, resvg=${Math.round(
55
+ resvgWasm.length / 1024,
56
+ )}KB)`,
57
+ );
@@ -0,0 +1,210 @@
1
+ /**
2
+ * Generates SHA-256 integrity hashes for all skills.
3
+ *
4
+ * Usage:
5
+ * npx tsx scripts/generate-skill-hashes.ts # Print hashes to console
6
+ * npx tsx scripts/generate-skill-hashes.ts --write # Update skills.ts directly
7
+ * npx tsx scripts/generate-skill-hashes.ts --check # Check if hashes are up-to-date (CI)
8
+ */
9
+
10
+ import { createHash } from "node:crypto";
11
+ import { readFileSync, writeFileSync } from "node:fs";
12
+ import { dirname, join } from "node:path";
13
+ import { fileURLToPath } from "node:url";
14
+
15
+ const __dirname = dirname(fileURLToPath(import.meta.url));
16
+ const SKILLS_TS_PATH = join(__dirname, "../src/commands/skills.ts");
17
+
18
+ const SKILLS_URL = "https://agents.zora.com/skill";
19
+
20
+ const computeIntegrity = (content: string): string => {
21
+ const hash = createHash("sha256").update(content, "utf8").digest("base64");
22
+ return `sha256-${hash}`;
23
+ };
24
+
25
+ /**
26
+ * Parse skill names from skills.ts to avoid import dependency chain.
27
+ * Single source of truth - names are extracted from the SKILLS array.
28
+ */
29
+ function parseSkillNamesFromFile(): string[] {
30
+ const content = readFileSync(SKILLS_TS_PATH, "utf8");
31
+ const names: string[] = [];
32
+
33
+ // Match all name: "skillname" patterns within the SKILLS array
34
+ const namePattern = /name:\s*"([^"]+)"/g;
35
+ let match;
36
+ while ((match = namePattern.exec(content)) !== null) {
37
+ names.push(match[1]);
38
+ }
39
+
40
+ if (names.length === 0) {
41
+ throw new Error("No skill names found in skills.ts");
42
+ }
43
+
44
+ return names;
45
+ }
46
+
47
+ /**
48
+ * Parse current hashes from skills.ts
49
+ */
50
+ function getCurrentHashes(): Map<string, string> {
51
+ const content = readFileSync(SKILLS_TS_PATH, "utf8");
52
+ const hashes = new Map<string, string>();
53
+ const skillNames = parseSkillNamesFromFile();
54
+
55
+ for (const name of skillNames) {
56
+ // Use lazy matching to find the integrity field for each skill
57
+ const pattern = new RegExp(
58
+ `name:\\s*"${name}"[\\s\\S]*?integrity:\\s*"([^"]*)"`,
59
+ );
60
+ const match = content.match(pattern);
61
+ if (match) {
62
+ hashes.set(name, match[1]);
63
+ }
64
+ }
65
+
66
+ return hashes;
67
+ }
68
+
69
+ async function fetchAllHashes(): Promise<Map<string, string>> {
70
+ const hashes = new Map<string, string>();
71
+ const skillNames = parseSkillNamesFromFile();
72
+
73
+ for (const name of skillNames) {
74
+ const url = `${SKILLS_URL}/${name}.md`;
75
+ const response = await fetch(url);
76
+ if (!response.ok) {
77
+ throw new Error(`Failed to fetch ${name}: HTTP ${response.status}`);
78
+ }
79
+ const content = await response.text();
80
+ hashes.set(name, computeIntegrity(content));
81
+ }
82
+
83
+ return hashes;
84
+ }
85
+
86
+ function updateSkillsFile(hashes: Map<string, string>): boolean {
87
+ const content = readFileSync(SKILLS_TS_PATH, "utf8");
88
+ let updated = content;
89
+ let changed = false;
90
+ const errors: string[] = [];
91
+
92
+ for (const [name, hash] of hashes) {
93
+ // First, verify this skill has an integrity field by checking
94
+ // that we can find it within its own object block (before the next skill)
95
+ // Find the position of this skill's name
96
+ const namePattern = new RegExp(`name:\\s*"${name}"`);
97
+ const nameMatch = namePattern.exec(updated);
98
+ if (!nameMatch) {
99
+ errors.push(`Skill "${name}" not found in skills.ts`);
100
+ continue;
101
+ }
102
+
103
+ const namePos = nameMatch.index;
104
+
105
+ // Find the next skill's name (if any) to bound our search
106
+ const remainingContent = updated.slice(namePos + nameMatch[0].length);
107
+ const nextSkillMatch = /name:\s*"[^"]+"/.exec(remainingContent);
108
+ const searchBound = nextSkillMatch
109
+ ? namePos + nameMatch[0].length + nextSkillMatch.index
110
+ : updated.length;
111
+
112
+ // Extract the bounded region for this skill
113
+ const skillRegion = updated.slice(namePos, searchBound);
114
+
115
+ // Check if integrity field exists in this skill's region
116
+ const integrityInRegion = /integrity:\s*"[^"]*"/.exec(skillRegion);
117
+ if (!integrityInRegion) {
118
+ errors.push(
119
+ `Skill "${name}" is missing integrity field - add 'integrity: "sha256-PLACEHOLDER"' to the skill definition`,
120
+ );
121
+ continue;
122
+ }
123
+
124
+ // Now safely replace the integrity value within the bounded region
125
+ const updatedRegion = skillRegion.replace(
126
+ /(integrity:\s*)"[^"]*"/,
127
+ `$1"${hash}"`,
128
+ );
129
+
130
+ if (updatedRegion !== skillRegion) {
131
+ changed = true;
132
+ updated = updated.slice(0, namePos) + updatedRegion + updated.slice(searchBound);
133
+ }
134
+ }
135
+
136
+ if (errors.length > 0) {
137
+ console.error("\nErrors found:");
138
+ for (const err of errors) {
139
+ console.error(` - ${err}`);
140
+ }
141
+ process.exit(1);
142
+ }
143
+
144
+ if (changed) {
145
+ writeFileSync(SKILLS_TS_PATH, updated);
146
+ }
147
+
148
+ return changed;
149
+ }
150
+
151
+ async function main() {
152
+ const args = process.argv.slice(2);
153
+ const writeMode = args.includes("--write");
154
+ const checkMode = args.includes("--check");
155
+
156
+ console.log("Fetching skills from production...\n");
157
+
158
+ let remoteHashes: Map<string, string>;
159
+ try {
160
+ remoteHashes = await fetchAllHashes();
161
+ } catch (err) {
162
+ console.error("Failed to fetch skills:", err);
163
+ process.exit(1);
164
+ }
165
+
166
+ if (checkMode) {
167
+ const currentHashes = getCurrentHashes();
168
+ let hasChanges = false;
169
+
170
+ for (const [name, remoteHash] of remoteHashes) {
171
+ const currentHash = currentHashes.get(name);
172
+ if (currentHash !== remoteHash) {
173
+ console.log(`${name}: CHANGED`);
174
+ console.log(` Current: ${currentHash}`);
175
+ console.log(` Expected: ${remoteHash}\n`);
176
+ hasChanges = true;
177
+ }
178
+ }
179
+
180
+ if (hasChanges) {
181
+ console.log("Skill hashes are out of date. Run with --write to update.");
182
+ process.exit(1);
183
+ } else {
184
+ console.log("All skill hashes are up to date.");
185
+ process.exit(0);
186
+ }
187
+ }
188
+
189
+ if (writeMode) {
190
+ const changed = updateSkillsFile(remoteHashes);
191
+ if (changed) {
192
+ console.log("Updated skills.ts with new hashes:");
193
+ for (const [name, hash] of remoteHashes) {
194
+ console.log(` ${name}: ${hash}`);
195
+ }
196
+ } else {
197
+ console.log("No changes needed - hashes are already up to date.");
198
+ }
199
+ } else {
200
+ console.log("Generated hashes (run with --write to update skills.ts):\n");
201
+ for (const [name, hash] of remoteHashes) {
202
+ console.log(` ${name}: ${hash}`);
203
+ }
204
+ }
205
+ }
206
+
207
+ main().catch((err) => {
208
+ console.error("Fatal error:", err);
209
+ process.exit(1);
210
+ });
@@ -0,0 +1,118 @@
1
+ // Work around an upstream packaging bug in @xmtp/node-bindings (present in 1.10.0,
2
+ // the version pulled in by @xmtp/node-sdk@6): the prebuilt macOS .node files link
3
+ // libiconv from a Nix store path (e.g. /nix/store/<hash>-libiconv-*/lib/libiconv.2.dylib)
4
+ // instead of the system one. On a stock Mac dyld falls back to /usr/lib and it
5
+ // loads, but environments that override DYLD_FALLBACK_LIBRARY_PATH (Nix shells,
6
+ // some sandboxes) can't fall back, so `Client.create` fails with a dlopen error.
7
+ //
8
+ // This repoints that load command to /usr/lib/libiconv.2.dylib in the installed
9
+ // .node so the binding loads on any Mac. It runs from `build`/`build:js` (fixes
10
+ // the node_modules install for source/dev runs), `postinstall` (npm i -g), and
11
+ // before `bun build --compile` for the `build:binary:mac-*` targets.
12
+ //
13
+ // Safe by design — exits 0 (no-op) when:
14
+ // • not running on macOS (the only place install_name_tool/codesign exist),
15
+ // • @xmtp/node-bindings isn't installed,
16
+ // • no macOS .node files are present,
17
+ // • the binding is already correctly linked (e.g. after XMTP ships a fix).
18
+ // Because it keys off the build *host* (process.platform), the macOS targets must
19
+ // be built on a macOS runner for the fix to apply (which signed releases need anyway).
20
+
21
+ import { execFileSync } from "node:child_process";
22
+ import { createRequire } from "node:module";
23
+ import { dirname, join } from "node:path";
24
+ import { existsSync, copyFileSync, renameSync } from "node:fs";
25
+
26
+ const SYSTEM_LIBICONV = "/usr/lib/libiconv.2.dylib";
27
+ const log = (msg) => console.log(`[patch-xmtp-binding] ${msg}`);
28
+
29
+ if (process.platform !== "darwin") {
30
+ log(`skipping on ${process.platform} — macOS-only fix`);
31
+ process.exit(0);
32
+ }
33
+
34
+ let distDir;
35
+ try {
36
+ // @xmtp/node-bindings is a transitive dep (via @xmtp/node-sdk), so under pnpm's
37
+ // strict node_modules it can't be resolved directly from here. Resolve node-sdk
38
+ // (a direct dep) first, then node-bindings from there. Its `exports` only expose
39
+ // ".", so resolve the entry (./dist/index.js) and take its directory — that's
40
+ // where the platform .node files live.
41
+ const require = createRequire(import.meta.url);
42
+ const sdkRequire = createRequire(require.resolve("@xmtp/node-sdk"));
43
+ distDir = dirname(sdkRequire.resolve("@xmtp/node-bindings"));
44
+ } catch (err) {
45
+ log(`@xmtp/node-bindings not found (${err.message}) — nothing to patch`);
46
+ process.exit(0);
47
+ }
48
+
49
+ const targets = [
50
+ "bindings_node.darwin-arm64.node",
51
+ "bindings_node.darwin-x64.node",
52
+ ]
53
+ .map((f) => join(distDir, f))
54
+ .filter(existsSync);
55
+
56
+ if (targets.length === 0) {
57
+ log("no macOS .node files present — nothing to patch");
58
+ process.exit(0);
59
+ }
60
+
61
+ // Resolve Apple's toolchain by absolute path so the patch still applies when run
62
+ // inside a Nix shell (or anything else that shadows these tools on PATH) — the
63
+ // case that left the binding broken for some contributors. `/usr/bin/{otool,
64
+ // install_name_tool,codesign}` are Apple stubs that dispatch through the active
65
+ // Xcode toolchain regardless of PATH. Falls back to a bare PATH lookup if the
66
+ // system copy isn't where we expect.
67
+ const systemTool = (name) =>
68
+ existsSync(`/usr/bin/${name}`) ? `/usr/bin/${name}` : name;
69
+ const OTOOL = systemTool("otool");
70
+ const INSTALL_NAME_TOOL = systemTool("install_name_tool");
71
+ const CODESIGN = systemTool("codesign");
72
+
73
+ const run = (cmd, args) => execFileSync(cmd, args, { encoding: "utf8" });
74
+
75
+ let patched = 0;
76
+ for (const file of targets) {
77
+ let deps;
78
+ try {
79
+ deps = run(OTOOL, ["-L", file]);
80
+ } catch (err) {
81
+ // Xcode command line tools unavailable — warn but don't fail the build.
82
+ log(
83
+ `otool unavailable (${err.message}); skipping — install Xcode CLT to apply the fix`,
84
+ );
85
+ process.exit(0);
86
+ }
87
+
88
+ // The load command for libiconv that isn't already the system path.
89
+ const bad = deps
90
+ .split("\n")
91
+ .map((line) => line.trim().split(/\s+/)[0])
92
+ .find((p) => /\/libiconv\.2\.dylib$/.test(p) && p !== SYSTEM_LIBICONV);
93
+
94
+ if (!bad) {
95
+ log(`already OK: ${file}`);
96
+ continue;
97
+ }
98
+
99
+ try {
100
+ log(`repointing libiconv: ${bad} -> ${SYSTEM_LIBICONV}`);
101
+ // Break the hard link into the pnpm store so we edit a private copy in
102
+ // node_modules, never the shared store object other projects link to.
103
+ const tmp = `${file}.patching`;
104
+ copyFileSync(file, tmp);
105
+ renameSync(tmp, file);
106
+ run(INSTALL_NAME_TOOL, ["-change", bad, SYSTEM_LIBICONV, file]);
107
+ // Editing load commands invalidates the signature; re-sign ad-hoc so the
108
+ // embedded binary still loads under macOS code-signing checks.
109
+ run(CODESIGN, ["-f", "-s", "-", file]);
110
+ patched += 1;
111
+ } catch (err) {
112
+ // Best-effort: never fail the caller (build / postinstall). On a stock Mac
113
+ // the dyld /usr/lib fallback still loads the binding without this patch.
114
+ log(`could not patch ${file} (${err.message}); leaving as-is`);
115
+ }
116
+ }
117
+
118
+ log(`done — ${patched} file(s) patched`);
@@ -2,6 +2,20 @@
2
2
 
3
3
  import path from "node:path";
4
4
  import os from "node:os";
5
+ import { execFileSync } from "node:child_process";
6
+ import { fileURLToPath } from "node:url";
7
+
8
+ // Best-effort: fix the XMTP macOS native binding's libiconv link so `zora`'s DM
9
+ // commands load without a DYLD_FALLBACK_LIBRARY_PATH workaround. No-op on
10
+ // non-macOS, missing Xcode toolchain, or when already correct. Never fails install.
11
+ try {
12
+ const here = path.dirname(fileURLToPath(import.meta.url));
13
+ execFileSync(process.execPath, [path.join(here, "patch-xmtp-binding.mjs")], {
14
+ stdio: "inherit",
15
+ });
16
+ } catch {
17
+ // ignore — see patch-xmtp-binding.mjs
18
+ }
5
19
 
6
20
  try {
7
21
  if (process.env.npm_config_global !== "true") {