@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/doctor.mjs
DELETED
|
@@ -1,185 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `clanker doctor` — local readiness checks (mise-doctor habit).
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { getAddress } from "viem";
|
|
6
|
-
import {
|
|
7
|
-
ANVIL_DEFAULT_ADDRESS,
|
|
8
|
-
isLocalRpc,
|
|
9
|
-
loadConfig,
|
|
10
|
-
loadOperator,
|
|
11
|
-
clankerHome,
|
|
12
|
-
} from "./profile.mjs";
|
|
13
|
-
import { detectSetupHints, formatSetupDetectTable } from "./setup-detect.mjs";
|
|
14
|
-
import { c, nextHint } from "./ui.mjs";
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* @param {{ home?: string, env?: NodeJS.ProcessEnv, openclawDir?: string, castBin?: string, spawn?: Function }} [opts]
|
|
18
|
-
*/
|
|
19
|
-
export function runDoctorChecks(opts = {}) {
|
|
20
|
-
const env = opts.env ?? process.env;
|
|
21
|
-
const home = opts.home ?? clankerHome(env);
|
|
22
|
-
const hints = detectSetupHints({
|
|
23
|
-
home,
|
|
24
|
-
env,
|
|
25
|
-
openclawDir: opts.openclawDir,
|
|
26
|
-
castBin: opts.castBin,
|
|
27
|
-
spawn: opts.spawn,
|
|
28
|
-
});
|
|
29
|
-
const config = hints.config ?? loadConfig(home);
|
|
30
|
-
const operator = hints.operator ?? loadOperator(home);
|
|
31
|
-
|
|
32
|
-
/** @type {{ id: string, ok: boolean, level: 'pass'|'warn'|'fail', message: string }[]} */
|
|
33
|
-
const checks = [];
|
|
34
|
-
|
|
35
|
-
checks.push({
|
|
36
|
-
id: "config",
|
|
37
|
-
ok: Boolean(config),
|
|
38
|
-
level: config ? "pass" : "fail",
|
|
39
|
-
message: config
|
|
40
|
-
? `config.json present (preset=${config.preset})`
|
|
41
|
-
: "config.json missing — run clanker setup",
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
const registry = config?.registryAddress;
|
|
45
|
-
const rpc = config?.chainRpcUrl ?? "";
|
|
46
|
-
checks.push({
|
|
47
|
-
id: "registry",
|
|
48
|
-
ok: Boolean(registry && /^0x[0-9a-fA-F]{40}$/.test(registry)),
|
|
49
|
-
level: registry && /^0x[0-9a-fA-F]{40}$/.test(registry) ? "pass" : "fail",
|
|
50
|
-
message:
|
|
51
|
-
registry && /^0x[0-9a-fA-F]{40}$/.test(registry)
|
|
52
|
-
? `registry ${registry}`
|
|
53
|
-
: "registry missing — run clanker setup --preset sepolia (or set after local deploy)",
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
const hasOwner =
|
|
57
|
-
Boolean(operator?.owner) && /^0x[0-9a-fA-F]{40}$/.test(operator.owner);
|
|
58
|
-
const hasLabel = Boolean(operator?.label);
|
|
59
|
-
checks.push({
|
|
60
|
-
id: "operator",
|
|
61
|
-
ok: hasOwner && hasLabel,
|
|
62
|
-
level: hasOwner && hasLabel ? "pass" : "fail",
|
|
63
|
-
message:
|
|
64
|
-
hasOwner && hasLabel
|
|
65
|
-
? `operator.json ${operator.label} · ${operator.owner}`
|
|
66
|
-
: "operator.json incomplete — run clanker setup",
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
if (hasOwner && rpc && !isLocalRpc(rpc)) {
|
|
70
|
-
const anvil =
|
|
71
|
-
getAddress(operator.owner).toLowerCase() ===
|
|
72
|
-
ANVIL_DEFAULT_ADDRESS.toLowerCase();
|
|
73
|
-
checks.push({
|
|
74
|
-
id: "anvil_public",
|
|
75
|
-
ok: !anvil,
|
|
76
|
-
level: anvil ? "fail" : "pass",
|
|
77
|
-
message: anvil
|
|
78
|
-
? "owner is Anvil #0 on a public RPC — refuse for mutate; fix owner address"
|
|
79
|
-
: "owner is not Anvil #0",
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const hasKey =
|
|
84
|
-
Boolean(operator?.key?.type === "keyFile" && operator.key.value) ||
|
|
85
|
-
Boolean(operator?.key?.type === "env" && operator.key.value) ||
|
|
86
|
-
Boolean(env.OPERATOR_PRIVATE_KEY);
|
|
87
|
-
checks.push({
|
|
88
|
-
id: "signing",
|
|
89
|
-
ok: true,
|
|
90
|
-
level: hasKey ? "pass" : "warn",
|
|
91
|
-
message: hasKey
|
|
92
|
-
? "signing key pointer available (mint/revoke OK)"
|
|
93
|
-
: "read-only profile — whoami/bots OK; mint needs --key-file or OPERATOR_PRIVATE_KEY",
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
checks.push({
|
|
97
|
-
id: "foundry",
|
|
98
|
-
ok: true,
|
|
99
|
-
level: hints.foundryAvailable ? "pass" : "warn",
|
|
100
|
-
message: hints.foundryAvailable
|
|
101
|
-
? `Foundry cast OK (${hints.foundryAccounts.length} account(s))`
|
|
102
|
-
: "Foundry cast not on PATH (optional)",
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
const readyWhoami = checks
|
|
106
|
-
.filter((ch) => ch.id === "config" || ch.id === "registry" || ch.id === "operator")
|
|
107
|
-
.every((ch) => ch.ok);
|
|
108
|
-
const readyMint = readyWhoami && hasKey &&
|
|
109
|
-
!checks.some((ch) => ch.id === "anvil_public" && !ch.ok);
|
|
110
|
-
|
|
111
|
-
return {
|
|
112
|
-
home,
|
|
113
|
-
hints,
|
|
114
|
-
checks,
|
|
115
|
-
readyWhoami,
|
|
116
|
-
readyMint,
|
|
117
|
-
ok: readyWhoami,
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* @param {string[]} argv
|
|
123
|
-
* @param {{ home?: string, env?: NodeJS.ProcessEnv }} [opts]
|
|
124
|
-
* @returns {Promise<{ exitCode: number, report: object }>}
|
|
125
|
-
*/
|
|
126
|
-
export async function runDoctor(argv = [], opts = {}) {
|
|
127
|
-
const json = argv.includes("--json");
|
|
128
|
-
const report = runDoctorChecks(opts);
|
|
129
|
-
|
|
130
|
-
if (json) {
|
|
131
|
-
console.log(
|
|
132
|
-
JSON.stringify(
|
|
133
|
-
{
|
|
134
|
-
ok: report.ok,
|
|
135
|
-
readyWhoami: report.readyWhoami,
|
|
136
|
-
readyMint: report.readyMint,
|
|
137
|
-
home: report.home,
|
|
138
|
-
checks: report.checks,
|
|
139
|
-
},
|
|
140
|
-
null,
|
|
141
|
-
2,
|
|
142
|
-
),
|
|
143
|
-
);
|
|
144
|
-
return { exitCode: report.ok ? 0 : 1, report };
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
console.log(c.bold("clanker doctor"));
|
|
148
|
-
console.log("");
|
|
149
|
-
console.log(formatSetupDetectTable(report.hints));
|
|
150
|
-
console.log("");
|
|
151
|
-
console.log(c.bold("Checks"));
|
|
152
|
-
for (const ch of report.checks) {
|
|
153
|
-
const mark =
|
|
154
|
-
ch.level === "pass"
|
|
155
|
-
? c.green("pass")
|
|
156
|
-
: ch.level === "warn"
|
|
157
|
-
? c.yellow("warn")
|
|
158
|
-
: c.red("fail");
|
|
159
|
-
console.log(` [${mark}] ${ch.message}`);
|
|
160
|
-
}
|
|
161
|
-
console.log("");
|
|
162
|
-
if (report.readyWhoami) {
|
|
163
|
-
console.log(c.green("Ready for: clanker whoami"));
|
|
164
|
-
} else {
|
|
165
|
-
console.log(c.red("Not ready for whoami"));
|
|
166
|
-
}
|
|
167
|
-
if (report.readyMint) {
|
|
168
|
-
console.log(c.green("Ready for: clanker operator mint / bot mint"));
|
|
169
|
-
} else {
|
|
170
|
-
console.log(c.dim("Mint/revoke: need signing key (and non-Anvil owner on public RPC)"));
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
if (!report.readyWhoami) {
|
|
174
|
-
nextHint(["clanker setup"]);
|
|
175
|
-
} else if (!report.readyMint) {
|
|
176
|
-
nextHint([
|
|
177
|
-
"clanker whoami",
|
|
178
|
-
"clanker setup --key-file ~/.clanker/op.key --force # to enable mint",
|
|
179
|
-
]);
|
|
180
|
-
} else {
|
|
181
|
-
nextHint(["clanker whoami", "clanker bots"]);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
return { exitCode: report.ok ? 0 : 1, report };
|
|
185
|
-
}
|
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
|
-
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
|
-
}
|