@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/bin/clanker.mjs +258 -92
- package/lib/doctor.mjs +226 -0
- package/lib/foundry.mjs +114 -0
- package/lib/openclaw-wire.mjs +145 -0
- package/lib/operator-key.mjs +50 -0
- package/lib/profile.mjs +17 -9
- package/lib/resolve.mjs +57 -1
- package/lib/setup-detect.mjs +224 -0
- package/lib/setup.mjs +741 -0
- package/lib/ui.mjs +104 -0
- package/package.json +3 -1
|
@@ -0,0 +1,224 @@
|
|
|
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
|
+
SEPOLIA_FAST_FROM_BLOCK,
|
|
16
|
+
} from "./profile.mjs";
|
|
17
|
+
import { normalizePrivateKey } from "./resolve.mjs";
|
|
18
|
+
|
|
19
|
+
export { SEPOLIA_FAST_FROM_BLOCK };
|
|
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
|
+
}
|