@clanker-chain/clanker-cli 2026.9.7 → 2026.9.8-2

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.mjs ADDED
@@ -0,0 +1,741 @@
1
+ /**
2
+ * Interactive / flag-driven `clanker setup`.
3
+ */
4
+
5
+ import { stdin as input } from "node:process";
6
+ import { existsSync } from "node:fs";
7
+ import { getAddress } from "viem";
8
+ import * as clack from "@clack/prompts";
9
+ import {
10
+ ANVIL_DEFAULT_ADDRESS,
11
+ PRESETS,
12
+ SEPOLIA_FAST_FROM_BLOCK,
13
+ clankerHome,
14
+ configPath,
15
+ initProfile,
16
+ isLocalRpc,
17
+ operatorPath,
18
+ writeOperator,
19
+ } from "./profile.mjs";
20
+ import {
21
+ publicClientFromRpc,
22
+ readOperator,
23
+ resolvePreferredOperator,
24
+ } from "./identity-query.mjs";
25
+ import {
26
+ addressFromEnv,
27
+ addressFromKeyFile,
28
+ detectSetupHints,
29
+ formatSetupDetectTable,
30
+ } from "./setup-detect.mjs";
31
+ import {
32
+ defaultOperatorKeyPath,
33
+ exportFoundryKey,
34
+ resolveFoundryAddress,
35
+ } from "./foundry.mjs";
36
+ import {
37
+ BASE_SEPOLIA_FAUCET_URL,
38
+ consumerFundHints,
39
+ generateOperatorKeyFile,
40
+ } from "./operator-key.mjs";
41
+ import { c, nextHint } from "./ui.mjs";
42
+ import { join } from "node:path";
43
+
44
+ /**
45
+ * @param {string[]} argv
46
+ */
47
+ export function parseSetupFlags(argv) {
48
+ let preset = null;
49
+ let operator = null;
50
+ let address = null;
51
+ let keyFile = null;
52
+ let keyEnv = null;
53
+ let fromBlock = null;
54
+ let force = false;
55
+ let yes = false;
56
+ let skipKey = false;
57
+ let skipChainCheck = false;
58
+ let foundryAccount = null;
59
+ let exportKey = false;
60
+ let generateKey = false;
61
+
62
+ for (let i = 0; i < argv.length; i += 1) {
63
+ const a = argv[i];
64
+ if (a === "--preset" && argv[i + 1]) preset = argv[++i];
65
+ else if (a === "--operator" && argv[i + 1]) operator = argv[++i];
66
+ else if (a === "--address" && argv[i + 1]) address = argv[++i];
67
+ else if (a === "--key-file" && argv[i + 1]) keyFile = argv[++i];
68
+ else if (a === "--key-env" && argv[i + 1]) keyEnv = argv[++i];
69
+ else if (a === "--from-block" && argv[i + 1]) fromBlock = BigInt(argv[++i]);
70
+ else if (a === "--foundry-account" && argv[i + 1]) foundryAccount = argv[++i];
71
+ else if (a === "--export-key") exportKey = true;
72
+ else if (a === "--generate-key") generateKey = true;
73
+ else if (a === "--force") force = true;
74
+ else if (a === "--yes" || a === "-y") yes = true;
75
+ else if (a === "--skip-key") skipKey = true;
76
+ else if (a === "--skip-chain-check") skipChainCheck = true;
77
+ }
78
+
79
+ return {
80
+ preset,
81
+ operator,
82
+ address,
83
+ keyFile,
84
+ keyEnv,
85
+ fromBlock,
86
+ force,
87
+ yes,
88
+ skipKey,
89
+ skipChainCheck,
90
+ foundryAccount,
91
+ exportKey,
92
+ generateKey,
93
+ };
94
+ }
95
+
96
+ function cancelIf(value) {
97
+ if (clack.isCancel(value)) {
98
+ clack.cancel("Setup aborted.");
99
+ process.exit(0);
100
+ }
101
+ return value;
102
+ }
103
+
104
+ /**
105
+ * Validate address + label against registry (and Anvil-on-public).
106
+ * @param {{ rpc: string, registry: string, label: string, address: string, skipChainCheck?: boolean }} opts
107
+ */
108
+ export async function assertSetupIdentity(opts) {
109
+ const address = getAddress(opts.address);
110
+ const local = isLocalRpc(opts.rpc);
111
+
112
+ if (!local && address.toLowerCase() === ANVIL_DEFAULT_ADDRESS.toLowerCase()) {
113
+ throw new Error(
114
+ "Refusing Anvil account #0 address on a non-local RPC. Use your real operator owner address.",
115
+ );
116
+ }
117
+
118
+ if (opts.skipChainCheck) {
119
+ return { address, operator: null, skipped: true };
120
+ }
121
+
122
+ if (!opts.registry || !/^0x[0-9a-fA-F]{40}$/.test(opts.registry)) {
123
+ if (local) {
124
+ return { address, operator: null, skipped: true };
125
+ }
126
+ throw new Error("Registry address required for chain check on public RPC.");
127
+ }
128
+
129
+ const pub = await publicClientFromRpc(opts.rpc);
130
+ const op = await readOperator(pub, opts.registry, opts.label);
131
+
132
+ if (op.registeredAt === 0n) {
133
+ return { address, operator: op, skipped: false, unregistered: true };
134
+ }
135
+
136
+ const resolved = await resolvePreferredOperator(pub, {
137
+ registry: opts.registry,
138
+ label: opts.label,
139
+ owner: address,
140
+ });
141
+ if (resolved.error) {
142
+ throw new Error(resolved.error);
143
+ }
144
+ return { address, operator: resolved.operator, skipped: false, unregistered: false };
145
+ }
146
+
147
+ /**
148
+ * @returns {{ type: 'env'|'keyFile', value: string }|null}
149
+ */
150
+ export function buildKeyPointer({ keyFile, keyEnv, skipKey, env = process.env }) {
151
+ if (skipKey) return null;
152
+ if (keyFile) {
153
+ if (!existsSync(keyFile)) throw new Error(`Key file not found: ${keyFile}`);
154
+ addressFromKeyFile(keyFile);
155
+ return { type: "keyFile", value: keyFile };
156
+ }
157
+ if (keyEnv) {
158
+ return { type: "env", value: keyEnv };
159
+ }
160
+ if (env.OPERATOR_PRIVATE_KEY) {
161
+ return { type: "env", value: "OPERATOR_PRIVATE_KEY" };
162
+ }
163
+ return null;
164
+ }
165
+
166
+ /**
167
+ * Apply setup choices to disk.
168
+ */
169
+ export function applySetup(choices, opts = {}) {
170
+ const home = opts.home ?? clankerHome(opts.env);
171
+ const force = Boolean(choices.force);
172
+
173
+ if (existsSync(configPath(home)) && !force) {
174
+ throw new Error(
175
+ `Profile already exists at ${configPath(home)}. Pass --force to overwrite.`,
176
+ );
177
+ }
178
+ if (existsSync(operatorPath(home)) && !force) {
179
+ throw new Error(
180
+ `Operator profile already exists at ${operatorPath(home)}. Pass --force to overwrite.`,
181
+ );
182
+ }
183
+
184
+ const { path: configFile, config } = initProfile(choices.preset, {
185
+ force: true,
186
+ home,
187
+ registryAddress: choices.registryAddress,
188
+ fromBlock: choices.fromBlock,
189
+ });
190
+
191
+ const { path: opFile, operator } = writeOperator(
192
+ {
193
+ label: choices.label,
194
+ owner: getAddress(choices.address),
195
+ key: choices.key,
196
+ },
197
+ home,
198
+ );
199
+
200
+ return { configPath: configFile, config, operatorPath: opFile, operator };
201
+ }
202
+
203
+ /**
204
+ * Non-interactive setup (agents / CI).
205
+ */
206
+ export async function runSetupNonInteractive(argv, opts = {}) {
207
+ const flags = parseSetupFlags(argv);
208
+ const env = opts.env ?? process.env;
209
+ const home = opts.home ?? clankerHome(env);
210
+
211
+ if (!flags.preset || !PRESETS[flags.preset]) {
212
+ throw new Error(
213
+ "Non-interactive setup requires --preset sepolia|local (stdin is not a TTY). " +
214
+ "Also pass --operator <label> and --generate-key (or --address / --key-file)",
215
+ );
216
+ }
217
+ if (!flags.operator) {
218
+ throw new Error("Non-interactive setup requires --operator <label>");
219
+ }
220
+
221
+ let address = flags.address;
222
+ let keyFile = flags.keyFile;
223
+ const castOpts = {
224
+ castBin: opts.castBin,
225
+ spawn: opts.spawn,
226
+ inheritStdio: false,
227
+ };
228
+
229
+ if (flags.generateKey) {
230
+ if (flags.skipKey) {
231
+ throw new Error("Cannot combine --generate-key with --skip-key");
232
+ }
233
+ const dest = keyFile || defaultOperatorKeyPath(home);
234
+ const created = generateOperatorKeyFile(dest, {
235
+ force: flags.force,
236
+ });
237
+ keyFile = created.path;
238
+ if (address && getAddress(address) !== getAddress(created.address)) {
239
+ throw new Error(
240
+ `Generated key address ${created.address} does not match --address ${getAddress(address)}`,
241
+ );
242
+ }
243
+ address = created.address;
244
+ }
245
+
246
+ if (flags.foundryAccount) {
247
+ if (!address) {
248
+ address = resolveFoundryAddress(flags.foundryAccount, castOpts);
249
+ }
250
+ if (flags.exportKey && !keyFile && !flags.skipKey) {
251
+ const dest = defaultOperatorKeyPath(home);
252
+ const exported = exportFoundryKey(flags.foundryAccount, dest, castOpts);
253
+ keyFile = exported.path;
254
+ if (getAddress(exported.address) !== getAddress(address)) {
255
+ throw new Error(
256
+ `Exported Foundry key address ${exported.address} does not match ${address}`,
257
+ );
258
+ }
259
+ }
260
+ }
261
+
262
+ if (!address && keyFile) address = addressFromKeyFile(keyFile);
263
+ if (!address && env.OPERATOR_PRIVATE_KEY) address = addressFromEnv(env);
264
+ if (!address) {
265
+ throw new Error(
266
+ "Non-interactive setup requires --generate-key, --address 0x…, --key-file, OPERATOR_PRIVATE_KEY, or --foundry-account",
267
+ );
268
+ }
269
+
270
+ const preset = PRESETS[flags.preset];
271
+ let fromBlock = flags.fromBlock;
272
+ if (fromBlock == null && flags.preset === "sepolia") {
273
+ fromBlock = SEPOLIA_FAST_FROM_BLOCK;
274
+ }
275
+
276
+ const rpc = preset.chainRpcUrl;
277
+ const registry = preset.registryAddress;
278
+
279
+ await assertSetupIdentity({
280
+ rpc,
281
+ registry,
282
+ label: flags.operator,
283
+ address,
284
+ skipChainCheck: flags.skipChainCheck || opts.skipChainCheck || !registry,
285
+ });
286
+
287
+ const key = buildKeyPointer({
288
+ keyFile,
289
+ keyEnv: flags.keyEnv,
290
+ skipKey: flags.skipKey,
291
+ env,
292
+ });
293
+
294
+ if (keyFile && flags.address) {
295
+ const fromKey = getAddress(addressFromKeyFile(keyFile));
296
+ if (fromKey !== getAddress(flags.address)) {
297
+ throw new Error(
298
+ `Key file address ${fromKey} does not match --address ${getAddress(flags.address)}`,
299
+ );
300
+ }
301
+ }
302
+
303
+ return applySetup(
304
+ {
305
+ preset: flags.preset,
306
+ force: flags.force || flags.yes,
307
+ fromBlock,
308
+ registryAddress: registry,
309
+ label: flags.operator,
310
+ address,
311
+ key,
312
+ },
313
+ { home, env },
314
+ );
315
+ }
316
+
317
+ /**
318
+ * Interactive wizard (Clack) when stdin is a TTY.
319
+ */
320
+ export async function runSetupInteractive(argv, opts = {}) {
321
+ const flags = parseSetupFlags(argv);
322
+ const env = opts.env ?? process.env;
323
+ const home = opts.home ?? clankerHome(env);
324
+ const hints = detectSetupHints({
325
+ home,
326
+ env,
327
+ castBin: opts.castBin,
328
+ spawn: opts.spawn,
329
+ openclawDir: opts.openclawDir,
330
+ });
331
+
332
+ clack.intro(c.bold("clanker setup"));
333
+ clack.log.step(
334
+ "Sets up your operator identity (org account) and network for OpenClaw bots.",
335
+ );
336
+ clack.log.message(
337
+ c.dim(
338
+ "New here? Create a key file — no wallet app or Foundry required. Mint needs a little test ETH later.",
339
+ ),
340
+ );
341
+ clack.log.message(c.dim(`Profile: ${home}`));
342
+ console.log("");
343
+ console.log(c.dim(formatSetupDetectTable(hints)));
344
+ console.log("");
345
+
346
+ if ((hints.hasConfig || hints.hasOperator) && !flags.force) {
347
+ if (hints.hasConfig && !hints.hasOperator) {
348
+ const cont = cancelIf(
349
+ await clack.confirm({
350
+ message: "Network config exists; continue to create operator.json?",
351
+ initialValue: true,
352
+ }),
353
+ );
354
+ if (!cont) {
355
+ clack.cancel("Aborted.");
356
+ process.exit(0);
357
+ }
358
+ flags.force = true;
359
+ } else {
360
+ const overwrite = cancelIf(
361
+ await clack.confirm({
362
+ message: "Replace existing config.json / operator.json?",
363
+ initialValue: false,
364
+ }),
365
+ );
366
+ if (!overwrite) {
367
+ clack.cancel("Aborted. Pass --force to replace.");
368
+ process.exit(0);
369
+ }
370
+ flags.force = true;
371
+ }
372
+ }
373
+
374
+ let preset = flags.preset || hints.config?.preset || null;
375
+ if (!preset) {
376
+ preset = cancelIf(
377
+ await clack.select({
378
+ message: "Network",
379
+ options: [
380
+ { value: "sepolia", label: "sepolia", hint: "public closed-beta hub" },
381
+ { value: "local", label: "local", hint: "Anvil" },
382
+ ],
383
+ initialValue: "sepolia",
384
+ }),
385
+ );
386
+ }
387
+ preset = String(preset).toLowerCase();
388
+ if (!PRESETS[preset]) throw new Error(`Unknown preset "${preset}"`);
389
+
390
+ // Fast Sepolia default — no interactive fromBlock question
391
+ let fromBlock = flags.fromBlock;
392
+ if (fromBlock == null && preset === "sepolia") {
393
+ fromBlock = SEPOLIA_FAST_FROM_BLOCK;
394
+ }
395
+
396
+ let address = flags.address ?? null;
397
+ let foundryAccountUsed = null;
398
+ let generatedKeyFile = null;
399
+ if (!address && flags.generateKey) {
400
+ const dest = flags.keyFile || defaultOperatorKeyPath(home);
401
+ let forceGen = flags.force;
402
+ if (existsSync(dest) && !forceGen) {
403
+ forceGen = cancelIf(
404
+ await clack.confirm({
405
+ message: `Overwrite existing ${dest}?`,
406
+ initialValue: false,
407
+ }),
408
+ );
409
+ if (!forceGen) {
410
+ clack.cancel("Aborted.");
411
+ process.exit(0);
412
+ }
413
+ }
414
+ const created = generateOperatorKeyFile(dest, { force: true });
415
+ generatedKeyFile = created.path;
416
+ address = created.address;
417
+ clack.log.success(`Created operator key at ${created.path}`);
418
+ clack.log.info(`Your operator address: ${created.address}`);
419
+ }
420
+ if (!address && flags.keyFile) {
421
+ address = addressFromKeyFile(flags.keyFile);
422
+ clack.log.info(`Address from --key-file: ${address}`);
423
+ }
424
+ if (!address && hints.envAddress) {
425
+ const useEnv = cancelIf(
426
+ await clack.confirm({
427
+ message: `Use OPERATOR_PRIVATE_KEY address (${hints.envAddress})?`,
428
+ initialValue: true,
429
+ }),
430
+ );
431
+ if (useEnv) address = hints.envAddress;
432
+ }
433
+ if (!address && hints.operator?.owner) {
434
+ const useOp = cancelIf(
435
+ await clack.confirm({
436
+ message: `Keep existing owner (${hints.operator.owner})?`,
437
+ initialValue: true,
438
+ }),
439
+ );
440
+ if (useOp) address = hints.operator.owner;
441
+ }
442
+ if (!address) {
443
+ const options = [
444
+ {
445
+ value: "__generate__",
446
+ label: "Create a new operator key for me",
447
+ hint: "writes ~/.clanker/op.key (recommended)",
448
+ },
449
+ {
450
+ value: "__keyfile__",
451
+ label: "Use an existing key file…",
452
+ hint: "path to a 0x private key file",
453
+ },
454
+ ];
455
+ if (hints.foundryAccounts.length) {
456
+ for (const n of hints.foundryAccounts) {
457
+ options.push({
458
+ value: n,
459
+ label: `Foundry: ${n}`,
460
+ hint: "advanced — cast wallet",
461
+ });
462
+ }
463
+ } else {
464
+ options.push({
465
+ value: "__foundry_missing__",
466
+ label: "Foundry account…",
467
+ hint: "advanced — install Foundry first",
468
+ });
469
+ }
470
+ options.push({
471
+ value: "__paste__",
472
+ label: "Paste an address…",
473
+ hint: "read-only unless you add a key later",
474
+ });
475
+
476
+ const pick = cancelIf(
477
+ await clack.select({
478
+ message: "How do you want to set your operator identity?",
479
+ options,
480
+ initialValue: "__generate__",
481
+ }),
482
+ );
483
+
484
+ if (pick === "__generate__") {
485
+ const dest = defaultOperatorKeyPath(home);
486
+ let forceGen = flags.force;
487
+ if (existsSync(dest) && !forceGen) {
488
+ forceGen = cancelIf(
489
+ await clack.confirm({
490
+ message: `Overwrite existing ${dest}?`,
491
+ initialValue: false,
492
+ }),
493
+ );
494
+ if (!forceGen) {
495
+ clack.cancel("Aborted.");
496
+ process.exit(0);
497
+ }
498
+ }
499
+ const created = generateOperatorKeyFile(dest, { force: true });
500
+ generatedKeyFile = created.path;
501
+ address = created.address;
502
+ clack.log.success(`Created operator key at ${created.path}`);
503
+ clack.log.info(`Your operator address: ${created.address}`);
504
+ } else if (pick === "__keyfile__") {
505
+ const path = cancelIf(
506
+ await clack.text({
507
+ message: "Path to operator key file",
508
+ placeholder: join(home, "op.key"),
509
+ validate: (v) =>
510
+ v && String(v).trim() && existsSync(String(v).trim())
511
+ ? undefined
512
+ : "File not found",
513
+ }),
514
+ );
515
+ generatedKeyFile = String(path).trim();
516
+ address = addressFromKeyFile(generatedKeyFile);
517
+ clack.log.info(`Address from key file: ${address}`);
518
+ } else if (pick === "__foundry_missing__") {
519
+ clack.log.warn(
520
+ "Foundry (cast) is not available. Install https://book.getfoundry.sh/ or choose Create a new operator key.",
521
+ );
522
+ address = cancelIf(
523
+ await clack.text({
524
+ message: "Operator owner address",
525
+ placeholder: "0x…",
526
+ validate: (v) =>
527
+ /^0x[0-9a-fA-F]{40}$/.test(v || "") ? undefined : "Need 0x + 40 hex",
528
+ }),
529
+ );
530
+ } else if (pick === "__paste__") {
531
+ address = cancelIf(
532
+ await clack.text({
533
+ message: "Operator owner address",
534
+ placeholder: "0x…",
535
+ validate: (v) =>
536
+ /^0x[0-9a-fA-F]{40}$/.test(v || "") ? undefined : "Need 0x + 40 hex",
537
+ }),
538
+ );
539
+ } else {
540
+ foundryAccountUsed = pick;
541
+ try {
542
+ clack.log.step(`Resolving address for Foundry account "${pick}" (unlock if prompted)…`);
543
+ address = resolveFoundryAddress(pick, {
544
+ castBin: opts.castBin,
545
+ spawn: opts.spawn,
546
+ inheritStdio: true,
547
+ });
548
+ clack.log.success(`Foundry address: ${address}`);
549
+ } catch (err) {
550
+ clack.log.warn(err.message);
551
+ address = cancelIf(
552
+ await clack.text({
553
+ message: `Paste 0x address for "${pick}"`,
554
+ placeholder: "0x…",
555
+ validate: (v) =>
556
+ /^0x[0-9a-fA-F]{40}$/.test(v || "") ? undefined : "Need 0x + 40 hex",
557
+ }),
558
+ );
559
+ }
560
+ }
561
+ }
562
+ address = getAddress(address);
563
+
564
+ let label = flags.operator || hints.operator?.label || null;
565
+ if (!label) {
566
+ label = cancelIf(
567
+ await clack.text({
568
+ message: "Operator label",
569
+ placeholder: "org.openclaw.pat",
570
+ validate: (v) => (v && v.trim() ? undefined : "Label required"),
571
+ }),
572
+ );
573
+ }
574
+
575
+ const presetCfg = PRESETS[preset];
576
+ const spin = clack.spinner();
577
+ spin.start(`Looking up "${label}" on ${preset}`);
578
+ let check;
579
+ try {
580
+ check = await assertSetupIdentity({
581
+ rpc: presetCfg.chainRpcUrl,
582
+ registry: presetCfg.registryAddress,
583
+ label,
584
+ address,
585
+ skipChainCheck: flags.skipChainCheck || !presetCfg.registryAddress,
586
+ });
587
+ spin.stop(
588
+ check.unregistered
589
+ ? c.yellow(`"${label}" not registered yet — mint after setup`)
590
+ : check.skipped
591
+ ? "Chain check skipped"
592
+ : c.green(`"${label}" active and owned by this address`),
593
+ );
594
+ } catch (err) {
595
+ spin.stop(c.red("Chain check failed"));
596
+ throw err;
597
+ }
598
+
599
+ let keyFile = flags.keyFile || generatedKeyFile;
600
+ let keyEnv = flags.keyEnv;
601
+ let skipKey = flags.skipKey;
602
+ if (!skipKey && !keyFile && !keyEnv) {
603
+ if (foundryAccountUsed) {
604
+ const doExport = cancelIf(
605
+ await clack.confirm({
606
+ message: `Export Foundry key for "${foundryAccountUsed}" to ~/.clanker/op.key for minting?`,
607
+ initialValue: true,
608
+ }),
609
+ );
610
+ if (doExport) {
611
+ const dest = defaultOperatorKeyPath(home);
612
+ clack.log.step("Exporting key via cast (unlock if prompted; key is not printed)…");
613
+ const exported = exportFoundryKey(foundryAccountUsed, dest, {
614
+ castBin: opts.castBin,
615
+ spawn: opts.spawn,
616
+ inheritStdio: true,
617
+ });
618
+ if (getAddress(exported.address) !== address) {
619
+ throw new Error(
620
+ `Exported key address ${exported.address} does not match owner ${address}`,
621
+ );
622
+ }
623
+ keyFile = exported.path;
624
+ clack.log.success(`Wrote ${keyFile} (mode 600)`);
625
+ } else {
626
+ skipKey = true;
627
+ }
628
+ } else if (hints.hasOperatorPrivateKeyEnv) {
629
+ const use = cancelIf(
630
+ await clack.confirm({
631
+ message: "Store OPERATOR_PRIVATE_KEY pointer for signing?",
632
+ initialValue: true,
633
+ }),
634
+ );
635
+ if (use) keyEnv = "OPERATOR_PRIVATE_KEY";
636
+ else skipKey = true;
637
+ } else {
638
+ const path = cancelIf(
639
+ await clack.text({
640
+ message: "Operator key file path (Enter = read-only)",
641
+ placeholder: join(home, "op.key"),
642
+ }),
643
+ );
644
+ if (path && String(path).trim()) keyFile = String(path).trim();
645
+ else skipKey = true;
646
+ }
647
+ }
648
+
649
+ const key = buildKeyPointer({ keyFile, keyEnv, skipKey, env });
650
+ if (key?.type === "keyFile") {
651
+ const fromKey = getAddress(addressFromKeyFile(key.value));
652
+ if (fromKey !== address) {
653
+ throw new Error(`Key file address ${fromKey} does not match chosen owner ${address}`);
654
+ }
655
+ }
656
+
657
+ clack.note(
658
+ [
659
+ `network: ${preset}`,
660
+ `fromBlock: ${fromBlock ?? presetCfg.fromBlock}`,
661
+ `label: ${label}`,
662
+ `owner: ${address}`,
663
+ `signing: ${key ? `${key.type}=${key.value}` : "none (read-only)"}`,
664
+ ].join("\n"),
665
+ "Summary",
666
+ );
667
+
668
+ if (!flags.yes) {
669
+ const ok = cancelIf(
670
+ await clack.confirm({ message: "Write these files?", initialValue: true }),
671
+ );
672
+ if (!ok) {
673
+ clack.cancel("Aborted.");
674
+ process.exit(0);
675
+ }
676
+ }
677
+
678
+ const result = applySetup(
679
+ {
680
+ preset,
681
+ force: true,
682
+ fromBlock: fromBlock ?? undefined,
683
+ registryAddress: presetCfg.registryAddress,
684
+ label,
685
+ address,
686
+ key,
687
+ },
688
+ { home, env },
689
+ );
690
+
691
+ clack.outro(c.green(`Wrote ${result.configPath}\nWrote ${result.operatorPath}`));
692
+ if (!key) {
693
+ nextHint([
694
+ "clanker whoami",
695
+ "clanker setup — choose Create a new operator key when you need mint",
696
+ ]);
697
+ } else if (preset === "sepolia" && generatedKeyFile) {
698
+ nextHint(consumerFundHints({ address, label }));
699
+ } else if (preset === "sepolia") {
700
+ nextHint([
701
+ `If this address needs test ETH: ${BASE_SEPOLIA_FAUCET_URL}`,
702
+ "clanker doctor",
703
+ "clanker whoami",
704
+ `clanker bot mint <label>`,
705
+ ]);
706
+ } else {
707
+ nextHint(["clanker whoami", `clanker bot mint <label>`]);
708
+ }
709
+ return result;
710
+ }
711
+
712
+ /**
713
+ * Entry: interactive if TTY (unless --yes with full flags), else non-interactive.
714
+ */
715
+ export async function runSetup(argv, opts = {}) {
716
+ const flags = parseSetupFlags(argv);
717
+ const isTTY = opts.isTTY ?? Boolean(input.isTTY);
718
+ const hasIdentitySource = Boolean(
719
+ flags.address ||
720
+ flags.keyFile ||
721
+ flags.generateKey ||
722
+ flags.foundryAccount ||
723
+ opts.env?.OPERATOR_PRIVATE_KEY ||
724
+ process.env.OPERATOR_PRIVATE_KEY,
725
+ );
726
+
727
+ if (!isTTY || (flags.yes && flags.preset && flags.operator && hasIdentitySource)) {
728
+ if (!isTTY && !(flags.preset && flags.operator)) {
729
+ return runSetupNonInteractive(argv, opts);
730
+ }
731
+ if (flags.yes && flags.preset && flags.operator && hasIdentitySource) {
732
+ return runSetupNonInteractive(argv, opts);
733
+ }
734
+ }
735
+
736
+ if (!isTTY) {
737
+ return runSetupNonInteractive(argv, opts);
738
+ }
739
+
740
+ return runSetupInteractive(argv, opts);
741
+ }