@clawops/cli 1.7.9 → 2.0.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/README.md +504 -76
- package/dist/apply-KYFPWI5U.js +27 -0
- package/dist/automation-RMAIBNVC.js +44 -0
- package/dist/bedrock-XVASJX2K.js +61 -0
- package/dist/{bootstrap-HL4DNUNG.js → bootstrap-HNGKY6WW.js} +31 -19
- package/dist/bootstrap.sh.tmpl +51 -31
- package/dist/channels-KBUR42IU.js +53 -0
- package/dist/chunk-2I7KCMXZ.js +142 -0
- package/dist/{chunk-DQDAWNGB.js → chunk-2Z7J3ERQ.js} +1 -1
- package/dist/chunk-3OLUJPCB.js +36 -0
- package/dist/chunk-5XT5IAJK.js +69 -0
- package/dist/chunk-AEEMRFZV.js +24 -0
- package/dist/chunk-BSLIYB5Q.js +106 -0
- package/dist/{chunk-3XK66DKK.js → chunk-E4NJSJ54.js} +57 -93
- package/dist/chunk-FEK7VJAY.js +16 -0
- package/dist/{chunk-UKTP332Z.js → chunk-GAZPPSKU.js} +6 -7
- package/dist/chunk-GIEVAGEK.js +126 -0
- package/dist/chunk-HSPHXG6W.js +67 -0
- package/dist/chunk-IVWP4QUJ.js +901 -0
- package/dist/{chunk-PVUVI35Y.js → chunk-JW5A7E3X.js} +5 -1
- package/dist/{chunk-GZN7PKP4.js → chunk-KC3GC3PE.js} +328 -121
- package/dist/chunk-LBE6COC4.js +244 -0
- package/dist/chunk-LEVIW2PV.js +890 -0
- package/dist/chunk-LT4XMO5D.js +50 -0
- package/dist/chunk-MC5ATCXS.js +108 -0
- package/dist/chunk-N4WZLWJ4.js +16 -0
- package/dist/chunk-NUDIHZEH.js +55 -0
- package/dist/chunk-NUL5F6YV.js +56 -0
- package/dist/chunk-RHOWYIWT.js +52 -0
- package/dist/chunk-STLRHLLV.js +193 -0
- package/dist/chunk-V25QRZRB.js +45 -0
- package/dist/chunk-VOEJYRIK.js +33 -0
- package/dist/{providers-2OABPW2E.js → chunk-VUFMZEIM.js} +3 -2
- package/dist/chunk-W3OLNNJW.js +51 -0
- package/dist/cli-WLD3EBEM.js +11 -0
- package/dist/cli-auth-GD5SK7JU.js +13 -0
- package/dist/cli.js +1296 -695
- package/dist/config-validate-K2N6XLDX.js +76 -0
- package/dist/context-7EVC6W45.js +20 -0
- package/dist/docker-3PDO4TS7.js +12 -0
- package/dist/{firewall-XQGO7HWK.js → firewall-5VKZ4WBH.js} +32 -1
- package/dist/generate-I6YQ5V2T.js +29 -0
- package/dist/{harden-URPXLSVF.js → harden-72JMRT4E.js} +51 -11
- package/dist/health-5CCOXYGG.js +11 -0
- package/dist/index.d.ts +56 -0
- package/dist/known-hosts-file-I3Q3NBYQ.js +22 -0
- package/dist/logs-5A7YYA53.js +15 -0
- package/dist/{mcp-wire-7E5JQY47.js → mcp-wire-3SWBM7D6.js} +32 -16
- package/dist/models-AGTQWYDL.js +32 -0
- package/dist/network-args-PYJRJBYO.js +64 -0
- package/dist/{package-5AR4ZPK4.js → package-5BCMEBTE.js} +11 -4
- package/dist/passphrase-MJBE45N7.js +15 -0
- package/dist/plugins-DTST3OER.js +49 -0
- package/dist/{pool-QGFQRMGU.js → pool-PIIKJ4L2.js} +3 -2
- package/dist/preflight-EF52S3WW.js +162 -0
- package/dist/preflight-MWWM5JT3.js +181 -0
- package/dist/preflight-TFD4N5ZU.js +141 -0
- package/dist/privileged-JFA6LLTV.js +17 -0
- package/dist/providers-FW6H6OPF.js +11 -0
- package/dist/ready-KDNRBNSU.js +95 -0
- package/dist/{remote-config-QDLN4UMG.js → remote-config-3HZLUWEA.js} +4 -3
- package/dist/runtime-X3KW5TCP.js +44 -0
- package/dist/server-GBA7BJGK.js +46 -0
- package/dist/spec-path-KQOOIQCA.js +9 -0
- package/dist/ssh-VAI5DU5M.js +11 -0
- package/dist/ssh-key-DUO6OKOI.js +7 -0
- package/dist/stack-config-R4SPSUQZ.js +12 -0
- package/dist/upgrade-ULD4KFYC.js +88 -0
- package/dist/validate-YVI3BJ55.js +14 -0
- package/dist/{version-guard-VUCNZXQS.js → version-guard-XVYW5WFJ.js} +2 -2
- package/dist/{versions-2UVAT6PQ.js → versions-W5LRQCAJ.js} +2 -2
- package/dist/wait-WO72TRRQ.js +79 -0
- package/package.json +11 -4
- package/spec/deploy-plan.schema.json +114 -25
- package/spec/integrations.yaml +135 -28
- package/spec/mcp-tools.yaml +89 -37
- package/spec/models.yaml +23 -3
- package/spec/openclaw-2.0.config.schema.json +1 -0
- package/spec/openclaw-versions.yaml +116 -27
- package/spec/providers.schema.json +163 -28
- package/dist/apply-F3KY2HTB.js +0 -17
- package/dist/automation-RULUSJBW.js +0 -30
- package/dist/aws-L4RR2LSE.js +0 -254
- package/dist/azure-7OXQENQL.js +0 -262
- package/dist/chunk-35WVQY3X.js +0 -122
- package/dist/chunk-6X45DDGH.js +0 -42
- package/dist/chunk-A4QR4TXW.js +0 -120
- package/dist/chunk-C5X375RO.js +0 -85
- package/dist/chunk-FNS56UQY.js +0 -741
- package/dist/chunk-P5LKWRPL.js +0 -114
- package/dist/chunk-SEZMVVAS.js +0 -156
- package/dist/context-5LCUG2QR.js +0 -10
- package/dist/gcp-UG6AWPIZ.js +0 -193
- package/dist/generate-OAGYT3VH.js +0 -15
- package/dist/local-DXBEVZ5C.js +0 -47
- package/dist/server-FIZCOLHL.js +0 -32
- package/dist/ssh-CNJNLM3E.js +0 -8
- package/dist/validate-CFQ4FH2T.js +0 -10
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/openclaw/health.ts
|
|
4
|
+
var HEALTH_PATHS = {
|
|
5
|
+
/** Process is up and serving. `{"ok":true,"status":"live"}` */
|
|
6
|
+
live: "/health",
|
|
7
|
+
/** Startup completed — the gate for "is this deploy finished". `{"ok":true,"status":"started"}` */
|
|
8
|
+
started: "/startupz",
|
|
9
|
+
/** Accepting work. `{"ready":true}` */
|
|
10
|
+
ready: "/readyz"
|
|
11
|
+
};
|
|
12
|
+
function probeCommand(kind, port, pathPrefix = "") {
|
|
13
|
+
return `${pathPrefix}curl -fsS -m 5 http://127.0.0.1:${port}${HEALTH_PATHS[kind]} 2>/dev/null || true`;
|
|
14
|
+
}
|
|
15
|
+
function interpretProbe(kind, body) {
|
|
16
|
+
const text = body.trim();
|
|
17
|
+
if (text === "") return { ok: false, reason: "no response from the gateway" };
|
|
18
|
+
if (text.startsWith("<")) {
|
|
19
|
+
return {
|
|
20
|
+
ok: false,
|
|
21
|
+
reason: `${HEALTH_PATHS[kind]} returned HTML, not JSON \u2014 the gateway serves its Control UI on a catch-all route, so an unknown path answers 200 with the SPA. The endpoint is missing or renamed on this OpenClaw version.`
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
let parsed;
|
|
25
|
+
try {
|
|
26
|
+
parsed = JSON.parse(text);
|
|
27
|
+
} catch {
|
|
28
|
+
return { ok: false, reason: `${HEALTH_PATHS[kind]} returned unparseable JSON: ${text.slice(0, 80)}` };
|
|
29
|
+
}
|
|
30
|
+
if (parsed === null || typeof parsed !== "object") {
|
|
31
|
+
return { ok: false, reason: `${HEALTH_PATHS[kind]} returned ${text.slice(0, 40)}` };
|
|
32
|
+
}
|
|
33
|
+
const obj = parsed;
|
|
34
|
+
if (kind === "ready") {
|
|
35
|
+
return obj["ready"] === true ? { ok: true } : { ok: false, reason: `gateway is not ready: ${text.slice(0, 80)}` };
|
|
36
|
+
}
|
|
37
|
+
if (obj["ok"] !== true) {
|
|
38
|
+
return { ok: false, reason: `gateway reported not ok: ${text.slice(0, 80)}` };
|
|
39
|
+
}
|
|
40
|
+
if (kind === "started" && obj["status"] !== "started") {
|
|
41
|
+
return { ok: false, reason: `gateway is still starting (status: ${String(obj["status"])})` };
|
|
42
|
+
}
|
|
43
|
+
return { ok: true };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export {
|
|
47
|
+
HEALTH_PATHS,
|
|
48
|
+
probeCommand,
|
|
49
|
+
interpretProbe
|
|
50
|
+
};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/transport/known-hosts.ts
|
|
4
|
+
import { createHmac, timingSafeEqual } from "crypto";
|
|
5
|
+
function hostEntryFor(host, port) {
|
|
6
|
+
return port === 22 ? host : `[${host}]:${port}`;
|
|
7
|
+
}
|
|
8
|
+
function parseKnownHosts(content) {
|
|
9
|
+
const entries = [];
|
|
10
|
+
for (const raw of content.split("\n")) {
|
|
11
|
+
const line = raw.trim();
|
|
12
|
+
if (!line || line.startsWith("#")) continue;
|
|
13
|
+
let parts = line.split(/\s+/);
|
|
14
|
+
let marker;
|
|
15
|
+
if (parts[0]?.startsWith("@")) {
|
|
16
|
+
marker = parts[0];
|
|
17
|
+
parts = parts.slice(1);
|
|
18
|
+
}
|
|
19
|
+
const hostField = parts[0];
|
|
20
|
+
if (!hostField) continue;
|
|
21
|
+
const hosts = hostField.split(",");
|
|
22
|
+
if (parts.length >= 3) {
|
|
23
|
+
entries.push({ hosts, keyType: parts[1], base64Key: parts[2], ...marker ? { marker } : {} });
|
|
24
|
+
} else if (parts.length === 2 && /^[0-9a-f]+$/i.test(parts[1] ?? "")) {
|
|
25
|
+
entries.push({ hosts, hexKey: parts[1], ...marker ? { marker } : {} });
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return entries;
|
|
29
|
+
}
|
|
30
|
+
function hashedHostMatches(token, hostEntry) {
|
|
31
|
+
const parts = token.split("|");
|
|
32
|
+
if (parts.length !== 4 || parts[1] !== "1") return false;
|
|
33
|
+
const salt = parts[2];
|
|
34
|
+
const expected = parts[3];
|
|
35
|
+
if (!salt || !expected) return false;
|
|
36
|
+
try {
|
|
37
|
+
const actual = createHmac("sha1", Buffer.from(salt, "base64")).update(hostEntry).digest("base64");
|
|
38
|
+
const a = Buffer.from(actual);
|
|
39
|
+
const b = Buffer.from(expected);
|
|
40
|
+
return a.length === b.length && timingSafeEqual(a, b);
|
|
41
|
+
} catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function patternToRegExp(pattern) {
|
|
46
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
|
|
47
|
+
return new RegExp(`^${escaped}$`, "i");
|
|
48
|
+
}
|
|
49
|
+
var isPattern = (host) => host.includes("*") || host.includes("?");
|
|
50
|
+
function entryMatchesHost(entry, hostEntry) {
|
|
51
|
+
let matched = false;
|
|
52
|
+
for (const host of entry.hosts) {
|
|
53
|
+
if (host.startsWith("|1|")) {
|
|
54
|
+
if (hashedHostMatches(host, hostEntry)) matched = true;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
const negated = host.startsWith("!");
|
|
58
|
+
const pattern = negated ? host.slice(1) : host;
|
|
59
|
+
const hit = isPattern(pattern) ? patternToRegExp(pattern).test(hostEntry) : pattern === hostEntry;
|
|
60
|
+
if (hit) {
|
|
61
|
+
if (negated) return false;
|
|
62
|
+
matched = true;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return matched;
|
|
66
|
+
}
|
|
67
|
+
function verifyAgainstKnownHosts(content, host, port, key) {
|
|
68
|
+
const hostEntry = hostEntryFor(host, port);
|
|
69
|
+
const base64Key = key.toString("base64");
|
|
70
|
+
const hexKey = key.toString("hex");
|
|
71
|
+
let sawHost = false;
|
|
72
|
+
for (const entry of parseKnownHosts(content)) {
|
|
73
|
+
if (!entryMatchesHost(entry, hostEntry)) continue;
|
|
74
|
+
if (entry.marker === "@cert-authority") continue;
|
|
75
|
+
sawHost = true;
|
|
76
|
+
const matches = entry.base64Key ? entry.base64Key === base64Key : entry.hexKey?.toLowerCase() === hexKey.toLowerCase();
|
|
77
|
+
if (matches) return entry.marker === "@revoked" ? "mismatch" : "match";
|
|
78
|
+
}
|
|
79
|
+
return sawHost ? "mismatch" : "unknown";
|
|
80
|
+
}
|
|
81
|
+
function withoutHost(content, host, port) {
|
|
82
|
+
const hostEntry = hostEntryFor(host, port);
|
|
83
|
+
const kept = [];
|
|
84
|
+
for (const raw of content.split("\n")) {
|
|
85
|
+
const [entry] = parseKnownHosts(raw);
|
|
86
|
+
if (entry && entryMatchesHost(entry, hostEntry)) continue;
|
|
87
|
+
kept.push(raw);
|
|
88
|
+
}
|
|
89
|
+
return kept.join("\n");
|
|
90
|
+
}
|
|
91
|
+
function formatKnownHostsLine(host, port, keyType, key) {
|
|
92
|
+
return `${hostEntryFor(host, port)} ${keyType} ${key.toString("base64")}
|
|
93
|
+
`;
|
|
94
|
+
}
|
|
95
|
+
function keyTypeFromBlob(key) {
|
|
96
|
+
if (key.length < 4) return void 0;
|
|
97
|
+
const len = key.readUInt32BE(0);
|
|
98
|
+
if (len === 0 || len > 64 || key.length < 4 + len) return void 0;
|
|
99
|
+
const type = key.subarray(4, 4 + len).toString("utf-8");
|
|
100
|
+
return /^[\x20-\x7e]+$/.test(type) ? type : void 0;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export {
|
|
104
|
+
verifyAgainstKnownHosts,
|
|
105
|
+
withoutHost,
|
|
106
|
+
formatKnownHostsLine,
|
|
107
|
+
keyTypeFromBlob
|
|
108
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/providers/aws/sizes.ts
|
|
4
|
+
var INSTANCE_TYPE_MAP = {
|
|
5
|
+
micro: "t3.micro",
|
|
6
|
+
small: "t3.small",
|
|
7
|
+
medium: "t3.medium",
|
|
8
|
+
large: "t3.large",
|
|
9
|
+
gpu: "g4dn.xlarge"
|
|
10
|
+
};
|
|
11
|
+
var DEFAULT_ALIAS = "small";
|
|
12
|
+
|
|
13
|
+
export {
|
|
14
|
+
INSTANCE_TYPE_MAP,
|
|
15
|
+
DEFAULT_ALIAS
|
|
16
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/transport/privileged.ts
|
|
4
|
+
function shellQuote(s) {
|
|
5
|
+
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
6
|
+
}
|
|
7
|
+
function sudoWrap(cmd) {
|
|
8
|
+
return `sudo -n bash -c ${shellQuote(cmd)}`;
|
|
9
|
+
}
|
|
10
|
+
function looksLikePermissionDenied(result) {
|
|
11
|
+
if (result.code === 0) return false;
|
|
12
|
+
const text = `${result.stderr} ${result.stdout}`.toLowerCase();
|
|
13
|
+
return text.includes("permission denied") || text.includes("docker.sock") || text.includes("operation not permitted");
|
|
14
|
+
}
|
|
15
|
+
var sudoNeeded = /* @__PURE__ */ new WeakMap();
|
|
16
|
+
function resetPrivilegeCache(session) {
|
|
17
|
+
sudoNeeded.delete(session);
|
|
18
|
+
}
|
|
19
|
+
async function execPrivileged(session, cmd, signal) {
|
|
20
|
+
const key = session;
|
|
21
|
+
if (sudoNeeded.get(key) === true) return session.exec(sudoWrap(cmd), signal);
|
|
22
|
+
const direct = await session.exec(cmd, signal);
|
|
23
|
+
if (!looksLikePermissionDenied(direct)) {
|
|
24
|
+
if (direct.code === 0) sudoNeeded.set(key, false);
|
|
25
|
+
return direct;
|
|
26
|
+
}
|
|
27
|
+
const escalated = await session.exec(sudoWrap(cmd), signal);
|
|
28
|
+
if (escalated.code === 0) sudoNeeded.set(key, true);
|
|
29
|
+
return escalated;
|
|
30
|
+
}
|
|
31
|
+
async function streamPrivileged(session, cmd, signal) {
|
|
32
|
+
const key = session;
|
|
33
|
+
if (sudoNeeded.get(key) === void 0) {
|
|
34
|
+
const probe = await session.exec('docker version --format "{{.Server.Version}}"', signal);
|
|
35
|
+
sudoNeeded.set(key, looksLikePermissionDenied(probe));
|
|
36
|
+
}
|
|
37
|
+
return session.stream(sudoNeeded.get(key) ? sudoWrap(cmd) : cmd, signal);
|
|
38
|
+
}
|
|
39
|
+
async function execPrivilegedWithInput(session, cmd, input, signal) {
|
|
40
|
+
const key = session;
|
|
41
|
+
if (sudoNeeded.get(key) === void 0) {
|
|
42
|
+
const probe = await session.exec('docker version --format "{{.Server.Version}}"', signal);
|
|
43
|
+
sudoNeeded.set(key, looksLikePermissionDenied(probe));
|
|
44
|
+
}
|
|
45
|
+
return session.execWithInput(sudoNeeded.get(key) ? sudoWrap(cmd) : cmd, input, signal);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export {
|
|
49
|
+
shellQuote,
|
|
50
|
+
sudoWrap,
|
|
51
|
+
resetPrivilegeCache,
|
|
52
|
+
execPrivileged,
|
|
53
|
+
streamPrivileged,
|
|
54
|
+
execPrivilegedWithInput
|
|
55
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/openclaw/logs.ts
|
|
4
|
+
var GATEWAY_LOGS_PROBE = "docker exec openclaw openclaw logs --limit 1 >/dev/null";
|
|
5
|
+
function gatewayLogsCommand(opts) {
|
|
6
|
+
return [
|
|
7
|
+
"docker exec openclaw openclaw logs",
|
|
8
|
+
`--limit ${opts.tail}`,
|
|
9
|
+
opts.follow ? "--follow" : "",
|
|
10
|
+
opts.json ? "--json" : ""
|
|
11
|
+
].filter(Boolean).join(" ");
|
|
12
|
+
}
|
|
13
|
+
function containerLogsCommand(opts) {
|
|
14
|
+
return [
|
|
15
|
+
"docker logs openclaw",
|
|
16
|
+
`-n ${opts.tail}`,
|
|
17
|
+
opts.follow ? "-f" : "",
|
|
18
|
+
opts.since ? `--since ${shellQuote(opts.since)}` : ""
|
|
19
|
+
].filter(Boolean).join(" ");
|
|
20
|
+
}
|
|
21
|
+
function chooseLogSource(opts) {
|
|
22
|
+
if (opts.since) {
|
|
23
|
+
return {
|
|
24
|
+
source: "container",
|
|
25
|
+
reason: "--since is a container-log filter; the gateway log command has no equivalent"
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
if (!opts.gatewayReachable) {
|
|
29
|
+
return {
|
|
30
|
+
source: "container",
|
|
31
|
+
reason: "the gateway is not answering, so its own logs cannot be read \u2014 showing container output"
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
return { source: "gateway", reason: "read from the gateway over RPC" };
|
|
35
|
+
}
|
|
36
|
+
function agentAuditCommand(opts) {
|
|
37
|
+
return [
|
|
38
|
+
"docker exec openclaw openclaw audit",
|
|
39
|
+
`--agent ${shellQuote(opts.agentId)}`,
|
|
40
|
+
"--kind agent_run",
|
|
41
|
+
"--json",
|
|
42
|
+
`--limit ${opts.limit}`,
|
|
43
|
+
opts.cursor ? `--cursor ${shellQuote(opts.cursor)}` : ""
|
|
44
|
+
].filter(Boolean).join(" ");
|
|
45
|
+
}
|
|
46
|
+
function shellQuote(value) {
|
|
47
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export {
|
|
51
|
+
GATEWAY_LOGS_PROBE,
|
|
52
|
+
gatewayLogsCommand,
|
|
53
|
+
containerLogsCommand,
|
|
54
|
+
chooseLogSource,
|
|
55
|
+
agentAuditCommand
|
|
56
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/providers/azure/cli-auth.ts
|
|
4
|
+
import path from "path";
|
|
5
|
+
import process from "process";
|
|
6
|
+
import { readFileSync } from "fs";
|
|
7
|
+
function azureConfigDir() {
|
|
8
|
+
const explicit = process.env["AZURE_CONFIG_DIR"];
|
|
9
|
+
if (explicit) return explicit;
|
|
10
|
+
const home = process.env["HOME"] ?? process.env["USERPROFILE"];
|
|
11
|
+
return home ? path.join(home, ".azure") : void 0;
|
|
12
|
+
}
|
|
13
|
+
function accountFromProfile(body) {
|
|
14
|
+
let parsed;
|
|
15
|
+
try {
|
|
16
|
+
parsed = JSON.parse(body.replace(/^\uFEFF/, ""));
|
|
17
|
+
} catch {
|
|
18
|
+
return void 0;
|
|
19
|
+
}
|
|
20
|
+
const subscriptions = parsed?.subscriptions;
|
|
21
|
+
if (!Array.isArray(subscriptions)) return void 0;
|
|
22
|
+
const entries = subscriptions;
|
|
23
|
+
const chosen = entries.find((s) => s["isDefault"] === true) ?? entries[0];
|
|
24
|
+
if (!chosen) return void 0;
|
|
25
|
+
const id = typeof chosen["id"] === "string" ? chosen["id"] : void 0;
|
|
26
|
+
if (!id) return void 0;
|
|
27
|
+
const user = chosen["user"];
|
|
28
|
+
return {
|
|
29
|
+
subscriptionId: id,
|
|
30
|
+
name: typeof chosen["name"] === "string" ? chosen["name"] : id,
|
|
31
|
+
...typeof user?.name === "string" ? { user: user.name } : {}
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function azureCliAccount() {
|
|
35
|
+
const dir = azureConfigDir();
|
|
36
|
+
if (!dir) return void 0;
|
|
37
|
+
try {
|
|
38
|
+
return accountFromProfile(readFileSync(path.join(dir, "azureProfile.json"), "utf-8"));
|
|
39
|
+
} catch {
|
|
40
|
+
return void 0;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function resolveSubscriptionId() {
|
|
44
|
+
return process.env["ARM_SUBSCRIPTION_ID"] ?? process.env["AZURE_SUBSCRIPTION_ID"] ?? azureCliAccount()?.subscriptionId;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export {
|
|
48
|
+
azureConfigDir,
|
|
49
|
+
accountFromProfile,
|
|
50
|
+
azureCliAccount,
|
|
51
|
+
resolveSubscriptionId
|
|
52
|
+
};
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
validatePlan
|
|
4
|
+
} from "./chunk-BSLIYB5Q.js";
|
|
5
|
+
import {
|
|
6
|
+
buildContext,
|
|
7
|
+
loadAdapterModule
|
|
8
|
+
} from "./chunk-5XT5IAJK.js";
|
|
9
|
+
import {
|
|
10
|
+
getConfig
|
|
11
|
+
} from "./chunk-CX5SL5HP.js";
|
|
12
|
+
import {
|
|
13
|
+
StateError,
|
|
14
|
+
UsageError
|
|
15
|
+
} from "./chunk-KGXPLI7W.js";
|
|
16
|
+
|
|
17
|
+
// src/plan/generate.ts
|
|
18
|
+
import { randomUUID } from "crypto";
|
|
19
|
+
var PREVIEW_LINE_RE = /^([+~-])\s+(\S+)\s+(\S+)?/;
|
|
20
|
+
function parseDiff(lines) {
|
|
21
|
+
const create = [];
|
|
22
|
+
const update = [];
|
|
23
|
+
const del = [];
|
|
24
|
+
const seen = /* @__PURE__ */ new Set();
|
|
25
|
+
for (const line of lines) {
|
|
26
|
+
const m = PREVIEW_LINE_RE.exec(line.trimStart());
|
|
27
|
+
if (!m) continue;
|
|
28
|
+
const [, op, resourceType, name] = m;
|
|
29
|
+
const urn = `urn:pulumi:::clawops::${resourceType}::${name ?? ""}`;
|
|
30
|
+
if (seen.has(`${op}${urn}`)) continue;
|
|
31
|
+
seen.add(`${op}${urn}`);
|
|
32
|
+
const ref = {
|
|
33
|
+
urn,
|
|
34
|
+
type: resourceType,
|
|
35
|
+
...name ? { name } : {}
|
|
36
|
+
};
|
|
37
|
+
if (op === "+") create.push(ref);
|
|
38
|
+
else if (op === "~") update.push({ resource: ref, before: null, after: null });
|
|
39
|
+
else if (op === "-") del.push(ref);
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
create,
|
|
43
|
+
update,
|
|
44
|
+
delete: del,
|
|
45
|
+
totalChanges: create.length + update.length + del.length
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
var INSTANCE_ALIASES = ["micro", "small", "medium", "large", "gpu"];
|
|
49
|
+
function isInstanceAlias(value) {
|
|
50
|
+
return INSTANCE_ALIASES.includes(value);
|
|
51
|
+
}
|
|
52
|
+
async function resolveInstanceType(intent) {
|
|
53
|
+
const requested = intent.instanceType ?? "small";
|
|
54
|
+
if (!isInstanceAlias(requested)) {
|
|
55
|
+
process.stderr.write(
|
|
56
|
+
`[clawops] note: "${requested}" is not a clawops size (${INSTANCE_ALIASES.join(", ")}), so it is passed to ${intent.provider} as written.
|
|
57
|
+
`
|
|
58
|
+
);
|
|
59
|
+
return requested;
|
|
60
|
+
}
|
|
61
|
+
const adapter = await loadAdapterModule(intent.provider);
|
|
62
|
+
return adapter.normalizeInstanceType(requested);
|
|
63
|
+
}
|
|
64
|
+
var parseDiffForTest = parseDiff;
|
|
65
|
+
async function generatePlan(intent, _opts) {
|
|
66
|
+
if (intent.provider === "local") {
|
|
67
|
+
throw new UsageError(
|
|
68
|
+
"plan/apply is not supported for the local provider. Use `clawops up` directly."
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
const { version } = await import("./package-5BCMEBTE.js");
|
|
72
|
+
const config = getConfig();
|
|
73
|
+
const instanceType = await resolveInstanceType(intent);
|
|
74
|
+
const { guardOpenclawVersion, defaultOpenclawVersion } = await import("./version-guard-XVYW5WFJ.js");
|
|
75
|
+
const openclawVersion = await guardOpenclawVersion(
|
|
76
|
+
intent.openclawVersion ?? await defaultOpenclawVersion()
|
|
77
|
+
);
|
|
78
|
+
const network = intent.network ?? {
|
|
79
|
+
allowedSshCidrs: [],
|
|
80
|
+
allowedGatewayCidrs: []
|
|
81
|
+
};
|
|
82
|
+
const { resolvePublicKey } = await import("./ssh-key-DUO6OKOI.js");
|
|
83
|
+
const publicKey = config ? resolvePublicKey(expandHome(config.ssh.keyPath)) : void 0;
|
|
84
|
+
if (!publicKey) {
|
|
85
|
+
process.stderr.write(
|
|
86
|
+
"[clawops] warning: no SSH public key could be resolved" + (config ? ` from ${config.ssh.keyPath}` : " \u2014 no config file") + ". `clawops apply` will refuse this plan: the instance would have no way to admit anyone. Run `clawops doctor` to see why the key is unusable.\n"
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
const stackName = intent.stackName;
|
|
90
|
+
const region = intent.region ?? config?.stacks[stackName]?.region;
|
|
91
|
+
const plan = {
|
|
92
|
+
apiVersion: "clawops.dev/v1",
|
|
93
|
+
kind: "DeployPlan",
|
|
94
|
+
metadata: {
|
|
95
|
+
name: stackName,
|
|
96
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
97
|
+
generator: "clawops",
|
|
98
|
+
generatorVersion: version
|
|
99
|
+
},
|
|
100
|
+
spec: {
|
|
101
|
+
provider: intent.provider,
|
|
102
|
+
region,
|
|
103
|
+
stackName,
|
|
104
|
+
instanceType,
|
|
105
|
+
openclaw: { version: openclawVersion },
|
|
106
|
+
network,
|
|
107
|
+
...publicKey ? { ssh: { publicKey } } : {},
|
|
108
|
+
...intent.tags ? { tags: intent.tags } : {}
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
let stack;
|
|
112
|
+
try {
|
|
113
|
+
const ctx = buildContext({ stack: stackName, provider: intent.provider });
|
|
114
|
+
stack = await ctx.getStack();
|
|
115
|
+
} catch (err) {
|
|
116
|
+
if (err instanceof UsageError) throw err;
|
|
117
|
+
throw new StateError(
|
|
118
|
+
`Cannot open the state backend for stack "${stackName}": ${messageOf(err)}
|
|
119
|
+
A plan is only as good as the state it was computed against, so this is not a plan clawops will write. Check the stateUrl in ~/.clawops/config.json, and that the bucket or container exists and your credentials can read it.`
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
try {
|
|
123
|
+
const { writeStackConfig } = await import("./stack-config-R4SPSUQZ.js");
|
|
124
|
+
await writeStackConfig(stack, plan);
|
|
125
|
+
const outputLines = [];
|
|
126
|
+
const preview = await stack.preview({
|
|
127
|
+
onOutput: (line) => outputLines.push(line)
|
|
128
|
+
});
|
|
129
|
+
const diff = parseDiff(outputLines);
|
|
130
|
+
if (diff.totalChanges === 0 && preview.changeSummary) {
|
|
131
|
+
const summary = preview.changeSummary;
|
|
132
|
+
diff.totalChanges = (summary["create"] ?? 0) + (summary["update"] ?? 0) + (summary["delete"] ?? 0);
|
|
133
|
+
}
|
|
134
|
+
plan.diff = diff;
|
|
135
|
+
const info = await stack.info();
|
|
136
|
+
if (info !== void 0) {
|
|
137
|
+
plan.metadata.stackVersion = info.version;
|
|
138
|
+
}
|
|
139
|
+
} catch (err) {
|
|
140
|
+
if (err instanceof UsageError) throw err;
|
|
141
|
+
process.stderr.write(
|
|
142
|
+
`[clawops] Warning: preview failed, diff section omitted: ${err instanceof Error ? err.message : String(err)}
|
|
143
|
+
`
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
const validation = validatePlan(plan);
|
|
147
|
+
if (!validation.ok) {
|
|
148
|
+
throw new Error(`Generated plan failed schema validation:
|
|
149
|
+
${validation.errors.join("\n")}`);
|
|
150
|
+
}
|
|
151
|
+
const { validatePlanConfig, validatePlanNetwork } = await import("./validate-YVI3BJ55.js");
|
|
152
|
+
const cfgCheck = await validatePlanConfig(plan);
|
|
153
|
+
for (const w of cfgCheck.warnings) process.stderr.write(`[clawops] warning: ${w}
|
|
154
|
+
`);
|
|
155
|
+
if (!cfgCheck.ok) {
|
|
156
|
+
throw new Error(
|
|
157
|
+
`The OpenClaw config in this plan would be rejected:
|
|
158
|
+
` + cfgCheck.errors.map((e) => ` - ${e}`).join("\n") + `
|
|
159
|
+
Fix spec.openclaw.config before applying \u2014 the gateway reads it verbatim.`
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
const netCheck = validatePlanNetwork(plan);
|
|
163
|
+
for (const w of netCheck.warnings) process.stderr.write(`[clawops] warning: ${w}
|
|
164
|
+
`);
|
|
165
|
+
if (!netCheck.ok) {
|
|
166
|
+
throw new Error(
|
|
167
|
+
`This plan's network settings contradict each other:
|
|
168
|
+
` + netCheck.errors.map((e) => ` - ${e}`).join("\n")
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
return plan;
|
|
172
|
+
}
|
|
173
|
+
function planId() {
|
|
174
|
+
return randomUUID().slice(0, 8);
|
|
175
|
+
}
|
|
176
|
+
function expandHome(p) {
|
|
177
|
+
return p.replace(/^~/, process.env["HOME"] ?? "~");
|
|
178
|
+
}
|
|
179
|
+
function messageOf(err) {
|
|
180
|
+
const raw = err instanceof Error ? err.message : String(err);
|
|
181
|
+
const lines = raw.split("\n").map((l) => l.trim()).filter((l) => l !== "");
|
|
182
|
+
const explained = lines.find((l) => /(^|\s)error:/i.test(l));
|
|
183
|
+
if (explained) return explained.replace(/^.*?error:\s*/i, "");
|
|
184
|
+
return lines[0] ?? raw;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export {
|
|
188
|
+
INSTANCE_ALIASES,
|
|
189
|
+
isInstanceAlias,
|
|
190
|
+
parseDiffForTest,
|
|
191
|
+
generatePlan,
|
|
192
|
+
planId
|
|
193
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/pulumi/passphrase.ts
|
|
4
|
+
import path from "path";
|
|
5
|
+
import process from "process";
|
|
6
|
+
import { randomBytes } from "crypto";
|
|
7
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync } from "fs";
|
|
8
|
+
var PASSPHRASE_FILE = "pulumi-passphrase";
|
|
9
|
+
function passphrasePath(configDir) {
|
|
10
|
+
return path.join(configDir, "secrets", PASSPHRASE_FILE);
|
|
11
|
+
}
|
|
12
|
+
function passphraseInEnvironment() {
|
|
13
|
+
return Boolean(
|
|
14
|
+
process.env["PULUMI_CONFIG_PASSPHRASE"] ?? process.env["PULUMI_CONFIG_PASSPHRASE_FILE"]
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
function passphraseStatus(configDir) {
|
|
18
|
+
if (passphraseInEnvironment()) return "environment";
|
|
19
|
+
return existsSync(passphrasePath(configDir)) ? "stored" : "absent";
|
|
20
|
+
}
|
|
21
|
+
function ensurePassphrase(configDir) {
|
|
22
|
+
if (passphraseInEnvironment()) return void 0;
|
|
23
|
+
const file = passphrasePath(configDir);
|
|
24
|
+
if (existsSync(file)) {
|
|
25
|
+
const stored = readFileSync(file, "utf-8").trim();
|
|
26
|
+
if (stored !== "") return stored;
|
|
27
|
+
}
|
|
28
|
+
const dir = path.dirname(file);
|
|
29
|
+
mkdirSync(dir, { recursive: true });
|
|
30
|
+
try {
|
|
31
|
+
chmodSync(dir, 448);
|
|
32
|
+
} catch {
|
|
33
|
+
}
|
|
34
|
+
const generated = randomBytes(32).toString("base64url");
|
|
35
|
+
writeFileSync(file, generated + "\n", { encoding: "utf-8", mode: 384 });
|
|
36
|
+
return generated;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export {
|
|
40
|
+
PASSPHRASE_FILE,
|
|
41
|
+
passphrasePath,
|
|
42
|
+
passphraseInEnvironment,
|
|
43
|
+
passphraseStatus,
|
|
44
|
+
ensurePassphrase
|
|
45
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
err,
|
|
4
|
+
ok
|
|
5
|
+
} from "./chunk-EI7CQQ7D.js";
|
|
6
|
+
|
|
7
|
+
// src/openclaw/run-flags.ts
|
|
8
|
+
var CONFIG_MOUNT_PATH = "/app/config.json";
|
|
9
|
+
var GATEWAY_PORT = 18789;
|
|
10
|
+
var CONFIG_PATH_ENV = `-e OPENCLAW_CONFIG_PATH=${CONFIG_MOUNT_PATH}`;
|
|
11
|
+
var ADD_HOST_FLAG = "--add-host=host.docker.internal:host-gateway";
|
|
12
|
+
var PORT_PIN = `--port ${GATEWAY_PORT}`;
|
|
13
|
+
var COMMON_RUN_FLAGS = `${CONFIG_PATH_ENV} ${ADD_HOST_FLAG}`;
|
|
14
|
+
var IMAGE_INSPECT_CMD = `docker inspect openclaw --format '{{.Config.Image}}'`;
|
|
15
|
+
function imageForRestart(inspectStdout) {
|
|
16
|
+
const image = inspectStdout.trim();
|
|
17
|
+
if (!image || image.includes("Error") || !image.includes(":")) {
|
|
18
|
+
return err(
|
|
19
|
+
"No running OpenClaw container found, so there is no image version to reuse. Deploy one with `clawops up --openclaw-version <version>`. clawops does not fall back to `latest` or `stable` here: both now point at OpenClaw 2.0, which this release line does not support."
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
return ok(image);
|
|
23
|
+
}
|
|
24
|
+
function versionOf(image) {
|
|
25
|
+
return image.split(":")[1] ?? "unknown";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export {
|
|
29
|
+
GATEWAY_PORT,
|
|
30
|
+
IMAGE_INSPECT_CMD,
|
|
31
|
+
imageForRestart,
|
|
32
|
+
versionOf
|
|
33
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/pulumi/cli.ts
|
|
4
|
+
import path from "path";
|
|
5
|
+
import process from "process";
|
|
6
|
+
import { PulumiCommand } from "@pulumi/pulumi/automation";
|
|
7
|
+
function pulumiCliRoot(configDir) {
|
|
8
|
+
return path.join(configDir, ".pulumi-cli");
|
|
9
|
+
}
|
|
10
|
+
function versionOf(cmd) {
|
|
11
|
+
return cmd.version ? `v${cmd.version.toString()}` : "unknown version";
|
|
12
|
+
}
|
|
13
|
+
async function pulumiCliStatus(configDir) {
|
|
14
|
+
const root = pulumiCliRoot(configDir);
|
|
15
|
+
try {
|
|
16
|
+
return { kind: "managed", version: versionOf(await PulumiCommand.get({ root })), root };
|
|
17
|
+
} catch {
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
return { kind: "path", version: versionOf(await PulumiCommand.get()) };
|
|
21
|
+
} catch {
|
|
22
|
+
return { kind: "missing" };
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
async function ensurePulumiCli(opts) {
|
|
26
|
+
const root = pulumiCliRoot(opts.configDir);
|
|
27
|
+
const status = await pulumiCliStatus(opts.configDir);
|
|
28
|
+
if (status.kind === "managed") return await PulumiCommand.get({ root });
|
|
29
|
+
if (status.kind === "path") return await PulumiCommand.get();
|
|
30
|
+
const announce = opts.onInstall ?? defaultAnnounce;
|
|
31
|
+
announce({ root });
|
|
32
|
+
try {
|
|
33
|
+
return await PulumiCommand.install({ root });
|
|
34
|
+
} catch (err) {
|
|
35
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
36
|
+
throw new Error(
|
|
37
|
+
`could not install the Pulumi CLI into ${root}: ${reason}
|
|
38
|
+
Install it yourself and clawops will use it: https://www.pulumi.com/docs/install/`
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function defaultAnnounce({ root }) {
|
|
43
|
+
process.stderr.write(`Pulumi CLI not found \u2014 installing it into ${root} (one time).
|
|
44
|
+
`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export {
|
|
48
|
+
pulumiCliRoot,
|
|
49
|
+
pulumiCliStatus,
|
|
50
|
+
ensurePulumiCli
|
|
51
|
+
};
|