@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.
Files changed (98) hide show
  1. package/README.md +504 -76
  2. package/dist/apply-KYFPWI5U.js +27 -0
  3. package/dist/automation-RMAIBNVC.js +44 -0
  4. package/dist/bedrock-XVASJX2K.js +61 -0
  5. package/dist/{bootstrap-HL4DNUNG.js → bootstrap-HNGKY6WW.js} +31 -19
  6. package/dist/bootstrap.sh.tmpl +51 -31
  7. package/dist/channels-KBUR42IU.js +53 -0
  8. package/dist/chunk-2I7KCMXZ.js +142 -0
  9. package/dist/{chunk-DQDAWNGB.js → chunk-2Z7J3ERQ.js} +1 -1
  10. package/dist/chunk-3OLUJPCB.js +36 -0
  11. package/dist/chunk-5XT5IAJK.js +69 -0
  12. package/dist/chunk-AEEMRFZV.js +24 -0
  13. package/dist/chunk-BSLIYB5Q.js +106 -0
  14. package/dist/{chunk-3XK66DKK.js → chunk-E4NJSJ54.js} +57 -93
  15. package/dist/chunk-FEK7VJAY.js +16 -0
  16. package/dist/{chunk-UKTP332Z.js → chunk-GAZPPSKU.js} +6 -7
  17. package/dist/chunk-GIEVAGEK.js +126 -0
  18. package/dist/chunk-HSPHXG6W.js +67 -0
  19. package/dist/chunk-IVWP4QUJ.js +901 -0
  20. package/dist/{chunk-PVUVI35Y.js → chunk-JW5A7E3X.js} +5 -1
  21. package/dist/{chunk-GZN7PKP4.js → chunk-KC3GC3PE.js} +328 -121
  22. package/dist/chunk-LBE6COC4.js +244 -0
  23. package/dist/chunk-LEVIW2PV.js +890 -0
  24. package/dist/chunk-LT4XMO5D.js +50 -0
  25. package/dist/chunk-MC5ATCXS.js +108 -0
  26. package/dist/chunk-N4WZLWJ4.js +16 -0
  27. package/dist/chunk-NUDIHZEH.js +55 -0
  28. package/dist/chunk-NUL5F6YV.js +56 -0
  29. package/dist/chunk-RHOWYIWT.js +52 -0
  30. package/dist/chunk-STLRHLLV.js +193 -0
  31. package/dist/chunk-V25QRZRB.js +45 -0
  32. package/dist/chunk-VOEJYRIK.js +33 -0
  33. package/dist/{providers-2OABPW2E.js → chunk-VUFMZEIM.js} +3 -2
  34. package/dist/chunk-W3OLNNJW.js +51 -0
  35. package/dist/cli-WLD3EBEM.js +11 -0
  36. package/dist/cli-auth-GD5SK7JU.js +13 -0
  37. package/dist/cli.js +1296 -695
  38. package/dist/config-validate-K2N6XLDX.js +76 -0
  39. package/dist/context-7EVC6W45.js +20 -0
  40. package/dist/docker-3PDO4TS7.js +12 -0
  41. package/dist/{firewall-XQGO7HWK.js → firewall-5VKZ4WBH.js} +32 -1
  42. package/dist/generate-I6YQ5V2T.js +29 -0
  43. package/dist/{harden-URPXLSVF.js → harden-72JMRT4E.js} +51 -11
  44. package/dist/health-5CCOXYGG.js +11 -0
  45. package/dist/index.d.ts +56 -0
  46. package/dist/known-hosts-file-I3Q3NBYQ.js +22 -0
  47. package/dist/logs-5A7YYA53.js +15 -0
  48. package/dist/{mcp-wire-7E5JQY47.js → mcp-wire-3SWBM7D6.js} +32 -16
  49. package/dist/models-AGTQWYDL.js +32 -0
  50. package/dist/network-args-PYJRJBYO.js +64 -0
  51. package/dist/{package-5AR4ZPK4.js → package-5BCMEBTE.js} +11 -4
  52. package/dist/passphrase-MJBE45N7.js +15 -0
  53. package/dist/plugins-DTST3OER.js +49 -0
  54. package/dist/{pool-QGFQRMGU.js → pool-PIIKJ4L2.js} +3 -2
  55. package/dist/preflight-EF52S3WW.js +162 -0
  56. package/dist/preflight-MWWM5JT3.js +181 -0
  57. package/dist/preflight-TFD4N5ZU.js +141 -0
  58. package/dist/privileged-JFA6LLTV.js +17 -0
  59. package/dist/providers-FW6H6OPF.js +11 -0
  60. package/dist/ready-KDNRBNSU.js +95 -0
  61. package/dist/{remote-config-QDLN4UMG.js → remote-config-3HZLUWEA.js} +4 -3
  62. package/dist/runtime-X3KW5TCP.js +44 -0
  63. package/dist/server-GBA7BJGK.js +46 -0
  64. package/dist/spec-path-KQOOIQCA.js +9 -0
  65. package/dist/ssh-VAI5DU5M.js +11 -0
  66. package/dist/ssh-key-DUO6OKOI.js +7 -0
  67. package/dist/stack-config-R4SPSUQZ.js +12 -0
  68. package/dist/upgrade-ULD4KFYC.js +88 -0
  69. package/dist/validate-YVI3BJ55.js +14 -0
  70. package/dist/{version-guard-VUCNZXQS.js → version-guard-XVYW5WFJ.js} +2 -2
  71. package/dist/{versions-2UVAT6PQ.js → versions-W5LRQCAJ.js} +2 -2
  72. package/dist/wait-WO72TRRQ.js +79 -0
  73. package/package.json +11 -4
  74. package/spec/deploy-plan.schema.json +114 -25
  75. package/spec/integrations.yaml +135 -28
  76. package/spec/mcp-tools.yaml +89 -37
  77. package/spec/models.yaml +23 -3
  78. package/spec/openclaw-2.0.config.schema.json +1 -0
  79. package/spec/openclaw-versions.yaml +116 -27
  80. package/spec/providers.schema.json +163 -28
  81. package/dist/apply-F3KY2HTB.js +0 -17
  82. package/dist/automation-RULUSJBW.js +0 -30
  83. package/dist/aws-L4RR2LSE.js +0 -254
  84. package/dist/azure-7OXQENQL.js +0 -262
  85. package/dist/chunk-35WVQY3X.js +0 -122
  86. package/dist/chunk-6X45DDGH.js +0 -42
  87. package/dist/chunk-A4QR4TXW.js +0 -120
  88. package/dist/chunk-C5X375RO.js +0 -85
  89. package/dist/chunk-FNS56UQY.js +0 -741
  90. package/dist/chunk-P5LKWRPL.js +0 -114
  91. package/dist/chunk-SEZMVVAS.js +0 -156
  92. package/dist/context-5LCUG2QR.js +0 -10
  93. package/dist/gcp-UG6AWPIZ.js +0 -193
  94. package/dist/generate-OAGYT3VH.js +0 -15
  95. package/dist/local-DXBEVZ5C.js +0 -47
  96. package/dist/server-FIZCOLHL.js +0 -32
  97. package/dist/ssh-CNJNLM3E.js +0 -8
  98. 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-DQDAWNGB.js";
5
- import "./chunk-3XK66DKK.js";
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
- return loadTemplate().replace(/\{\{OPENCLAW_VERSION\}\}/g, openclawVersion);
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 script = renderScript(opts.openclawVersion);
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}:${GATEWAY_PORT}`,
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(host, port, signal) {
84
- const url = `http://${host}:${port}/health`;
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
- try {
91
- const res = await fetch(url, { signal });
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 at ${url} did not become healthy within ${HEALTH_TIMEOUT_MS / 1e3}s`
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
  };
@@ -5,7 +5,7 @@
5
5
  set -euo pipefail
6
6
 
7
7
  OPENCLAW_VERSION="{{OPENCLAW_VERSION}}"
8
- OPENCLAW_PORT=18789
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
- OPENCLAW_CONFIG=/home/${OPENCLAW_USER}/openclaw.json
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
- OPENCLAW_CONFIG="${HOME}/.config/openclaw/config.json"
36
- mkdir -p "${HOME}/.config/openclaw"
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}" "${HOME}/.config/openclaw" 2>/dev/null || true
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
- cat > "${OPENCLAW_CONFIG}" <<'OPENCLAWJSON'
147
- {"meta":{"lastTouchedVersion":"2026.4"},"gateway":{"port":18789,"auth":{"mode":"token"}},"models":{},"channels":{}}
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
- if [ "$OS" = "Linux" ]; then
150
- chown "${OPENCLAW_USER}:${OPENCLAW_USER}" "${OPENCLAW_CONFIG}"
151
- else
152
- chown "${OPENCLAW_USER}" "${OPENCLAW_CONFIG}" 2>/dev/null || true
153
- fi
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/docker run --name openclaw --rm \
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
- docker stop openclaw 2>/dev/null || true
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
+ };
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  connect
4
- } from "./chunk-3XK66DKK.js";
4
+ } from "./chunk-E4NJSJ54.js";
5
5
  import {
6
6
  NetworkError
7
7
  } from "./chunk-KGXPLI7W.js";
@@ -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
+ };