@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/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
- }
@@ -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
- }