@clanker-chain/clanker-cli 2026.9.7-3 → 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 +15 -26
- package/lib/profile.mjs +5 -10
- package/lib/resolve.mjs +1 -57
- package/package.json +1 -1
- package/lib/setup-detect.mjs +0 -224
- package/lib/setup.mjs +0 -527
package/bin/clanker.mjs
CHANGED
|
@@ -23,8 +23,7 @@ import {
|
|
|
23
23
|
readBot,
|
|
24
24
|
resolvePreferredOperator,
|
|
25
25
|
} from "../lib/identity-query.mjs";
|
|
26
|
-
import { resolveForRead, resolveOperatorKey
|
|
27
|
-
import { runSetup } from "../lib/setup.mjs";
|
|
26
|
+
import { resolveForRead, resolveOperatorKey } from "../lib/resolve.mjs";
|
|
28
27
|
|
|
29
28
|
function resolveFoundryBinary(name) {
|
|
30
29
|
const home = os.homedir();
|
|
@@ -145,7 +144,6 @@ function usage() {
|
|
|
145
144
|
console.log(`clanker - clanker-chain helper CLI
|
|
146
145
|
|
|
147
146
|
Usage:
|
|
148
|
-
clanker setup [--preset sepolia|local] [--operator <label>] [--address 0x…] [--key-file path] [--force]
|
|
149
147
|
clanker init --preset sepolia|local [--force]
|
|
150
148
|
clanker whoami [--json] [--operator <label>] [--address 0x…]
|
|
151
149
|
clanker operator mint <label> [--json]
|
|
@@ -163,14 +161,12 @@ Usage:
|
|
|
163
161
|
|
|
164
162
|
Profile:
|
|
165
163
|
~/.clanker/config.json network preset (registry, RPC, broker URLs)
|
|
166
|
-
~/.clanker/operator.json label + owner +
|
|
164
|
+
~/.clanker/operator.json label + owner + key pointer (never raw hex)
|
|
167
165
|
~/.clanker/keys/ bot keys (also written to ~/.openclaw/keys/)
|
|
168
166
|
|
|
169
|
-
Humans: prefer \`clanker setup\` (detects Foundry/OpenClaw hints, writes profile).
|
|
170
167
|
Key resolution (mutating commands):
|
|
171
168
|
--key > --key-file > OPERATOR_PRIVATE_KEY > profile keyFile > profile env
|
|
172
169
|
Anvil account #0 is allowed only on localhost RPC.
|
|
173
|
-
Reads (whoami/bots) can use profile owner without a signing key.
|
|
174
170
|
|
|
175
171
|
See docs/operator-cli.md.
|
|
176
172
|
`);
|
|
@@ -217,14 +213,16 @@ async function cmdWhoami(argv) {
|
|
|
217
213
|
let address;
|
|
218
214
|
let source;
|
|
219
215
|
let network;
|
|
220
|
-
|
|
221
|
-
|
|
216
|
+
const addressFlag = flagValue(argv, "--address");
|
|
217
|
+
if (addressFlag) {
|
|
218
|
+
network = resolveForRead(argv);
|
|
219
|
+
address = getAddress(addressFlag);
|
|
220
|
+
source = "--address";
|
|
221
|
+
} else {
|
|
222
|
+
const resolved = resolveOperatorKey(argv);
|
|
222
223
|
address = resolved.address;
|
|
223
224
|
source = resolved.source;
|
|
224
225
|
network = resolved.network;
|
|
225
|
-
} catch (err) {
|
|
226
|
-
console.error(err.message);
|
|
227
|
-
process.exit(1);
|
|
228
226
|
}
|
|
229
227
|
|
|
230
228
|
const pub = await publicClientFromRpc(network.rpc);
|
|
@@ -290,13 +288,14 @@ async function cmdWhoami(argv) {
|
|
|
290
288
|
async function cmdBots(argv) {
|
|
291
289
|
let address;
|
|
292
290
|
let network;
|
|
293
|
-
|
|
294
|
-
|
|
291
|
+
const addressFlag = flagValue(argv, "--address");
|
|
292
|
+
if (addressFlag) {
|
|
293
|
+
network = resolveForRead(argv);
|
|
294
|
+
address = getAddress(addressFlag);
|
|
295
|
+
} else {
|
|
296
|
+
const resolved = resolveOperatorKey(argv);
|
|
295
297
|
address = resolved.address;
|
|
296
298
|
network = resolved.network;
|
|
297
|
-
} catch (err) {
|
|
298
|
-
console.error(err.message);
|
|
299
|
-
process.exit(1);
|
|
300
299
|
}
|
|
301
300
|
const { label, pub } = await resolveOperatorLabel(argv, address, network);
|
|
302
301
|
const bots = await findBotsByOperator(pub, {
|
|
@@ -358,16 +357,6 @@ async function main() {
|
|
|
358
357
|
|
|
359
358
|
const repoRoot = findRepoRoot();
|
|
360
359
|
|
|
361
|
-
if (cmd === "setup") {
|
|
362
|
-
try {
|
|
363
|
-
await runSetup(rest);
|
|
364
|
-
} catch (err) {
|
|
365
|
-
console.error(err.message);
|
|
366
|
-
process.exit(1);
|
|
367
|
-
}
|
|
368
|
-
return;
|
|
369
|
-
}
|
|
370
|
-
|
|
371
360
|
if (cmd === "init") {
|
|
372
361
|
const preset = flagValue(rest, "--preset") ?? rest.find((a) => !a.startsWith("--"));
|
|
373
362
|
if (!preset || preset === "init") {
|
package/lib/profile.mjs
CHANGED
|
@@ -63,7 +63,7 @@ export function openclawKeysDir() {
|
|
|
63
63
|
|
|
64
64
|
/**
|
|
65
65
|
* @param {string} presetName
|
|
66
|
-
* @param {{ force?: boolean, registryAddress?: string|null, home?: string
|
|
66
|
+
* @param {{ force?: boolean, registryAddress?: string|null, home?: string }} [opts]
|
|
67
67
|
*/
|
|
68
68
|
export function initProfile(presetName, opts = {}) {
|
|
69
69
|
const name = String(presetName || "").toLowerCase();
|
|
@@ -79,15 +79,13 @@ export function initProfile(presetName, opts = {}) {
|
|
|
79
79
|
}
|
|
80
80
|
mkdirSync(home, { recursive: true });
|
|
81
81
|
const preset = PRESETS[name];
|
|
82
|
-
const fromBlock =
|
|
83
|
-
opts.fromBlock != null ? BigInt(opts.fromBlock) : preset.fromBlock;
|
|
84
82
|
const config = {
|
|
85
83
|
preset: preset.preset,
|
|
86
84
|
registryAddress: opts.registryAddress ?? preset.registryAddress,
|
|
87
85
|
chainRpcUrl: preset.chainRpcUrl,
|
|
88
86
|
brokerUrl: preset.brokerUrl,
|
|
89
87
|
mqttAuthServiceUrl: preset.mqttAuthServiceUrl,
|
|
90
|
-
fromBlock: fromBlock.toString(),
|
|
88
|
+
fromBlock: preset.fromBlock.toString(),
|
|
91
89
|
};
|
|
92
90
|
if (name === "local" && !config.registryAddress) {
|
|
93
91
|
// Placeholder until chain deploy; operator must set after forge deploy.
|
|
@@ -122,9 +120,8 @@ export function loadOperator(home = clankerHome()) {
|
|
|
122
120
|
}
|
|
123
121
|
|
|
124
122
|
/**
|
|
125
|
-
* Write operator profile (label + owner +
|
|
126
|
-
*
|
|
127
|
-
* @param {{ label: string, owner: string, key?: { type: 'env'|'keyFile', value?: string }|null }} op
|
|
123
|
+
* Write operator profile (label + owner + key pointer). Never stores hex keys.
|
|
124
|
+
* @param {{ label: string, owner: string, key?: { type: 'env'|'keyFile', value?: string } }} op
|
|
128
125
|
* @param {string} [home]
|
|
129
126
|
*/
|
|
130
127
|
export function writeOperator(op, home = clankerHome()) {
|
|
@@ -133,10 +130,8 @@ export function writeOperator(op, home = clankerHome()) {
|
|
|
133
130
|
const out = {
|
|
134
131
|
label: op.label,
|
|
135
132
|
owner: op.owner,
|
|
133
|
+
key: op.key ?? { type: "env", value: "OPERATOR_PRIVATE_KEY" },
|
|
136
134
|
};
|
|
137
|
-
if (op.key != null) {
|
|
138
|
-
out.key = op.key;
|
|
139
|
-
}
|
|
140
135
|
writeFileSync(path, `${JSON.stringify(out, null, 2)}\n`, { mode: 0o600 });
|
|
141
136
|
return { path, operator: out };
|
|
142
137
|
}
|
package/lib/resolve.mjs
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { existsSync, readFileSync } from "node:fs";
|
|
6
|
-
import { getAddress } from "viem";
|
|
7
6
|
import { privateKeyToAccount } from "viem/accounts";
|
|
8
7
|
import {
|
|
9
8
|
ANVIL_DEFAULT_PRIVATE_KEY,
|
|
@@ -175,63 +174,8 @@ export function resolveForRead(argv = [], opts = {}) {
|
|
|
175
174
|
if (!network.registry || !/^0x[0-9a-fA-F]{40}$/.test(network.registry)) {
|
|
176
175
|
throw new Error(
|
|
177
176
|
"REGISTRY_ADDRESS or --registry is required (0x + 40 hex). " +
|
|
178
|
-
"Run `clanker init --preset sepolia` or
|
|
177
|
+
"Run `clanker init --preset sepolia` or set registry after `clanker chain deploy`.",
|
|
179
178
|
);
|
|
180
179
|
}
|
|
181
180
|
return network;
|
|
182
181
|
}
|
|
183
|
-
|
|
184
|
-
/**
|
|
185
|
-
* Resolve address for read-only commands.
|
|
186
|
-
* Order: --address > signing key > operator.json owner > error (point at setup).
|
|
187
|
-
*
|
|
188
|
-
* @param {string[]} argv
|
|
189
|
-
* @param {{ home?: string, env?: NodeJS.ProcessEnv }} [opts]
|
|
190
|
-
* @returns {{ address: string, source: string, network: object }}
|
|
191
|
-
*/
|
|
192
|
-
export function resolveReadIdentity(argv = [], opts = {}) {
|
|
193
|
-
const network = resolveForRead(argv, opts);
|
|
194
|
-
const home = opts.home ?? network.home;
|
|
195
|
-
|
|
196
|
-
let addressFlag = null;
|
|
197
|
-
for (let i = 0; i < argv.length; i += 1) {
|
|
198
|
-
if (argv[i] === "--address" && argv[i + 1] && !argv[i + 1].startsWith("--")) {
|
|
199
|
-
addressFlag = argv[++i];
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
if (addressFlag) {
|
|
203
|
-
return {
|
|
204
|
-
address: getAddress(addressFlag),
|
|
205
|
-
source: "--address",
|
|
206
|
-
network,
|
|
207
|
-
};
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
try {
|
|
211
|
-
const resolved = resolveOperatorKey(argv, { ...opts, home });
|
|
212
|
-
return {
|
|
213
|
-
address: resolved.address,
|
|
214
|
-
source: resolved.source,
|
|
215
|
-
network: resolved.network,
|
|
216
|
-
};
|
|
217
|
-
} catch (err) {
|
|
218
|
-
const msg = err?.message ?? String(err);
|
|
219
|
-
if (!/private key required|Anvil account #0|Key file not found|Profile keyFile/i.test(msg)) {
|
|
220
|
-
throw err;
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
const operator = loadOperator(home);
|
|
225
|
-
if (operator?.owner && /^0x[0-9a-fA-F]{40}$/.test(operator.owner)) {
|
|
226
|
-
return {
|
|
227
|
-
address: getAddress(operator.owner),
|
|
228
|
-
source: "profile owner",
|
|
229
|
-
network,
|
|
230
|
-
};
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
throw new Error(
|
|
234
|
-
"No operator identity configured. Run `clanker setup`, pass --address 0x…, " +
|
|
235
|
-
"or set a signing key (OPERATOR_PRIVATE_KEY / --key-file / operator.json key pointer).",
|
|
236
|
-
);
|
|
237
|
-
}
|
package/package.json
CHANGED
package/lib/setup-detect.mjs
DELETED
|
@@ -1,224 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Local identity hints for `clanker setup` (no secrets printed).
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
6
|
-
import { basename, join } from "node:path";
|
|
7
|
-
import { spawnSync } from "node:child_process";
|
|
8
|
-
import { privateKeyToAccount } from "viem/accounts";
|
|
9
|
-
import {
|
|
10
|
-
ANVIL_DEFAULT_ADDRESS,
|
|
11
|
-
clankerHome,
|
|
12
|
-
loadConfig,
|
|
13
|
-
loadOperator,
|
|
14
|
-
openclawKeysDir,
|
|
15
|
-
} from "./profile.mjs";
|
|
16
|
-
import { normalizePrivateKey } from "./resolve.mjs";
|
|
17
|
-
|
|
18
|
-
/** Faster Sepolia log floor for public RPC (registry floor remains SEPOLIA_FROM_BLOCK). */
|
|
19
|
-
export const SEPOLIA_FAST_FROM_BLOCK = 46_000_000n;
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Parse `cast wallet list` stdout into account names.
|
|
23
|
-
* @param {string} stdout
|
|
24
|
-
* @returns {string[]}
|
|
25
|
-
*/
|
|
26
|
-
export function parseCastWalletList(stdout) {
|
|
27
|
-
const names = [];
|
|
28
|
-
for (const line of String(stdout ?? "").split(/\r?\n/)) {
|
|
29
|
-
const trimmed = line.trim();
|
|
30
|
-
if (!trimmed) continue;
|
|
31
|
-
// Formats: "name (Local)" or "0xname (Local)" or just "name"
|
|
32
|
-
const m = trimmed.match(/^(\S+)/);
|
|
33
|
-
if (!m) continue;
|
|
34
|
-
let name = m[1];
|
|
35
|
-
if (name.startsWith("0x") && name.length > 2 && !/^0x[a-fA-F0-9]{40}$/.test(name)) {
|
|
36
|
-
// cast sometimes prefixes 0x to the account name display
|
|
37
|
-
name = name.slice(2);
|
|
38
|
-
}
|
|
39
|
-
if (/^0x[a-fA-F0-9]{40}$/.test(name)) continue;
|
|
40
|
-
names.push(name);
|
|
41
|
-
}
|
|
42
|
-
return [...new Set(names)];
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* @param {{ castBin?: string, spawn?: typeof spawnSync }} [opts]
|
|
47
|
-
* @returns {{ available: boolean, accounts: string[] }}
|
|
48
|
-
*/
|
|
49
|
-
export function listFoundryAccounts(opts = {}) {
|
|
50
|
-
const spawn = opts.spawn ?? spawnSync;
|
|
51
|
-
const castBin = opts.castBin ?? "cast";
|
|
52
|
-
const result = spawn(castBin, ["wallet", "list"], {
|
|
53
|
-
encoding: "utf8",
|
|
54
|
-
shell: false,
|
|
55
|
-
});
|
|
56
|
-
if (result.error || result.status !== 0) {
|
|
57
|
-
return { available: false, accounts: [] };
|
|
58
|
-
}
|
|
59
|
-
return { available: true, accounts: parseCastWalletList(result.stdout ?? "") };
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Bot key basenames under ~/.openclaw/keys (context only).
|
|
64
|
-
* @param {{ openclawDir?: string }} [opts]
|
|
65
|
-
* @returns {string[]}
|
|
66
|
-
*/
|
|
67
|
-
export function listOpenclawBotKeys(opts = {}) {
|
|
68
|
-
const dir = opts.openclawDir ?? openclawKeysDir();
|
|
69
|
-
if (!existsSync(dir)) return [];
|
|
70
|
-
return readdirSync(dir)
|
|
71
|
-
.filter((f) => f.endsWith(".key"))
|
|
72
|
-
.map((f) => basename(f, ".key"))
|
|
73
|
-
.sort();
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Derive address from a key file (first line). Does not log the key.
|
|
78
|
-
* @param {string} path
|
|
79
|
-
* @returns {string}
|
|
80
|
-
*/
|
|
81
|
-
export function addressFromKeyFile(path) {
|
|
82
|
-
if (!existsSync(path)) throw new Error(`Key file not found: ${path}`);
|
|
83
|
-
const key = normalizePrivateKey(readFileSync(path, "utf8").split(/\r?\n/)[0]);
|
|
84
|
-
return privateKeyToAccount(key).address;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Derive address from OPERATOR_PRIVATE_KEY (or named env).
|
|
89
|
-
* @param {NodeJS.ProcessEnv} [env]
|
|
90
|
-
* @param {string} [envName]
|
|
91
|
-
* @returns {string|null}
|
|
92
|
-
*/
|
|
93
|
-
export function addressFromEnv(env = process.env, envName = "OPERATOR_PRIVATE_KEY") {
|
|
94
|
-
const raw = env[envName];
|
|
95
|
-
if (!raw) return null;
|
|
96
|
-
const key = normalizePrivateKey(raw);
|
|
97
|
-
return privateKeyToAccount(key).address;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Snapshot of local hints for setup UX.
|
|
102
|
-
* @param {{ home?: string, env?: NodeJS.ProcessEnv, openclawDir?: string, castBin?: string, spawn?: typeof spawnSync }} [opts]
|
|
103
|
-
*/
|
|
104
|
-
export function detectSetupHints(opts = {}) {
|
|
105
|
-
const env = opts.env ?? process.env;
|
|
106
|
-
const home = opts.home ?? clankerHome(env);
|
|
107
|
-
const config = loadConfig(home);
|
|
108
|
-
const operator = loadOperator(home);
|
|
109
|
-
const foundry = listFoundryAccounts({
|
|
110
|
-
castBin: opts.castBin,
|
|
111
|
-
spawn: opts.spawn,
|
|
112
|
-
});
|
|
113
|
-
const openclawBots = listOpenclawBotKeys({ openclawDir: opts.openclawDir });
|
|
114
|
-
let envAddress = null;
|
|
115
|
-
try {
|
|
116
|
-
envAddress = addressFromEnv(env);
|
|
117
|
-
} catch {
|
|
118
|
-
envAddress = null;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
return {
|
|
122
|
-
home,
|
|
123
|
-
configPath: join(home, "config.json"),
|
|
124
|
-
operatorPath: join(home, "operator.json"),
|
|
125
|
-
hasConfig: Boolean(config),
|
|
126
|
-
hasOperator: Boolean(operator),
|
|
127
|
-
config,
|
|
128
|
-
operator,
|
|
129
|
-
hasOperatorPrivateKeyEnv: Boolean(env.OPERATOR_PRIVATE_KEY),
|
|
130
|
-
envAddress,
|
|
131
|
-
foundryAvailable: foundry.available,
|
|
132
|
-
foundryAccounts: foundry.accounts,
|
|
133
|
-
openclawBots,
|
|
134
|
-
anvilDefaultAddress: ANVIL_DEFAULT_ADDRESS,
|
|
135
|
-
};
|
|
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
DELETED
|
@@ -1,527 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Interactive / flag-driven `clanker setup`.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { createInterface } from "node:readline/promises";
|
|
6
|
-
import { stdin as input, stdout as output } from "node:process";
|
|
7
|
-
import { existsSync } from "node:fs";
|
|
8
|
-
import { getAddress } from "viem";
|
|
9
|
-
import {
|
|
10
|
-
ANVIL_DEFAULT_ADDRESS,
|
|
11
|
-
PRESETS,
|
|
12
|
-
clankerHome,
|
|
13
|
-
configPath,
|
|
14
|
-
initProfile,
|
|
15
|
-
isLocalRpc,
|
|
16
|
-
operatorPath,
|
|
17
|
-
writeOperator,
|
|
18
|
-
} from "./profile.mjs";
|
|
19
|
-
import {
|
|
20
|
-
publicClientFromRpc,
|
|
21
|
-
readOperator,
|
|
22
|
-
resolvePreferredOperator,
|
|
23
|
-
} from "./identity-query.mjs";
|
|
24
|
-
import {
|
|
25
|
-
SEPOLIA_FAST_FROM_BLOCK,
|
|
26
|
-
addressFromEnv,
|
|
27
|
-
addressFromKeyFile,
|
|
28
|
-
detectSetupHints,
|
|
29
|
-
formatSetupDetectTable,
|
|
30
|
-
} from "./setup-detect.mjs";
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* @param {string[]} argv
|
|
34
|
-
*/
|
|
35
|
-
export function parseSetupFlags(argv) {
|
|
36
|
-
let preset = null;
|
|
37
|
-
let operator = null;
|
|
38
|
-
let address = null;
|
|
39
|
-
let keyFile = null;
|
|
40
|
-
let keyEnv = null;
|
|
41
|
-
let fromBlock = null;
|
|
42
|
-
let force = false;
|
|
43
|
-
let yes = false;
|
|
44
|
-
let skipKey = false;
|
|
45
|
-
let skipChainCheck = false;
|
|
46
|
-
|
|
47
|
-
for (let i = 0; i < argv.length; i += 1) {
|
|
48
|
-
const a = argv[i];
|
|
49
|
-
if (a === "--preset" && argv[i + 1]) preset = argv[++i];
|
|
50
|
-
else if (a === "--operator" && argv[i + 1]) operator = argv[++i];
|
|
51
|
-
else if (a === "--address" && argv[i + 1]) address = argv[++i];
|
|
52
|
-
else if (a === "--key-file" && argv[i + 1]) keyFile = argv[++i];
|
|
53
|
-
else if (a === "--key-env" && argv[i + 1]) keyEnv = argv[++i];
|
|
54
|
-
else if (a === "--from-block" && argv[i + 1]) fromBlock = BigInt(argv[++i]);
|
|
55
|
-
else if (a === "--force") force = true;
|
|
56
|
-
else if (a === "--yes" || a === "-y") yes = true;
|
|
57
|
-
else if (a === "--skip-key") skipKey = true;
|
|
58
|
-
else if (a === "--skip-chain-check") skipChainCheck = true;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
return {
|
|
62
|
-
preset,
|
|
63
|
-
operator,
|
|
64
|
-
address,
|
|
65
|
-
keyFile,
|
|
66
|
-
keyEnv,
|
|
67
|
-
fromBlock,
|
|
68
|
-
force,
|
|
69
|
-
yes,
|
|
70
|
-
skipKey,
|
|
71
|
-
skipChainCheck,
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Validate address + label against registry (and Anvil-on-public).
|
|
77
|
-
* @param {{ rpc: string, registry: string, label: string, address: string, skipChainCheck?: boolean }} opts
|
|
78
|
-
*/
|
|
79
|
-
export async function assertSetupIdentity(opts) {
|
|
80
|
-
const address = getAddress(opts.address);
|
|
81
|
-
const local = isLocalRpc(opts.rpc);
|
|
82
|
-
|
|
83
|
-
if (!local && address.toLowerCase() === ANVIL_DEFAULT_ADDRESS.toLowerCase()) {
|
|
84
|
-
throw new Error(
|
|
85
|
-
"Refusing Anvil account #0 address on a non-local RPC. Use your real operator owner address.",
|
|
86
|
-
);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
if (opts.skipChainCheck) {
|
|
90
|
-
return { address, operator: null, skipped: true };
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
if (!opts.registry || !/^0x[0-9a-fA-F]{40}$/.test(opts.registry)) {
|
|
94
|
-
if (local) {
|
|
95
|
-
return { address, operator: null, skipped: true };
|
|
96
|
-
}
|
|
97
|
-
throw new Error("Registry address required for chain check on public RPC.");
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const pub = await publicClientFromRpc(opts.rpc);
|
|
101
|
-
const op = await readOperator(pub, opts.registry, opts.label);
|
|
102
|
-
|
|
103
|
-
if (op.registeredAt === 0n) {
|
|
104
|
-
// New operator — fine; mint later
|
|
105
|
-
return { address, operator: op, skipped: false, unregistered: true };
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const resolved = await resolvePreferredOperator(pub, {
|
|
109
|
-
registry: opts.registry,
|
|
110
|
-
label: opts.label,
|
|
111
|
-
owner: address,
|
|
112
|
-
});
|
|
113
|
-
if (resolved.error) {
|
|
114
|
-
throw new Error(resolved.error);
|
|
115
|
-
}
|
|
116
|
-
return { address, operator: resolved.operator, skipped: false, unregistered: false };
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* Build key pointer from flags / choices.
|
|
121
|
-
* @returns {{ type: 'env'|'keyFile', value: string }|null}
|
|
122
|
-
*/
|
|
123
|
-
export function buildKeyPointer({ keyFile, keyEnv, skipKey, env = process.env }) {
|
|
124
|
-
if (skipKey) return null;
|
|
125
|
-
if (keyFile) {
|
|
126
|
-
if (!existsSync(keyFile)) throw new Error(`Key file not found: ${keyFile}`);
|
|
127
|
-
// validate readable
|
|
128
|
-
addressFromKeyFile(keyFile);
|
|
129
|
-
return { type: "keyFile", value: keyFile };
|
|
130
|
-
}
|
|
131
|
-
if (keyEnv) {
|
|
132
|
-
return { type: "env", value: keyEnv };
|
|
133
|
-
}
|
|
134
|
-
if (env.OPERATOR_PRIVATE_KEY) {
|
|
135
|
-
return { type: "env", value: "OPERATOR_PRIVATE_KEY" };
|
|
136
|
-
}
|
|
137
|
-
return null;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* Apply setup choices to disk.
|
|
142
|
-
*/
|
|
143
|
-
export function applySetup(choices, opts = {}) {
|
|
144
|
-
const home = opts.home ?? clankerHome(opts.env);
|
|
145
|
-
const force = Boolean(choices.force);
|
|
146
|
-
|
|
147
|
-
if (existsSync(configPath(home)) && !force) {
|
|
148
|
-
throw new Error(
|
|
149
|
-
`Profile already exists at ${configPath(home)}. Pass --force to overwrite.`,
|
|
150
|
-
);
|
|
151
|
-
}
|
|
152
|
-
if (existsSync(operatorPath(home)) && !force) {
|
|
153
|
-
throw new Error(
|
|
154
|
-
`Operator profile already exists at ${operatorPath(home)}. Pass --force to overwrite.`,
|
|
155
|
-
);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
const { path: configFile, config } = initProfile(choices.preset, {
|
|
159
|
-
force: true,
|
|
160
|
-
home,
|
|
161
|
-
registryAddress: choices.registryAddress,
|
|
162
|
-
fromBlock: choices.fromBlock,
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
const { path: opFile, operator } = writeOperator(
|
|
166
|
-
{
|
|
167
|
-
label: choices.label,
|
|
168
|
-
owner: getAddress(choices.address),
|
|
169
|
-
key: choices.key,
|
|
170
|
-
},
|
|
171
|
-
home,
|
|
172
|
-
);
|
|
173
|
-
|
|
174
|
-
return { configPath: configFile, config, operatorPath: opFile, operator };
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/**
|
|
178
|
-
* Non-interactive setup (agents / CI).
|
|
179
|
-
* @param {string[]} argv
|
|
180
|
-
* @param {{ home?: string, env?: NodeJS.ProcessEnv, skipChainCheck?: boolean }} [opts]
|
|
181
|
-
*/
|
|
182
|
-
export async function runSetupNonInteractive(argv, opts = {}) {
|
|
183
|
-
const flags = parseSetupFlags(argv);
|
|
184
|
-
const env = opts.env ?? process.env;
|
|
185
|
-
const home = opts.home ?? clankerHome(env);
|
|
186
|
-
|
|
187
|
-
if (!flags.preset || !PRESETS[flags.preset]) {
|
|
188
|
-
throw new Error(
|
|
189
|
-
"Non-interactive setup requires --preset sepolia|local (stdin is not a TTY). " +
|
|
190
|
-
"Also pass --operator <label> and --address 0x…",
|
|
191
|
-
);
|
|
192
|
-
}
|
|
193
|
-
if (!flags.operator) {
|
|
194
|
-
throw new Error("Non-interactive setup requires --operator <label>");
|
|
195
|
-
}
|
|
196
|
-
if (!flags.address && !flags.keyFile && !env.OPERATOR_PRIVATE_KEY) {
|
|
197
|
-
throw new Error(
|
|
198
|
-
"Non-interactive setup requires --address 0x…, --key-file, or OPERATOR_PRIVATE_KEY",
|
|
199
|
-
);
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
let address = flags.address;
|
|
203
|
-
if (!address && flags.keyFile) address = addressFromKeyFile(flags.keyFile);
|
|
204
|
-
if (!address && env.OPERATOR_PRIVATE_KEY) address = addressFromEnv(env);
|
|
205
|
-
|
|
206
|
-
const preset = PRESETS[flags.preset];
|
|
207
|
-
let fromBlock = flags.fromBlock;
|
|
208
|
-
if (fromBlock == null && flags.preset === "sepolia") {
|
|
209
|
-
fromBlock = SEPOLIA_FAST_FROM_BLOCK;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
const rpc = preset.chainRpcUrl;
|
|
213
|
-
const registry = preset.registryAddress;
|
|
214
|
-
|
|
215
|
-
await assertSetupIdentity({
|
|
216
|
-
rpc,
|
|
217
|
-
registry,
|
|
218
|
-
label: flags.operator,
|
|
219
|
-
address,
|
|
220
|
-
skipChainCheck: flags.skipChainCheck || opts.skipChainCheck || !registry,
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
const key = buildKeyPointer({
|
|
224
|
-
keyFile: flags.keyFile,
|
|
225
|
-
keyEnv: flags.keyEnv,
|
|
226
|
-
skipKey: flags.skipKey,
|
|
227
|
-
env,
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
// Address from key must match --address when both set
|
|
231
|
-
if (flags.keyFile && flags.address) {
|
|
232
|
-
const fromKey = getAddress(addressFromKeyFile(flags.keyFile));
|
|
233
|
-
if (fromKey !== getAddress(flags.address)) {
|
|
234
|
-
throw new Error(
|
|
235
|
-
`Key file address ${fromKey} does not match --address ${getAddress(flags.address)}`,
|
|
236
|
-
);
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
return applySetup(
|
|
241
|
-
{
|
|
242
|
-
preset: flags.preset,
|
|
243
|
-
force: flags.force || flags.yes,
|
|
244
|
-
fromBlock,
|
|
245
|
-
registryAddress: registry,
|
|
246
|
-
label: flags.operator,
|
|
247
|
-
address,
|
|
248
|
-
key,
|
|
249
|
-
},
|
|
250
|
-
{ home, env },
|
|
251
|
-
);
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
/**
|
|
255
|
-
* Interactive wizard when stdin is a TTY.
|
|
256
|
-
*/
|
|
257
|
-
export async function runSetupInteractive(argv, opts = {}) {
|
|
258
|
-
const flags = parseSetupFlags(argv);
|
|
259
|
-
const env = opts.env ?? process.env;
|
|
260
|
-
const home = opts.home ?? clankerHome(env);
|
|
261
|
-
const hints = detectSetupHints({
|
|
262
|
-
home,
|
|
263
|
-
env,
|
|
264
|
-
castBin: opts.castBin,
|
|
265
|
-
spawn: opts.spawn,
|
|
266
|
-
openclawDir: opts.openclawDir,
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
const rl =
|
|
270
|
-
opts.rl ??
|
|
271
|
-
createInterface({ input, output });
|
|
272
|
-
const ask = async (q, def) => {
|
|
273
|
-
const suffix = def != null && def !== "" ? ` [${def}]` : "";
|
|
274
|
-
const ans = (await rl.question(`${q}${suffix}: `)).trim();
|
|
275
|
-
return ans || def || "";
|
|
276
|
-
};
|
|
277
|
-
const askYesNo = async (q, defaultYes = true) => {
|
|
278
|
-
const def = defaultYes ? "Y/n" : "y/N";
|
|
279
|
-
const ans = (await rl.question(`${q} (${def}): `)).trim().toLowerCase();
|
|
280
|
-
if (!ans) return defaultYes;
|
|
281
|
-
return ans === "y" || ans === "yes";
|
|
282
|
-
};
|
|
283
|
-
|
|
284
|
-
try {
|
|
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));
|
|
299
|
-
console.log("");
|
|
300
|
-
|
|
301
|
-
if ((hints.hasConfig || hints.hasOperator) && !flags.force) {
|
|
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;
|
|
326
|
-
}
|
|
327
|
-
console.log("");
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
console.log("— Network —");
|
|
331
|
-
let preset =
|
|
332
|
-
flags.preset ||
|
|
333
|
-
hints.config?.preset ||
|
|
334
|
-
(await ask("Which network? sepolia (public hub) or local (Anvil)", "sepolia"));
|
|
335
|
-
preset = String(preset).toLowerCase();
|
|
336
|
-
if (!PRESETS[preset]) throw new Error(`Unknown preset "${preset}"`);
|
|
337
|
-
|
|
338
|
-
let fromBlock = flags.fromBlock;
|
|
339
|
-
if (preset === "sepolia") {
|
|
340
|
-
const useFast =
|
|
341
|
-
fromBlock != null
|
|
342
|
-
? false
|
|
343
|
-
: await askYesNo(
|
|
344
|
-
`Speed up chain scans? Use fromBlock ${SEPOLIA_FAST_FROM_BLOCK} (recommended on public RPC)`,
|
|
345
|
-
true,
|
|
346
|
-
);
|
|
347
|
-
if (fromBlock == null) {
|
|
348
|
-
fromBlock = useFast ? SEPOLIA_FAST_FROM_BLOCK : PRESETS.sepolia.fromBlock;
|
|
349
|
-
}
|
|
350
|
-
}
|
|
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
|
-
);
|
|
357
|
-
let address = flags.address ?? null;
|
|
358
|
-
if (!address && flags.keyFile) {
|
|
359
|
-
address = addressFromKeyFile(flags.keyFile);
|
|
360
|
-
console.log(`Using address from --key-file: ${address}`);
|
|
361
|
-
}
|
|
362
|
-
if (!address && hints.envAddress) {
|
|
363
|
-
const useEnv = await askYesNo(
|
|
364
|
-
`Use address from OPERATOR_PRIVATE_KEY (${hints.envAddress})?`,
|
|
365
|
-
true,
|
|
366
|
-
);
|
|
367
|
-
if (useEnv) address = hints.envAddress;
|
|
368
|
-
}
|
|
369
|
-
if (!address && hints.operator?.owner) {
|
|
370
|
-
const useOp = await askYesNo(
|
|
371
|
-
`Keep existing operator.json owner (${hints.operator.owner})?`,
|
|
372
|
-
true,
|
|
373
|
-
);
|
|
374
|
-
if (useOp) address = hints.operator.owner;
|
|
375
|
-
}
|
|
376
|
-
if (!address && hints.foundryAccounts.length) {
|
|
377
|
-
console.log("");
|
|
378
|
-
console.log("Foundry accounts on this machine (passwords are never stored here):");
|
|
379
|
-
hints.foundryAccounts.forEach((n, i) => console.log(` ${i + 1}. ${n}`));
|
|
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
|
-
);
|
|
384
|
-
if (pick) {
|
|
385
|
-
address = await ask(
|
|
386
|
-
`Paste the 0x address for "${pick}" (cast wallet address ${pick} after unlock)`,
|
|
387
|
-
"",
|
|
388
|
-
);
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
if (!address) {
|
|
392
|
-
address = await ask("Operator owner address (0x…)", "");
|
|
393
|
-
}
|
|
394
|
-
if (!address || !/^0x[0-9a-fA-F]{40}$/.test(address)) {
|
|
395
|
-
throw new Error("A valid 0x operator address is required");
|
|
396
|
-
}
|
|
397
|
-
address = getAddress(address);
|
|
398
|
-
|
|
399
|
-
let label =
|
|
400
|
-
flags.operator ||
|
|
401
|
-
hints.operator?.label ||
|
|
402
|
-
(await ask("Operator label on-chain (e.g. org.openclaw.pat or org.you)", ""));
|
|
403
|
-
if (!label) throw new Error("Operator label is required");
|
|
404
|
-
|
|
405
|
-
const presetCfg = PRESETS[preset];
|
|
406
|
-
console.log("");
|
|
407
|
-
console.log("— Chain check —");
|
|
408
|
-
console.log(`Looking up "${label}" on ${preset}…`);
|
|
409
|
-
const check = await assertSetupIdentity({
|
|
410
|
-
rpc: presetCfg.chainRpcUrl,
|
|
411
|
-
registry: presetCfg.registryAddress,
|
|
412
|
-
label,
|
|
413
|
-
address,
|
|
414
|
-
skipChainCheck: flags.skipChainCheck || !presetCfg.registryAddress,
|
|
415
|
-
});
|
|
416
|
-
if (check.unregistered) {
|
|
417
|
-
console.log(
|
|
418
|
-
`OK: "${label}" is not registered yet — after setup, run: clanker operator mint ${label}`,
|
|
419
|
-
);
|
|
420
|
-
} else if (check.operator) {
|
|
421
|
-
console.log(`OK: "${label}" is active on-chain and owned by this address.`);
|
|
422
|
-
}
|
|
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
|
-
);
|
|
429
|
-
let keyFile = flags.keyFile;
|
|
430
|
-
let keyEnv = flags.keyEnv;
|
|
431
|
-
let skipKey = flags.skipKey;
|
|
432
|
-
if (!skipKey && !keyFile && !keyEnv) {
|
|
433
|
-
if (hints.hasOperatorPrivateKeyEnv) {
|
|
434
|
-
const use = await askYesNo(
|
|
435
|
-
"Remember OPERATOR_PRIVATE_KEY as the signing pointer in operator.json?",
|
|
436
|
-
true,
|
|
437
|
-
);
|
|
438
|
-
if (use) keyEnv = "OPERATOR_PRIVATE_KEY";
|
|
439
|
-
else skipKey = true;
|
|
440
|
-
} else {
|
|
441
|
-
const path = await ask(
|
|
442
|
-
"Path to operator private-key file (blank = read-only profile)",
|
|
443
|
-
"",
|
|
444
|
-
);
|
|
445
|
-
if (path) keyFile = path;
|
|
446
|
-
else skipKey = true;
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
const key = buildKeyPointer({ keyFile, keyEnv, skipKey, env });
|
|
451
|
-
if (key?.type === "keyFile") {
|
|
452
|
-
const fromKey = getAddress(addressFromKeyFile(key.value));
|
|
453
|
-
if (fromKey !== address) {
|
|
454
|
-
throw new Error(`Key file address ${fromKey} does not match chosen owner ${address}`);
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
console.log("");
|
|
459
|
-
console.log("— Summary (about to write) —");
|
|
460
|
-
console.log(` network: ${preset}`);
|
|
461
|
-
console.log(` fromBlock:${fromBlock ?? presetCfg.fromBlock}`);
|
|
462
|
-
console.log(` label: ${label}`);
|
|
463
|
-
console.log(` owner: ${address}`);
|
|
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));
|
|
468
|
-
if (!ok) throw new Error("Aborted");
|
|
469
|
-
|
|
470
|
-
const result = applySetup(
|
|
471
|
-
{
|
|
472
|
-
preset,
|
|
473
|
-
force: true,
|
|
474
|
-
fromBlock: fromBlock ?? undefined,
|
|
475
|
-
registryAddress: presetCfg.registryAddress,
|
|
476
|
-
label,
|
|
477
|
-
address,
|
|
478
|
-
key,
|
|
479
|
-
},
|
|
480
|
-
{ home, env },
|
|
481
|
-
);
|
|
482
|
-
|
|
483
|
-
console.log("");
|
|
484
|
-
console.log(`Wrote ${result.configPath}`);
|
|
485
|
-
console.log(`Wrote ${result.operatorPath}`);
|
|
486
|
-
if (!key) {
|
|
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
|
-
);
|
|
492
|
-
} else {
|
|
493
|
-
console.log("");
|
|
494
|
-
console.log("Next: clanker whoami");
|
|
495
|
-
}
|
|
496
|
-
return result;
|
|
497
|
-
} finally {
|
|
498
|
-
if (!opts.rl) rl.close();
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
/**
|
|
503
|
-
* Entry: interactive if TTY (unless --yes with full flags), else non-interactive.
|
|
504
|
-
*/
|
|
505
|
-
export async function runSetup(argv, opts = {}) {
|
|
506
|
-
const flags = parseSetupFlags(argv);
|
|
507
|
-
const isTTY = opts.isTTY ?? Boolean(input.isTTY);
|
|
508
|
-
|
|
509
|
-
// Fully flagged non-interactive path
|
|
510
|
-
if (
|
|
511
|
-
!isTTY ||
|
|
512
|
-
(flags.yes && flags.preset && flags.operator && (flags.address || flags.keyFile))
|
|
513
|
-
) {
|
|
514
|
-
if (!isTTY && !(flags.preset && flags.operator)) {
|
|
515
|
-
return runSetupNonInteractive(argv, opts);
|
|
516
|
-
}
|
|
517
|
-
if (flags.yes && flags.preset && flags.operator && (flags.address || flags.keyFile || opts.env?.OPERATOR_PRIVATE_KEY || process.env.OPERATOR_PRIVATE_KEY)) {
|
|
518
|
-
return runSetupNonInteractive(argv, opts);
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
if (!isTTY) {
|
|
523
|
-
return runSetupNonInteractive(argv, opts);
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
return runSetupInteractive(argv, opts);
|
|
527
|
-
}
|