@clanker-chain/clanker-cli 2026.9.7-1 → 2026.9.7-3
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/lib/setup-detect.mjs +88 -0
- package/lib/setup.mjs +98 -41
- package/package.json +1 -1
package/lib/setup-detect.mjs
CHANGED
|
@@ -134,3 +134,91 @@ export function detectSetupHints(opts = {}) {
|
|
|
134
134
|
anvilDefaultAddress: ANVIL_DEFAULT_ADDRESS,
|
|
135
135
|
};
|
|
136
136
|
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Pad string to width (truncate with … if longer).
|
|
140
|
+
* @param {string} s
|
|
141
|
+
* @param {number} width
|
|
142
|
+
*/
|
|
143
|
+
function cell(s, width) {
|
|
144
|
+
const t = String(s ?? "");
|
|
145
|
+
if (t.length === width) return t;
|
|
146
|
+
if (t.length < width) return t + " ".repeat(width - t.length);
|
|
147
|
+
if (width <= 1) return "…";
|
|
148
|
+
return `${t.slice(0, width - 1)}…`;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Render detection hints as an aligned two-column table for the terminal.
|
|
153
|
+
* @param {ReturnType<typeof detectSetupHints>} hints
|
|
154
|
+
* @returns {string}
|
|
155
|
+
*/
|
|
156
|
+
export function formatSetupDetectTable(hints) {
|
|
157
|
+
/** @type {[string, string][]} */
|
|
158
|
+
const rows = [];
|
|
159
|
+
rows.push(["Profile dir", hints.home]);
|
|
160
|
+
|
|
161
|
+
if (hints.hasConfig) {
|
|
162
|
+
rows.push(["config.json", `present · preset=${hints.config?.preset ?? "?"}`]);
|
|
163
|
+
rows.push([
|
|
164
|
+
"registry",
|
|
165
|
+
hints.config?.registryAddress ? String(hints.config.registryAddress) : "(none)",
|
|
166
|
+
]);
|
|
167
|
+
} else {
|
|
168
|
+
rows.push(["config.json", "missing · will create"]);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (hints.hasOperator) {
|
|
172
|
+
rows.push([
|
|
173
|
+
"operator.json",
|
|
174
|
+
`present · ${hints.operator?.label ?? "?"} · ${hints.operator?.owner ?? "?"}`,
|
|
175
|
+
]);
|
|
176
|
+
} else {
|
|
177
|
+
rows.push(["operator.json", "missing · needed for whoami"]);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (hints.foundryAvailable && hints.foundryAccounts.length) {
|
|
181
|
+
rows.push([
|
|
182
|
+
"Foundry accounts",
|
|
183
|
+
`${hints.foundryAccounts.length}: ${hints.foundryAccounts.join(", ")}`,
|
|
184
|
+
]);
|
|
185
|
+
} else if (hints.foundryAvailable) {
|
|
186
|
+
rows.push(["Foundry accounts", "none listed"]);
|
|
187
|
+
} else {
|
|
188
|
+
rows.push(["Foundry cast", "not on PATH"]);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (hints.openclawBots.length) {
|
|
192
|
+
rows.push([
|
|
193
|
+
"OpenClaw bot keys",
|
|
194
|
+
`${hints.openclawBots.length} files (bot signing only)`,
|
|
195
|
+
]);
|
|
196
|
+
for (const b of hints.openclawBots) {
|
|
197
|
+
rows.push([" ·", b]);
|
|
198
|
+
}
|
|
199
|
+
} else {
|
|
200
|
+
rows.push(["OpenClaw bot keys", "none"]);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (hints.hasOperatorPrivateKeyEnv) {
|
|
204
|
+
rows.push([
|
|
205
|
+
"OPERATOR_PRIVATE_KEY",
|
|
206
|
+
hints.envAddress ? `set · ${hints.envAddress}` : "set · (invalid)",
|
|
207
|
+
]);
|
|
208
|
+
} else {
|
|
209
|
+
rows.push(["OPERATOR_PRIVATE_KEY", "unset"]);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const col0 = Math.min(
|
|
213
|
+
22,
|
|
214
|
+
Math.max(4, ...rows.map(([k]) => k.length)),
|
|
215
|
+
);
|
|
216
|
+
const lines = [
|
|
217
|
+
`${cell("What", col0)} Value`,
|
|
218
|
+
`${"-".repeat(col0)} ${"-".repeat(48)}`,
|
|
219
|
+
];
|
|
220
|
+
for (const [k, v] of rows) {
|
|
221
|
+
lines.push(`${cell(k, col0)} ${v}`);
|
|
222
|
+
}
|
|
223
|
+
return lines.join("\n");
|
|
224
|
+
}
|
package/lib/setup.mjs
CHANGED
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
addressFromEnv,
|
|
27
27
|
addressFromKeyFile,
|
|
28
28
|
detectSetupHints,
|
|
29
|
+
formatSetupDetectTable,
|
|
29
30
|
} from "./setup-detect.mjs";
|
|
30
31
|
|
|
31
32
|
/**
|
|
@@ -281,37 +282,56 @@ export async function runSetupInteractive(argv, opts = {}) {
|
|
|
281
282
|
};
|
|
282
283
|
|
|
283
284
|
try {
|
|
284
|
-
console.log("
|
|
285
|
-
console.log(
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
if (hints.hasOperatorPrivateKeyEnv) {
|
|
299
|
-
console.log(`OPERATOR_PRIVATE_KEY: set → ${hints.envAddress ?? "(invalid)"}`);
|
|
300
|
-
}
|
|
285
|
+
console.log("");
|
|
286
|
+
console.log("clanker setup — create your local operator profile");
|
|
287
|
+
console.log("");
|
|
288
|
+
console.log(
|
|
289
|
+
"This writes ~/.clanker/config.json (network) and operator.json (who you are).",
|
|
290
|
+
);
|
|
291
|
+
console.log(
|
|
292
|
+
"Reads like `clanker whoami` can use the owner address without a private key;",
|
|
293
|
+
);
|
|
294
|
+
console.log("mint/revoke still need a key file or OPERATOR_PRIVATE_KEY later.");
|
|
295
|
+
console.log("");
|
|
296
|
+
console.log(`Profile directory: ${home}`);
|
|
297
|
+
console.log("");
|
|
298
|
+
console.log(formatSetupDetectTable(hints));
|
|
301
299
|
console.log("");
|
|
302
300
|
|
|
303
301
|
if ((hints.hasConfig || hints.hasOperator) && !flags.force) {
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
302
|
+
if (hints.hasConfig && !hints.hasOperator) {
|
|
303
|
+
console.log(
|
|
304
|
+
"You already have network settings from `clanker init`, but no operator identity.",
|
|
305
|
+
);
|
|
306
|
+
console.log(
|
|
307
|
+
"Continuing will refresh config.json if needed and create operator.json.",
|
|
308
|
+
);
|
|
309
|
+
const cont = await askYesNo("Continue setup?", true);
|
|
310
|
+
if (!cont) {
|
|
311
|
+
throw new Error("Aborted. Re-run `clanker setup` when ready.");
|
|
312
|
+
}
|
|
313
|
+
flags.force = true;
|
|
314
|
+
} else {
|
|
315
|
+
console.log(
|
|
316
|
+
"A full profile already exists. Continuing will REPLACE config.json and/or operator.json",
|
|
317
|
+
);
|
|
318
|
+
console.log("with the answers you give next (same files, new contents).");
|
|
319
|
+
const overwrite = await askYesNo("Replace existing profile files?", false);
|
|
320
|
+
if (!overwrite) {
|
|
321
|
+
throw new Error(
|
|
322
|
+
"Aborted. Pass --force to replace, or edit ~/.clanker/*.json by hand.",
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
flags.force = true;
|
|
307
326
|
}
|
|
308
|
-
|
|
327
|
+
console.log("");
|
|
309
328
|
}
|
|
310
329
|
|
|
330
|
+
console.log("— Network —");
|
|
311
331
|
let preset =
|
|
312
332
|
flags.preset ||
|
|
313
333
|
hints.config?.preset ||
|
|
314
|
-
(await ask("
|
|
334
|
+
(await ask("Which network? sepolia (public hub) or local (Anvil)", "sepolia"));
|
|
315
335
|
preset = String(preset).toLowerCase();
|
|
316
336
|
if (!PRESETS[preset]) throw new Error(`Unknown preset "${preset}"`);
|
|
317
337
|
|
|
@@ -321,7 +341,7 @@ export async function runSetupInteractive(argv, opts = {}) {
|
|
|
321
341
|
fromBlock != null
|
|
322
342
|
? false
|
|
323
343
|
: await askYesNo(
|
|
324
|
-
`Use
|
|
344
|
+
`Speed up chain scans? Use fromBlock ${SEPOLIA_FAST_FROM_BLOCK} (recommended on public RPC)`,
|
|
325
345
|
true,
|
|
326
346
|
);
|
|
327
347
|
if (fromBlock == null) {
|
|
@@ -329,25 +349,41 @@ export async function runSetupInteractive(argv, opts = {}) {
|
|
|
329
349
|
}
|
|
330
350
|
}
|
|
331
351
|
|
|
352
|
+
console.log("");
|
|
353
|
+
console.log("— Operator identity —");
|
|
354
|
+
console.log(
|
|
355
|
+
"We need the wallet address that owns (or will own) your on-chain operator label.",
|
|
356
|
+
);
|
|
332
357
|
let address = flags.address ?? null;
|
|
333
358
|
if (!address && flags.keyFile) {
|
|
334
359
|
address = addressFromKeyFile(flags.keyFile);
|
|
360
|
+
console.log(`Using address from --key-file: ${address}`);
|
|
335
361
|
}
|
|
336
362
|
if (!address && hints.envAddress) {
|
|
337
|
-
const useEnv = await askYesNo(
|
|
363
|
+
const useEnv = await askYesNo(
|
|
364
|
+
`Use address from OPERATOR_PRIVATE_KEY (${hints.envAddress})?`,
|
|
365
|
+
true,
|
|
366
|
+
);
|
|
338
367
|
if (useEnv) address = hints.envAddress;
|
|
339
368
|
}
|
|
340
369
|
if (!address && hints.operator?.owner) {
|
|
341
|
-
const useOp = await askYesNo(
|
|
370
|
+
const useOp = await askYesNo(
|
|
371
|
+
`Keep existing operator.json owner (${hints.operator.owner})?`,
|
|
372
|
+
true,
|
|
373
|
+
);
|
|
342
374
|
if (useOp) address = hints.operator.owner;
|
|
343
375
|
}
|
|
344
376
|
if (!address && hints.foundryAccounts.length) {
|
|
345
|
-
console.log("
|
|
377
|
+
console.log("");
|
|
378
|
+
console.log("Foundry accounts on this machine (passwords are never stored here):");
|
|
346
379
|
hints.foundryAccounts.forEach((n, i) => console.log(` ${i + 1}. ${n}`));
|
|
347
|
-
const pick = await ask(
|
|
380
|
+
const pick = await ask(
|
|
381
|
+
"Type a Foundry account name to use, or leave blank to paste an address",
|
|
382
|
+
hints.foundryAccounts[0] ?? "",
|
|
383
|
+
);
|
|
348
384
|
if (pick) {
|
|
349
385
|
address = await ask(
|
|
350
|
-
`
|
|
386
|
+
`Paste the 0x address for "${pick}" (cast wallet address ${pick} after unlock)`,
|
|
351
387
|
"",
|
|
352
388
|
);
|
|
353
389
|
}
|
|
@@ -363,11 +399,13 @@ export async function runSetupInteractive(argv, opts = {}) {
|
|
|
363
399
|
let label =
|
|
364
400
|
flags.operator ||
|
|
365
401
|
hints.operator?.label ||
|
|
366
|
-
(await ask("Operator label (e.g. org.you)", ""));
|
|
402
|
+
(await ask("Operator label on-chain (e.g. org.openclaw.pat or org.you)", ""));
|
|
367
403
|
if (!label) throw new Error("Operator label is required");
|
|
368
404
|
|
|
369
405
|
const presetCfg = PRESETS[preset];
|
|
370
|
-
console.log("
|
|
406
|
+
console.log("");
|
|
407
|
+
console.log("— Chain check —");
|
|
408
|
+
console.log(`Looking up "${label}" on ${preset}…`);
|
|
371
409
|
const check = await assertSetupIdentity({
|
|
372
410
|
rpc: presetCfg.chainRpcUrl,
|
|
373
411
|
registry: presetCfg.registryAddress,
|
|
@@ -376,22 +414,32 @@ export async function runSetupInteractive(argv, opts = {}) {
|
|
|
376
414
|
skipChainCheck: flags.skipChainCheck || !presetCfg.registryAddress,
|
|
377
415
|
});
|
|
378
416
|
if (check.unregistered) {
|
|
379
|
-
console.log(
|
|
417
|
+
console.log(
|
|
418
|
+
`OK: "${label}" is not registered yet — after setup, run: clanker operator mint ${label}`,
|
|
419
|
+
);
|
|
380
420
|
} else if (check.operator) {
|
|
381
|
-
console.log(`
|
|
421
|
+
console.log(`OK: "${label}" is active on-chain and owned by this address.`);
|
|
382
422
|
}
|
|
383
423
|
|
|
424
|
+
console.log("");
|
|
425
|
+
console.log("— Signing key (optional) —");
|
|
426
|
+
console.log(
|
|
427
|
+
"Only needed for mint/revoke/transfer. Skip for read-only whoami/bots.",
|
|
428
|
+
);
|
|
384
429
|
let keyFile = flags.keyFile;
|
|
385
430
|
let keyEnv = flags.keyEnv;
|
|
386
431
|
let skipKey = flags.skipKey;
|
|
387
432
|
if (!skipKey && !keyFile && !keyEnv) {
|
|
388
433
|
if (hints.hasOperatorPrivateKeyEnv) {
|
|
389
|
-
const use = await askYesNo(
|
|
434
|
+
const use = await askYesNo(
|
|
435
|
+
"Remember OPERATOR_PRIVATE_KEY as the signing pointer in operator.json?",
|
|
436
|
+
true,
|
|
437
|
+
);
|
|
390
438
|
if (use) keyEnv = "OPERATOR_PRIVATE_KEY";
|
|
391
439
|
else skipKey = true;
|
|
392
440
|
} else {
|
|
393
441
|
const path = await ask(
|
|
394
|
-
"Path to operator key file
|
|
442
|
+
"Path to operator private-key file (blank = read-only profile)",
|
|
395
443
|
"",
|
|
396
444
|
);
|
|
397
445
|
if (path) keyFile = path;
|
|
@@ -407,13 +455,16 @@ export async function runSetupInteractive(argv, opts = {}) {
|
|
|
407
455
|
}
|
|
408
456
|
}
|
|
409
457
|
|
|
410
|
-
console.log("
|
|
411
|
-
console.log(
|
|
458
|
+
console.log("");
|
|
459
|
+
console.log("— Summary (about to write) —");
|
|
460
|
+
console.log(` network: ${preset}`);
|
|
412
461
|
console.log(` fromBlock:${fromBlock ?? presetCfg.fromBlock}`);
|
|
413
462
|
console.log(` label: ${label}`);
|
|
414
463
|
console.log(` owner: ${address}`);
|
|
415
|
-
console.log(
|
|
416
|
-
|
|
464
|
+
console.log(
|
|
465
|
+
` signing: ${key ? `${key.type}=${key.value}` : "none (read-only whoami/bots)"}`,
|
|
466
|
+
);
|
|
467
|
+
const ok = flags.yes || (await askYesNo("Write these files now?", true));
|
|
417
468
|
if (!ok) throw new Error("Aborted");
|
|
418
469
|
|
|
419
470
|
const result = applySetup(
|
|
@@ -429,12 +480,18 @@ export async function runSetupInteractive(argv, opts = {}) {
|
|
|
429
480
|
{ home, env },
|
|
430
481
|
);
|
|
431
482
|
|
|
432
|
-
console.log(
|
|
483
|
+
console.log("");
|
|
484
|
+
console.log(`Wrote ${result.configPath}`);
|
|
433
485
|
console.log(`Wrote ${result.operatorPath}`);
|
|
434
486
|
if (!key) {
|
|
435
|
-
console.log("
|
|
487
|
+
console.log("");
|
|
488
|
+
console.log("Next: clanker whoami");
|
|
489
|
+
console.log(
|
|
490
|
+
"For mint/revoke later: re-run setup with a key file, or pass --key-file / OPERATOR_PRIVATE_KEY.",
|
|
491
|
+
);
|
|
436
492
|
} else {
|
|
437
|
-
console.log("
|
|
493
|
+
console.log("");
|
|
494
|
+
console.log("Next: clanker whoami");
|
|
438
495
|
}
|
|
439
496
|
return result;
|
|
440
497
|
} finally {
|