@zodiaceco/sdk 1.12.1

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.
@@ -0,0 +1,90 @@
1
+ //#region src/cli/config.d.ts
2
+ type Contracts = {
3
+ [chain: string]: ContractsNode;
4
+ };
5
+ type ContractsNode = `0x${string}` | {
6
+ [name: string]: ContractsNode;
7
+ };
8
+ interface ZodiacConfig {
9
+ /**
10
+ * API key authorizing this directory against a Zodiac org.
11
+ *
12
+ * Optional — defaults to `process.env.ZODIAC_API_KEY` (populated by
13
+ * `zodiac init`). Set this explicitly only when you need to override
14
+ * the env var from inside the config file.
15
+ */
16
+ apiKey?: `zodiac_${string}`;
17
+ /**
18
+ * Contracts the `allow` kit should know about, keyed by chain prefix.
19
+ * Nested objects are allowed for grouping related addresses.
20
+ */
21
+ contracts?: Contracts;
22
+ /**
23
+ * Directory where fetched ABIs are stored and read from.
24
+ * Resolved relative to the project root (config file's directory).
25
+ * Defaults to `./abis`.
26
+ */
27
+ abisDir?: string;
28
+ }
29
+ /** User-provided config plus the resolved API key + project root. */
30
+ interface ResolvedConfig extends Omit<ZodiacConfig, 'apiKey'> {
31
+ apiKey: `zodiac_${string}`;
32
+ rootDir: string;
33
+ }
34
+ /**
35
+ * Loose base used as the *inference* constraint for `defineConfig`.
36
+ * `contracts` is `Record<string, unknown>` here so `const T` can preserve the
37
+ * caller's exact address literals rather than collapsing them into the
38
+ * recursive `ContractsNode` union.
39
+ */
40
+ type DefineConfigInput = {
41
+ apiKey?: `zodiac_${string}`;
42
+ contracts?: Record<string, unknown>;
43
+ abisDir?: string;
44
+ };
45
+ /**
46
+ * Recursive leaf-level check: every leaf in `contracts` must be
47
+ * `` `0x${string}` ``; any other value collapses the branch to `never`,
48
+ * which surfaces as a type error at the call site.
49
+ */
50
+ type ValidateContracts<C> = { [K in keyof C]: C[K] extends `0x${string}` ? C[K] : C[K] extends object ? ValidateContracts<C[K]> : never };
51
+ declare const defineConfig: <const T extends DefineConfigInput>(config: T & {
52
+ contracts?: ValidateContracts<NonNullable<T["contracts"]>>;
53
+ }) => T;
54
+ type LoadConfigOptions = {
55
+ /**
56
+ * Called when neither `config.apiKey` nor `process.env.ZODIAC_API_KEY` is
57
+ * set. Receives the resolved project root and must return a freshly minted
58
+ * API key. Typically wired to the interactive `init()` flow.
59
+ *
60
+ * If omitted, `loadConfig` throws when no key is found.
61
+ */
62
+ onMissingKey?: (rootDir: string) => Promise<string>;
63
+ /**
64
+ * When the config file doesn't exist, write a minimal stub before
65
+ * loading. Used by the CLI's `pull` commands so first-time setup
66
+ * works without the user having to scaffold the file themselves.
67
+ */
68
+ createIfMissing?: boolean;
69
+ };
70
+ /**
71
+ * Write a starter `zodiac.config.ts` if no file exists at `absolutePath`.
72
+ * Returns `true` if a file was written.
73
+ */
74
+ declare const ensureConfigStub: (absolutePath: string) => boolean;
75
+ declare function loadConfig(configPath?: string, options?: LoadConfigOptions): Promise<ResolvedConfig>;
76
+ declare const DEFAULT_ABIS_DIR = "abis";
77
+ /**
78
+ * Resolves the absolute path to the ABIs directory. Only `rootDir` (and
79
+ * optionally `abisDir`) are read, so the parameter is intentionally narrower
80
+ * than `ResolvedConfig`: userland code can pass `{ ...config, rootDir }`
81
+ * without first satisfying `apiKey` (normally supplied via the
82
+ * `ZODIAC_API_KEY` env var at runtime).
83
+ */
84
+ declare function resolveAbisDir(config: {
85
+ rootDir: string;
86
+ abisDir?: string;
87
+ }): string;
88
+ //#endregion
89
+ export { Contracts, ContractsNode, DEFAULT_ABIS_DIR, ResolvedConfig, ZodiacConfig, defineConfig, ensureConfigStub, loadConfig, resolveAbisDir };
90
+ //# sourceMappingURL=config.d.mts.map
@@ -0,0 +1,2 @@
1
+ import { a as resolveAbisDir, i as loadConfig, n as defineConfig, r as ensureConfigStub, t as DEFAULT_ABIS_DIR } from "../config-D5ArAp07.mjs";
2
+ export { DEFAULT_ABIS_DIR, defineConfig, ensureConfigStub, loadConfig, resolveAbisDir };
package/dist/cli.d.mts ADDED
@@ -0,0 +1 @@
1
+ export { };
package/dist/cli.mjs ADDED
@@ -0,0 +1,574 @@
1
+ #!/usr/bin/env node
2
+ import { n as resolveZodiacDir, t as ApiClient } from "./api-C49DoaPC.mjs";
3
+ import { a as walkContracts, i as readAbi, n as chainIdFor, o as writeAbi, r as abiFilePath } from "./networks-BTW1qAAa.mjs";
4
+ import { a as resolveAbisDir, i as loadConfig, o as findProjectRoot, r as ensureConfigStub } from "./config-D5ArAp07.mjs";
5
+ import fs, { existsSync, readFileSync, writeFileSync } from "node:fs";
6
+ import path, { basename, join, resolve } from "node:path";
7
+ import { invariant } from "@epic-web/invariant";
8
+ import { getAddress } from "ethers";
9
+ import { Command } from "commander";
10
+ import { config } from "dotenv";
11
+ import { createHash, randomBytes } from "node:crypto";
12
+ import { createServer } from "node:http";
13
+ import open from "open";
14
+ import { join as join$1 } from "path";
15
+ import { ModuleKind, Project, ScriptTarget, VariableDeclarationKind } from "ts-morph";
16
+ import { mkdirSync, writeFileSync as writeFileSync$1 } from "fs";
17
+ //#region src/cli/commands/init.ts
18
+ const DEFAULT_APP_URL = "https://app.zodiac.eco";
19
+ const CALLBACK_TIMEOUT_MS = 300 * 1e3;
20
+ const init = async (options = {}) => {
21
+ const rootDir = options.rootDir ?? findProjectRoot();
22
+ const appUrl = (options.appUrl ?? process.env.ZODIAC_APP_URL ?? DEFAULT_APP_URL).replace(/\/$/, "");
23
+ const label = basename(resolve(rootDir)) || "zodiac-cli";
24
+ const codeVerifier = randomBytes(32).toString("base64url");
25
+ const codeChallenge = createHash("sha256").update(codeVerifier).digest("base64url");
26
+ const state = randomBytes(32).toString("base64url");
27
+ const { port, waitForCode, close } = await startCallbackServer({
28
+ expectedState: state,
29
+ appUrl
30
+ });
31
+ const callbackUrl = `http://127.0.0.1:${port}/callback`;
32
+ const authUrl = new URL("/cli-auth", appUrl);
33
+ authUrl.searchParams.set("callback", callbackUrl);
34
+ authUrl.searchParams.set("state", state);
35
+ authUrl.searchParams.set("code_challenge", codeChallenge);
36
+ authUrl.searchParams.set("label", label);
37
+ console.log(`Opening ${authUrl} in your browser. Approve the request to receive an API key.`);
38
+ await open(authUrl.toString());
39
+ let code;
40
+ try {
41
+ code = await waitForCode(CALLBACK_TIMEOUT_MS);
42
+ } finally {
43
+ await close();
44
+ }
45
+ const apiKey = await exchangeCodeForKey(appUrl, code, codeVerifier);
46
+ const envPath = join(rootDir, ".env");
47
+ const apiUrl = `${appUrl}/api/v1`;
48
+ writeEnv(envPath, {
49
+ ZODIAC_API_KEY: apiKey,
50
+ ZODIAC_API_URL: apiUrl
51
+ });
52
+ process.env.ZODIAC_API_KEY = apiKey;
53
+ process.env.ZODIAC_API_URL = apiUrl;
54
+ console.log(`✅ API key written to ${envPath}`);
55
+ const configPath = join(rootDir, "zodiac.config.ts");
56
+ if (ensureConfigStub(configPath)) console.log(`✅ Created ${configPath}`);
57
+ return apiKey;
58
+ };
59
+ const exchangeCodeForKey = async (appUrl, code, codeVerifier) => {
60
+ const response = await fetch(`${appUrl}/cli-auth/exchange`, {
61
+ method: "POST",
62
+ headers: { "content-type": "application/json" },
63
+ body: JSON.stringify({
64
+ code,
65
+ code_verifier: codeVerifier
66
+ })
67
+ });
68
+ if (!response.ok) {
69
+ let detail = "";
70
+ try {
71
+ const body = await response.json();
72
+ detail = body.error ? `: ${body.error}` : "";
73
+ } catch {}
74
+ throw new Error(`Failed to exchange auth code (${response.status})${detail}`);
75
+ }
76
+ const { key } = await response.json();
77
+ if (typeof key !== "string" || !key.startsWith("zodiac_")) throw new Error("Exchange endpoint returned an unexpected response (missing or malformed key)");
78
+ return key;
79
+ };
80
+ const startCallbackServer = async ({ expectedState, appUrl }) => {
81
+ let resolveCode = () => {};
82
+ let rejectCode = () => {};
83
+ const codePromise = new Promise((res, rej) => {
84
+ resolveCode = res;
85
+ rejectCode = rej;
86
+ });
87
+ const handler = (req, res) => {
88
+ if (req.method !== "GET" || !req.url?.startsWith("/callback")) {
89
+ res.statusCode = 404;
90
+ res.end();
91
+ return;
92
+ }
93
+ const url = new URL(req.url, `http://127.0.0.1`);
94
+ const code = url.searchParams.get("code");
95
+ if (url.searchParams.get("state") !== expectedState) {
96
+ respondHtml(res, 403, renderHtml("Mismatched state", `<p>This callback didn't match the running <code>zodiac init</code> session. You can close this tab.</p>`));
97
+ return;
98
+ }
99
+ if (typeof code !== "string" || code.length === 0) {
100
+ respondHtml(res, 400, renderHtml("Missing code", `<p>The callback URL didn't include an auth <code>code</code>. Please re-run <code>zodiac init</code>.</p>`));
101
+ return;
102
+ }
103
+ respondHtml(res, 200, renderHtml("Authorized", `<p>Your CLI now has its API key. You can close this tab and return to your terminal.</p>
104
+ <p><a href="${escapeHtml(appUrl)}">Back to Zodiac</a></p>`));
105
+ resolveCode(code);
106
+ };
107
+ const server = createServer(handler);
108
+ await new Promise((res) => server.listen(0, "127.0.0.1", res));
109
+ const address = server.address();
110
+ if (address == null || typeof address === "string") throw new Error("Failed to bind loopback callback server");
111
+ const port = address.port;
112
+ const close = () => new Promise((resolve) => {
113
+ server.close(() => resolve());
114
+ });
115
+ const waitForCode = (timeoutMs) => Promise.race([codePromise, new Promise((_, rej) => setTimeout(() => {
116
+ rejectCode(/* @__PURE__ */ new Error(`Timed out waiting for CLI auth callback`));
117
+ rej(/* @__PURE__ */ new Error(`Timed out waiting for CLI auth callback`));
118
+ }, timeoutMs))]);
119
+ return {
120
+ port,
121
+ waitForCode,
122
+ close
123
+ };
124
+ };
125
+ const respondHtml = (res, status, body) => {
126
+ res.setHeader("Connection", "close");
127
+ res.setHeader("content-type", "text/html; charset=utf-8");
128
+ res.statusCode = status;
129
+ res.end(body);
130
+ };
131
+ const renderHtml = (title, bodyHtml) => `<!doctype html>
132
+ <html lang="en">
133
+ <head>
134
+ <meta charset="utf-8" />
135
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
136
+ <title>${escapeHtml(title)} — Zodiac CLI</title>
137
+ <style>
138
+ body { font: 16px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; max-width: 32rem; margin: 4rem auto; padding: 0 1rem; color: #1f2937; }
139
+ h1 { font-weight: 300; font-size: 1.75rem; margin-bottom: 1rem; }
140
+ code { background: #f3f4f6; padding: 0.125rem 0.375rem; border-radius: 0.25rem; }
141
+ a { color: #1d4ed8; }
142
+ </style>
143
+ </head>
144
+ <body>
145
+ <h1>${escapeHtml(title)}</h1>
146
+ ${bodyHtml}
147
+ </body>
148
+ </html>
149
+ `;
150
+ const escapeHtml = (value) => value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
151
+ const writeEnv = (envPath, vars) => {
152
+ let contents = existsSync(envPath) ? readFileSync(envPath, "utf8") : "";
153
+ for (const [key, value] of Object.entries(vars)) contents = upsertEnvLine(contents, key, value);
154
+ writeFileSync(envPath, contents, "utf8");
155
+ };
156
+ const upsertEnvLine = (contents, key, value) => {
157
+ const line = `${key}=${value}`;
158
+ const lines = contents.split(/\r?\n/);
159
+ const idx = lines.findIndex((l) => new RegExp(`^\\s*${key}\\s*=`).test(l));
160
+ if (idx >= 0) {
161
+ lines[idx] = line;
162
+ return lines.join("\n");
163
+ }
164
+ return contents.length === 0 || contents.endsWith("\n") ? `${contents}${line}\n` : `${contents}\n${line}\n`;
165
+ };
166
+ //#endregion
167
+ //#region src/cli/commands/pullOrg.ts
168
+ const toLiteral = (value, indent = 0) => {
169
+ const pad = " ".repeat(indent);
170
+ const childPad = " ".repeat(indent + 1);
171
+ if (value === null) return "null";
172
+ if (typeof value === "bigint") return `${value}n`;
173
+ if (typeof value === "string") return JSON.stringify(value);
174
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
175
+ if (Array.isArray(value)) {
176
+ if (value.length === 0) return "[]";
177
+ return `[\n${value.map((v) => `${childPad}${toLiteral(v, indent + 1)}`).join(",\n")},\n${pad}]`;
178
+ }
179
+ if (typeof value === "object") {
180
+ const entries = Object.entries(value);
181
+ if (entries.length === 0) return "{}";
182
+ return `{\n${entries.map(([k, v]) => `${childPad}${/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(k) ? k : JSON.stringify(k)}: ${toLiteral(v, indent + 1)}`).join(",\n")},\n${pad}}`;
183
+ }
184
+ return String(value);
185
+ };
186
+ const pullOrg = async (config) => {
187
+ const client = new ApiClient({ apiKey: config.apiKey });
188
+ const [users, workspaceAccounts] = await Promise.all([client.listUsers(), client.listAccounts()]);
189
+ const resolvableAccounts = workspaceAccounts.flatMap((ws) => ws.accounts).filter((a) => a.spec != null || a.vault);
190
+ const resolved = /* @__PURE__ */ new Map();
191
+ if (resolvableAccounts.length > 0) {
192
+ const response = await client.resolveConstellation(workspaceAccounts[0].workspaceId, { specification: resolvableAccounts.map((account, i) => account.spec != null ? account.spec : {
193
+ ref: `vault_${i}`,
194
+ type: "SAFE",
195
+ chain: account.chain,
196
+ address: account.address
197
+ }) });
198
+ invariant(response?.result?.length === resolvableAccounts.length, `resolveConstellation returned ${response?.result?.length ?? 0} accounts for ${resolvableAccounts.length} accounts`);
199
+ resolvableAccounts.forEach((account, i) => {
200
+ resolved.set(account.id, response.result[i]);
201
+ });
202
+ }
203
+ const accountsRecord = {};
204
+ for (const ws of workspaceAccounts) {
205
+ const safes = {};
206
+ const rolesMods = {};
207
+ const delays = {};
208
+ const bucketsByType = {
209
+ SAFE: safes,
210
+ ROLES: rolesMods,
211
+ DELAY: delays
212
+ };
213
+ const isNodeType = (type) => type === "SAFE" || type === "ROLES" || type === "DELAY";
214
+ const labelCountByType = {
215
+ SAFE: /* @__PURE__ */ new Map(),
216
+ ROLES: /* @__PURE__ */ new Map(),
217
+ DELAY: /* @__PURE__ */ new Map()
218
+ };
219
+ for (const account of ws.accounts) {
220
+ const { label } = account;
221
+ if (label == null || !isNodeType(account.type)) continue;
222
+ const counts = labelCountByType[account.type];
223
+ counts.set(label, (counts.get(label) ?? 0) + 1);
224
+ }
225
+ for (const account of ws.accounts) {
226
+ const { label } = account;
227
+ if (label == null || !isNodeType(account.type)) continue;
228
+ const onChain = resolved.get(account.id);
229
+ const key = (labelCountByType[account.type].get(label) ?? 0) > 1 ? `${label} (${getAddress(account.address)})` : label;
230
+ bucketsByType[account.type][key] = {
231
+ id: account.id,
232
+ label,
233
+ address: account.address,
234
+ chain: account.chain,
235
+ vault: account.vault,
236
+ ...onChain?.type === "SAFE" && {
237
+ threshold: onChain.threshold,
238
+ owners: [...onChain.owners],
239
+ modules: [...onChain.modules]
240
+ }
241
+ };
242
+ }
243
+ accountsRecord[ws.workspaceName] = {
244
+ workspaceId: ws.workspaceId,
245
+ workspaceName: ws.workspaceName,
246
+ safes,
247
+ rolesMods,
248
+ delays
249
+ };
250
+ }
251
+ const nameCount = /* @__PURE__ */ new Map();
252
+ for (const user of users) nameCount.set(user.fullName, (nameCount.get(user.fullName) ?? 0) + 1);
253
+ const usersRecord = {};
254
+ for (const user of users) {
255
+ const handle = nameCount.get(user.fullName) > 1 ? `${user.fullName} (${user.id})` : user.fullName;
256
+ usersRecord[handle] = {
257
+ id: user.id,
258
+ fullName: user.fullName,
259
+ personalSafes: user.personalSafes
260
+ };
261
+ }
262
+ const outDir = resolveZodiacDir(config.rootDir);
263
+ mkdirSync(outDir, { recursive: true });
264
+ writeFileSync$1(join$1(outDir, "package.json"), JSON.stringify({
265
+ type: "commonjs",
266
+ main: "index.js",
267
+ types: "index.d.ts"
268
+ }, null, 2));
269
+ const sourceFile = new Project({
270
+ compilerOptions: {
271
+ declaration: true,
272
+ module: ModuleKind.CommonJS,
273
+ target: ScriptTarget.ESNext,
274
+ outDir
275
+ },
276
+ useInMemoryFileSystem: true
277
+ }).createSourceFile("index.ts", "");
278
+ sourceFile.addVariableStatement({
279
+ isExported: true,
280
+ declarationKind: VariableDeclarationKind.Const,
281
+ declarations: [{
282
+ name: "users",
283
+ initializer: `${toLiteral(usersRecord)} as const`
284
+ }]
285
+ });
286
+ sourceFile.addVariableStatement({
287
+ isExported: true,
288
+ declarationKind: VariableDeclarationKind.Const,
289
+ declarations: [{
290
+ name: "accounts",
291
+ initializer: `${toLiteral(accountsRecord)} as const`
292
+ }]
293
+ });
294
+ const emitResult = sourceFile.getEmitOutput();
295
+ for (const outputFile of emitResult.getOutputFiles()) {
296
+ const fileName = outputFile.getFilePath().includes(".d.ts") ? "index.d.ts" : "index.js";
297
+ let contents = outputFile.getText();
298
+ if (fileName === "index.d.ts") contents += `
299
+ declare global {
300
+ interface ZodiacGeneratedCodegen {
301
+ users: typeof users;
302
+ accounts: typeof accounts;
303
+ }
304
+ }
305
+ `;
306
+ writeFileSync$1(join$1(outDir, fileName), contents);
307
+ }
308
+ };
309
+ //#endregion
310
+ //#region src/allow/fetch.ts
311
+ async function fetchAbi(chainId, address) {
312
+ const url = `https://api.abi.pub/v1/chains/${chainId}/etherscan?module=contract&action=getabi&address=${address}`;
313
+ let body;
314
+ try {
315
+ const resp = await fetch(url);
316
+ if (!resp.ok) return null;
317
+ body = await resp.json();
318
+ } catch {
319
+ return null;
320
+ }
321
+ if (body.status !== "1" || typeof body.result !== "string") return null;
322
+ try {
323
+ const parsed = JSON.parse(body.result);
324
+ if (!Array.isArray(parsed) || parsed.length === 0) return null;
325
+ return parsed;
326
+ } catch {
327
+ return null;
328
+ }
329
+ }
330
+ //#endregion
331
+ //#region src/allow/codegen.ts
332
+ function generateAllowTypes(abisDir, contractsConfig) {
333
+ const root = /* @__PURE__ */ new Map();
334
+ for (const node of walkContracts(contractsConfig)) {
335
+ const abi = readAbi(abisDir, node);
336
+ if (!abi) continue;
337
+ insertIntoTree(root, [node.chain, ...node.segments], {
338
+ node,
339
+ abi
340
+ });
341
+ }
342
+ const out = [];
343
+ out.push("// AUTO-GENERATED by `zodiac pull-contracts`. Do not edit.");
344
+ out.push("/* eslint-disable */");
345
+ out.push("");
346
+ out.push(`import type { FunctionPermission, TargetPermission } from "zodiac-roles-sdk";`);
347
+ out.push(`import type { Scoping, Options, EVERYTHING } from "@zodiaceco/sdk/allow";`);
348
+ out.push("");
349
+ out.push("declare global {");
350
+ out.push(" interface AllowKit " + renderTree(root, " "));
351
+ out.push("}");
352
+ out.push("");
353
+ out.push("export {};");
354
+ out.push("");
355
+ return out.join("\n");
356
+ }
357
+ function insertIntoTree(tree, segments, leaf) {
358
+ const [first, ...rest] = segments;
359
+ if (!first) throw new Error("empty segments");
360
+ if (rest.length === 0) {
361
+ tree.set(first, leaf);
362
+ return;
363
+ }
364
+ let child = tree.get(first);
365
+ if (!child || child.node) {
366
+ child = /* @__PURE__ */ new Map();
367
+ tree.set(first, child);
368
+ }
369
+ insertIntoTree(child, rest, leaf);
370
+ }
371
+ function renderTree(tree, indent) {
372
+ const lines = ["{"];
373
+ for (const [key, value] of tree) {
374
+ const safeKey = renderPropKey(key);
375
+ if (value instanceof Map) lines.push(`${indent} ${safeKey}: ${renderTree(value, indent + " ")};`);
376
+ else {
377
+ const { node, abi } = value;
378
+ lines.push(`${indent} ${safeKey}: ${renderContractType(node, abi, indent + " ")};`);
379
+ }
380
+ }
381
+ lines.push(`${indent}}`);
382
+ return lines.join("\n");
383
+ }
384
+ function renderPropKey(name) {
385
+ return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) ? name : JSON.stringify(name);
386
+ }
387
+ function renderContractType(node, abi, indent) {
388
+ const members = [];
389
+ const seen = /* @__PURE__ */ new Set();
390
+ members.push(`${indent} [EVERYTHING]: (options?: Options) => TargetPermission;`);
391
+ for (const fragment of abi) {
392
+ if (fragment.type !== "function") continue;
393
+ if (fragment.stateMutability === "view" || fragment.stateMutability === "pure") continue;
394
+ const name = fragment.name;
395
+ if (!name || seen.has(name)) continue;
396
+ seen.add(name);
397
+ members.push(renderFunctionSignature(fragment, indent + " "));
398
+ }
399
+ return [
400
+ "{",
401
+ ...members,
402
+ `${indent}}`
403
+ ].join("\n");
404
+ }
405
+ function renderFunctionSignature(fragment, indent) {
406
+ const name = renderPropKey(fragment.name);
407
+ const params = [];
408
+ for (const input of fragment.inputs ?? []) {
409
+ const paramName = sanitizeParamName(input.name || `arg${params.length}`, params.length);
410
+ params.push(`${paramName}?: Scoping<${tsTypeFor(input)}>`);
411
+ }
412
+ params.push(`options?: Options`);
413
+ return `${indent}${name}: (${params.join(", ")}) => FunctionPermission;`;
414
+ }
415
+ function sanitizeParamName(name, index) {
416
+ const cleaned = name.replace(/[^A-Za-z0-9_$]/g, "_");
417
+ if (!cleaned || /^[0-9]/.test(cleaned)) return `arg${index}`;
418
+ return new Set([
419
+ "function",
420
+ "class",
421
+ "new",
422
+ "number",
423
+ "string",
424
+ "object",
425
+ "boolean",
426
+ "symbol",
427
+ "default",
428
+ "return",
429
+ "this",
430
+ "void",
431
+ "delete",
432
+ "in",
433
+ "of",
434
+ "for",
435
+ "while",
436
+ "switch",
437
+ "case",
438
+ "if",
439
+ "else",
440
+ "null",
441
+ "true",
442
+ "false",
443
+ "undefined",
444
+ "any",
445
+ "never",
446
+ "unknown"
447
+ ]).has(cleaned) ? `_${cleaned}` : cleaned;
448
+ }
449
+ function tsTypeFor(fragment) {
450
+ const type = fragment.type;
451
+ const arrayMatch = /^(.*)\[(\d*)\]$/.exec(type);
452
+ if (arrayMatch) return `readonly (${tsTypeFor({
453
+ ...fragment,
454
+ type: arrayMatch[1]
455
+ })})[]`;
456
+ if (type === "tuple") {
457
+ const components = fragment.components ?? [];
458
+ if (components.length === 0) return "Record<string, unknown>";
459
+ return `{ ${components.map((c, i) => {
460
+ return `${renderPropKey(sanitizeParamName(c.name || `f${i}`, i))}: ${tsTypeFor(c)}`;
461
+ }).join("; ")} }`;
462
+ }
463
+ if (type === "address") return "`0x${string}`";
464
+ if (type === "bool") return "boolean";
465
+ if (type === "string") return "string";
466
+ if (type === "bytes") return "import(\"ethers\").BytesLike";
467
+ if (/^bytes\d+$/.test(type)) return "`0x${string}`";
468
+ if (/^u?int\d*$/.test(type)) return "import(\"ethers\").BigNumberish";
469
+ return "unknown";
470
+ }
471
+ function writeGenerated(outFile, source) {
472
+ fs.mkdirSync(path.dirname(outFile), { recursive: true });
473
+ fs.writeFileSync(outFile, source, "utf8");
474
+ }
475
+ //#endregion
476
+ //#region src/cli/commands/pullContracts.ts
477
+ const pullContracts = async (config) => {
478
+ if (!config.contracts || Object.keys(config.contracts).length === 0) {
479
+ console.log("No contracts defined in config, skipping.");
480
+ return;
481
+ }
482
+ const abisDir = resolveAbisDir(config);
483
+ const generatedFile = path.join(resolveZodiacDir(config.rootDir), "allow.d.ts");
484
+ let missing = 0;
485
+ let fetched = 0;
486
+ let existing = 0;
487
+ for (const node of walkContracts(config.contracts)) {
488
+ const file = abiFilePath(abisDir, node);
489
+ if (fs.existsSync(file)) {
490
+ existing++;
491
+ report(node.chain, node.segments, node.address, "ok", file);
492
+ continue;
493
+ }
494
+ let chainId;
495
+ try {
496
+ chainId = chainIdFor(node.chain);
497
+ } catch (error) {
498
+ missing++;
499
+ report(node.chain, node.segments, node.address, "missing", file, error.message);
500
+ continue;
501
+ }
502
+ const abi = await fetchAbi(chainId, node.address);
503
+ if (!abi) {
504
+ missing++;
505
+ report(node.chain, node.segments, node.address, "missing", file, `api.abi.pub returned no ABI for chain ${chainId}`);
506
+ continue;
507
+ }
508
+ writeAbi(abisDir, node, abi);
509
+ fetched++;
510
+ report(node.chain, node.segments, node.address, "fetched", file);
511
+ }
512
+ console.log("");
513
+ console.log(`Contracts summary: ${existing} existing, ${fetched} fetched, ${missing} missing.`);
514
+ if (missing > 0) {
515
+ console.log("");
516
+ console.log("Missing ABIs must be provided manually. Paste the contract");
517
+ console.log("ABI JSON at the paths listed above and re-run `zodiac pull-contracts`.");
518
+ }
519
+ writeGenerated(generatedFile, generateAllowTypes(abisDir, config.contracts));
520
+ console.log("");
521
+ console.log(`Wrote typings to ${path.relative(process.cwd(), generatedFile)}`);
522
+ if (missing > 0) process.exit(1);
523
+ };
524
+ function report(chain, segments, address, status, file, reason) {
525
+ const label = `${chain}.${segments.join(".")}`.padEnd(40, " ");
526
+ const tag = {
527
+ ok: " cached ",
528
+ fetched: " fetched ",
529
+ missing: " MISSING "
530
+ }[status];
531
+ const suffix = reason ? ` — ${reason}` : "";
532
+ console.log(`${tag} ${label} ${address}${suffix}`);
533
+ if (status === "missing") console.log(` → paste ABI at ${file}`);
534
+ }
535
+ //#endregion
536
+ //#region src/cli/run.ts
537
+ config({ quiet: true });
538
+ const loadConfigOrInit = (configPath) => loadConfig(configPath, {
539
+ createIfMissing: true,
540
+ onMissingKey: async (rootDir) => {
541
+ console.log("No ZODIAC_API_KEY found. Starting authorization to mint one for this directory…");
542
+ return init({ rootDir });
543
+ }
544
+ });
545
+ const run = async (argv = process.argv) => {
546
+ const program = new Command();
547
+ program.name("zodiac").description("Zodiac SDK CLI – pull org data and contract ABIs").version("1.0.0").option("-c, --config <path>", "path to the config file", "zodiac.config.ts");
548
+ program.command("pull").description("Fetch Zodiac org data and contract ABIs, generating typed SDK functions. Authorizes this directory automatically on first run.").action(async (_opts, cmd) => {
549
+ const config = await loadConfigOrInit(cmd.optsWithGlobals().config);
550
+ await Promise.all([pullOrg(config), pullContracts(config)]);
551
+ });
552
+ program.command("pull-org").description("Fetch Zodiac users and accounts, generate TypeScript types").action(async (_opts, cmd) => {
553
+ await pullOrg(await loadConfigOrInit(cmd.optsWithGlobals().config));
554
+ });
555
+ program.command("pull-contracts").description("Fetch contract ABIs, generate typed permissions kit").action(async (_opts, cmd) => {
556
+ await pullContracts(await loadConfigOrInit(cmd.optsWithGlobals().config));
557
+ });
558
+ program.command("init").description("Re-authorize this directory / mint a new API key (run automatically by `pull` on first use).").option("--app-url <url>", "Override the Zodiac app URL (defaults to ZODIAC_APP_URL or app.zodiac.eco)").action(async (opts) => {
559
+ await init({ appUrl: opts.appUrl });
560
+ });
561
+ await program.parseAsync(argv);
562
+ };
563
+ //#endregion
564
+ //#region src/cli/index.ts
565
+ run().then(() => {
566
+ process.exit(0);
567
+ }, (error) => {
568
+ if (error) console.error(error);
569
+ process.exit(1);
570
+ });
571
+ //#endregion
572
+ export {};
573
+
574
+ //# sourceMappingURL=cli.mjs.map