@clanker-chain/clanker-cli 2026.9.7-4 → 2026.9.7
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/bin/clanker.mjs +51 -203
- package/lib/profile.mjs +9 -17
- package/lib/resolve.mjs +1 -57
- package/package.json +1 -3
- package/lib/doctor.mjs +0 -185
- package/lib/setup-detect.mjs +0 -224
- package/lib/setup.mjs +0 -549
- package/lib/ui.mjs +0 -104
package/lib/setup.mjs
DELETED
|
@@ -1,549 +0,0 @@
|
|
|
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 { c, nextHint } from "./ui.mjs";
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* @param {string[]} argv
|
|
35
|
-
*/
|
|
36
|
-
export function parseSetupFlags(argv) {
|
|
37
|
-
let preset = null;
|
|
38
|
-
let operator = null;
|
|
39
|
-
let address = null;
|
|
40
|
-
let keyFile = null;
|
|
41
|
-
let keyEnv = null;
|
|
42
|
-
let fromBlock = null;
|
|
43
|
-
let force = false;
|
|
44
|
-
let yes = false;
|
|
45
|
-
let skipKey = false;
|
|
46
|
-
let skipChainCheck = false;
|
|
47
|
-
|
|
48
|
-
for (let i = 0; i < argv.length; i += 1) {
|
|
49
|
-
const a = argv[i];
|
|
50
|
-
if (a === "--preset" && argv[i + 1]) preset = argv[++i];
|
|
51
|
-
else if (a === "--operator" && argv[i + 1]) operator = argv[++i];
|
|
52
|
-
else if (a === "--address" && argv[i + 1]) address = argv[++i];
|
|
53
|
-
else if (a === "--key-file" && argv[i + 1]) keyFile = argv[++i];
|
|
54
|
-
else if (a === "--key-env" && argv[i + 1]) keyEnv = argv[++i];
|
|
55
|
-
else if (a === "--from-block" && argv[i + 1]) fromBlock = BigInt(argv[++i]);
|
|
56
|
-
else if (a === "--force") force = true;
|
|
57
|
-
else if (a === "--yes" || a === "-y") yes = true;
|
|
58
|
-
else if (a === "--skip-key") skipKey = true;
|
|
59
|
-
else if (a === "--skip-chain-check") skipChainCheck = true;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
return {
|
|
63
|
-
preset,
|
|
64
|
-
operator,
|
|
65
|
-
address,
|
|
66
|
-
keyFile,
|
|
67
|
-
keyEnv,
|
|
68
|
-
fromBlock,
|
|
69
|
-
force,
|
|
70
|
-
yes,
|
|
71
|
-
skipKey,
|
|
72
|
-
skipChainCheck,
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function cancelIf(value) {
|
|
77
|
-
if (clack.isCancel(value)) {
|
|
78
|
-
clack.cancel("Setup aborted.");
|
|
79
|
-
process.exit(0);
|
|
80
|
-
}
|
|
81
|
-
return value;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Validate address + label against registry (and Anvil-on-public).
|
|
86
|
-
* @param {{ rpc: string, registry: string, label: string, address: string, skipChainCheck?: boolean }} opts
|
|
87
|
-
*/
|
|
88
|
-
export async function assertSetupIdentity(opts) {
|
|
89
|
-
const address = getAddress(opts.address);
|
|
90
|
-
const local = isLocalRpc(opts.rpc);
|
|
91
|
-
|
|
92
|
-
if (!local && address.toLowerCase() === ANVIL_DEFAULT_ADDRESS.toLowerCase()) {
|
|
93
|
-
throw new Error(
|
|
94
|
-
"Refusing Anvil account #0 address on a non-local RPC. Use your real operator owner address.",
|
|
95
|
-
);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
if (opts.skipChainCheck) {
|
|
99
|
-
return { address, operator: null, skipped: true };
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
if (!opts.registry || !/^0x[0-9a-fA-F]{40}$/.test(opts.registry)) {
|
|
103
|
-
if (local) {
|
|
104
|
-
return { address, operator: null, skipped: true };
|
|
105
|
-
}
|
|
106
|
-
throw new Error("Registry address required for chain check on public RPC.");
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
const pub = await publicClientFromRpc(opts.rpc);
|
|
110
|
-
const op = await readOperator(pub, opts.registry, opts.label);
|
|
111
|
-
|
|
112
|
-
if (op.registeredAt === 0n) {
|
|
113
|
-
return { address, operator: op, skipped: false, unregistered: true };
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const resolved = await resolvePreferredOperator(pub, {
|
|
117
|
-
registry: opts.registry,
|
|
118
|
-
label: opts.label,
|
|
119
|
-
owner: address,
|
|
120
|
-
});
|
|
121
|
-
if (resolved.error) {
|
|
122
|
-
throw new Error(resolved.error);
|
|
123
|
-
}
|
|
124
|
-
return { address, operator: resolved.operator, skipped: false, unregistered: false };
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* @returns {{ type: 'env'|'keyFile', value: string }|null}
|
|
129
|
-
*/
|
|
130
|
-
export function buildKeyPointer({ keyFile, keyEnv, skipKey, env = process.env }) {
|
|
131
|
-
if (skipKey) return null;
|
|
132
|
-
if (keyFile) {
|
|
133
|
-
if (!existsSync(keyFile)) throw new Error(`Key file not found: ${keyFile}`);
|
|
134
|
-
addressFromKeyFile(keyFile);
|
|
135
|
-
return { type: "keyFile", value: keyFile };
|
|
136
|
-
}
|
|
137
|
-
if (keyEnv) {
|
|
138
|
-
return { type: "env", value: keyEnv };
|
|
139
|
-
}
|
|
140
|
-
if (env.OPERATOR_PRIVATE_KEY) {
|
|
141
|
-
return { type: "env", value: "OPERATOR_PRIVATE_KEY" };
|
|
142
|
-
}
|
|
143
|
-
return null;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* Apply setup choices to disk.
|
|
148
|
-
*/
|
|
149
|
-
export function applySetup(choices, opts = {}) {
|
|
150
|
-
const home = opts.home ?? clankerHome(opts.env);
|
|
151
|
-
const force = Boolean(choices.force);
|
|
152
|
-
|
|
153
|
-
if (existsSync(configPath(home)) && !force) {
|
|
154
|
-
throw new Error(
|
|
155
|
-
`Profile already exists at ${configPath(home)}. Pass --force to overwrite.`,
|
|
156
|
-
);
|
|
157
|
-
}
|
|
158
|
-
if (existsSync(operatorPath(home)) && !force) {
|
|
159
|
-
throw new Error(
|
|
160
|
-
`Operator profile already exists at ${operatorPath(home)}. Pass --force to overwrite.`,
|
|
161
|
-
);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
const { path: configFile, config } = initProfile(choices.preset, {
|
|
165
|
-
force: true,
|
|
166
|
-
home,
|
|
167
|
-
registryAddress: choices.registryAddress,
|
|
168
|
-
fromBlock: choices.fromBlock,
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
const { path: opFile, operator } = writeOperator(
|
|
172
|
-
{
|
|
173
|
-
label: choices.label,
|
|
174
|
-
owner: getAddress(choices.address),
|
|
175
|
-
key: choices.key,
|
|
176
|
-
},
|
|
177
|
-
home,
|
|
178
|
-
);
|
|
179
|
-
|
|
180
|
-
return { configPath: configFile, config, operatorPath: opFile, operator };
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/**
|
|
184
|
-
* Non-interactive setup (agents / CI).
|
|
185
|
-
*/
|
|
186
|
-
export async function runSetupNonInteractive(argv, opts = {}) {
|
|
187
|
-
const flags = parseSetupFlags(argv);
|
|
188
|
-
const env = opts.env ?? process.env;
|
|
189
|
-
const home = opts.home ?? clankerHome(env);
|
|
190
|
-
|
|
191
|
-
if (!flags.preset || !PRESETS[flags.preset]) {
|
|
192
|
-
throw new Error(
|
|
193
|
-
"Non-interactive setup requires --preset sepolia|local (stdin is not a TTY). " +
|
|
194
|
-
"Also pass --operator <label> and --address 0x…",
|
|
195
|
-
);
|
|
196
|
-
}
|
|
197
|
-
if (!flags.operator) {
|
|
198
|
-
throw new Error("Non-interactive setup requires --operator <label>");
|
|
199
|
-
}
|
|
200
|
-
if (!flags.address && !flags.keyFile && !env.OPERATOR_PRIVATE_KEY) {
|
|
201
|
-
throw new Error(
|
|
202
|
-
"Non-interactive setup requires --address 0x…, --key-file, or OPERATOR_PRIVATE_KEY",
|
|
203
|
-
);
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
let address = flags.address;
|
|
207
|
-
if (!address && flags.keyFile) address = addressFromKeyFile(flags.keyFile);
|
|
208
|
-
if (!address && env.OPERATOR_PRIVATE_KEY) address = addressFromEnv(env);
|
|
209
|
-
|
|
210
|
-
const preset = PRESETS[flags.preset];
|
|
211
|
-
let fromBlock = flags.fromBlock;
|
|
212
|
-
if (fromBlock == null && flags.preset === "sepolia") {
|
|
213
|
-
fromBlock = SEPOLIA_FAST_FROM_BLOCK;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
const rpc = preset.chainRpcUrl;
|
|
217
|
-
const registry = preset.registryAddress;
|
|
218
|
-
|
|
219
|
-
await assertSetupIdentity({
|
|
220
|
-
rpc,
|
|
221
|
-
registry,
|
|
222
|
-
label: flags.operator,
|
|
223
|
-
address,
|
|
224
|
-
skipChainCheck: flags.skipChainCheck || opts.skipChainCheck || !registry,
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
const key = buildKeyPointer({
|
|
228
|
-
keyFile: flags.keyFile,
|
|
229
|
-
keyEnv: flags.keyEnv,
|
|
230
|
-
skipKey: flags.skipKey,
|
|
231
|
-
env,
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
if (flags.keyFile && flags.address) {
|
|
235
|
-
const fromKey = getAddress(addressFromKeyFile(flags.keyFile));
|
|
236
|
-
if (fromKey !== getAddress(flags.address)) {
|
|
237
|
-
throw new Error(
|
|
238
|
-
`Key file address ${fromKey} does not match --address ${getAddress(flags.address)}`,
|
|
239
|
-
);
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
return applySetup(
|
|
244
|
-
{
|
|
245
|
-
preset: flags.preset,
|
|
246
|
-
force: flags.force || flags.yes,
|
|
247
|
-
fromBlock,
|
|
248
|
-
registryAddress: registry,
|
|
249
|
-
label: flags.operator,
|
|
250
|
-
address,
|
|
251
|
-
key,
|
|
252
|
-
},
|
|
253
|
-
{ home, env },
|
|
254
|
-
);
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
/**
|
|
258
|
-
* Interactive wizard (Clack) when stdin is a TTY.
|
|
259
|
-
*/
|
|
260
|
-
export async function runSetupInteractive(argv, opts = {}) {
|
|
261
|
-
const flags = parseSetupFlags(argv);
|
|
262
|
-
const env = opts.env ?? process.env;
|
|
263
|
-
const home = opts.home ?? clankerHome(env);
|
|
264
|
-
const hints = detectSetupHints({
|
|
265
|
-
home,
|
|
266
|
-
env,
|
|
267
|
-
castBin: opts.castBin,
|
|
268
|
-
spawn: opts.spawn,
|
|
269
|
-
openclawDir: opts.openclawDir,
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
clack.intro(c.bold("clanker setup"));
|
|
273
|
-
clack.log.step("Creates ~/.clanker/config.json (network) and operator.json (identity).");
|
|
274
|
-
clack.log.message(
|
|
275
|
-
c.dim("whoami works from owner without a key; mint/revoke need a key pointer later."),
|
|
276
|
-
);
|
|
277
|
-
clack.log.message(c.dim(`Profile: ${home}`));
|
|
278
|
-
console.log("");
|
|
279
|
-
console.log(c.dim(formatSetupDetectTable(hints)));
|
|
280
|
-
console.log("");
|
|
281
|
-
|
|
282
|
-
if ((hints.hasConfig || hints.hasOperator) && !flags.force) {
|
|
283
|
-
if (hints.hasConfig && !hints.hasOperator) {
|
|
284
|
-
const cont = cancelIf(
|
|
285
|
-
await clack.confirm({
|
|
286
|
-
message: "Network config exists; continue to create operator.json?",
|
|
287
|
-
initialValue: true,
|
|
288
|
-
}),
|
|
289
|
-
);
|
|
290
|
-
if (!cont) {
|
|
291
|
-
clack.cancel("Aborted.");
|
|
292
|
-
process.exit(0);
|
|
293
|
-
}
|
|
294
|
-
flags.force = true;
|
|
295
|
-
} else {
|
|
296
|
-
const overwrite = cancelIf(
|
|
297
|
-
await clack.confirm({
|
|
298
|
-
message: "Replace existing config.json / operator.json?",
|
|
299
|
-
initialValue: false,
|
|
300
|
-
}),
|
|
301
|
-
);
|
|
302
|
-
if (!overwrite) {
|
|
303
|
-
clack.cancel("Aborted. Pass --force to replace.");
|
|
304
|
-
process.exit(0);
|
|
305
|
-
}
|
|
306
|
-
flags.force = true;
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
let preset = flags.preset || hints.config?.preset || null;
|
|
311
|
-
if (!preset) {
|
|
312
|
-
preset = cancelIf(
|
|
313
|
-
await clack.select({
|
|
314
|
-
message: "Network",
|
|
315
|
-
options: [
|
|
316
|
-
{ value: "sepolia", label: "sepolia", hint: "public closed-beta hub" },
|
|
317
|
-
{ value: "local", label: "local", hint: "Anvil" },
|
|
318
|
-
],
|
|
319
|
-
initialValue: "sepolia",
|
|
320
|
-
}),
|
|
321
|
-
);
|
|
322
|
-
}
|
|
323
|
-
preset = String(preset).toLowerCase();
|
|
324
|
-
if (!PRESETS[preset]) throw new Error(`Unknown preset "${preset}"`);
|
|
325
|
-
|
|
326
|
-
// Fast Sepolia default — no interactive fromBlock question
|
|
327
|
-
let fromBlock = flags.fromBlock;
|
|
328
|
-
if (fromBlock == null && preset === "sepolia") {
|
|
329
|
-
fromBlock = SEPOLIA_FAST_FROM_BLOCK;
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
let address = flags.address ?? null;
|
|
333
|
-
if (!address && flags.keyFile) {
|
|
334
|
-
address = addressFromKeyFile(flags.keyFile);
|
|
335
|
-
clack.log.info(`Address from --key-file: ${address}`);
|
|
336
|
-
}
|
|
337
|
-
if (!address && hints.envAddress) {
|
|
338
|
-
const useEnv = cancelIf(
|
|
339
|
-
await clack.confirm({
|
|
340
|
-
message: `Use OPERATOR_PRIVATE_KEY address (${hints.envAddress})?`,
|
|
341
|
-
initialValue: true,
|
|
342
|
-
}),
|
|
343
|
-
);
|
|
344
|
-
if (useEnv) address = hints.envAddress;
|
|
345
|
-
}
|
|
346
|
-
if (!address && hints.operator?.owner) {
|
|
347
|
-
const useOp = cancelIf(
|
|
348
|
-
await clack.confirm({
|
|
349
|
-
message: `Keep existing owner (${hints.operator.owner})?`,
|
|
350
|
-
initialValue: true,
|
|
351
|
-
}),
|
|
352
|
-
);
|
|
353
|
-
if (useOp) address = hints.operator.owner;
|
|
354
|
-
}
|
|
355
|
-
if (!address && hints.foundryAccounts.length) {
|
|
356
|
-
const options = [
|
|
357
|
-
...hints.foundryAccounts.map((n) => ({
|
|
358
|
-
value: n,
|
|
359
|
-
label: n,
|
|
360
|
-
hint: "Foundry keystore — paste 0x next",
|
|
361
|
-
})),
|
|
362
|
-
{ value: "__paste__", label: "Paste an address…", hint: "0x…" },
|
|
363
|
-
];
|
|
364
|
-
const pick = cancelIf(
|
|
365
|
-
await clack.select({
|
|
366
|
-
message: "Operator owner source",
|
|
367
|
-
options,
|
|
368
|
-
initialValue: hints.foundryAccounts[0],
|
|
369
|
-
}),
|
|
370
|
-
);
|
|
371
|
-
if (pick === "__paste__") {
|
|
372
|
-
address = cancelIf(
|
|
373
|
-
await clack.text({
|
|
374
|
-
message: "Operator owner address",
|
|
375
|
-
placeholder: "0x…",
|
|
376
|
-
validate: (v) =>
|
|
377
|
-
/^0x[0-9a-fA-F]{40}$/.test(v || "") ? undefined : "Need 0x + 40 hex",
|
|
378
|
-
}),
|
|
379
|
-
);
|
|
380
|
-
} else {
|
|
381
|
-
address = cancelIf(
|
|
382
|
-
await clack.text({
|
|
383
|
-
message: `Paste 0x address for Foundry account "${pick}"`,
|
|
384
|
-
placeholder: "0x…",
|
|
385
|
-
validate: (v) =>
|
|
386
|
-
/^0x[0-9a-fA-F]{40}$/.test(v || "") ? undefined : "Need 0x + 40 hex",
|
|
387
|
-
}),
|
|
388
|
-
);
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
if (!address) {
|
|
392
|
-
address = cancelIf(
|
|
393
|
-
await clack.text({
|
|
394
|
-
message: "Operator owner address",
|
|
395
|
-
placeholder: "0x…",
|
|
396
|
-
validate: (v) =>
|
|
397
|
-
/^0x[0-9a-fA-F]{40}$/.test(v || "") ? undefined : "Need 0x + 40 hex",
|
|
398
|
-
}),
|
|
399
|
-
);
|
|
400
|
-
}
|
|
401
|
-
address = getAddress(address);
|
|
402
|
-
|
|
403
|
-
let label = flags.operator || hints.operator?.label || null;
|
|
404
|
-
if (!label) {
|
|
405
|
-
label = cancelIf(
|
|
406
|
-
await clack.text({
|
|
407
|
-
message: "Operator label",
|
|
408
|
-
placeholder: "org.openclaw.pat",
|
|
409
|
-
validate: (v) => (v && v.trim() ? undefined : "Label required"),
|
|
410
|
-
}),
|
|
411
|
-
);
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
const presetCfg = PRESETS[preset];
|
|
415
|
-
const spin = clack.spinner();
|
|
416
|
-
spin.start(`Looking up "${label}" on ${preset}`);
|
|
417
|
-
let check;
|
|
418
|
-
try {
|
|
419
|
-
check = await assertSetupIdentity({
|
|
420
|
-
rpc: presetCfg.chainRpcUrl,
|
|
421
|
-
registry: presetCfg.registryAddress,
|
|
422
|
-
label,
|
|
423
|
-
address,
|
|
424
|
-
skipChainCheck: flags.skipChainCheck || !presetCfg.registryAddress,
|
|
425
|
-
});
|
|
426
|
-
spin.stop(
|
|
427
|
-
check.unregistered
|
|
428
|
-
? c.yellow(`"${label}" not registered yet — mint after setup`)
|
|
429
|
-
: check.skipped
|
|
430
|
-
? "Chain check skipped"
|
|
431
|
-
: c.green(`"${label}" active and owned by this address`),
|
|
432
|
-
);
|
|
433
|
-
} catch (err) {
|
|
434
|
-
spin.stop(c.red("Chain check failed"));
|
|
435
|
-
throw err;
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
let keyFile = flags.keyFile;
|
|
439
|
-
let keyEnv = flags.keyEnv;
|
|
440
|
-
let skipKey = flags.skipKey;
|
|
441
|
-
if (!skipKey && !keyFile && !keyEnv) {
|
|
442
|
-
if (hints.hasOperatorPrivateKeyEnv) {
|
|
443
|
-
const use = cancelIf(
|
|
444
|
-
await clack.confirm({
|
|
445
|
-
message: "Store OPERATOR_PRIVATE_KEY pointer for signing?",
|
|
446
|
-
initialValue: true,
|
|
447
|
-
}),
|
|
448
|
-
);
|
|
449
|
-
if (use) keyEnv = "OPERATOR_PRIVATE_KEY";
|
|
450
|
-
else skipKey = true;
|
|
451
|
-
} else {
|
|
452
|
-
const path = cancelIf(
|
|
453
|
-
await clack.text({
|
|
454
|
-
message: "Operator key file path (Enter = read-only)",
|
|
455
|
-
placeholder: "~/.clanker/op.key",
|
|
456
|
-
}),
|
|
457
|
-
);
|
|
458
|
-
if (path && String(path).trim()) keyFile = String(path).trim();
|
|
459
|
-
else skipKey = true;
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
const key = buildKeyPointer({ keyFile, keyEnv, skipKey, env });
|
|
464
|
-
if (key?.type === "keyFile") {
|
|
465
|
-
const fromKey = getAddress(addressFromKeyFile(key.value));
|
|
466
|
-
if (fromKey !== address) {
|
|
467
|
-
throw new Error(`Key file address ${fromKey} does not match chosen owner ${address}`);
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
clack.note(
|
|
472
|
-
[
|
|
473
|
-
`network: ${preset}`,
|
|
474
|
-
`fromBlock: ${fromBlock ?? presetCfg.fromBlock}`,
|
|
475
|
-
`label: ${label}`,
|
|
476
|
-
`owner: ${address}`,
|
|
477
|
-
`signing: ${key ? `${key.type}=${key.value}` : "none (read-only)"}`,
|
|
478
|
-
].join("\n"),
|
|
479
|
-
"Summary",
|
|
480
|
-
);
|
|
481
|
-
|
|
482
|
-
if (!flags.yes) {
|
|
483
|
-
const ok = cancelIf(
|
|
484
|
-
await clack.confirm({ message: "Write these files?", initialValue: true }),
|
|
485
|
-
);
|
|
486
|
-
if (!ok) {
|
|
487
|
-
clack.cancel("Aborted.");
|
|
488
|
-
process.exit(0);
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
const result = applySetup(
|
|
493
|
-
{
|
|
494
|
-
preset,
|
|
495
|
-
force: true,
|
|
496
|
-
fromBlock: fromBlock ?? undefined,
|
|
497
|
-
registryAddress: presetCfg.registryAddress,
|
|
498
|
-
label,
|
|
499
|
-
address,
|
|
500
|
-
key,
|
|
501
|
-
},
|
|
502
|
-
{ home, env },
|
|
503
|
-
);
|
|
504
|
-
|
|
505
|
-
clack.outro(c.green(`Wrote ${result.configPath}\nWrote ${result.operatorPath}`));
|
|
506
|
-
if (!key) {
|
|
507
|
-
nextHint([
|
|
508
|
-
"clanker whoami",
|
|
509
|
-
"clanker setup --key-file ~/.clanker/op.key --force # when you need mint",
|
|
510
|
-
]);
|
|
511
|
-
} else {
|
|
512
|
-
nextHint(["clanker whoami", `clanker bot mint <label>`]);
|
|
513
|
-
}
|
|
514
|
-
return result;
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
/**
|
|
518
|
-
* Entry: interactive if TTY (unless --yes with full flags), else non-interactive.
|
|
519
|
-
*/
|
|
520
|
-
export async function runSetup(argv, opts = {}) {
|
|
521
|
-
const flags = parseSetupFlags(argv);
|
|
522
|
-
const isTTY = opts.isTTY ?? Boolean(input.isTTY);
|
|
523
|
-
|
|
524
|
-
if (
|
|
525
|
-
!isTTY ||
|
|
526
|
-
(flags.yes && flags.preset && flags.operator && (flags.address || flags.keyFile))
|
|
527
|
-
) {
|
|
528
|
-
if (!isTTY && !(flags.preset && flags.operator)) {
|
|
529
|
-
return runSetupNonInteractive(argv, opts);
|
|
530
|
-
}
|
|
531
|
-
if (
|
|
532
|
-
flags.yes &&
|
|
533
|
-
flags.preset &&
|
|
534
|
-
flags.operator &&
|
|
535
|
-
(flags.address ||
|
|
536
|
-
flags.keyFile ||
|
|
537
|
-
opts.env?.OPERATOR_PRIVATE_KEY ||
|
|
538
|
-
process.env.OPERATOR_PRIVATE_KEY)
|
|
539
|
-
) {
|
|
540
|
-
return runSetupNonInteractive(argv, opts);
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
if (!isTTY) {
|
|
545
|
-
return runSetupNonInteractive(argv, opts);
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
return runSetupInteractive(argv, opts);
|
|
549
|
-
}
|
package/lib/ui.mjs
DELETED
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared human-path CLI UX (color, plans, next hints, structured errors).
|
|
3
|
-
* Respects NO_COLOR / non-TTY via picocolors.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import pc from "picocolors";
|
|
7
|
-
import * as clack from "@clack/prompts";
|
|
8
|
-
|
|
9
|
-
export const c = {
|
|
10
|
-
dim: (s) => pc.dim(String(s)),
|
|
11
|
-
green: (s) => pc.green(String(s)),
|
|
12
|
-
yellow: (s) => pc.yellow(String(s)),
|
|
13
|
-
red: (s) => pc.red(String(s)),
|
|
14
|
-
bold: (s) => pc.bold(String(s)),
|
|
15
|
-
cyan: (s) => pc.cyan(String(s)),
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* @param {string[]} argv
|
|
20
|
-
* @param {{ stdinTTY?: boolean }} [opts]
|
|
21
|
-
*/
|
|
22
|
-
export function isInteractive(argv = [], opts = {}) {
|
|
23
|
-
const tty = opts.stdinTTY ?? Boolean(process.stdin.isTTY);
|
|
24
|
-
if (!tty) return false;
|
|
25
|
-
if (argv.includes("--yes") || argv.includes("-y")) return false;
|
|
26
|
-
if (argv.includes("--json")) return false;
|
|
27
|
-
return true;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* @param {string|string[]} lines
|
|
32
|
-
*/
|
|
33
|
-
export function nextHint(lines) {
|
|
34
|
-
const list = Array.isArray(lines) ? lines : [lines];
|
|
35
|
-
console.log("");
|
|
36
|
-
console.log(c.bold("Next:"));
|
|
37
|
-
for (const line of list) {
|
|
38
|
-
console.log(` ${c.cyan(line)}`);
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Cargo-style human error.
|
|
44
|
-
* @param {{ error: string, because?: string, try?: string[] }} opts
|
|
45
|
-
* @returns {string}
|
|
46
|
-
*/
|
|
47
|
-
export function formatCliError(opts) {
|
|
48
|
-
const lines = [c.red(`error: ${opts.error}`)];
|
|
49
|
-
if (opts.because) {
|
|
50
|
-
lines.push(c.dim(`because: ${opts.because}`));
|
|
51
|
-
}
|
|
52
|
-
if (opts.try?.length) {
|
|
53
|
-
lines.push(c.yellow("try:"));
|
|
54
|
-
for (const t of opts.try) {
|
|
55
|
-
lines.push(` ${t}`);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
return lines.join("\n");
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Print a plan table (key/value rows).
|
|
63
|
-
* @param {[string, string][]} rows
|
|
64
|
-
* @param {string} [title]
|
|
65
|
-
*/
|
|
66
|
-
export function printPlan(rows, title = "Plan") {
|
|
67
|
-
console.log("");
|
|
68
|
-
console.log(c.bold(title));
|
|
69
|
-
const w = Math.min(18, Math.max(4, ...rows.map(([k]) => k.length)));
|
|
70
|
-
for (const [k, v] of rows) {
|
|
71
|
-
const pad = k + " ".repeat(Math.max(0, w - k.length));
|
|
72
|
-
console.log(` ${c.dim(pad)} ${v}`);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Confirm a mutating plan. Skips when not interactive.
|
|
78
|
-
* @param {string[]} argv
|
|
79
|
-
* @param {[string, string][]} rows
|
|
80
|
-
* @param {string} [message]
|
|
81
|
-
* @returns {Promise<boolean>}
|
|
82
|
-
*/
|
|
83
|
-
export async function confirmPlan(argv, rows, message = "Proceed?") {
|
|
84
|
-
printPlan(rows);
|
|
85
|
-
if (!isInteractive(argv)) return true;
|
|
86
|
-
const ok = await clack.confirm({
|
|
87
|
-
message,
|
|
88
|
-
initialValue: true,
|
|
89
|
-
});
|
|
90
|
-
if (clack.isCancel(ok) || ok === false) {
|
|
91
|
-
clack.cancel("Aborted.");
|
|
92
|
-
return false;
|
|
93
|
-
}
|
|
94
|
-
return true;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* Print structured error to stderr and exit.
|
|
99
|
-
* @param {{ error: string, because?: string, try?: string[], status?: number }} opts
|
|
100
|
-
*/
|
|
101
|
-
export function exitCliError(opts) {
|
|
102
|
-
console.error(formatCliError(opts));
|
|
103
|
-
process.exit(opts.status ?? 1);
|
|
104
|
-
}
|