@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,27 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
applyPlan
|
|
4
|
+
} from "./chunk-LBE6COC4.js";
|
|
5
|
+
import "./chunk-BOPSG2LI.js";
|
|
6
|
+
import "./chunk-6ZFIFDBJ.js";
|
|
7
|
+
import "./chunk-2I7KCMXZ.js";
|
|
8
|
+
import "./chunk-HSPHXG6W.js";
|
|
9
|
+
import "./chunk-AEEMRFZV.js";
|
|
10
|
+
import "./chunk-BSLIYB5Q.js";
|
|
11
|
+
import "./chunk-5XT5IAJK.js";
|
|
12
|
+
import "./chunk-NUDIHZEH.js";
|
|
13
|
+
import "./chunk-JW5A7E3X.js";
|
|
14
|
+
import "./chunk-LEVIW2PV.js";
|
|
15
|
+
import "./chunk-GIEVAGEK.js";
|
|
16
|
+
import "./chunk-VOEJYRIK.js";
|
|
17
|
+
import "./chunk-EI7CQQ7D.js";
|
|
18
|
+
import "./chunk-FEK7VJAY.js";
|
|
19
|
+
import "./chunk-RHOWYIWT.js";
|
|
20
|
+
import "./chunk-N4WZLWJ4.js";
|
|
21
|
+
import "./chunk-A2I76FTA.js";
|
|
22
|
+
import "./chunk-CX5SL5HP.js";
|
|
23
|
+
import "./chunk-KGXPLI7W.js";
|
|
24
|
+
import "./chunk-VUFMZEIM.js";
|
|
25
|
+
export {
|
|
26
|
+
applyPlan
|
|
27
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
ensurePulumiCli
|
|
4
|
+
} from "./chunk-W3OLNNJW.js";
|
|
5
|
+
import {
|
|
6
|
+
ensurePassphrase
|
|
7
|
+
} from "./chunk-V25QRZRB.js";
|
|
8
|
+
import {
|
|
9
|
+
getConfigDir
|
|
10
|
+
} from "./chunk-CX5SL5HP.js";
|
|
11
|
+
import "./chunk-KGXPLI7W.js";
|
|
12
|
+
|
|
13
|
+
// src/pulumi/automation.ts
|
|
14
|
+
import path from "path";
|
|
15
|
+
import { LocalWorkspace } from "@pulumi/pulumi/automation";
|
|
16
|
+
async function getOrCreateStack(opts) {
|
|
17
|
+
const configDir = opts.configDir ?? getConfigDir();
|
|
18
|
+
const pulumiCommand = await ensurePulumiCli({ configDir, onInstall: opts.onInstall });
|
|
19
|
+
const passphrase = ensurePassphrase(configDir);
|
|
20
|
+
return await LocalWorkspace.createOrSelectStack(
|
|
21
|
+
{
|
|
22
|
+
stackName: opts.stack,
|
|
23
|
+
projectName: "clawops",
|
|
24
|
+
program: opts.program
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
pulumiCommand,
|
|
28
|
+
pulumiHome: path.join(configDir, ".pulumi"),
|
|
29
|
+
envVars: {
|
|
30
|
+
PULUMI_BACKEND_URL: opts.stateUrl,
|
|
31
|
+
PULUMI_SKIP_UPDATE_CHECK: "1",
|
|
32
|
+
// A self-managed backend has no key service, so a new stack cannot be created without
|
|
33
|
+
// a secrets manager, and the only one available is a passphrase. Undefined when the
|
|
34
|
+
// operator supplies their own — overriding it would orphan their existing stacks.
|
|
35
|
+
// See ./passphrase.ts and ADR 0011.
|
|
36
|
+
...passphrase ? { PULUMI_CONFIG_PASSPHRASE: passphrase } : {}
|
|
37
|
+
// R6: cloud credentials inherited from process.env, never set here
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
export {
|
|
43
|
+
getOrCreateStack
|
|
44
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/openclaw/bedrock.ts
|
|
4
|
+
function resolveInferenceProfile(foundationModelId, profiles, region) {
|
|
5
|
+
const candidates = profiles.map((p) => p.inferenceProfileId).filter((id) => id.endsWith(`.${foundationModelId}`));
|
|
6
|
+
if (candidates.length === 0) {
|
|
7
|
+
return {
|
|
8
|
+
ok: false,
|
|
9
|
+
error: `No Bedrock inference profile for "${foundationModelId}" in ${region}. Bedrock refuses bare foundation-model ids for on-demand inference, so this model cannot be used there. Check the model is enabled in this region, or choose another.`
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
const geo = geographyFor(region);
|
|
13
|
+
const regional = candidates.find((id) => id.startsWith(`${geo}.`));
|
|
14
|
+
if (regional) return { ok: true, profileId: regional, why: `${geo} profile for ${region}` };
|
|
15
|
+
const global = candidates.find((id) => id.startsWith("global."));
|
|
16
|
+
if (global) {
|
|
17
|
+
return {
|
|
18
|
+
ok: true,
|
|
19
|
+
profileId: global,
|
|
20
|
+
why: `no ${geo} profile for ${region}; using the global profile`
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
ok: false,
|
|
25
|
+
error: `Bedrock has inference profiles for "${foundationModelId}" (${candidates.join(", ")}) but none for ${region} or global. Using one would route inference to another geography; choose a region where the model is offered.`
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function geographyFor(region) {
|
|
29
|
+
const prefix = region.split("-")[0] ?? region;
|
|
30
|
+
if (prefix === "ap") return "apac";
|
|
31
|
+
return prefix;
|
|
32
|
+
}
|
|
33
|
+
async function fetchInferenceProfiles(region, signal) {
|
|
34
|
+
try {
|
|
35
|
+
const { BedrockClient, ListInferenceProfilesCommand } = await import("@aws-sdk/client-bedrock");
|
|
36
|
+
const client = new BedrockClient({ region });
|
|
37
|
+
const profiles = [];
|
|
38
|
+
let nextToken;
|
|
39
|
+
do {
|
|
40
|
+
const page = await client.send(
|
|
41
|
+
new ListInferenceProfilesCommand({ maxResults: 100, nextToken }),
|
|
42
|
+
{ abortSignal: signal }
|
|
43
|
+
);
|
|
44
|
+
for (const p of page.inferenceProfileSummaries ?? []) {
|
|
45
|
+
if (p.inferenceProfileId) profiles.push({ inferenceProfileId: p.inferenceProfileId });
|
|
46
|
+
}
|
|
47
|
+
nextToken = page.nextToken;
|
|
48
|
+
} while (nextToken);
|
|
49
|
+
return { ok: true, profiles };
|
|
50
|
+
} catch (err) {
|
|
51
|
+
return {
|
|
52
|
+
ok: false,
|
|
53
|
+
error: `Could not list Bedrock inference profiles in ${region}: ${err instanceof Error ? err.message : String(err)}. The deploying identity needs bedrock:ListInferenceProfiles.`
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
export {
|
|
58
|
+
fetchInferenceProfiles,
|
|
59
|
+
geographyFor,
|
|
60
|
+
resolveInferenceProfile
|
|
61
|
+
};
|
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
acquireSession
|
|
4
|
-
} from "./chunk-
|
|
5
|
-
import "./chunk-
|
|
4
|
+
} from "./chunk-2Z7J3ERQ.js";
|
|
5
|
+
import "./chunk-E4NJSJ54.js";
|
|
6
|
+
import "./chunk-MC5ATCXS.js";
|
|
7
|
+
import {
|
|
8
|
+
gatewayRunArgs,
|
|
9
|
+
gatewayRunCommand
|
|
10
|
+
} from "./chunk-GIEVAGEK.js";
|
|
11
|
+
import {
|
|
12
|
+
GATEWAY_PORT
|
|
13
|
+
} from "./chunk-VOEJYRIK.js";
|
|
14
|
+
import "./chunk-EI7CQQ7D.js";
|
|
6
15
|
import {
|
|
7
16
|
writeLocalState
|
|
8
17
|
} from "./chunk-A2I76FTA.js";
|
|
@@ -15,7 +24,6 @@ import {
|
|
|
15
24
|
import { readFileSync, existsSync } from "fs";
|
|
16
25
|
import path from "path";
|
|
17
26
|
import { fileURLToPath } from "url";
|
|
18
|
-
var GATEWAY_PORT = 18789;
|
|
19
27
|
var HEALTH_POLL_INTERVAL_MS = 3e3;
|
|
20
28
|
var HEALTH_TIMEOUT_MS = 12e4;
|
|
21
29
|
function loadTemplate() {
|
|
@@ -31,11 +39,18 @@ function loadTemplate() {
|
|
|
31
39
|
`Cannot find bootstrap.sh.tmpl (looked in: ${candidates.join(", ")}). If this is an installed package, the template may be missing from the build output.`
|
|
32
40
|
);
|
|
33
41
|
}
|
|
34
|
-
function renderScript(openclawVersion) {
|
|
35
|
-
|
|
42
|
+
function renderScript(openclawVersion, gatewayPort = GATEWAY_PORT) {
|
|
43
|
+
const shared = {
|
|
44
|
+
image: "ghcr.io/openclaw/openclaw:${OPENCLAW_VERSION}",
|
|
45
|
+
stateDir: '"${OPENCLAW_STATE_DIR}"',
|
|
46
|
+
envFilePath: '"${OPENCLAW_ENV_FILE}"',
|
|
47
|
+
port: "${OPENCLAW_PORT}"
|
|
48
|
+
};
|
|
49
|
+
return loadTemplate().replace(/\{\{GATEWAY_RUN_SYSTEMD\}\}/g, gatewayRunArgs({ ...shared, supervisor: "systemd" })).replace(/\{\{GATEWAY_RUN_DARWIN\}\}/g, gatewayRunCommand(shared)).replace(/\{\{OPENCLAW_VERSION\}\}/g, openclawVersion).replace(/\{\{OPENCLAW_PORT\}\}/g, String(gatewayPort));
|
|
36
50
|
}
|
|
37
51
|
async function localBootstrap(opts) {
|
|
38
|
-
const
|
|
52
|
+
const gatewayPort = opts.gatewayPort ?? GATEWAY_PORT;
|
|
53
|
+
const script = renderScript(opts.openclawVersion, gatewayPort);
|
|
39
54
|
const b64 = Buffer.from(script, "utf-8").toString("base64");
|
|
40
55
|
const command = opts.sudoPassword ? `printf '%s
|
|
41
56
|
' ${shellQuote(opts.sudoPassword)} | sudo -S -p '' bash -c "echo '${b64}' | base64 -d | bash"` : `echo '${b64}' | base64 -d | sudo bash`;
|
|
@@ -59,13 +74,14 @@ ${result.stderr || result.stdout}`
|
|
|
59
74
|
);
|
|
60
75
|
}
|
|
61
76
|
}
|
|
77
|
+
if (!opts.noWait) await waitForGateway(session, gatewayPort, opts.signal);
|
|
62
78
|
} finally {
|
|
63
79
|
release();
|
|
64
80
|
}
|
|
65
81
|
const state = {
|
|
66
82
|
instanceId: `local:${opts.host}`,
|
|
67
83
|
publicIp: opts.host,
|
|
68
|
-
gatewayUrl: `http://${opts.host}:${
|
|
84
|
+
gatewayUrl: `http://${opts.host}:${gatewayPort}`,
|
|
69
85
|
sshHost: opts.host,
|
|
70
86
|
sshPort: opts.port,
|
|
71
87
|
sshUser: opts.user,
|
|
@@ -75,27 +91,22 @@ ${result.stderr || result.stdout}`
|
|
|
75
91
|
knownHostsPath: opts.knownHostsPath
|
|
76
92
|
};
|
|
77
93
|
writeLocalState(opts.stackName, state);
|
|
78
|
-
if (!opts.noWait) {
|
|
79
|
-
await waitForGateway(opts.host, GATEWAY_PORT, opts.signal);
|
|
80
|
-
}
|
|
81
94
|
return state;
|
|
82
95
|
}
|
|
83
|
-
async function waitForGateway(
|
|
84
|
-
const
|
|
96
|
+
async function waitForGateway(session, gatewayPort, signal) {
|
|
97
|
+
const { probeCommand, interpretProbe } = await import("./health-5CCOXYGG.js");
|
|
98
|
+
const probe = probeCommand("started", gatewayPort);
|
|
85
99
|
const deadline = Date.now() + HEALTH_TIMEOUT_MS;
|
|
86
100
|
while (Date.now() < deadline) {
|
|
87
101
|
if (signal?.aborted) {
|
|
88
102
|
throw new ProviderError("Bootstrap aborted while waiting for gateway");
|
|
89
103
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
if (res.ok) return;
|
|
93
|
-
} catch {
|
|
94
|
-
}
|
|
104
|
+
const result = await session.exec(probe, signal);
|
|
105
|
+
if (interpretProbe("started", result.stdout).ok) return;
|
|
95
106
|
await sleep(HEALTH_POLL_INTERVAL_MS);
|
|
96
107
|
}
|
|
97
108
|
throw new ProviderError(
|
|
98
|
-
`Gateway
|
|
109
|
+
`Gateway did not finish starting within ${HEALTH_TIMEOUT_MS / 1e3}s. Probed /startupz on the host's loopback; check \`clawops logs\`.`
|
|
99
110
|
);
|
|
100
111
|
}
|
|
101
112
|
function sleep(ms) {
|
|
@@ -132,5 +143,6 @@ ${stderr}`);
|
|
|
132
143
|
}
|
|
133
144
|
}
|
|
134
145
|
export {
|
|
135
|
-
localBootstrap
|
|
146
|
+
localBootstrap,
|
|
147
|
+
renderScript
|
|
136
148
|
};
|
package/dist/bootstrap.sh.tmpl
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
set -euo pipefail
|
|
6
6
|
|
|
7
7
|
OPENCLAW_VERSION="{{OPENCLAW_VERSION}}"
|
|
8
|
-
OPENCLAW_PORT=
|
|
8
|
+
OPENCLAW_PORT={{OPENCLAW_PORT}}
|
|
9
9
|
|
|
10
10
|
# ── OS detection ──────────────────────────────────────────────────────────────
|
|
11
11
|
OS="$(uname -s)"
|
|
@@ -25,17 +25,20 @@ case "$OS" in
|
|
|
25
25
|
exit 1
|
|
26
26
|
fi
|
|
27
27
|
OPENCLAW_USER=clawops
|
|
28
|
-
|
|
28
|
+
OPENCLAW_STATE_DIR=/var/lib/clawops/openclaw
|
|
29
|
+
OPENCLAW_CONFIG="${OPENCLAW_STATE_DIR}/openclaw.json"
|
|
30
|
+
OPENCLAW_LEGACY_CONFIG=/home/${OPENCLAW_USER}/openclaw.json
|
|
29
31
|
;;
|
|
30
32
|
Darwin)
|
|
31
33
|
PKG_MANAGER=darwin
|
|
32
34
|
# When invoked via sudo the script runs as root but SUDO_USER is the real caller.
|
|
33
35
|
# Fall back to id -un for passwordless (non-sudo) invocations.
|
|
34
36
|
OPENCLAW_USER="${SUDO_USER:-$(id -un)}"
|
|
35
|
-
|
|
36
|
-
|
|
37
|
+
OPENCLAW_STATE_DIR="${HOME}/.clawops/openclaw"
|
|
38
|
+
OPENCLAW_CONFIG="${OPENCLAW_STATE_DIR}/openclaw.json"
|
|
39
|
+
OPENCLAW_LEGACY_CONFIG="${HOME}/.config/openclaw/config.json"
|
|
37
40
|
# Fix ownership so subsequent non-sudo SSH writes (atomicWriteConfig) can overwrite.
|
|
38
|
-
chown "${OPENCLAW_USER}" "${
|
|
41
|
+
chown "${OPENCLAW_USER}" "${OPENCLAW_STATE_DIR}" 2>/dev/null || true
|
|
39
42
|
# sudo resets PATH; add common Docker install locations so `command -v docker` works.
|
|
40
43
|
# Covers: Docker Desktop (Intel + AS), Homebrew (AS), Colima.
|
|
41
44
|
export PATH="/usr/local/bin:/opt/homebrew/bin:/Applications/Docker.app/Contents/Resources/bin:$PATH"
|
|
@@ -140,17 +143,51 @@ if [ ! -s "${OPENCLAW_ENV_FILE}" ]; then
|
|
|
140
143
|
chown "${OPENCLAW_USER}" "${OPENCLAW_ENV_FILE}" 2>/dev/null || true
|
|
141
144
|
fi
|
|
142
145
|
|
|
146
|
+
# ── State directory ───────────────────────────────────────────────────────────
|
|
147
|
+
# One directory holds config, the SQLite state and any plugins installed into it, so a
|
|
148
|
+
# container replacement no longer destroys sessions and credentials.
|
|
149
|
+
mkdir -p "${OPENCLAW_STATE_DIR}"
|
|
150
|
+
|
|
151
|
+
# Migrate a pre-2.0 deployment. The config used to be a bare file elsewhere; without this
|
|
152
|
+
# an in-place upgrade comes up with no configuration at all. Guarded on the target being
|
|
153
|
+
# absent, so re-running the bootstrap never clobbers a later edit.
|
|
154
|
+
if [ -f "${OPENCLAW_LEGACY_CONFIG}" ] && [ ! -f "${OPENCLAW_CONFIG}" ]; then
|
|
155
|
+
cp -p "${OPENCLAW_LEGACY_CONFIG}" "${OPENCLAW_CONFIG}"
|
|
156
|
+
mv "${OPENCLAW_LEGACY_CONFIG}" "${OPENCLAW_LEGACY_CONFIG}.migrated"
|
|
157
|
+
echo "clawops: migrated config from ${OPENCLAW_LEGACY_CONFIG}"
|
|
158
|
+
OPENCLAW_MIGRATED=1
|
|
159
|
+
fi
|
|
160
|
+
|
|
143
161
|
# ── Write default openclaw.json if absent ─────────────────────────────────────
|
|
144
162
|
# clawops up --config will overwrite this with the plan config overlay.
|
|
145
163
|
if [ ! -f "${OPENCLAW_CONFIG}" ]; then
|
|
146
|
-
|
|
147
|
-
|
|
164
|
+
# Unquoted heredoc, so ${OPENCLAW_PORT} expands. gateway.port and the -p mapping have to
|
|
165
|
+
# be the same number: a container that publishes one port while the gateway listens on
|
|
166
|
+
# another starts, looks healthy to Docker, and answers nothing.
|
|
167
|
+
cat > "${OPENCLAW_CONFIG}" <<OPENCLAWJSON
|
|
168
|
+
{"meta":{"lastTouchedVersion":"2026.9"},"gateway":{"mode":"local","port":${OPENCLAW_PORT},"auth":{"mode":"token"}},"models":{},"channels":{}}
|
|
148
169
|
OPENCLAWJSON
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
170
|
+
fi
|
|
171
|
+
|
|
172
|
+
# Ownership last, so it covers the config and anything migrated above.
|
|
173
|
+
if [ "$OS" = "Linux" ]; then
|
|
174
|
+
# NUMERIC, never clawops:clawops. useradd gets uid 1001 on Ubuntu 24.04 because the
|
|
175
|
+
# ubuntu user already holds 1000, and the container runs as 1000 — a 1001-owned state
|
|
176
|
+
# dir makes the gateway exit 1 with EACCES on its own SQLite WAL.
|
|
177
|
+
chown -R 1000:1000 "${OPENCLAW_STATE_DIR}"
|
|
178
|
+
else
|
|
179
|
+
chown -R "${OPENCLAW_USER}" "${OPENCLAW_STATE_DIR}" 2>/dev/null || true
|
|
180
|
+
fi
|
|
181
|
+
|
|
182
|
+
# A 1.x config has no gateway.mode, and clawops no longer passes --allow-unconfigured, so a
|
|
183
|
+
# migrated deployment would exit 78. Normalised with OpenClaw's own tooling — schema-aware,
|
|
184
|
+
# atomic, and it applies OpenClaw's internal config migrations too. After the chown, so the
|
|
185
|
+
# container can write.
|
|
186
|
+
if [ "${OPENCLAW_MIGRATED:-0}" = "1" ]; then
|
|
187
|
+
docker run --rm -v "${OPENCLAW_STATE_DIR}":/home/node/.openclaw \
|
|
188
|
+
"ghcr.io/openclaw/openclaw:${OPENCLAW_VERSION}" \
|
|
189
|
+
openclaw config set gateway.mode local || \
|
|
190
|
+
echo "clawops: WARNING could not set gateway.mode; gateway may refuse to start" >&2
|
|
154
191
|
fi
|
|
155
192
|
|
|
156
193
|
# ── Start service ─────────────────────────────────────────────────────────────
|
|
@@ -168,13 +205,7 @@ Restart=always
|
|
|
168
205
|
RestartSec=5
|
|
169
206
|
ExecStartPre=-/usr/bin/docker stop openclaw
|
|
170
207
|
ExecStartPre=-/usr/bin/docker rm openclaw
|
|
171
|
-
ExecStart=/usr/bin/
|
|
172
|
-
-p ${OPENCLAW_PORT}:${OPENCLAW_PORT} \
|
|
173
|
-
-e OPENCLAW_CONFIG_PATH=/app/config.json --add-host=host.docker.internal:host-gateway \
|
|
174
|
-
--env-file ${OPENCLAW_ENV_FILE} \
|
|
175
|
-
-v ${OPENCLAW_CONFIG}:/app/config.json:ro \
|
|
176
|
-
ghcr.io/openclaw/openclaw:${OPENCLAW_VERSION} \
|
|
177
|
-
node openclaw.mjs gateway run --allow-unconfigured --port ${OPENCLAW_PORT}
|
|
208
|
+
ExecStart=/usr/bin/{{GATEWAY_RUN_SYSTEMD}}
|
|
178
209
|
ExecStop=/usr/bin/docker stop openclaw
|
|
179
210
|
|
|
180
211
|
[Install]
|
|
@@ -187,18 +218,7 @@ UNIT
|
|
|
187
218
|
elif [ "$OS" = "Darwin" ]; then
|
|
188
219
|
# macOS: no systemd; run as foreground-detached container.
|
|
189
220
|
# LaunchAgent support is a future enhancement.
|
|
190
|
-
|
|
191
|
-
docker rm openclaw 2>/dev/null || true
|
|
192
|
-
docker run -d \
|
|
193
|
-
--name openclaw \
|
|
194
|
-
--restart unless-stopped \
|
|
195
|
-
-p "${OPENCLAW_PORT}:${OPENCLAW_PORT}" \
|
|
196
|
-
-e OPENCLAW_CONFIG_PATH=/app/config.json --add-host=host.docker.internal:host-gateway \
|
|
197
|
-
--env-file "${OPENCLAW_ENV_FILE}" \
|
|
198
|
-
--env-file ${OPENCLAW_ENV_FILE} \
|
|
199
|
-
-v "${OPENCLAW_CONFIG}":/app/config.json:ro \
|
|
200
|
-
"ghcr.io/openclaw/openclaw:${OPENCLAW_VERSION}" \
|
|
201
|
-
node openclaw.mjs gateway run --allow-unconfigured --port ${OPENCLAW_PORT}
|
|
221
|
+
{{GATEWAY_RUN_DARWIN}}
|
|
202
222
|
fi
|
|
203
223
|
|
|
204
224
|
echo "clawops: bootstrap complete"
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/openclaw/channels.ts
|
|
4
|
+
function requiredChannelPlugins(cfg, catalog) {
|
|
5
|
+
const configured = configuredChannelKeys(cfg);
|
|
6
|
+
const out = [];
|
|
7
|
+
for (const entry of catalog.integrations) {
|
|
8
|
+
const plugin = entry.plugin;
|
|
9
|
+
if (!plugin || plugin.source === "bundled" || !plugin.package) continue;
|
|
10
|
+
if (configured.has(entry.channelKey)) {
|
|
11
|
+
out.push({
|
|
12
|
+
channelKey: entry.channelKey,
|
|
13
|
+
package: plugin.package,
|
|
14
|
+
version: plugin.version ?? ""
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return out;
|
|
19
|
+
}
|
|
20
|
+
function channelInstallCommand(plugin, image, stateDir) {
|
|
21
|
+
const spec = plugin.version ? `${plugin.package}@${plugin.version}` : plugin.package;
|
|
22
|
+
return `docker run --rm -v ${stateDir}:/home/node/.openclaw ${image} openclaw plugins install '${spec}' --accept-capabilities`;
|
|
23
|
+
}
|
|
24
|
+
var CHANNELS_LIST_CMD = "docker exec openclaw openclaw channels list --all --json";
|
|
25
|
+
function missingChannels(cfg, channelsListJson, catalog) {
|
|
26
|
+
let installed;
|
|
27
|
+
try {
|
|
28
|
+
const parsed = JSON.parse(channelsListJson);
|
|
29
|
+
const chat = parsed.chat ?? {};
|
|
30
|
+
installed = new Set(
|
|
31
|
+
Object.entries(chat).filter(([, v]) => v.installed === true).map(([k]) => k)
|
|
32
|
+
);
|
|
33
|
+
} catch {
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
const bundled = new Set(
|
|
37
|
+
catalog.integrations.filter((i) => i.plugin?.source === "bundled").map((i) => i.channelKey)
|
|
38
|
+
);
|
|
39
|
+
return [...configuredChannelKeys(cfg)].filter((k) => !installed.has(k) && !bundled.has(k));
|
|
40
|
+
}
|
|
41
|
+
function configuredChannelKeys(cfg) {
|
|
42
|
+
if (cfg === null || typeof cfg !== "object") return /* @__PURE__ */ new Set();
|
|
43
|
+
const channels = cfg["channels"];
|
|
44
|
+
if (channels === null || typeof channels !== "object") return /* @__PURE__ */ new Set();
|
|
45
|
+
const notAChannel = /* @__PURE__ */ new Set(["defaults", "modelByChannel"]);
|
|
46
|
+
return new Set(Object.keys(channels).filter((k) => !notAChannel.has(k)));
|
|
47
|
+
}
|
|
48
|
+
export {
|
|
49
|
+
CHANNELS_LIST_CMD,
|
|
50
|
+
channelInstallCommand,
|
|
51
|
+
missingChannels,
|
|
52
|
+
requiredChannelPlugins
|
|
53
|
+
};
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
execPrivileged
|
|
4
|
+
} from "./chunk-NUDIHZEH.js";
|
|
5
|
+
import {
|
|
6
|
+
CONTAINER_UID,
|
|
7
|
+
PUBLISH_INSPECT_CMD,
|
|
8
|
+
configPathForOS,
|
|
9
|
+
gatewayRunCommand,
|
|
10
|
+
publishForRestart,
|
|
11
|
+
stateDirForOS
|
|
12
|
+
} from "./chunk-GIEVAGEK.js";
|
|
13
|
+
import {
|
|
14
|
+
GATEWAY_PORT,
|
|
15
|
+
IMAGE_INSPECT_CMD,
|
|
16
|
+
imageForRestart
|
|
17
|
+
} from "./chunk-VOEJYRIK.js";
|
|
18
|
+
|
|
19
|
+
// src/plan/remote-config.ts
|
|
20
|
+
var OPENCLAW_CONFIG_LINUX = configPathForOS("Linux");
|
|
21
|
+
var OPENCLAW_CONFIG_MACOS = configPathForOS("Darwin");
|
|
22
|
+
var OPENCLAW_CONFIG = OPENCLAW_CONFIG_LINUX;
|
|
23
|
+
var OPENCLAW_TMP = "/tmp/clawops-config.json.tmp";
|
|
24
|
+
async function detectOS(session, signal) {
|
|
25
|
+
const result = await session.exec("uname -s", signal);
|
|
26
|
+
return result.stdout.trim() === "Darwin" ? "Darwin" : "Linux";
|
|
27
|
+
}
|
|
28
|
+
async function readRemoteConfig(session, signal) {
|
|
29
|
+
const os = await detectOS(session, signal);
|
|
30
|
+
const configPath = configPathForOS(os);
|
|
31
|
+
const result = os === "Darwin" ? await session.exec(`cat ${configPath}`, signal) : await execPrivileged(session, `cat ${configPath}`, signal);
|
|
32
|
+
if (result.code !== 0) {
|
|
33
|
+
throw new Error(`Cannot read ${configPath}: ${result.stderr}`);
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
return JSON.parse(result.stdout);
|
|
37
|
+
} catch {
|
|
38
|
+
throw new Error(`Cannot parse ${configPath}: invalid JSON`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
async function atomicWriteConfig(session, cfg, signal, opts = {}) {
|
|
42
|
+
const os = await detectOS(session, signal);
|
|
43
|
+
const configPath = configPathForOS(os);
|
|
44
|
+
const json = JSON.stringify(cfg, null, 2);
|
|
45
|
+
if (!opts.skipValidation) {
|
|
46
|
+
const [{ validateConfig }, yaml] = await Promise.all([
|
|
47
|
+
import("./config-validate-K2N6XLDX.js"),
|
|
48
|
+
import("./js-yaml-PTEEG4FO.js")
|
|
49
|
+
]);
|
|
50
|
+
const { loadVersionSpec } = await import("./versions-W5LRQCAJ.js");
|
|
51
|
+
const spec = loadVersionSpec(yaml);
|
|
52
|
+
const { errors, warnings } = await validateConfig(cfg, {
|
|
53
|
+
openclawVersion: opts.openclawVersion,
|
|
54
|
+
schemaCapturedFrom: spec.runtime?.configSchemaCapturedFrom
|
|
55
|
+
});
|
|
56
|
+
for (const w of warnings) process.stderr.write(`warning: ${w}
|
|
57
|
+
`);
|
|
58
|
+
if (errors.length > 0) {
|
|
59
|
+
const rejected = `${configPath}.rejected.${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}`;
|
|
60
|
+
const b64r = Buffer.from(json, "utf-8").toString("base64");
|
|
61
|
+
await execPrivileged(session, `echo '${b64r}' | base64 -d > ${rejected}`, signal);
|
|
62
|
+
throw new Error(
|
|
63
|
+
`Refusing to write an invalid OpenClaw config:
|
|
64
|
+
` + errors.map((e) => ` - ${e}`).join("\n") + `
|
|
65
|
+
The rejected config was kept at ${rejected}. The deployment's current config is unchanged.`
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const b64 = Buffer.from(json, "utf-8").toString("base64");
|
|
70
|
+
const chown = os === "Linux" ? ` && chown ${CONTAINER_UID}:${CONTAINER_UID} ${configPath}` : "";
|
|
71
|
+
const cmd = `echo '${b64}' | base64 -d > ${OPENCLAW_TMP} && mv ${OPENCLAW_TMP} ${configPath}` + chown;
|
|
72
|
+
const result = os === "Darwin" ? await session.exec(cmd, signal) : await execPrivileged(session, cmd, signal);
|
|
73
|
+
if (result.code !== 0) {
|
|
74
|
+
throw new Error(`Failed to write config: ${result.stderr}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
async function restartGateway(session, signal) {
|
|
78
|
+
const os = await detectOS(session, signal);
|
|
79
|
+
const pathPrefix = os === "Darwin" ? 'export PATH="/usr/local/bin:/opt/homebrew/bin:/Applications/Docker.app/Contents/Resources/bin:$PATH" && ' : "";
|
|
80
|
+
const imgCmd = `${pathPrefix}${IMAGE_INSPECT_CMD}`;
|
|
81
|
+
const imgResult = os === "Darwin" ? await session.exec(imgCmd, signal) : await execPrivileged(session, imgCmd, signal);
|
|
82
|
+
const resolved = imageForRestart(imgResult.stdout);
|
|
83
|
+
if (!resolved.ok) throw new Error(resolved.error);
|
|
84
|
+
const image = resolved.value;
|
|
85
|
+
const pubCmd = `${pathPrefix}${PUBLISH_INSPECT_CMD}`;
|
|
86
|
+
const pubResult = os === "Darwin" ? await session.exec(pubCmd, signal) : await execPrivileged(session, pubCmd, signal);
|
|
87
|
+
const restartCmd = gatewayRunCommand({
|
|
88
|
+
image,
|
|
89
|
+
stateDir: stateDirForOS(os),
|
|
90
|
+
pathPrefix,
|
|
91
|
+
publish: publishForRestart(pubResult.stdout)
|
|
92
|
+
});
|
|
93
|
+
const result = os === "Darwin" ? await session.exec(restartCmd, signal) : await execPrivileged(session, restartCmd, signal);
|
|
94
|
+
if (result.code !== 0) {
|
|
95
|
+
throw new Error(`Gateway restart failed: ${result.stderr}`);
|
|
96
|
+
}
|
|
97
|
+
const healthy = await waitForGateway(session, pathPrefix, signal);
|
|
98
|
+
if (!healthy.ok) {
|
|
99
|
+
throw new Error(
|
|
100
|
+
`Gateway restarted but did not finish starting on port ${GATEWAY_PORT}` + (healthy.reason ? ` \u2014 ${healthy.reason}` : "") + `. The previous container has already been replaced; inspect it with \`docker logs openclaw\`. If the newly-applied config is at fault, revert it and restart \u2014 before v1.7.2 this config was never applied, so a value that has sat unused may now be taking effect.`
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
async function waitForGateway(session, pathPrefix, signal, attempts = 15) {
|
|
105
|
+
const { probeCommand, interpretProbe } = await import("./health-5CCOXYGG.js");
|
|
106
|
+
const probe = probeCommand("started", GATEWAY_PORT, pathPrefix);
|
|
107
|
+
let last = "no response from the gateway";
|
|
108
|
+
for (let i = 0; i < attempts; i++) {
|
|
109
|
+
if (signal?.aborted) return { ok: false, reason: "aborted" };
|
|
110
|
+
const r = await session.exec(probe, signal);
|
|
111
|
+
const verdict = interpretProbe("started", r.stdout);
|
|
112
|
+
if (verdict.ok) return { ok: true };
|
|
113
|
+
last = verdict.reason ?? last;
|
|
114
|
+
await new Promise((resolve) => setTimeout(resolve, 2e3));
|
|
115
|
+
}
|
|
116
|
+
return { ok: false, reason: last };
|
|
117
|
+
}
|
|
118
|
+
function deepMerge(base, overlay) {
|
|
119
|
+
const result = { ...base };
|
|
120
|
+
for (const [key, val] of Object.entries(overlay)) {
|
|
121
|
+
if (val !== null && typeof val === "object" && !Array.isArray(val) && typeof result[key] === "object" && result[key] !== null && !Array.isArray(result[key])) {
|
|
122
|
+
result[key] = deepMerge(
|
|
123
|
+
result[key],
|
|
124
|
+
val
|
|
125
|
+
);
|
|
126
|
+
} else {
|
|
127
|
+
result[key] = val;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return result;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export {
|
|
134
|
+
OPENCLAW_CONFIG_LINUX,
|
|
135
|
+
OPENCLAW_CONFIG_MACOS,
|
|
136
|
+
OPENCLAW_CONFIG,
|
|
137
|
+
OPENCLAW_TMP,
|
|
138
|
+
readRemoteConfig,
|
|
139
|
+
atomicWriteConfig,
|
|
140
|
+
restartGateway,
|
|
141
|
+
deepMerge
|
|
142
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
execPrivileged
|
|
4
|
+
} from "./chunk-NUDIHZEH.js";
|
|
5
|
+
|
|
6
|
+
// src/openclaw/docker.ts
|
|
7
|
+
function looksLikeStillBooting(detail) {
|
|
8
|
+
const text = detail.toLowerCase();
|
|
9
|
+
return text.includes("command not found") || text.includes("cannot connect to the docker daemon") || text.includes("is the docker daemon running") || text.includes("no such file or directory");
|
|
10
|
+
}
|
|
11
|
+
function saysNoSuchObject(text) {
|
|
12
|
+
return /no such (object|container)/i.test(text);
|
|
13
|
+
}
|
|
14
|
+
async function inspectContainer(session, name, format, signal, dockerCmd = "docker") {
|
|
15
|
+
const result = await execPrivileged(
|
|
16
|
+
session,
|
|
17
|
+
`${dockerCmd} inspect ${name} --format '${format}'`,
|
|
18
|
+
signal
|
|
19
|
+
);
|
|
20
|
+
if (result.code === 0) return { kind: "ok", value: result.stdout.trim() };
|
|
21
|
+
const text = `${result.stderr} ${result.stdout}`.trim();
|
|
22
|
+
if (saysNoSuchObject(text)) return { kind: "missing" };
|
|
23
|
+
return { kind: "error", detail: text === "" ? `docker inspect exited ${result.code}` : text };
|
|
24
|
+
}
|
|
25
|
+
async function containerStatus(session, name = "openclaw", signal) {
|
|
26
|
+
const result = await inspectContainer(session, name, "{{.State.Status}}", signal);
|
|
27
|
+
if (result.kind === "ok") return { status: result.value || "unknown" };
|
|
28
|
+
if (result.kind === "missing") return { status: "not found" };
|
|
29
|
+
return { status: "unknown", error: result.detail };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export {
|
|
33
|
+
looksLikeStillBooting,
|
|
34
|
+
inspectContainer,
|
|
35
|
+
containerStatus
|
|
36
|
+
};
|