@clanker-chain/clanker-cli 2026.9.7-4 → 2026.9.8-1
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 +43 -43
- package/lib/doctor.mjs +44 -3
- package/lib/foundry.mjs +114 -0
- package/lib/openclaw-wire.mjs +104 -0
- package/lib/operator-key.mjs +50 -0
- package/lib/setup.mjs +243 -51
- package/package.json +1 -1
package/bin/clanker.mjs
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { spawnSync } from "node:child_process";
|
|
4
|
-
import { existsSync,
|
|
4
|
+
import { existsSync, mkdirSync } from "node:fs";
|
|
5
5
|
import { dirname, join } from "node:path";
|
|
6
6
|
import os from "node:os";
|
|
7
7
|
import process from "node:process";
|
|
8
8
|
import { getAddress } from "viem";
|
|
9
9
|
import {
|
|
10
10
|
ANVIL_DEFAULT_PRIVATE_KEY,
|
|
11
|
-
harnessSnippet,
|
|
12
11
|
initProfile,
|
|
13
12
|
isLocalRpc,
|
|
14
13
|
loadConfig,
|
|
@@ -26,6 +25,10 @@ import {
|
|
|
26
25
|
import { resolveForRead, resolveOperatorKey, resolveReadIdentity } from "../lib/resolve.mjs";
|
|
27
26
|
import { runSetup } from "../lib/setup.mjs";
|
|
28
27
|
import { runDoctor } from "../lib/doctor.mjs";
|
|
28
|
+
import {
|
|
29
|
+
hubConnectChecklist,
|
|
30
|
+
wireOpenClawMqtt,
|
|
31
|
+
} from "../lib/openclaw-wire.mjs";
|
|
29
32
|
import {
|
|
30
33
|
c,
|
|
31
34
|
confirmPlan,
|
|
@@ -599,19 +602,38 @@ async function main() {
|
|
|
599
602
|
);
|
|
600
603
|
if (!ok) process.exit(0);
|
|
601
604
|
const result = await chainMintBot(botLabel, operatorLabel, flags);
|
|
602
|
-
|
|
603
|
-
|
|
605
|
+
const mqtt = result.channels_mqtt ?? {};
|
|
606
|
+
const wire = wireOpenClawMqtt({
|
|
607
|
+
botId: botLabel,
|
|
608
|
+
operatorId: operatorLabel,
|
|
609
|
+
network: {
|
|
610
|
+
rpc: mqtt.chainRpcUrl,
|
|
611
|
+
registry: mqtt.registryAddress,
|
|
612
|
+
brokerUrl: mqtt.brokerUrl,
|
|
613
|
+
mqttAuthServiceUrl: mqtt.mqttAuthServiceUrl,
|
|
614
|
+
},
|
|
615
|
+
keyPath: result.key_path,
|
|
616
|
+
});
|
|
617
|
+
if (hasFlag(flags, "--json")) {
|
|
618
|
+
printJson({ ...result, openclaw: wire });
|
|
619
|
+
} else {
|
|
604
620
|
console.log(c.green(`Minted bot ${botLabel} under ${operatorLabel}`));
|
|
605
621
|
console.log(`botKey: ${result.bot_key}`);
|
|
606
622
|
console.log(`key file: ${result.key_path}`);
|
|
607
623
|
console.log(`also: ${result.clanker_key_path}`);
|
|
608
624
|
console.log(`tx: ${result.tx}`);
|
|
609
|
-
console.log(
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
625
|
+
console.log(
|
|
626
|
+
wire.created
|
|
627
|
+
? c.green(`Wrote ${wire.path}`)
|
|
628
|
+
: c.green(`Updated channels.mqtt in ${wire.path}`),
|
|
629
|
+
);
|
|
630
|
+
nextHint(
|
|
631
|
+
hubConnectChecklist({
|
|
632
|
+
botId: botLabel,
|
|
633
|
+
channelsMqtt: wire.channelsMqtt,
|
|
634
|
+
pluginPin: wire.pluginPin,
|
|
635
|
+
}),
|
|
636
|
+
);
|
|
615
637
|
}
|
|
616
638
|
return;
|
|
617
639
|
}
|
|
@@ -869,22 +891,6 @@ async function main() {
|
|
|
869
891
|
}
|
|
870
892
|
|
|
871
893
|
if (cmd === "init-openclaw") {
|
|
872
|
-
const home = os.homedir();
|
|
873
|
-
const openclawDir = join(home, ".openclaw");
|
|
874
|
-
const cfgPath = join(openclawDir, "openclaw.json");
|
|
875
|
-
if (!existsSync(openclawDir)) {
|
|
876
|
-
mkdirSync(openclawDir, { recursive: true });
|
|
877
|
-
}
|
|
878
|
-
|
|
879
|
-
if (existsSync(cfgPath)) {
|
|
880
|
-
console.log(`${cfgPath} already exists.`);
|
|
881
|
-
console.log(
|
|
882
|
-
'Ensure plugins.enabled includes "mqtt" and "mqtt-tools", and channels.mqtt has botId, operatorId, brokerUrl, chainRpcUrl, registryAddress.',
|
|
883
|
-
);
|
|
884
|
-
console.log("See SETUP.md and docs/operator-cli.md.");
|
|
885
|
-
process.exit(0);
|
|
886
|
-
}
|
|
887
|
-
|
|
888
894
|
const profile = loadConfig();
|
|
889
895
|
const operator = loadOperator();
|
|
890
896
|
// No profile: local defaults only — do not mix Sepolia registry with localhost MQTT.
|
|
@@ -902,27 +908,21 @@ async function main() {
|
|
|
902
908
|
mqttAuthServiceUrl: "http://localhost:9090",
|
|
903
909
|
};
|
|
904
910
|
|
|
905
|
-
const
|
|
911
|
+
const wire = wireOpenClawMqtt({
|
|
906
912
|
botId: "openclaw.your-bot.local",
|
|
907
913
|
operatorId: operator?.label ?? "org.openclaw.your-operator",
|
|
908
914
|
network,
|
|
909
915
|
});
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
},
|
|
921
|
-
};
|
|
922
|
-
writeFileSync(cfgPath, JSON.stringify(cfg, null, 2), "utf8");
|
|
923
|
-
console.log(`Created ${cfgPath} with mqtt + mqtt-tools from ${profile ? "active" : "default"} preset.`);
|
|
924
|
-
console.log("Next: clanker bot mint <label>, then set channels.mqtt.botId / operatorId.");
|
|
925
|
-
console.log("See docs/operator-cli.md and SETUP.md.");
|
|
916
|
+
console.log(
|
|
917
|
+
wire.created
|
|
918
|
+
? `Created ${wire.path} with mqtt + mqtt-tools from ${profile ? "active" : "default"} preset.`
|
|
919
|
+
: `Updated channels.mqtt in ${wire.path} (plugins mqtt + mqtt-tools ensured).`,
|
|
920
|
+
);
|
|
921
|
+
nextHint([
|
|
922
|
+
"clanker bot mint <label>",
|
|
923
|
+
"Then channels.mqtt.botId / operatorId / privateKeyFile update automatically",
|
|
924
|
+
"See docs/operator-cli.md and docs/closed-beta-invite.md",
|
|
925
|
+
]);
|
|
926
926
|
process.exit(0);
|
|
927
927
|
}
|
|
928
928
|
|
package/lib/doctor.mjs
CHANGED
|
@@ -14,9 +14,9 @@ import { detectSetupHints, formatSetupDetectTable } from "./setup-detect.mjs";
|
|
|
14
14
|
import { c, nextHint } from "./ui.mjs";
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
|
-
* @param {{ home?: string, env?: NodeJS.ProcessEnv, openclawDir?: string, castBin?: string, spawn?: Function }} [opts]
|
|
17
|
+
* @param {{ home?: string, env?: NodeJS.ProcessEnv, openclawDir?: string, castBin?: string, spawn?: Function, fetchImpl?: typeof fetch }} [opts]
|
|
18
18
|
*/
|
|
19
|
-
export function runDoctorChecks(opts = {}) {
|
|
19
|
+
export async function runDoctorChecks(opts = {}) {
|
|
20
20
|
const env = opts.env ?? process.env;
|
|
21
21
|
const home = opts.home ?? clankerHome(env);
|
|
22
22
|
const hints = detectSetupHints({
|
|
@@ -28,6 +28,7 @@ export function runDoctorChecks(opts = {}) {
|
|
|
28
28
|
});
|
|
29
29
|
const config = hints.config ?? loadConfig(home);
|
|
30
30
|
const operator = hints.operator ?? loadOperator(home);
|
|
31
|
+
const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
|
|
31
32
|
|
|
32
33
|
/** @type {{ id: string, ok: boolean, level: 'pass'|'warn'|'fail', message: string }[]} */
|
|
33
34
|
const checks = [];
|
|
@@ -102,6 +103,46 @@ export function runDoctorChecks(opts = {}) {
|
|
|
102
103
|
: "Foundry cast not on PATH (optional)",
|
|
103
104
|
});
|
|
104
105
|
|
|
106
|
+
const authUrl = config?.mqttAuthServiceUrl;
|
|
107
|
+
if (authUrl && typeof fetchImpl === "function") {
|
|
108
|
+
const healthUrl = `${String(authUrl).replace(/\/$/, "")}/health`;
|
|
109
|
+
try {
|
|
110
|
+
const ac = new AbortController();
|
|
111
|
+
const t = setTimeout(() => ac.abort(), 5000);
|
|
112
|
+
const res = await fetchImpl(healthUrl, { signal: ac.signal });
|
|
113
|
+
clearTimeout(t);
|
|
114
|
+
if (res.ok) {
|
|
115
|
+
checks.push({
|
|
116
|
+
id: "mqtt_auth_health",
|
|
117
|
+
ok: true,
|
|
118
|
+
level: "pass",
|
|
119
|
+
message: `mqtt-auth health OK (${healthUrl})`,
|
|
120
|
+
});
|
|
121
|
+
} else {
|
|
122
|
+
checks.push({
|
|
123
|
+
id: "mqtt_auth_health",
|
|
124
|
+
ok: false,
|
|
125
|
+
level: "fail",
|
|
126
|
+
message: `mqtt-auth health HTTP ${res.status} (${healthUrl})`,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
} catch (err) {
|
|
130
|
+
checks.push({
|
|
131
|
+
id: "mqtt_auth_health",
|
|
132
|
+
ok: false,
|
|
133
|
+
level: "warn",
|
|
134
|
+
message: `mqtt-auth health unreachable: ${err.message ?? err}`,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
} else if (config) {
|
|
138
|
+
checks.push({
|
|
139
|
+
id: "mqtt_auth_health",
|
|
140
|
+
ok: true,
|
|
141
|
+
level: "warn",
|
|
142
|
+
message: "mqttAuthServiceUrl not set — skip hub health",
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
105
146
|
const readyWhoami = checks
|
|
106
147
|
.filter((ch) => ch.id === "config" || ch.id === "registry" || ch.id === "operator")
|
|
107
148
|
.every((ch) => ch.ok);
|
|
@@ -125,7 +166,7 @@ export function runDoctorChecks(opts = {}) {
|
|
|
125
166
|
*/
|
|
126
167
|
export async function runDoctor(argv = [], opts = {}) {
|
|
127
168
|
const json = argv.includes("--json");
|
|
128
|
-
const report = runDoctorChecks(opts);
|
|
169
|
+
const report = await runDoctorChecks(opts);
|
|
129
170
|
|
|
130
171
|
if (json) {
|
|
131
172
|
console.log(
|
package/lib/foundry.mjs
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Foundry cast helpers for setup (address resolve + key export).
|
|
3
|
+
* Never logs private keys.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { dirname } from "node:path";
|
|
8
|
+
import { spawnSync } from "node:child_process";
|
|
9
|
+
import { getAddress } from "viem";
|
|
10
|
+
import { privateKeyToAccount } from "viem/accounts";
|
|
11
|
+
import { normalizePrivateKey } from "./resolve.mjs";
|
|
12
|
+
import { parseCastWalletList } from "./setup-detect.mjs";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @param {{ castBin?: string, spawn?: typeof spawnSync }} [opts]
|
|
16
|
+
*/
|
|
17
|
+
export function listFoundryAccounts(opts = {}) {
|
|
18
|
+
const spawn = opts.spawn ?? spawnSync;
|
|
19
|
+
const castBin = opts.castBin ?? "cast";
|
|
20
|
+
const result = spawn(castBin, ["wallet", "list"], {
|
|
21
|
+
encoding: "utf8",
|
|
22
|
+
shell: false,
|
|
23
|
+
});
|
|
24
|
+
if (result.error || result.status !== 0) {
|
|
25
|
+
return { available: false, accounts: [] };
|
|
26
|
+
}
|
|
27
|
+
return { available: true, accounts: parseCastWalletList(result.stdout ?? "") };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Resolve 0x address for a Foundry account name.
|
|
32
|
+
* Uses inherit stdio so unlock prompts work when interactive.
|
|
33
|
+
*
|
|
34
|
+
* @param {string} account
|
|
35
|
+
* @param {{ castBin?: string, spawn?: typeof spawnSync, inheritStdio?: boolean }} [opts]
|
|
36
|
+
* @returns {string} checksummed address
|
|
37
|
+
*/
|
|
38
|
+
export function resolveFoundryAddress(account, opts = {}) {
|
|
39
|
+
const spawn = opts.spawn ?? spawnSync;
|
|
40
|
+
const castBin = opts.castBin ?? "cast";
|
|
41
|
+
const inherit = opts.inheritStdio !== false;
|
|
42
|
+
const result = spawn(castBin, ["wallet", "address", account], {
|
|
43
|
+
encoding: "utf8",
|
|
44
|
+
shell: false,
|
|
45
|
+
stdio: inherit ? ["inherit", "pipe", "inherit"] : "pipe",
|
|
46
|
+
});
|
|
47
|
+
if (result.error) {
|
|
48
|
+
throw new Error(`cast failed: ${result.error.message}`);
|
|
49
|
+
}
|
|
50
|
+
if (result.status !== 0) {
|
|
51
|
+
const err = (result.stderr || result.stdout || "").trim();
|
|
52
|
+
throw new Error(
|
|
53
|
+
err || `cast wallet address ${account} failed (status ${result.status})`,
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
const line = String(result.stdout ?? "")
|
|
57
|
+
.split(/\r?\n/)
|
|
58
|
+
.map((l) => l.trim())
|
|
59
|
+
.find((l) => /^0x[0-9a-fA-F]{40}$/.test(l));
|
|
60
|
+
if (!line) {
|
|
61
|
+
throw new Error(`Could not parse address from cast wallet address ${account}`);
|
|
62
|
+
}
|
|
63
|
+
return getAddress(line);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Export Foundry account private key to destPath (mode 0o600).
|
|
68
|
+
* Does not print the key. Unlock prompts via inherit stdio when interactive.
|
|
69
|
+
*
|
|
70
|
+
* @param {string} account
|
|
71
|
+
* @param {string} destPath
|
|
72
|
+
* @param {{ castBin?: string, spawn?: typeof spawnSync, inheritStdio?: boolean }} [opts]
|
|
73
|
+
* @returns {{ path: string, address: string }}
|
|
74
|
+
*/
|
|
75
|
+
export function exportFoundryKey(account, destPath, opts = {}) {
|
|
76
|
+
const spawn = opts.spawn ?? spawnSync;
|
|
77
|
+
const castBin = opts.castBin ?? "cast";
|
|
78
|
+
const inherit = opts.inheritStdio !== false;
|
|
79
|
+
const result = spawn(castBin, ["wallet", "private-key", account], {
|
|
80
|
+
encoding: "utf8",
|
|
81
|
+
shell: false,
|
|
82
|
+
stdio: inherit ? ["inherit", "pipe", "inherit"] : "pipe",
|
|
83
|
+
});
|
|
84
|
+
if (result.error) {
|
|
85
|
+
throw new Error(`cast failed: ${result.error.message}`);
|
|
86
|
+
}
|
|
87
|
+
if (result.status !== 0) {
|
|
88
|
+
const err = (result.stderr || "").trim();
|
|
89
|
+
throw new Error(
|
|
90
|
+
err || `cast wallet private-key ${account} failed (status ${result.status})`,
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
const raw = String(result.stdout ?? "")
|
|
94
|
+
.split(/\r?\n/)
|
|
95
|
+
.map((l) => l.trim())
|
|
96
|
+
.find((l) => l.length > 0);
|
|
97
|
+
if (!raw) {
|
|
98
|
+
throw new Error("cast returned empty private key");
|
|
99
|
+
}
|
|
100
|
+
const key = normalizePrivateKey(raw);
|
|
101
|
+
const address = privateKeyToAccount(key).address;
|
|
102
|
+
|
|
103
|
+
mkdirSync(dirname(destPath), { recursive: true });
|
|
104
|
+
writeFileSync(destPath, `${key}\n`, { mode: 0o600 });
|
|
105
|
+
return { path: destPath, address };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Default operator key path under clanker home.
|
|
110
|
+
* @param {string} home
|
|
111
|
+
*/
|
|
112
|
+
export function defaultOperatorKeyPath(home) {
|
|
113
|
+
return `${home.replace(/\/$/, "")}/op.key`;
|
|
114
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Merge / create ~/.openclaw/openclaw.json channels.mqtt from clanker profile.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { harnessSnippet } from "./profile.mjs";
|
|
9
|
+
|
|
10
|
+
/** Current published OpenClaw plugin pins for closed-beta invite. */
|
|
11
|
+
export const OPENCLAW_PLUGIN_PIN = "2026.7.29";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @param {string} [openclawHome]
|
|
15
|
+
*/
|
|
16
|
+
export function openclawConfigPath(openclawHome = join(homedir(), ".openclaw")) {
|
|
17
|
+
return join(openclawHome, "openclaw.json");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Ensure mqtt + mqtt-tools are in plugins.enabled.
|
|
22
|
+
* @param {object} cfg
|
|
23
|
+
*/
|
|
24
|
+
export function ensureMqttPlugins(cfg) {
|
|
25
|
+
const out = { ...cfg };
|
|
26
|
+
const plugins = { ...(out.plugins ?? {}) };
|
|
27
|
+
const enabled = Array.isArray(plugins.enabled) ? [...plugins.enabled] : [];
|
|
28
|
+
for (const id of ["mqtt", "mqtt-tools"]) {
|
|
29
|
+
if (!enabled.includes(id)) enabled.push(id);
|
|
30
|
+
}
|
|
31
|
+
plugins.enabled = enabled;
|
|
32
|
+
out.plugins = plugins;
|
|
33
|
+
return out;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Wire channels.mqtt into openclaw.json.
|
|
38
|
+
*
|
|
39
|
+
* @param {{
|
|
40
|
+
* botId: string,
|
|
41
|
+
* operatorId: string,
|
|
42
|
+
* network: { rpc: string, registry: string|null, brokerUrl?: string, mqttAuthServiceUrl?: string },
|
|
43
|
+
* keyPath?: string|null,
|
|
44
|
+
* openclawHome?: string,
|
|
45
|
+
* }} opts
|
|
46
|
+
*/
|
|
47
|
+
export function wireOpenClawMqtt(opts) {
|
|
48
|
+
const openclawHome = opts.openclawHome ?? join(homedir(), ".openclaw");
|
|
49
|
+
const cfgPath = openclawConfigPath(openclawHome);
|
|
50
|
+
mkdirSync(openclawHome, { recursive: true });
|
|
51
|
+
|
|
52
|
+
const mqtt = harnessSnippet({
|
|
53
|
+
botId: opts.botId,
|
|
54
|
+
operatorId: opts.operatorId,
|
|
55
|
+
network: opts.network,
|
|
56
|
+
});
|
|
57
|
+
if (!mqtt.registryAddress) {
|
|
58
|
+
mqtt.registryAddress = "0x0000000000000000000000000000000000000000";
|
|
59
|
+
}
|
|
60
|
+
if (opts.keyPath) {
|
|
61
|
+
mqtt.privateKeyFile = opts.keyPath;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
let created = false;
|
|
65
|
+
let cfg;
|
|
66
|
+
if (!existsSync(cfgPath)) {
|
|
67
|
+
created = true;
|
|
68
|
+
cfg = {
|
|
69
|
+
plugins: { enabled: ["mqtt", "mqtt-tools"] },
|
|
70
|
+
channels: { mqtt },
|
|
71
|
+
};
|
|
72
|
+
} else {
|
|
73
|
+
cfg = JSON.parse(readFileSync(cfgPath, "utf8"));
|
|
74
|
+
cfg = ensureMqttPlugins(cfg);
|
|
75
|
+
const channels = { ...(cfg.channels ?? {}) };
|
|
76
|
+
const prev = channels.mqtt && typeof channels.mqtt === "object" ? channels.mqtt : {};
|
|
77
|
+
channels.mqtt = { ...prev, ...mqtt };
|
|
78
|
+
cfg.channels = channels;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
writeFileSync(cfgPath, `${JSON.stringify(cfg, null, 2)}\n`, "utf8");
|
|
82
|
+
return {
|
|
83
|
+
created,
|
|
84
|
+
path: cfgPath,
|
|
85
|
+
channelsMqtt: cfg.channels.mqtt,
|
|
86
|
+
pluginPin: OPENCLAW_PLUGIN_PIN,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Human checklist lines after wiring.
|
|
92
|
+
* @param {{ botId: string, channelsMqtt: object, pluginPin: string }} opts
|
|
93
|
+
*/
|
|
94
|
+
export function hubConnectChecklist(opts) {
|
|
95
|
+
const pin = opts.pluginPin ?? OPENCLAW_PLUGIN_PIN;
|
|
96
|
+
return [
|
|
97
|
+
`openclaw plugins install @clanker-chain/mqtt-channel-plugin@${pin}`,
|
|
98
|
+
`openclaw plugins install @clanker-chain/mqtt-tools@${pin}`,
|
|
99
|
+
"Enable plugin ids mqtt + mqtt-tools (already set in openclaw.json if we wired it)",
|
|
100
|
+
`CONNECT broker: ${opts.channelsMqtt?.brokerUrl ?? "mqtts://mqtt.clanker-chain.com:8883"}`,
|
|
101
|
+
`Ask hub operator to allow bot_id "${opts.botId}" in france dmPolicy / allowFrom`,
|
|
102
|
+
"DM openclaw.france.prod-1 to smoke the mesh",
|
|
103
|
+
];
|
|
104
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Consumer-path operator key helpers (generate local op.key).
|
|
3
|
+
* Never logs private keys.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { dirname } from "node:path";
|
|
8
|
+
import { generatePrivateKey, privateKeyToAccount } from "viem/accounts";
|
|
9
|
+
import { defaultOperatorKeyPath } from "./foundry.mjs";
|
|
10
|
+
|
|
11
|
+
export { defaultOperatorKeyPath };
|
|
12
|
+
|
|
13
|
+
/** Coinbase Developer Platform faucet UI (Base Sepolia). */
|
|
14
|
+
export const BASE_SEPOLIA_FAUCET_URL =
|
|
15
|
+
"https://portal.cdp.coinbase.com/products/faucet";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Generate a new secp256k1 key and write it to destPath (mode 0o600).
|
|
19
|
+
* Does not print the key.
|
|
20
|
+
*
|
|
21
|
+
* @param {string} destPath
|
|
22
|
+
* @param {{ force?: boolean }} [opts]
|
|
23
|
+
* @returns {{ path: string, address: string }}
|
|
24
|
+
*/
|
|
25
|
+
export function generateOperatorKeyFile(destPath, opts = {}) {
|
|
26
|
+
if (existsSync(destPath) && !opts.force) {
|
|
27
|
+
throw new Error(
|
|
28
|
+
`Key file already exists: ${destPath} (pass --force to overwrite)`,
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
const key = generatePrivateKey();
|
|
32
|
+
const address = privateKeyToAccount(key).address;
|
|
33
|
+
mkdirSync(dirname(destPath), { recursive: true });
|
|
34
|
+
writeFileSync(destPath, `${key}\n`, { mode: 0o600 });
|
|
35
|
+
return { path: destPath, address };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Plain-language next steps after creating an operator key on Sepolia.
|
|
40
|
+
* @param {{ address: string, label?: string }} opts
|
|
41
|
+
*/
|
|
42
|
+
export function consumerFundHints(opts) {
|
|
43
|
+
const mintLabel = opts.label ? ` ${opts.label}` : "";
|
|
44
|
+
return [
|
|
45
|
+
`Your operator address is ${opts.address} — fund it with Base Sepolia ETH: ${BASE_SEPOLIA_FAUCET_URL}`,
|
|
46
|
+
"clanker doctor",
|
|
47
|
+
`clanker operator mint${mintLabel} --yes`,
|
|
48
|
+
"clanker bot mint <bot_label> --yes",
|
|
49
|
+
];
|
|
50
|
+
}
|
package/lib/setup.mjs
CHANGED
|
@@ -28,7 +28,18 @@ import {
|
|
|
28
28
|
detectSetupHints,
|
|
29
29
|
formatSetupDetectTable,
|
|
30
30
|
} from "./setup-detect.mjs";
|
|
31
|
+
import {
|
|
32
|
+
defaultOperatorKeyPath,
|
|
33
|
+
exportFoundryKey,
|
|
34
|
+
resolveFoundryAddress,
|
|
35
|
+
} from "./foundry.mjs";
|
|
36
|
+
import {
|
|
37
|
+
BASE_SEPOLIA_FAUCET_URL,
|
|
38
|
+
consumerFundHints,
|
|
39
|
+
generateOperatorKeyFile,
|
|
40
|
+
} from "./operator-key.mjs";
|
|
31
41
|
import { c, nextHint } from "./ui.mjs";
|
|
42
|
+
import { join } from "node:path";
|
|
32
43
|
|
|
33
44
|
/**
|
|
34
45
|
* @param {string[]} argv
|
|
@@ -44,6 +55,9 @@ export function parseSetupFlags(argv) {
|
|
|
44
55
|
let yes = false;
|
|
45
56
|
let skipKey = false;
|
|
46
57
|
let skipChainCheck = false;
|
|
58
|
+
let foundryAccount = null;
|
|
59
|
+
let exportKey = false;
|
|
60
|
+
let generateKey = false;
|
|
47
61
|
|
|
48
62
|
for (let i = 0; i < argv.length; i += 1) {
|
|
49
63
|
const a = argv[i];
|
|
@@ -53,6 +67,9 @@ export function parseSetupFlags(argv) {
|
|
|
53
67
|
else if (a === "--key-file" && argv[i + 1]) keyFile = argv[++i];
|
|
54
68
|
else if (a === "--key-env" && argv[i + 1]) keyEnv = argv[++i];
|
|
55
69
|
else if (a === "--from-block" && argv[i + 1]) fromBlock = BigInt(argv[++i]);
|
|
70
|
+
else if (a === "--foundry-account" && argv[i + 1]) foundryAccount = argv[++i];
|
|
71
|
+
else if (a === "--export-key") exportKey = true;
|
|
72
|
+
else if (a === "--generate-key") generateKey = true;
|
|
56
73
|
else if (a === "--force") force = true;
|
|
57
74
|
else if (a === "--yes" || a === "-y") yes = true;
|
|
58
75
|
else if (a === "--skip-key") skipKey = true;
|
|
@@ -70,6 +87,9 @@ export function parseSetupFlags(argv) {
|
|
|
70
87
|
yes,
|
|
71
88
|
skipKey,
|
|
72
89
|
skipChainCheck,
|
|
90
|
+
foundryAccount,
|
|
91
|
+
exportKey,
|
|
92
|
+
generateKey,
|
|
73
93
|
};
|
|
74
94
|
}
|
|
75
95
|
|
|
@@ -191,21 +211,61 @@ export async function runSetupNonInteractive(argv, opts = {}) {
|
|
|
191
211
|
if (!flags.preset || !PRESETS[flags.preset]) {
|
|
192
212
|
throw new Error(
|
|
193
213
|
"Non-interactive setup requires --preset sepolia|local (stdin is not a TTY). " +
|
|
194
|
-
"Also pass --operator <label> and --address
|
|
214
|
+
"Also pass --operator <label> and --generate-key (or --address / --key-file)",
|
|
195
215
|
);
|
|
196
216
|
}
|
|
197
217
|
if (!flags.operator) {
|
|
198
218
|
throw new Error("Non-interactive setup requires --operator <label>");
|
|
199
219
|
}
|
|
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
220
|
|
|
206
221
|
let address = flags.address;
|
|
207
|
-
|
|
222
|
+
let keyFile = flags.keyFile;
|
|
223
|
+
const castOpts = {
|
|
224
|
+
castBin: opts.castBin,
|
|
225
|
+
spawn: opts.spawn,
|
|
226
|
+
inheritStdio: false,
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
if (flags.generateKey) {
|
|
230
|
+
if (flags.skipKey) {
|
|
231
|
+
throw new Error("Cannot combine --generate-key with --skip-key");
|
|
232
|
+
}
|
|
233
|
+
const dest = keyFile || defaultOperatorKeyPath(home);
|
|
234
|
+
const created = generateOperatorKeyFile(dest, {
|
|
235
|
+
force: flags.force,
|
|
236
|
+
});
|
|
237
|
+
keyFile = created.path;
|
|
238
|
+
if (address && getAddress(address) !== getAddress(created.address)) {
|
|
239
|
+
throw new Error(
|
|
240
|
+
`Generated key address ${created.address} does not match --address ${getAddress(address)}`,
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
address = created.address;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (flags.foundryAccount) {
|
|
247
|
+
if (!address) {
|
|
248
|
+
address = resolveFoundryAddress(flags.foundryAccount, castOpts);
|
|
249
|
+
}
|
|
250
|
+
if (flags.exportKey && !keyFile && !flags.skipKey) {
|
|
251
|
+
const dest = defaultOperatorKeyPath(home);
|
|
252
|
+
const exported = exportFoundryKey(flags.foundryAccount, dest, castOpts);
|
|
253
|
+
keyFile = exported.path;
|
|
254
|
+
if (getAddress(exported.address) !== getAddress(address)) {
|
|
255
|
+
throw new Error(
|
|
256
|
+
`Exported Foundry key address ${exported.address} does not match ${address}`,
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (!address && keyFile) address = addressFromKeyFile(keyFile);
|
|
208
263
|
if (!address && env.OPERATOR_PRIVATE_KEY) address = addressFromEnv(env);
|
|
264
|
+
if (!address) {
|
|
265
|
+
throw new Error(
|
|
266
|
+
"Non-interactive setup requires --generate-key, --address 0x…, --key-file, OPERATOR_PRIVATE_KEY, or --foundry-account",
|
|
267
|
+
);
|
|
268
|
+
}
|
|
209
269
|
|
|
210
270
|
const preset = PRESETS[flags.preset];
|
|
211
271
|
let fromBlock = flags.fromBlock;
|
|
@@ -225,14 +285,14 @@ export async function runSetupNonInteractive(argv, opts = {}) {
|
|
|
225
285
|
});
|
|
226
286
|
|
|
227
287
|
const key = buildKeyPointer({
|
|
228
|
-
keyFile
|
|
288
|
+
keyFile,
|
|
229
289
|
keyEnv: flags.keyEnv,
|
|
230
290
|
skipKey: flags.skipKey,
|
|
231
291
|
env,
|
|
232
292
|
});
|
|
233
293
|
|
|
234
|
-
if (
|
|
235
|
-
const fromKey = getAddress(addressFromKeyFile(
|
|
294
|
+
if (keyFile && flags.address) {
|
|
295
|
+
const fromKey = getAddress(addressFromKeyFile(keyFile));
|
|
236
296
|
if (fromKey !== getAddress(flags.address)) {
|
|
237
297
|
throw new Error(
|
|
238
298
|
`Key file address ${fromKey} does not match --address ${getAddress(flags.address)}`,
|
|
@@ -270,9 +330,13 @@ export async function runSetupInteractive(argv, opts = {}) {
|
|
|
270
330
|
});
|
|
271
331
|
|
|
272
332
|
clack.intro(c.bold("clanker setup"));
|
|
273
|
-
clack.log.step(
|
|
333
|
+
clack.log.step(
|
|
334
|
+
"Sets up your operator identity (org account) and network for OpenClaw bots.",
|
|
335
|
+
);
|
|
274
336
|
clack.log.message(
|
|
275
|
-
c.dim(
|
|
337
|
+
c.dim(
|
|
338
|
+
"New here? Create a key file — no wallet app or Foundry required. Mint needs a little test ETH later.",
|
|
339
|
+
),
|
|
276
340
|
);
|
|
277
341
|
clack.log.message(c.dim(`Profile: ${home}`));
|
|
278
342
|
console.log("");
|
|
@@ -330,6 +394,29 @@ export async function runSetupInteractive(argv, opts = {}) {
|
|
|
330
394
|
}
|
|
331
395
|
|
|
332
396
|
let address = flags.address ?? null;
|
|
397
|
+
let foundryAccountUsed = null;
|
|
398
|
+
let generatedKeyFile = null;
|
|
399
|
+
if (!address && flags.generateKey) {
|
|
400
|
+
const dest = flags.keyFile || defaultOperatorKeyPath(home);
|
|
401
|
+
let forceGen = flags.force;
|
|
402
|
+
if (existsSync(dest) && !forceGen) {
|
|
403
|
+
forceGen = cancelIf(
|
|
404
|
+
await clack.confirm({
|
|
405
|
+
message: `Overwrite existing ${dest}?`,
|
|
406
|
+
initialValue: false,
|
|
407
|
+
}),
|
|
408
|
+
);
|
|
409
|
+
if (!forceGen) {
|
|
410
|
+
clack.cancel("Aborted.");
|
|
411
|
+
process.exit(0);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
const created = generateOperatorKeyFile(dest, { force: true });
|
|
415
|
+
generatedKeyFile = created.path;
|
|
416
|
+
address = created.address;
|
|
417
|
+
clack.log.success(`Created operator key at ${created.path}`);
|
|
418
|
+
clack.log.info(`Your operator address: ${created.address}`);
|
|
419
|
+
}
|
|
333
420
|
if (!address && flags.keyFile) {
|
|
334
421
|
address = addressFromKeyFile(flags.keyFile);
|
|
335
422
|
clack.log.info(`Address from --key-file: ${address}`);
|
|
@@ -352,23 +439,86 @@ export async function runSetupInteractive(argv, opts = {}) {
|
|
|
352
439
|
);
|
|
353
440
|
if (useOp) address = hints.operator.owner;
|
|
354
441
|
}
|
|
355
|
-
if (!address
|
|
442
|
+
if (!address) {
|
|
356
443
|
const options = [
|
|
357
|
-
|
|
358
|
-
value:
|
|
359
|
-
label:
|
|
360
|
-
hint: "
|
|
361
|
-
}
|
|
362
|
-
{
|
|
444
|
+
{
|
|
445
|
+
value: "__generate__",
|
|
446
|
+
label: "Create a new operator key for me",
|
|
447
|
+
hint: "writes ~/.clanker/op.key (recommended)",
|
|
448
|
+
},
|
|
449
|
+
{
|
|
450
|
+
value: "__keyfile__",
|
|
451
|
+
label: "Use an existing key file…",
|
|
452
|
+
hint: "path to a 0x private key file",
|
|
453
|
+
},
|
|
363
454
|
];
|
|
455
|
+
if (hints.foundryAccounts.length) {
|
|
456
|
+
for (const n of hints.foundryAccounts) {
|
|
457
|
+
options.push({
|
|
458
|
+
value: n,
|
|
459
|
+
label: `Foundry: ${n}`,
|
|
460
|
+
hint: "advanced — cast wallet",
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
} else {
|
|
464
|
+
options.push({
|
|
465
|
+
value: "__foundry_missing__",
|
|
466
|
+
label: "Foundry account…",
|
|
467
|
+
hint: "advanced — install Foundry first",
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
options.push({
|
|
471
|
+
value: "__paste__",
|
|
472
|
+
label: "Paste an address…",
|
|
473
|
+
hint: "read-only unless you add a key later",
|
|
474
|
+
});
|
|
475
|
+
|
|
364
476
|
const pick = cancelIf(
|
|
365
477
|
await clack.select({
|
|
366
|
-
message: "
|
|
478
|
+
message: "How do you want to set your operator identity?",
|
|
367
479
|
options,
|
|
368
|
-
initialValue:
|
|
480
|
+
initialValue: "__generate__",
|
|
369
481
|
}),
|
|
370
482
|
);
|
|
371
|
-
|
|
483
|
+
|
|
484
|
+
if (pick === "__generate__") {
|
|
485
|
+
const dest = defaultOperatorKeyPath(home);
|
|
486
|
+
let forceGen = flags.force;
|
|
487
|
+
if (existsSync(dest) && !forceGen) {
|
|
488
|
+
forceGen = cancelIf(
|
|
489
|
+
await clack.confirm({
|
|
490
|
+
message: `Overwrite existing ${dest}?`,
|
|
491
|
+
initialValue: false,
|
|
492
|
+
}),
|
|
493
|
+
);
|
|
494
|
+
if (!forceGen) {
|
|
495
|
+
clack.cancel("Aborted.");
|
|
496
|
+
process.exit(0);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
const created = generateOperatorKeyFile(dest, { force: true });
|
|
500
|
+
generatedKeyFile = created.path;
|
|
501
|
+
address = created.address;
|
|
502
|
+
clack.log.success(`Created operator key at ${created.path}`);
|
|
503
|
+
clack.log.info(`Your operator address: ${created.address}`);
|
|
504
|
+
} else if (pick === "__keyfile__") {
|
|
505
|
+
const path = cancelIf(
|
|
506
|
+
await clack.text({
|
|
507
|
+
message: "Path to operator key file",
|
|
508
|
+
placeholder: join(home, "op.key"),
|
|
509
|
+
validate: (v) =>
|
|
510
|
+
v && String(v).trim() && existsSync(String(v).trim())
|
|
511
|
+
? undefined
|
|
512
|
+
: "File not found",
|
|
513
|
+
}),
|
|
514
|
+
);
|
|
515
|
+
generatedKeyFile = String(path).trim();
|
|
516
|
+
address = addressFromKeyFile(generatedKeyFile);
|
|
517
|
+
clack.log.info(`Address from key file: ${address}`);
|
|
518
|
+
} else if (pick === "__foundry_missing__") {
|
|
519
|
+
clack.log.warn(
|
|
520
|
+
"Foundry (cast) is not available. Install https://book.getfoundry.sh/ or choose Create a new operator key.",
|
|
521
|
+
);
|
|
372
522
|
address = cancelIf(
|
|
373
523
|
await clack.text({
|
|
374
524
|
message: "Operator owner address",
|
|
@@ -377,27 +527,38 @@ export async function runSetupInteractive(argv, opts = {}) {
|
|
|
377
527
|
/^0x[0-9a-fA-F]{40}$/.test(v || "") ? undefined : "Need 0x + 40 hex",
|
|
378
528
|
}),
|
|
379
529
|
);
|
|
380
|
-
} else {
|
|
530
|
+
} else if (pick === "__paste__") {
|
|
381
531
|
address = cancelIf(
|
|
382
532
|
await clack.text({
|
|
383
|
-
message:
|
|
533
|
+
message: "Operator owner address",
|
|
384
534
|
placeholder: "0x…",
|
|
385
535
|
validate: (v) =>
|
|
386
536
|
/^0x[0-9a-fA-F]{40}$/.test(v || "") ? undefined : "Need 0x + 40 hex",
|
|
387
537
|
}),
|
|
388
538
|
);
|
|
539
|
+
} else {
|
|
540
|
+
foundryAccountUsed = pick;
|
|
541
|
+
try {
|
|
542
|
+
clack.log.step(`Resolving address for Foundry account "${pick}" (unlock if prompted)…`);
|
|
543
|
+
address = resolveFoundryAddress(pick, {
|
|
544
|
+
castBin: opts.castBin,
|
|
545
|
+
spawn: opts.spawn,
|
|
546
|
+
inheritStdio: true,
|
|
547
|
+
});
|
|
548
|
+
clack.log.success(`Foundry address: ${address}`);
|
|
549
|
+
} catch (err) {
|
|
550
|
+
clack.log.warn(err.message);
|
|
551
|
+
address = cancelIf(
|
|
552
|
+
await clack.text({
|
|
553
|
+
message: `Paste 0x address for "${pick}"`,
|
|
554
|
+
placeholder: "0x…",
|
|
555
|
+
validate: (v) =>
|
|
556
|
+
/^0x[0-9a-fA-F]{40}$/.test(v || "") ? undefined : "Need 0x + 40 hex",
|
|
557
|
+
}),
|
|
558
|
+
);
|
|
559
|
+
}
|
|
389
560
|
}
|
|
390
561
|
}
|
|
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
562
|
address = getAddress(address);
|
|
402
563
|
|
|
403
564
|
let label = flags.operator || hints.operator?.label || null;
|
|
@@ -435,11 +596,36 @@ export async function runSetupInteractive(argv, opts = {}) {
|
|
|
435
596
|
throw err;
|
|
436
597
|
}
|
|
437
598
|
|
|
438
|
-
let keyFile = flags.keyFile;
|
|
599
|
+
let keyFile = flags.keyFile || generatedKeyFile;
|
|
439
600
|
let keyEnv = flags.keyEnv;
|
|
440
601
|
let skipKey = flags.skipKey;
|
|
441
602
|
if (!skipKey && !keyFile && !keyEnv) {
|
|
442
|
-
if (
|
|
603
|
+
if (foundryAccountUsed) {
|
|
604
|
+
const doExport = cancelIf(
|
|
605
|
+
await clack.confirm({
|
|
606
|
+
message: `Export Foundry key for "${foundryAccountUsed}" to ~/.clanker/op.key for minting?`,
|
|
607
|
+
initialValue: true,
|
|
608
|
+
}),
|
|
609
|
+
);
|
|
610
|
+
if (doExport) {
|
|
611
|
+
const dest = defaultOperatorKeyPath(home);
|
|
612
|
+
clack.log.step("Exporting key via cast (unlock if prompted; key is not printed)…");
|
|
613
|
+
const exported = exportFoundryKey(foundryAccountUsed, dest, {
|
|
614
|
+
castBin: opts.castBin,
|
|
615
|
+
spawn: opts.spawn,
|
|
616
|
+
inheritStdio: true,
|
|
617
|
+
});
|
|
618
|
+
if (getAddress(exported.address) !== address) {
|
|
619
|
+
throw new Error(
|
|
620
|
+
`Exported key address ${exported.address} does not match owner ${address}`,
|
|
621
|
+
);
|
|
622
|
+
}
|
|
623
|
+
keyFile = exported.path;
|
|
624
|
+
clack.log.success(`Wrote ${keyFile} (mode 600)`);
|
|
625
|
+
} else {
|
|
626
|
+
skipKey = true;
|
|
627
|
+
}
|
|
628
|
+
} else if (hints.hasOperatorPrivateKeyEnv) {
|
|
443
629
|
const use = cancelIf(
|
|
444
630
|
await clack.confirm({
|
|
445
631
|
message: "Store OPERATOR_PRIVATE_KEY pointer for signing?",
|
|
@@ -452,7 +638,7 @@ export async function runSetupInteractive(argv, opts = {}) {
|
|
|
452
638
|
const path = cancelIf(
|
|
453
639
|
await clack.text({
|
|
454
640
|
message: "Operator key file path (Enter = read-only)",
|
|
455
|
-
placeholder: "
|
|
641
|
+
placeholder: join(home, "op.key"),
|
|
456
642
|
}),
|
|
457
643
|
);
|
|
458
644
|
if (path && String(path).trim()) keyFile = String(path).trim();
|
|
@@ -506,7 +692,16 @@ export async function runSetupInteractive(argv, opts = {}) {
|
|
|
506
692
|
if (!key) {
|
|
507
693
|
nextHint([
|
|
508
694
|
"clanker whoami",
|
|
509
|
-
"clanker setup
|
|
695
|
+
"clanker setup — choose Create a new operator key when you need mint",
|
|
696
|
+
]);
|
|
697
|
+
} else if (preset === "sepolia" && generatedKeyFile) {
|
|
698
|
+
nextHint(consumerFundHints({ address, label }));
|
|
699
|
+
} else if (preset === "sepolia") {
|
|
700
|
+
nextHint([
|
|
701
|
+
`If this address needs test ETH: ${BASE_SEPOLIA_FAUCET_URL}`,
|
|
702
|
+
"clanker doctor",
|
|
703
|
+
"clanker whoami",
|
|
704
|
+
`clanker bot mint <label>`,
|
|
510
705
|
]);
|
|
511
706
|
} else {
|
|
512
707
|
nextHint(["clanker whoami", `clanker bot mint <label>`]);
|
|
@@ -520,23 +715,20 @@ export async function runSetupInteractive(argv, opts = {}) {
|
|
|
520
715
|
export async function runSetup(argv, opts = {}) {
|
|
521
716
|
const flags = parseSetupFlags(argv);
|
|
522
717
|
const isTTY = opts.isTTY ?? Boolean(input.isTTY);
|
|
718
|
+
const hasIdentitySource = Boolean(
|
|
719
|
+
flags.address ||
|
|
720
|
+
flags.keyFile ||
|
|
721
|
+
flags.generateKey ||
|
|
722
|
+
flags.foundryAccount ||
|
|
723
|
+
opts.env?.OPERATOR_PRIVATE_KEY ||
|
|
724
|
+
process.env.OPERATOR_PRIVATE_KEY,
|
|
725
|
+
);
|
|
523
726
|
|
|
524
|
-
if (
|
|
525
|
-
!isTTY ||
|
|
526
|
-
(flags.yes && flags.preset && flags.operator && (flags.address || flags.keyFile))
|
|
527
|
-
) {
|
|
727
|
+
if (!isTTY || (flags.yes && flags.preset && flags.operator && hasIdentitySource)) {
|
|
528
728
|
if (!isTTY && !(flags.preset && flags.operator)) {
|
|
529
729
|
return runSetupNonInteractive(argv, opts);
|
|
530
730
|
}
|
|
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
|
-
) {
|
|
731
|
+
if (flags.yes && flags.preset && flags.operator && hasIdentitySource) {
|
|
540
732
|
return runSetupNonInteractive(argv, opts);
|
|
541
733
|
}
|
|
542
734
|
}
|