@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
package/dist/cli.js CHANGED
@@ -1,26 +1,51 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ monitor_default,
4
+ runDiagnostics
5
+ } from "./chunk-IVWP4QUJ.js";
6
+ import {
7
+ GATEWAY_LOGS_PROBE,
8
+ chooseLogSource,
9
+ containerLogsCommand,
10
+ gatewayLogsCommand
11
+ } from "./chunk-NUL5F6YV.js";
2
12
  import {
3
13
  MCP_APPS,
4
14
  buildMcpEntry,
5
15
  writeAppConfigs
6
16
  } from "./chunk-OIGTOLB3.js";
17
+ import "./chunk-3OLUJPCB.js";
7
18
  import {
8
- monitor_default,
9
- validateOpenclawConfig
10
- } from "./chunk-FNS56UQY.js";
11
- import "./chunk-DQDAWNGB.js";
19
+ execPrivileged,
20
+ execPrivilegedWithInput,
21
+ streamPrivileged
22
+ } from "./chunk-NUDIHZEH.js";
12
23
  import {
13
- IMAGE_INSPECT_CMD,
24
+ resolveSpecDir
25
+ } from "./chunk-JW5A7E3X.js";
26
+ import {
27
+ interpretProbe,
28
+ probeCommand
29
+ } from "./chunk-LT4XMO5D.js";
30
+ import "./chunk-LEVIW2PV.js";
31
+ import {
32
+ CONFIG_FILENAME,
33
+ CONTAINER_UID,
34
+ PUBLISH_INSPECT_CMD,
35
+ STATE_DIR_HOST_LINUX,
36
+ configPathForOS,
14
37
  gatewayRunCommand,
38
+ publishForRestart
39
+ } from "./chunk-GIEVAGEK.js";
40
+ import {
41
+ GATEWAY_PORT,
42
+ IMAGE_INSPECT_CMD,
15
43
  imageForRestart,
16
44
  versionOf
17
- } from "./chunk-SEZMVVAS.js";
18
- import "./chunk-P5LKWRPL.js";
45
+ } from "./chunk-VOEJYRIK.js";
19
46
  import "./chunk-EI7CQQ7D.js";
20
- import {
21
- resolveSpecDir
22
- } from "./chunk-PVUVI35Y.js";
23
- import "./chunk-3XK66DKK.js";
47
+ import "./chunk-FEK7VJAY.js";
48
+ import "./chunk-RHOWYIWT.js";
24
49
  import {
25
50
  REPO_URL,
26
51
  chalk,
@@ -31,7 +56,7 @@ import {
31
56
  success,
32
57
  warn
33
58
  } from "./chunk-Q7NQY5HV.js";
34
- import "./chunk-A2I76FTA.js";
59
+ import "./chunk-N4WZLWJ4.js";
35
60
  import {
36
61
  getConfig,
37
62
  getConfigDir,
@@ -42,16 +67,95 @@ import {
42
67
  ClawopsError,
43
68
  UsageError
44
69
  } from "./chunk-KGXPLI7W.js";
70
+ import "./chunk-VUFMZEIM.js";
45
71
 
46
72
  // src/cli/index.ts
47
- import { defineCommand as defineCommand23, runMain } from "citty";
73
+ import { defineCommand as defineCommand24, runMain } from "citty";
48
74
 
49
75
  // src/cli/commands/init.ts
50
76
  import { defineCommand } from "citty";
51
- import { generateKeyPairSync } from "crypto";
77
+ import { spawnSync } from "child_process";
52
78
  import { writeFileSync, mkdirSync, existsSync } from "fs";
53
79
  import path from "path";
54
80
  import process2 from "process";
81
+
82
+ // src/providers/state-bucket.ts
83
+ function deriveStateBucket(provider, scope) {
84
+ if (provider === "azure") {
85
+ return { ok: true, name: "clawops-state" };
86
+ }
87
+ if (!scope.account) {
88
+ return {
89
+ ok: false,
90
+ needs: provider === "aws" ? "an AWS account id \u2014 set AWS_PROFILE, or run `aws sso login --profile <name>`" : "a GCP project \u2014 run `gcloud config set project <id>`, or set GOOGLE_CLOUD_PROJECT"
91
+ };
92
+ }
93
+ if (provider === "gcp") return { ok: true, name: `clawops-state-${scope.account}` };
94
+ if (!scope.region) return { ok: false, needs: "a region" };
95
+ return { ok: true, name: `clawops-state-${scope.account}-${scope.region}` };
96
+ }
97
+ function stateUrlFor(provider, bucket) {
98
+ const verdict = validateStateBucket(provider, bucket);
99
+ if (verdict !== true) throw new Error(`Not a usable ${provider} state backend name: ${verdict}`);
100
+ const scheme = provider === "aws" ? "s3://" : provider === "gcp" ? "gs://" : "azblob://";
101
+ return provider === "azure" ? `${scheme}${bucket}` : `${scheme}${bucket}/clawops`;
102
+ }
103
+ var IPV4 = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/;
104
+ function validateStateBucket(provider, name) {
105
+ const n = name.trim();
106
+ if (n === "") return "Required";
107
+ if (n !== name) return "No leading or trailing spaces";
108
+ if (n.length < 3) return `Too short \u2014 ${labelFor(provider)} names are at least 3 characters`;
109
+ const dotted = provider === "gcp" && n.includes(".");
110
+ const max = dotted ? 222 : 63;
111
+ if (n.length > max) return `Too long \u2014 ${labelFor(provider)} names are at most ${max} characters`;
112
+ if (dotted && n.split(".").some((part) => part.length > 63)) {
113
+ return "Each dot-separated part of a Cloud Storage name is at most 63 characters";
114
+ }
115
+ if (n !== n.toLowerCase()) return "Lowercase only";
116
+ if (provider === "azure") {
117
+ if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(n)) {
118
+ return "Letters, digits and single hyphens only, starting and ending with a letter or digit";
119
+ }
120
+ return true;
121
+ }
122
+ if (!/^[a-z0-9][a-z0-9._-]*[a-z0-9]$/.test(n)) {
123
+ return "Letters, digits, dots, hyphens and underscores only, starting and ending with a letter or digit";
124
+ }
125
+ if (n.includes("..")) return "No consecutive dots";
126
+ if (IPV4.test(n)) return "Cannot be formatted as an IP address";
127
+ if (provider === "aws") {
128
+ if (n.startsWith("xn--")) return 'Cannot start with "xn--"';
129
+ if (n.startsWith("sthree-")) return 'Cannot start with "sthree-"';
130
+ if (n.endsWith("-s3alias")) return 'Cannot end with "-s3alias"';
131
+ if (n.endsWith("--ol-s3")) return 'Cannot end with "--ol-s3"';
132
+ if (n.includes("_")) return "Underscores are not allowed in S3 bucket names";
133
+ return true;
134
+ }
135
+ if (n.startsWith("goog")) return 'Cannot start with "goog"';
136
+ if (n.includes("google")) return 'Cannot contain "google"';
137
+ return true;
138
+ }
139
+ function labelFor(provider) {
140
+ if (provider === "aws") return "S3 bucket";
141
+ if (provider === "gcp") return "Cloud Storage bucket";
142
+ return "blob container";
143
+ }
144
+ async function resolveScopeAccount(provider, signal) {
145
+ if (provider === "azure") return void 0;
146
+ try {
147
+ if (provider === "aws") {
148
+ const { callerAccount } = await import("./preflight-TFD4N5ZU.js");
149
+ return await callerAccount(signal);
150
+ }
151
+ const { resolveProjectId } = await import("./preflight-EF52S3WW.js");
152
+ return resolveProjectId();
153
+ } catch {
154
+ return void 0;
155
+ }
156
+ }
157
+
158
+ // src/cli/commands/init.ts
55
159
  var SUPPORTED_PROVIDERS = ["gcp", "aws", "azure", "local"];
56
160
  var PROVIDER_DEFAULTS = {
57
161
  gcp: { region: "us-central1", credEnv: "GOOGLE_APPLICATION_CREDENTIALS", stateScheme: "gs://" },
@@ -93,9 +197,9 @@ var init_default = defineCommand({
93
197
  );
94
198
  }
95
199
  const existing = getConfig();
96
- if (existing && !forceOverwrite && !nonInteractive) {
200
+ if (existing?.stacks[stackName] && !forceOverwrite) {
97
201
  failure(
98
- `Config already exists at ${path.join(getConfigDir(), "config.json")}. Use --force to overwrite.`
202
+ `Stack "${stackName}" already exists in ${path.join(getConfigDir(), "config.json")} (${existing.stacks[stackName].stateUrl}). Use --force to overwrite its settings, or pass a different --stack name to add another one.`
99
203
  );
100
204
  process2.exit(1);
101
205
  }
@@ -108,12 +212,21 @@ var init_default = defineCommand({
108
212
  throw new UsageError(`SSH key not found at ${keyPath}`);
109
213
  }
110
214
  info("Generating SSH key pair...");
111
- const { privateKey } = generateKeyPairSync("ed25519", {
112
- privateKeyEncoding: { type: "pkcs8", format: "pem" },
113
- publicKeyEncoding: { type: "spki", format: "pem" }
114
- });
115
- writeFileSync(keyPath, privateKey, { mode: 384 });
116
- success(`SSH private key written to ${keyPath}`);
215
+ const gen = spawnSync(
216
+ "ssh-keygen",
217
+ ["-t", "ed25519", "-f", keyPath, "-N", "", "-C", "clawops", "-q"],
218
+ { stdio: ["ignore", "pipe", "pipe"] }
219
+ );
220
+ if (gen.error || gen.status !== 0) {
221
+ const reason = gen.error?.message ?? gen.stderr?.toString().trim() ?? `exit ${gen.status}`;
222
+ throw new UsageError(
223
+ `Could not generate an SSH key: ${reason}
224
+ Generate one yourself and point clawops at it:
225
+ ssh-keygen -t ed25519 -f ${keyPath} -N '' -C clawops
226
+ clawops init --key-path ${keyPath}`
227
+ );
228
+ }
229
+ success(`SSH key pair written to ${keyPath} (and ${keyPath}.pub)`);
117
230
  } else {
118
231
  info(`Using existing SSH key at ${keyPath}`);
119
232
  }
@@ -128,43 +241,41 @@ var init_default = defineCommand({
128
241
  }
129
242
  const sshUser = typeof args["ssh-user"] === "string" ? args["ssh-user"] : "root";
130
243
  const sshPort = typeof args["ssh-port"] === "string" ? parseInt(args["ssh-port"], 10) : 22;
131
- config = {
132
- version: 1,
133
- defaults: { stack: stackName, provider },
134
- stacks: {
135
- [stackName]: {
136
- provider,
137
- stateUrl: "file://~/.clawops/state",
138
- credentialsRef: { source: "file", envVars: [] },
139
- localOpts: { host, sshUser, sshPort, sshKeyPath: keyPath }
140
- }
141
- },
142
- ssh: { keyPath, knownHostsPath }
143
- };
244
+ config = merge(existing, stackName, provider, keyPath, knownHostsPath, {
245
+ provider,
246
+ stateUrl: "file://~/.clawops/state",
247
+ credentialsRef: { source: "file", envVars: [] },
248
+ localOpts: { host, sshUser, sshPort, sshKeyPath: keyPath }
249
+ });
144
250
  } else {
145
251
  const defaults = PROVIDER_DEFAULTS[provider];
146
252
  const region = typeof args.region === "string" ? args.region : defaults.region;
147
- const stateUrl = typeof args.state === "string" ? args.state : `${defaults.stateScheme}CHANGEME/clawops`;
148
- config = {
149
- version: 1,
150
- defaults: { stack: stackName, provider },
151
- stacks: {
152
- [stackName]: {
153
- provider,
154
- stateUrl,
155
- region,
156
- credentialsRef: { source: "env", envVars: [defaults.credEnv] }
157
- }
158
- },
159
- ssh: { keyPath, knownHostsPath }
160
- };
161
- if (stateUrl.includes("CHANGEME")) {
162
- process2.stdout.write("\n");
163
- info(
164
- `Update stateUrl in the config to a real state backend before running \`clawops up\`.
165
- Example: clawops init --provider ${provider} --state ${defaults.stateScheme}your-bucket/clawops`
166
- );
253
+ let stateUrl;
254
+ if (typeof args.state === "string") {
255
+ stateUrl = args.state;
256
+ } else {
257
+ const account = await resolveScopeAccount(provider);
258
+ const derived = deriveStateBucket(provider, { account, region });
259
+ if (!derived.ok) {
260
+ throw new UsageError(
261
+ `Could not name a state backend for ${provider}: clawops needs ${derived.needs}.
262
+ Authenticate and run this again, or name the backend yourself:
263
+ clawops init --provider ${provider} --state ${defaults.stateScheme}your-bucket/clawops`
264
+ );
265
+ }
266
+ stateUrl = stateUrlFor(provider, derived.name);
167
267
  }
268
+ config = merge(existing, stackName, provider, keyPath, knownHostsPath, {
269
+ provider,
270
+ stateUrl,
271
+ region,
272
+ credentialsRef: { source: "env", envVars: [defaults.credEnv] }
273
+ });
274
+ process2.stdout.write("\n");
275
+ info(
276
+ `State backend: ${stateUrl}
277
+ clawops doctor --provider ${provider} checks it exists, and clawops setup offers to create it.`
278
+ );
168
279
  process2.stdout.write("\n");
169
280
  success(`Provider: ${provider} Region: ${region} Stack: ${stackName}`);
170
281
  setConfig(config);
@@ -177,6 +288,15 @@ var init_default = defineCommand({
177
288
  success(`Provider: ${provider} Stack: ${stackName}`);
178
289
  }
179
290
  });
291
+ function merge(existing, stackName, provider, keyPath, knownHostsPath, stack) {
292
+ return {
293
+ ...existing ?? {},
294
+ version: 1,
295
+ defaults: { stack: stackName, provider },
296
+ stacks: { ...existing?.stacks ?? {}, [stackName]: stack },
297
+ ssh: { keyPath, knownHostsPath }
298
+ };
299
+ }
180
300
 
181
301
  // src/cli/commands/up.ts
182
302
  import { defineCommand as defineCommand2 } from "citty";
@@ -199,7 +319,6 @@ function renderTable(headers, rows) {
199
319
  }
200
320
 
201
321
  // src/cli/commands/up.ts
202
- var VALID_INSTANCE_TYPES = ["micro", "small", "medium", "large", "gpu"];
203
322
  var up_default = defineCommand2({
204
323
  meta: {
205
324
  name: "up",
@@ -208,16 +327,20 @@ var up_default = defineCommand2({
208
327
  args: {
209
328
  provider: { type: "string", description: "Cloud provider (gcp|aws|azure|local)" },
210
329
  region: { type: "string", description: "Cloud region" },
211
- "instance-type": { type: "string", description: "Instance size alias (micro|small|medium|large|gpu)" },
330
+ "instance-type": { type: "string", description: "Instance size: a clawops alias (micro|small|medium|large|gpu) or a type your cloud names itself, e.g. t3.small" },
212
331
  "dry-run": { type: "boolean", description: "Preview without applying" },
213
332
  "no-wait": { type: "boolean", description: "Return immediately without waiting for healthy state" },
214
333
  "openclaw-version": { type: "string", description: "OpenClaw release (e.g. 2026.7.1-2). Moving tags are resolved and range-checked" },
215
334
  stack: { type: "string", description: "Target stack name" },
216
- config: { type: "string", description: "Path to openclaw config overlay JSON (local provider only)" }
335
+ config: { type: "string", description: "Path to openclaw config overlay JSON (local provider only)" },
336
+ "gateway-port": { type: "string", description: `Host port to publish the gateway on (default ${GATEWAY_PORT})` },
337
+ "ssh-cidr": { type: "string", description: "CIDR(s) allowed to reach SSH, comma-separated, or 'auto' for this machine's IP. Omitted = none, and nothing will be able to connect" },
338
+ "gateway-cidr": { type: "string", description: "CIDR(s) allowed to reach the gateway port, or 'auto'. Requires --publish-gateway all" },
339
+ "publish-gateway": { type: "string", description: 'loopback (default) or all. "all" serves plaintext HTTP \u2014 put TLS in front of it' }
217
340
  },
218
341
  async run({ args }) {
219
- const { buildContext } = await import("./context-5LCUG2QR.js");
220
- const { guardOpenclawVersion, defaultOpenclawVersion } = await import("./version-guard-VUCNZXQS.js");
342
+ const { buildContext } = await import("./context-7EVC6W45.js");
343
+ const { guardOpenclawVersion, defaultOpenclawVersion } = await import("./version-guard-XVYW5WFJ.js");
221
344
  const ctx = buildContext(args);
222
345
  const requestedVersion = typeof args["openclaw-version"] === "string" ? args["openclaw-version"] : await defaultOpenclawVersion();
223
346
  const openclawVersion = await guardOpenclawVersion(requestedVersion);
@@ -229,10 +352,11 @@ var up_default = defineCommand2({
229
352
  );
230
353
  }
231
354
  const { localOpts } = stackConfig;
232
- const { localBootstrap } = await import("./bootstrap-HL4DNUNG.js");
233
- const abortController = new AbortController();
234
- process3.on("SIGINT", () => abortController.abort());
235
- process3.on("SIGTERM", () => abortController.abort());
355
+ const { localBootstrap } = await import("./bootstrap-HNGKY6WW.js");
356
+ const gatewayPort = parseGatewayPort(args["gateway-port"]);
357
+ const abortController2 = new AbortController();
358
+ process3.on("SIGINT", () => abortController2.abort());
359
+ process3.on("SIGTERM", () => abortController2.abort());
236
360
  const spin2 = spinner(`Bootstrapping local host "${localOpts.host}"...`);
237
361
  try {
238
362
  const state = await localBootstrap({
@@ -243,8 +367,9 @@ var up_default = defineCommand2({
243
367
  knownHostsPath: ctx.config.ssh.knownHostsPath,
244
368
  openclawVersion,
245
369
  stackName: ctx.stackName,
370
+ gatewayPort,
246
371
  noWait: Boolean(args["no-wait"]),
247
- signal: abortController.signal
372
+ signal: abortController2.signal
248
373
  });
249
374
  spin2.succeed(`Host "${localOpts.host}" bootstrapped`);
250
375
  info(`Gateway URL: ${state.gatewayUrl}`);
@@ -257,7 +382,7 @@ var up_default = defineCommand2({
257
382
  user: localOpts.sshUser,
258
383
  privateKeyPath: localOpts.sshKeyPath,
259
384
  knownHostsPath: ctx.config.ssh.knownHostsPath,
260
- signal: abortController.signal
385
+ signal: abortController2.signal
261
386
  });
262
387
  info("Config overlay applied and gateway restarted.");
263
388
  }
@@ -267,64 +392,90 @@ var up_default = defineCommand2({
267
392
  }
268
393
  return;
269
394
  }
270
- const instanceAlias = typeof args["instance-type"] === "string" ? args["instance-type"] : "small";
271
- if (!VALID_INSTANCE_TYPES.includes(instanceAlias)) {
272
- throw new UsageError(
273
- `Invalid --instance-type: ${instanceAlias}. Valid values: ${VALID_INSTANCE_TYPES.join(", ")}`
274
- );
275
- }
395
+ const { generatePlan } = await import("./generate-I6YQ5V2T.js");
396
+ const { applyPlan } = await import("./apply-KYFPWI5U.js");
397
+ const { resolveNetworkFlags } = await import("./network-args-PYJRJBYO.js");
398
+ const { detectEgressIp } = await import("./firewall-5VKZ4WBH.js");
276
399
  const isDryRun = Boolean(args["dry-run"]);
277
400
  const validation = await ctx.adapter.validateConfig();
278
401
  if (!validation.ok) {
279
402
  for (const e of validation.errors) failure(e);
280
403
  process3.exit(3);
281
404
  }
282
- const stack = await ctx.getStack();
283
- const region = typeof args.region === "string" ? args.region : ctx.adapter.defaultRegion();
284
- const instanceType = ctx.adapter.normalizeInstanceType(
285
- instanceAlias
286
- );
287
- await stack.setConfig("region", { value: region });
288
- await stack.setConfig("instanceType", { value: instanceType });
289
- await stack.setConfig("openclawVersion", { value: openclawVersion });
290
- if (isDryRun) {
291
- info("Previewing changes (--dry-run)...");
292
- const preview = await stack.preview({ onOutput: (out) => process3.stdout.write(out) });
293
- process3.stdout.write("\n");
294
- if (preview.changeSummary) {
295
- const rows = Object.entries(preview.changeSummary).filter(([, count]) => count > 0).map(([op, count]) => [op, String(count)]);
296
- if (rows.length > 0) {
297
- process3.stdout.write(renderTable(["Operation", "Count"], rows) + "\n");
298
- }
299
- }
300
- success("Preview complete (no resources changed)");
301
- return;
405
+ if (typeof args.config === "string") {
406
+ info("--config applies to local stacks only; put the overlay in the plan for a cloud stack.");
302
407
  }
408
+ const abortController = new AbortController();
409
+ process3.on("SIGINT", () => abortController.abort());
410
+ process3.on("SIGTERM", () => abortController.abort());
411
+ const network = await resolveNetworkFlags(
412
+ {
413
+ sshCidr: strArg(args["ssh-cidr"]),
414
+ gatewayCidr: strArg(args["gateway-cidr"]),
415
+ publishGateway: strArg(args["publish-gateway"])
416
+ },
417
+ { detectEgressIp: () => detectEgressIp("https://ifconfig.me/ip") }
418
+ );
419
+ const cloudGatewayPort = strArg(args["gateway-port"]);
420
+ if (cloudGatewayPort) network.gatewayPort = parseGatewayPort(cloudGatewayPort);
303
421
  const spin = spinner(`Deploying stack "${ctx.stackName}"...`);
304
422
  try {
305
- const result = await stack.up({
306
- onOutput: (out) => {
307
- spin.text = out.trim() || spin.text;
423
+ const plan = await generatePlan(
424
+ {
425
+ stackName: ctx.stackName,
426
+ provider: ctx.adapter.name,
427
+ ...strArg(args.region) ? { region: strArg(args.region) } : {},
428
+ ...strArg(args["instance-type"]) ? { instanceType: strArg(args["instance-type"]) } : {},
429
+ openclawVersion,
430
+ network
431
+ },
432
+ { signal: abortController.signal }
433
+ );
434
+ if (isDryRun) {
435
+ spin.stop();
436
+ info("Previewing changes (--dry-run)...");
437
+ const diff = plan.diff;
438
+ if (diff && diff.totalChanges > 0) {
439
+ const rows = [
440
+ ["create", String(diff.create.length)],
441
+ ["update", String(diff.update.length)],
442
+ ["delete", String(diff.delete.length)]
443
+ ].filter(([, count]) => count !== "0");
444
+ process3.stdout.write(renderTable(["Operation", "Count"], rows) + "\n");
308
445
  }
446
+ success("Preview complete (no resources changed)");
447
+ return;
448
+ }
449
+ const result = await applyPlan(plan, {
450
+ onOutput: (line2) => {
451
+ spin.text = line2.trim() || spin.text;
452
+ },
453
+ onProgress: (line2) => {
454
+ const text = line2.trim();
455
+ if (!text) return;
456
+ spin.text = text;
457
+ if (!process3.stderr.isTTY) process3.stderr.write(`${text}
458
+ `);
459
+ },
460
+ signal: abortController.signal,
461
+ skipReadiness: Boolean(args["no-wait"])
309
462
  });
310
463
  spin.succeed(`Stack "${ctx.stackName}" deployed`);
311
- const outputs = result.outputs;
312
- if (outputs["publicIp"]) {
313
- info(`Public IP: ${outputs["publicIp"].value}`);
314
- }
315
- if (outputs["gatewayUrl"]) {
316
- info(`Gateway URL: ${outputs["gatewayUrl"].value}`);
317
- }
464
+ if (result.outputs["publicIp"]) info(`Public IP: ${String(result.outputs["publicIp"])}`);
465
+ if (result.outputs["gatewayUrl"]) info(`Gateway URL: ${String(result.outputs["gatewayUrl"])}`);
318
466
  } catch (err) {
319
467
  spin.fail("Deployment failed");
320
468
  throw err;
321
469
  }
322
470
  }
323
471
  });
472
+ function strArg(value) {
473
+ return typeof value === "string" ? value : void 0;
474
+ }
324
475
  async function applyLocalConfigOverlay(opts) {
325
- const { connect } = await import("./ssh-CNJNLM3E.js");
476
+ const { connect } = await import("./ssh-VAI5DU5M.js");
326
477
  const { resolveSecrets } = await import("./secrets-SVZWNAPK.js");
327
- const { readRemoteConfig, atomicWriteConfig, restartGateway, deepMerge } = await import("./remote-config-QDLN4UMG.js");
478
+ const { readRemoteConfig, atomicWriteConfig, restartGateway, deepMerge } = await import("./remote-config-3HZLUWEA.js");
328
479
  let overlay;
329
480
  try {
330
481
  overlay = JSON.parse(readFileSync(opts.configPath, "utf-8"));
@@ -349,6 +500,14 @@ async function applyLocalConfigOverlay(opts) {
349
500
  session.close();
350
501
  }
351
502
  }
503
+ function parseGatewayPort(raw) {
504
+ if (typeof raw !== "string" || raw.trim() === "") return void 0;
505
+ const port = Number(raw);
506
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
507
+ throw new UsageError(`--gateway-port must be a port number between 1 and 65535, got "${raw}"`);
508
+ }
509
+ return port;
510
+ }
352
511
 
353
512
  // src/cli/commands/down.ts
354
513
  import { defineCommand as defineCommand3 } from "citty";
@@ -364,7 +523,7 @@ var down_default = defineCommand3({
364
523
  "dry-run": { type: "boolean", description: "Show what would be destroyed without destroying" }
365
524
  },
366
525
  async run({ args }) {
367
- const { buildContext } = await import("./context-5LCUG2QR.js");
526
+ const { buildContext } = await import("./context-7EVC6W45.js");
368
527
  const ctx = buildContext(args);
369
528
  if (args["dry-run"]) {
370
529
  info(`Dry run \u2014 would destroy stack "${ctx.stackName}"`);
@@ -428,7 +587,7 @@ var status_default = defineCommand4({
428
587
  json: { type: "boolean", description: "Emit JSON" }
429
588
  },
430
589
  async run({ args }) {
431
- const { buildContext } = await import("./context-5LCUG2QR.js");
590
+ const { buildContext } = await import("./context-7EVC6W45.js");
432
591
  const ctx = buildContext(args);
433
592
  if (ctx.adapter.name === "local") {
434
593
  const state = ctx.localState;
@@ -495,13 +654,18 @@ var plan_default = defineCommand5({
495
654
  provider: { type: "string", description: "Cloud provider (aws|gcp|azure)" },
496
655
  stack: { type: "string", description: "Target stack name" },
497
656
  region: { type: "string", description: "Cloud region" },
498
- "instance-type": { type: "string", description: "Instance size alias (micro|small|medium|large|gpu)" },
657
+ "instance-type": { type: "string", description: "Instance size: a clawops alias (micro|small|medium|large|gpu) or a type your cloud names itself, e.g. t3.small" },
499
658
  "openclaw-version": { type: "string", description: "semver or 'stable'/'dev'" },
659
+ "ssh-cidr": { type: "string", description: "CIDR(s) allowed to reach SSH, comma-separated, or 'auto' for this machine's IP. Omitted = none, and nothing will be able to connect" },
660
+ "gateway-cidr": { type: "string", description: "CIDR(s) allowed to reach the gateway port, comma-separated, or 'auto'. Requires --publish-gateway all" },
661
+ "publish-gateway": { type: "string", description: 'loopback (default) or all. "all" serves plaintext HTTP \u2014 put TLS in front of it' },
500
662
  out: { type: "string", description: "Write plan JSON to this absolute path (default: stdout)" }
501
663
  },
502
664
  async run({ args }) {
503
- const { buildContext } = await import("./context-5LCUG2QR.js");
504
- const { generatePlan } = await import("./generate-OAGYT3VH.js");
665
+ const { buildContext } = await import("./context-7EVC6W45.js");
666
+ const { generatePlan } = await import("./generate-I6YQ5V2T.js");
667
+ const { resolveNetworkFlags } = await import("./network-args-PYJRJBYO.js");
668
+ const { detectEgressIp } = await import("./firewall-5VKZ4WBH.js");
505
669
  const ctx = buildContext(args);
506
670
  if (ctx.adapter.name === "local") {
507
671
  throw new UsageError(
@@ -513,6 +677,14 @@ var plan_default = defineCommand5({
513
677
  if (outPath && !isAbsolute(outPath)) {
514
678
  throw new UsageError("--out path must be absolute (R7). Use an absolute path like /tmp/plan.json.");
515
679
  }
680
+ const network = await resolveNetworkFlags(
681
+ {
682
+ sshCidr: strArg2(args["ssh-cidr"]),
683
+ gatewayCidr: strArg2(args["gateway-cidr"]),
684
+ publishGateway: strArg2(args["publish-gateway"])
685
+ },
686
+ { detectEgressIp: () => detectEgressIp("https://ifconfig.me/ip") }
687
+ );
516
688
  const abortController = new AbortController();
517
689
  process7.on("SIGINT", () => abortController.abort());
518
690
  process7.on("SIGTERM", () => abortController.abort());
@@ -525,7 +697,8 @@ var plan_default = defineCommand5({
525
697
  provider,
526
698
  region: typeof args.region === "string" ? args.region : void 0,
527
699
  instanceType: typeof args["instance-type"] === "string" ? args["instance-type"] : void 0,
528
- openclawVersion: typeof args["openclaw-version"] === "string" ? args["openclaw-version"] : void 0
700
+ openclawVersion: typeof args["openclaw-version"] === "string" ? args["openclaw-version"] : void 0,
701
+ network
529
702
  },
530
703
  { signal: abortController.signal }
531
704
  );
@@ -544,6 +717,7 @@ var plan_default = defineCommand5({
544
717
  const { spec, metadata } = plan;
545
718
  const sshCidrs = spec.network.allowedSshCidrs.join(", ") || "(none)";
546
719
  const gatewayCidrs = spec.network.allowedGatewayCidrs.join(", ") || "(none)";
720
+ const publish = spec.network.publishGateway ?? "loopback";
547
721
  process7.stderr.write(
548
722
  `
549
723
  Plan: ${metadata.name} (${spec.provider}${spec.region ? ` / ${spec.region}` : ""})
@@ -551,6 +725,7 @@ Plan: ${metadata.name} (${spec.provider}${spec.region ? ` / ${spec.region}` : "
551
725
  OpenClaw: ${spec.openclaw.version}
552
726
  SSH CIDRs: ${sshCidrs}
553
727
  Gateway: ${gatewayCidrs}
728
+ Published: ${publish === "all" ? "0.0.0.0 \u2014 reachable from the network; plaintext HTTP unless you add TLS" : "127.0.0.1 only \u2014 use `clawops tunnel` or a proxy on the host"}
554
729
  `
555
730
  );
556
731
  if (plan.diff) {
@@ -573,6 +748,9 @@ Changes: ${create.length} to create, ${update.length} to update, ${del.length} t
573
748
  }
574
749
  }
575
750
  });
751
+ function strArg2(value) {
752
+ return typeof value === "string" ? value : void 0;
753
+ }
576
754
 
577
755
  // src/cli/commands/apply.ts
578
756
  import { defineCommand as defineCommand6 } from "citty";
@@ -590,8 +768,8 @@ var apply_default = defineCommand6({
590
768
  "dry-run": { type: "boolean", description: "Validate plan and show diff without applying" }
591
769
  },
592
770
  async run({ args }) {
593
- const { validatePlan } = await import("./validate-CFQ4FH2T.js");
594
- const { applyPlan } = await import("./apply-F3KY2HTB.js");
771
+ const { validatePlan } = await import("./validate-YVI3BJ55.js");
772
+ const { applyPlan } = await import("./apply-KYFPWI5U.js");
595
773
  const planPath = args._?.[0];
596
774
  if (!planPath) {
597
775
  throw new UsageError("Usage: clawops apply <plan.json>");
@@ -657,8 +835,18 @@ ${validation.errors.join("\n")}`);
657
835
  const spin = spinner(`Applying plan for stack "${typedPlan.spec.stackName}"\u2026`);
658
836
  try {
659
837
  const result = await applyPlan(typedPlan, {
660
- onOutput: (line) => {
661
- spin.text = line.trim() || spin.text;
838
+ onOutput: (line2) => {
839
+ spin.text = line2.trim() || spin.text;
840
+ },
841
+ // A spinner renders nothing without a TTY, so a scripted or CI deploy sat silent
842
+ // through the minutes an image pull takes. These lines are rare — one per half
843
+ // minute at most — so they are cheap to print outright where nothing else will.
844
+ onProgress: (line2) => {
845
+ const text = line2.trim();
846
+ if (!text) return;
847
+ spin.text = text;
848
+ if (!process8.stderr.isTTY) process8.stderr.write(`${text}
849
+ `);
662
850
  },
663
851
  signal: abortController.signal,
664
852
  confirmDrift: args.yes ? void 0 : async () => {
@@ -707,7 +895,7 @@ var destroy_default = defineCommand7({
707
895
  "dry-run": { type: "boolean", description: "Show what would be destroyed without destroying" }
708
896
  },
709
897
  async run({ args }) {
710
- const { buildContext } = await import("./context-5LCUG2QR.js");
898
+ const { buildContext } = await import("./context-7EVC6W45.js");
711
899
  const ctx = buildContext(args);
712
900
  if (ctx.adapter.name === "local") {
713
901
  throw new UsageError(
@@ -745,6 +933,7 @@ Pass --yes to proceed with destruction.
745
933
  const abortController = new AbortController();
746
934
  process9.on("SIGINT", () => abortController.abort());
747
935
  process9.on("SIGTERM", () => abortController.abort());
936
+ const doomedHost = await hostOf(stack, ctx);
748
937
  const spin = spinner(`Destroying stack "${ctx.stackName}"\u2026`);
749
938
  try {
750
939
  await stack.destroy({
@@ -758,8 +947,35 @@ Pass --yes to proceed with destruction.
758
947
  spin.fail("Destroy failed");
759
948
  throw err;
760
949
  }
950
+ if (doomedHost) {
951
+ const { forgetHost } = await import("./known-hosts-file-I3Q3NBYQ.js");
952
+ const removed = forgetHost(
953
+ expandHome(ctx.config.ssh.knownHostsPath),
954
+ doomedHost.host,
955
+ doomedHost.port
956
+ );
957
+ if (removed) info(`Forgot the host key for ${doomedHost.host} \u2014 its instance is gone.`);
958
+ }
761
959
  }
762
960
  });
961
+ async function hostOf(stack, ctx) {
962
+ try {
963
+ const outputMap = await stack.outputs();
964
+ const raw = Object.fromEntries(Object.entries(outputMap).map(([k, v]) => [k, v.value]));
965
+ const { extractBaseOutputs } = await import("./outputs-DJHBY7EE.js");
966
+ const conn = ctx.adapter.getConnectionInfo({
967
+ ...extractBaseOutputs(raw),
968
+ privateKeyPath: ctx.config.ssh.keyPath,
969
+ knownHostsPath: ctx.config.ssh.knownHostsPath
970
+ });
971
+ return conn.host ? { host: conn.host, port: conn.port } : void 0;
972
+ } catch {
973
+ return void 0;
974
+ }
975
+ }
976
+ function expandHome(p) {
977
+ return p.replace(/^~/, process9.env["HOME"] ?? "~");
978
+ }
763
979
 
764
980
  // src/cli/commands/ssh.ts
765
981
  import { defineCommand as defineCommand8 } from "citty";
@@ -774,8 +990,8 @@ var ssh_default = defineCommand8({
774
990
  command: { type: "string", description: "Remote command to run (instead of interactive shell)" }
775
991
  },
776
992
  async run({ args }) {
777
- const { buildContext } = await import("./context-5LCUG2QR.js");
778
- const { acquireSession, drainPool } = await import("./pool-QGFQRMGU.js");
993
+ const { buildContext } = await import("./context-7EVC6W45.js");
994
+ const { acquireSession, drainPool } = await import("./pool-PIIKJ4L2.js");
779
995
  const ctx = buildContext(args);
780
996
  let conn;
781
997
  if (ctx.adapter.name === "local") {
@@ -861,9 +1077,9 @@ var tunnel_default = defineCommand9({
861
1077
  "no-open": { type: "boolean", description: "Do not open browser after tunnel is ready" }
862
1078
  },
863
1079
  async run({ args }) {
864
- const { buildContext } = await import("./context-5LCUG2QR.js");
1080
+ const { buildContext } = await import("./context-7EVC6W45.js");
865
1081
  const { extractBaseOutputs } = await import("./outputs-DJHBY7EE.js");
866
- const { acquireSession } = await import("./pool-QGFQRMGU.js");
1082
+ const { acquireSession } = await import("./pool-PIIKJ4L2.js");
867
1083
  const ctx = buildContext(args);
868
1084
  const stack = await ctx.getStack();
869
1085
  const outputMap = await stack.outputs();
@@ -926,12 +1142,13 @@ var logs_default = defineCommand10({
926
1142
  stack: { type: "string", description: "Target stack name" },
927
1143
  follow: { type: "boolean", alias: "f", description: "Follow log output" },
928
1144
  tail: { type: "string", description: "Number of lines to show from end (default: 100)" },
929
- since: { type: "string", description: "Show logs since duration (e.g. 5m, 1h)" }
1145
+ since: { type: "string", description: "Show logs since duration (e.g. 5m, 1h). Reads container output \u2014 the gateway log command has no time filter" },
1146
+ json: { type: "boolean", description: "Emit the gateway's structured JSON log lines" }
930
1147
  },
931
1148
  async run({ args }) {
932
- const { buildContext } = await import("./context-5LCUG2QR.js");
1149
+ const { buildContext } = await import("./context-7EVC6W45.js");
933
1150
  const { extractBaseOutputs } = await import("./outputs-DJHBY7EE.js");
934
- const { acquireSession, drainPool } = await import("./pool-QGFQRMGU.js");
1151
+ const { acquireSession, drainPool } = await import("./pool-PIIKJ4L2.js");
935
1152
  const ctx = buildContext(args);
936
1153
  const stack = await ctx.getStack();
937
1154
  const outputMap = await stack.outputs();
@@ -950,18 +1167,7 @@ var logs_default = defineCommand10({
950
1167
  });
951
1168
  const tailLines = typeof args.tail === "string" ? parseInt(args.tail, 10) : 100;
952
1169
  const follow = Boolean(args.follow);
953
- const sinceFlag = typeof args.since === "string" ? `--since "${args.since}"` : "";
954
- const followFlag = follow ? "-f" : "";
955
- const command = [
956
- "journalctl -u openclaw",
957
- `-n ${tailLines}`,
958
- followFlag,
959
- sinceFlag,
960
- "2>/dev/null",
961
- "|| docker logs openclaw",
962
- `-n ${tailLines}`,
963
- follow ? "-f" : ""
964
- ].filter(Boolean).join(" ");
1170
+ const since = typeof args.since === "string" ? args.since : void 0;
965
1171
  const abortController = new AbortController();
966
1172
  process12.on("SIGINT", () => abortController.abort());
967
1173
  process12.on("SIGTERM", () => abortController.abort());
@@ -976,8 +1182,13 @@ var logs_default = defineCommand10({
976
1182
  });
977
1183
  spin.stop();
978
1184
  try {
1185
+ const probe = since ? { code: 1, stderr: "" } : await execPrivileged(session, GATEWAY_LOGS_PROBE, abortController.signal);
1186
+ const choice = chooseLogSource({ since, gatewayReachable: probe.code === 0 });
1187
+ const opts = { tail: tailLines, follow, since, json: Boolean(args.json) };
1188
+ const command = choice.source === "gateway" ? gatewayLogsCommand(opts) : containerLogsCommand(opts);
1189
+ info(`Logs: ${choice.source} \u2014 ${choice.reason}`);
979
1190
  if (follow) {
980
- const logStream = await session.stream(command, abortController.signal);
1191
+ const logStream = await streamPrivileged(session, command, abortController.signal);
981
1192
  logStream.pipe(process12.stdout);
982
1193
  await new Promise((resolve) => {
983
1194
  logStream.on("end", resolve);
@@ -985,7 +1196,7 @@ var logs_default = defineCommand10({
985
1196
  abortController.signal.addEventListener("abort", () => resolve(), { once: true });
986
1197
  });
987
1198
  } else {
988
- const result = await session.exec(command, abortController.signal);
1199
+ const result = await execPrivileged(session, command, abortController.signal);
989
1200
  process12.stdout.write(result.stdout);
990
1201
  if (result.stderr) process12.stderr.write(result.stderr);
991
1202
  }
@@ -999,10 +1210,9 @@ var logs_default = defineCommand10({
999
1210
  // src/cli/commands/config.ts
1000
1211
  import { defineCommand as defineCommand11 } from "citty";
1001
1212
  import process13 from "process";
1002
- var OPENCLAW_CONFIG = "/home/clawops/openclaw.json";
1003
- var OPENCLAW_TMP = "/tmp/clawops-config.json.tmp";
1213
+ var OPENCLAW_CONFIG = configPathForOS("Linux");
1004
1214
  function dockerRunCmd(image) {
1005
- return gatewayRunCommand({ image, configPath: OPENCLAW_CONFIG });
1215
+ return gatewayRunCommand({ image, stateDir: STATE_DIR_HOST_LINUX });
1006
1216
  }
1007
1217
  function getPath(obj, dotKey) {
1008
1218
  return dotKey.split(".").reduce((cur, k) => {
@@ -1042,9 +1252,9 @@ var config_default = defineCommand11({
1042
1252
  "dry-run": { type: "boolean", description: "Show what would change without writing" }
1043
1253
  },
1044
1254
  async run({ args }) {
1045
- const { buildContext } = await import("./context-5LCUG2QR.js");
1255
+ const { buildContext } = await import("./context-7EVC6W45.js");
1046
1256
  const { extractBaseOutputs } = await import("./outputs-DJHBY7EE.js");
1047
- const { acquireSession, drainPool } = await import("./pool-QGFQRMGU.js");
1257
+ const { acquireSession, drainPool } = await import("./pool-PIIKJ4L2.js");
1048
1258
  const [action, key, value] = args._ ?? [];
1049
1259
  if (!action || !["get", "set", "unset", "validate"].includes(action)) {
1050
1260
  failure("Usage: clawops config <get [key] | set key value | unset key | validate>");
@@ -1100,12 +1310,19 @@ var config_default = defineCommand11({
1100
1310
  return;
1101
1311
  }
1102
1312
  if (action === "validate") {
1103
- const issues = validateOpenclawConfig(cfg);
1104
- if (issues.length === 0) {
1105
- success("Config is valid.");
1313
+ const { validateConfig } = await import("./config-validate-K2N6XLDX.js");
1314
+ const yaml = await import("./js-yaml-PTEEG4FO.js");
1315
+ const { loadVersionSpec } = await import("./versions-W5LRQCAJ.js");
1316
+ const spec = loadVersionSpec(yaml);
1317
+ const { errors, warnings } = await validateConfig(cfg, {
1318
+ schemaCapturedFrom: spec.runtime?.configSchemaCapturedFrom
1319
+ });
1320
+ for (const w of warnings) warn(w);
1321
+ if (errors.length === 0) {
1322
+ success(warnings.length === 0 ? "Config is valid." : "Config is valid, with notes.");
1106
1323
  } else {
1107
- for (const issue of issues) warn(issue);
1108
- failure(`Config has ${issues.length} issue(s).`);
1324
+ for (const e of errors) failure(e);
1325
+ failure(`Config has ${errors.length} error(s).`);
1109
1326
  process13.exit(1);
1110
1327
  }
1111
1328
  return;
@@ -1125,23 +1342,23 @@ var config_default = defineCommand11({
1125
1342
  process13.stdout.write(JSON.stringify(cfg, null, 2) + "\n");
1126
1343
  return;
1127
1344
  }
1128
- const json = JSON.stringify(cfg, null, 2);
1129
- const b64 = Buffer.from(json, "utf-8").toString("base64");
1130
- const writeCmd = `echo '${b64}' | base64 -d > ${OPENCLAW_TMP} && mv ${OPENCLAW_TMP} ${OPENCLAW_CONFIG} && chown clawops:clawops ${OPENCLAW_CONFIG}`;
1131
- const writeResult = await session.exec(writeCmd, abortController.signal);
1132
- if (writeResult.code !== 0) {
1133
- failure(`Failed to write config: ${writeResult.stderr}`);
1345
+ const { atomicWriteConfig } = await import("./remote-config-3HZLUWEA.js");
1346
+ try {
1347
+ await atomicWriteConfig(session, cfg, abortController.signal);
1348
+ } catch (err) {
1349
+ failure(err.message);
1134
1350
  process13.exit(1);
1135
1351
  }
1136
1352
  success(`config ${action}: ${key ?? "(all)"}`);
1137
1353
  if (args.restart) {
1138
1354
  info("Restarting gateway...");
1139
- const imgResult = await session.exec(
1355
+ const imgResult = await execPrivileged(
1356
+ session,
1140
1357
  `docker inspect openclaw --format '{{.Config.Image}}' 2>/dev/null || echo 'ghcr.io/openclaw/openclaw:stable'`,
1141
1358
  abortController.signal
1142
1359
  );
1143
1360
  const image = imgResult.stdout.trim();
1144
- const restartResult = await session.exec(dockerRunCmd(image), abortController.signal);
1361
+ const restartResult = await execPrivileged(session, dockerRunCmd(image), abortController.signal);
1145
1362
  if (restartResult.code !== 0) {
1146
1363
  failure(`Restart failed: ${restartResult.stderr}`);
1147
1364
  process13.exit(1);
@@ -1161,19 +1378,27 @@ import process14 from "process";
1161
1378
  var agents_default = defineCommand12({
1162
1379
  meta: {
1163
1380
  name: "agents",
1164
- description: "Manage OpenClaw agents (list | restart [name] | logs <name>)"
1381
+ description: "Manage OpenClaw agents (list | logs <name>)"
1165
1382
  },
1166
1383
  args: {
1167
1384
  stack: { type: "string", description: "Target stack name" },
1168
- json: { type: "boolean", description: "Emit JSON (for list)" }
1385
+ json: { type: "boolean", description: "Emit JSON (for list and logs)" },
1386
+ limit: { type: "string", description: "Max activity records for `logs` (default 50)" },
1387
+ cursor: { type: "string", description: "Continue from a previous `logs` result cursor" }
1169
1388
  },
1170
1389
  async run({ args }) {
1171
- const { buildContext } = await import("./context-5LCUG2QR.js");
1390
+ const { buildContext } = await import("./context-7EVC6W45.js");
1172
1391
  const { extractBaseOutputs } = await import("./outputs-DJHBY7EE.js");
1173
- const { acquireSession, drainPool } = await import("./pool-QGFQRMGU.js");
1392
+ const { acquireSession, drainPool } = await import("./pool-PIIKJ4L2.js");
1174
1393
  const [action, name] = args._ ?? [];
1175
- if (!action || !["list", "restart", "logs"].includes(action)) {
1176
- failure("Usage: clawops agents <list | restart [name] | logs <name>>");
1394
+ if (action === "restart") {
1395
+ failure(
1396
+ "clawops agents restart was removed in clawops 2.0.\n OpenClaw 2.0 has no per-agent restart; the only restart it offers is\n gateway-wide and interrupts every agent on the host.\n Run `clawops gateway restart` if that is what you want."
1397
+ );
1398
+ process14.exit(2);
1399
+ }
1400
+ if (!action || !["list", "logs"].includes(action)) {
1401
+ failure("Usage: clawops agents <list | logs <name>>");
1177
1402
  process14.exit(2);
1178
1403
  }
1179
1404
  if (action === "logs" && !name) {
@@ -1205,15 +1430,24 @@ var agents_default = defineCommand12({
1205
1430
  });
1206
1431
  try {
1207
1432
  if (action === "list") {
1208
- const result = await session.exec(
1209
- "docker exec openclaw openclaw agents list --json 2>/dev/null || echo '[]'",
1433
+ const result = await execPrivileged(
1434
+ session,
1435
+ "docker exec openclaw openclaw agents list --json",
1210
1436
  abortController.signal
1211
1437
  );
1212
- let agents = [];
1438
+ if (result.code !== 0) {
1439
+ const why = result.stderr.trim() || result.stdout.trim() || `exit ${result.code}`;
1440
+ failure(`Cannot list agents: ${why}`);
1441
+ process14.exitCode = 1;
1442
+ return;
1443
+ }
1444
+ let agents;
1213
1445
  try {
1214
- agents = JSON.parse(result.stdout.trim());
1446
+ agents = JSON.parse(result.stdout.trim() || "[]");
1215
1447
  } catch {
1216
- agents = [];
1448
+ failure(`Cannot list agents: unexpected output from OpenClaw: ${result.stdout.trim().slice(0, 200)}`);
1449
+ process14.exitCode = 1;
1450
+ return;
1217
1451
  }
1218
1452
  if (args.json) {
1219
1453
  printJson(jsonOk(agents));
@@ -1227,25 +1461,24 @@ var agents_default = defineCommand12({
1227
1461
  ) + "\n\n"
1228
1462
  );
1229
1463
  }
1230
- } else if (action === "restart") {
1231
- const cmd = name ? `docker exec openclaw openclaw agents restart ${name}` : "docker exec openclaw openclaw agents restart";
1232
- const result = await session.exec(cmd, abortController.signal);
1233
- if (result.code !== 0) {
1234
- failure(`Restart failed: ${result.stderr}`);
1235
- process14.exit(1);
1236
- }
1237
- success(name ? `Agent '${name}' restarted.` : "All agents restarted.");
1238
1464
  } else {
1239
- const logStream = await session.stream(
1240
- `docker exec -t openclaw openclaw agents logs ${name} --follow`,
1465
+ const { agentAuditCommand } = await import("./logs-5A7YYA53.js");
1466
+ const limit = typeof args.limit === "string" ? parseInt(args.limit, 10) : 50;
1467
+ const result = await execPrivileged(
1468
+ session,
1469
+ agentAuditCommand({ agentId: name, limit, cursor: typeof args.cursor === "string" ? args.cursor : void 0 }),
1241
1470
  abortController.signal
1242
1471
  );
1243
- logStream.pipe(process14.stdout);
1244
- await new Promise((resolve) => {
1245
- logStream.on("end", resolve);
1246
- logStream.on("close", resolve);
1247
- abortController.signal.addEventListener("abort", () => resolve(), { once: true });
1248
- });
1472
+ if (result.code !== 0) {
1473
+ failure(`Cannot read activity for "${name}": ${result.stderr.trim() || result.stdout.trim() || `exit ${result.code}`}`);
1474
+ process14.exitCode = 1;
1475
+ return;
1476
+ }
1477
+ if (args.json) {
1478
+ process14.stdout.write(result.stdout);
1479
+ } else {
1480
+ renderAgentRuns(name, result.stdout);
1481
+ }
1249
1482
  }
1250
1483
  } finally {
1251
1484
  release();
@@ -1253,35 +1486,285 @@ var agents_default = defineCommand12({
1253
1486
  }
1254
1487
  }
1255
1488
  });
1489
+ function renderAgentRuns(agentId, stdout) {
1490
+ let page;
1491
+ try {
1492
+ page = JSON.parse(stdout.trim() || "{}");
1493
+ } catch {
1494
+ failure(`Cannot read activity for "${agentId}": unexpected output: ${stdout.trim().slice(0, 200)}`);
1495
+ process14.exitCode = 1;
1496
+ return;
1497
+ }
1498
+ const records = page.records ?? [];
1499
+ if (records.length === 0) {
1500
+ info(`No recorded activity for agent "${agentId}".`);
1501
+ return;
1502
+ }
1503
+ process14.stdout.write(
1504
+ "\n" + renderTable(
1505
+ ["When", "Status", "Detail"],
1506
+ records.map((r) => [r.at ?? "\u2014", r.status ?? "\u2014", r.summary ?? r.message ?? "\u2014"])
1507
+ ) + "\n"
1508
+ );
1509
+ if (page.cursor) {
1510
+ info(`More records: clawops agents logs ${agentId} --cursor ${page.cursor}`);
1511
+ }
1512
+ process14.stdout.write("\n");
1513
+ }
1256
1514
 
1257
- // src/cli/commands/gateway.ts
1515
+ // src/cli/commands/migrate.ts
1258
1516
  import { defineCommand as defineCommand13 } from "citty";
1259
1517
  import process15 from "process";
1260
- var OPENCLAW_CONFIG2 = "/home/clawops/openclaw.json";
1261
- function dockerRunCmd2(version) {
1518
+
1519
+ // src/openclaw/migrate.ts
1520
+ async function migrate(steps) {
1521
+ const sourceImage = await steps.inspectSource();
1522
+ if (!sourceImage) {
1523
+ return {
1524
+ kind: "nothing-to-migrate",
1525
+ reason: "No running OpenClaw container was found, so there is no 1.x state to extract. If this deployment was restarted on a clawops release before 2.0, its state was already lost \u2014 clawops mounted none, so every container replacement discarded it. Deploy fresh with `clawops up`."
1526
+ };
1527
+ }
1528
+ const identityBefore = await steps.deviceId();
1529
+ const backup = await steps.backup();
1530
+ if (!backup.ok) {
1531
+ return {
1532
+ kind: "refused",
1533
+ reason: `Refusing to migrate without a verified backup: ${backup.detail}
1534
+ This replaces the running container, and the state it extracts is the only copy.`
1535
+ };
1536
+ }
1537
+ const extracted = await steps.extract();
1538
+ if (!extracted.ok) {
1539
+ return {
1540
+ kind: "failed",
1541
+ reason: "Could not extract state from the running container; it has not been touched.",
1542
+ backupDetail: backup.detail
1543
+ };
1544
+ }
1545
+ await steps.chown();
1546
+ await steps.removeSource();
1547
+ await steps.writeConfig();
1548
+ await steps.start();
1549
+ let restarts = 0;
1550
+ let gate = await steps.gate();
1551
+ if (!gate.ok) {
1552
+ await steps.start();
1553
+ restarts = 1;
1554
+ gate = await steps.gate();
1555
+ }
1556
+ if (!gate.ok) {
1557
+ return {
1558
+ kind: "failed",
1559
+ reason: gate.reason ?? "the gateway did not start after migration",
1560
+ backupDetail: backup.detail
1561
+ };
1562
+ }
1563
+ const identityAfter = await steps.deviceId();
1564
+ const identity = identityBefore === void 0 || identityAfter === void 0 ? "unknown" : identityBefore === identityAfter ? "preserved" : "changed";
1565
+ return { kind: "migrated", entries: extracted.entries, identity, restarts };
1566
+ }
1567
+ function describeMigration(o) {
1568
+ switch (o.kind) {
1569
+ case "migrated": {
1570
+ const lines = [
1571
+ `Migrated to the 2.0 runtime contract. Carried over: ${o.entries.join(", ") || "(nothing)"}.`
1572
+ ];
1573
+ if (o.restarts > 0) {
1574
+ lines.push(
1575
+ "The gateway needed a second start to finish its state-schema migration, which is expected for a 1.x database."
1576
+ );
1577
+ }
1578
+ if (o.identity === "preserved") {
1579
+ lines.push("Device identity preserved \u2014 paired devices do not need re-pairing.");
1580
+ } else if (o.identity === "changed") {
1581
+ lines.push(
1582
+ "Device identity CHANGED. Paired devices and nodes must be re-paired \u2014 check `openclaw devices list` on the host for what was paired."
1583
+ );
1584
+ } else {
1585
+ lines.push(
1586
+ "Device identity could not be compared, so pairings may or may not have carried over. Check `openclaw devices list` before relying on them."
1587
+ );
1588
+ }
1589
+ lines.push("Your 1.x config was NOT applied \u2014 it never took effect on 1.x either. Review it");
1590
+ lines.push("and set what you need with `clawops config set`.");
1591
+ return lines.join("\n");
1592
+ }
1593
+ case "nothing-to-migrate":
1594
+ return o.reason;
1595
+ case "refused":
1596
+ return o.reason;
1597
+ case "failed":
1598
+ return `Migration failed: ${o.reason}
1599
+ A verified backup was taken first: ${o.backupDetail}
1600
+ Restore it with \`clawops backup restore --file <archive>\` before retrying.`;
1601
+ }
1602
+ }
1603
+
1604
+ // src/cli/commands/migrate.ts
1605
+ var SYNTHESISED_CONFIG = JSON.stringify({
1606
+ meta: { lastTouchedVersion: "2026.9.2" },
1607
+ gateway: { mode: "local", port: GATEWAY_PORT, auth: { mode: "token" } },
1608
+ models: {},
1609
+ channels: {}
1610
+ });
1611
+ var migrate_default = defineCommand13({
1612
+ meta: {
1613
+ name: "migrate",
1614
+ description: "Move a 1.x OpenClaw deployment onto the 2.0 runtime contract"
1615
+ },
1616
+ args: {
1617
+ stack: { type: "string", description: "Target stack name" },
1618
+ "openclaw-version": { type: "string", description: "OpenClaw 2.x version to migrate to" },
1619
+ yes: { type: "boolean", description: "Skip the confirmation prompt" }
1620
+ },
1621
+ async run({ args }) {
1622
+ const { buildContext } = await import("./context-7EVC6W45.js");
1623
+ const { extractBaseOutputs } = await import("./outputs-DJHBY7EE.js");
1624
+ const { acquireSession, drainPool } = await import("./pool-PIIKJ4L2.js");
1625
+ const { guardOpenclawVersion, defaultOpenclawVersion } = await import("./version-guard-XVYW5WFJ.js");
1626
+ const requested = typeof args["openclaw-version"] === "string" ? args["openclaw-version"] : void 0;
1627
+ const target = requested ? await guardOpenclawVersion(requested) : await defaultOpenclawVersion();
1628
+ const targetImage = `ghcr.io/openclaw/openclaw:${target}`;
1629
+ const ctx = buildContext(args);
1630
+ const stack = await ctx.getStack();
1631
+ const outputs = Object.fromEntries(
1632
+ Object.entries(await stack.outputs()).map(([k, v]) => [k, v.value])
1633
+ );
1634
+ const conn = ctx.adapter.getConnectionInfo({
1635
+ ...extractBaseOutputs(outputs),
1636
+ privateKeyPath: ctx.config.ssh.keyPath,
1637
+ knownHostsPath: ctx.config.ssh.knownHostsPath
1638
+ });
1639
+ const ac = new AbortController();
1640
+ process15.on("SIGINT", () => ac.abort());
1641
+ const { session, release } = await acquireSession({
1642
+ host: conn.host,
1643
+ port: conn.port,
1644
+ user: conn.user,
1645
+ privateKeyPath: conn.privateKeyPath,
1646
+ knownHostsPath: conn.knownHostsPath,
1647
+ signal: ac.signal
1648
+ });
1649
+ const spin = spinner("Inspecting the deployment...");
1650
+ const run = (cmd) => execPrivileged(session, cmd, ac.signal);
1651
+ const archive = "/tmp/clawops-premigration.tar.gz";
1652
+ const configPath = `${STATE_DIR_HOST_LINUX}/${CONFIG_FILENAME}`;
1653
+ const steps = {
1654
+ inspectSource: async () => {
1655
+ const r = await run(`docker inspect openclaw --format '{{.Config.Image}}'`);
1656
+ const image = r.stdout.trim();
1657
+ return r.code === 0 && image.includes(":") ? image : void 0;
1658
+ },
1659
+ backup: async () => {
1660
+ spin.text = "Taking a verified backup...";
1661
+ const r = await run(
1662
+ `docker exec openclaw openclaw backup create --output ${archive} --verify --json`
1663
+ );
1664
+ return r.code === 0 ? { ok: true, detail: `${archive} on the host (verified)` } : { ok: false, detail: (r.stderr || r.stdout).slice(0, 200) };
1665
+ },
1666
+ extract: async () => {
1667
+ spin.text = "Extracting state from the running container...";
1668
+ const mk = await run(`mkdir -p ${STATE_DIR_HOST_LINUX}`);
1669
+ if (mk.code !== 0) return { ok: false, entries: [] };
1670
+ const cp = await run(`docker cp openclaw:/home/node/.openclaw/. ${STATE_DIR_HOST_LINUX}/`);
1671
+ if (cp.code !== 0) return { ok: false, entries: [] };
1672
+ const ls = await run(`ls ${STATE_DIR_HOST_LINUX}`);
1673
+ return { ok: true, entries: ls.stdout.trim().split(/\s+/).filter(Boolean) };
1674
+ },
1675
+ chown: async () => {
1676
+ await run(`chown -R ${CONTAINER_UID}:${CONTAINER_UID} ${STATE_DIR_HOST_LINUX}`);
1677
+ },
1678
+ removeSource: async () => {
1679
+ spin.text = "Stopping the 1.x container...";
1680
+ await run("docker stop openclaw 2>/dev/null || true");
1681
+ await run("docker rm openclaw 2>/dev/null || true");
1682
+ },
1683
+ writeConfig: async () => {
1684
+ const b64 = Buffer.from(SYNTHESISED_CONFIG, "utf-8").toString("base64");
1685
+ await run(
1686
+ `echo '${b64}' | base64 -d > ${configPath} && chown ${CONTAINER_UID}:${CONTAINER_UID} ${configPath}`
1687
+ );
1688
+ },
1689
+ start: async () => {
1690
+ spin.text = "Starting the 2.0 gateway...";
1691
+ await run(gatewayRunCommand({ image: targetImage, stateDir: STATE_DIR_HOST_LINUX }));
1692
+ },
1693
+ gate: async () => {
1694
+ spin.text = "Waiting for the gateway to start...";
1695
+ let last;
1696
+ for (let i = 0; i < 15; i++) {
1697
+ if (ac.signal.aborted) return { ok: false, reason: "aborted" };
1698
+ const r = await run(probeCommand("started", GATEWAY_PORT));
1699
+ const v = interpretProbe("started", r.stdout);
1700
+ if (v.ok) return { ok: true };
1701
+ last = v.reason;
1702
+ await new Promise((res) => setTimeout(res, 2e3));
1703
+ }
1704
+ return { ok: false, reason: last ?? "no response" };
1705
+ },
1706
+ deviceId: async () => {
1707
+ const file = await run(
1708
+ `docker exec openclaw sh -c 'cat ~/.openclaw/identity/device.json 2>/dev/null' || cat ${STATE_DIR_HOST_LINUX}/identity/device.json 2>/dev/null || true`
1709
+ );
1710
+ try {
1711
+ const parsed = JSON.parse(file.stdout.trim());
1712
+ return parsed.deviceId;
1713
+ } catch {
1714
+ return void 0;
1715
+ }
1716
+ }
1717
+ };
1718
+ try {
1719
+ const outcome = await migrate(steps);
1720
+ spin.stop();
1721
+ const report = describeMigration(outcome);
1722
+ if (outcome.kind === "migrated") {
1723
+ success(report.split("\n")[0]);
1724
+ for (const line2 of report.split("\n").slice(1)) {
1725
+ if (/CHANGED|could not be compared/.test(line2)) warn(line2);
1726
+ else info(line2);
1727
+ }
1728
+ info(`Pre-migration backup: ${archive}`);
1729
+ } else {
1730
+ failure(report);
1731
+ process15.exit(outcome.kind === "nothing-to-migrate" ? 0 : 1);
1732
+ }
1733
+ } finally {
1734
+ release();
1735
+ drainPool();
1736
+ }
1737
+ }
1738
+ });
1739
+
1740
+ // src/cli/commands/gateway.ts
1741
+ import { defineCommand as defineCommand14 } from "citty";
1742
+ import process16 from "process";
1743
+ function dockerRunCmd2(version, publish = "loopback") {
1262
1744
  return gatewayRunCommand({
1263
1745
  image: `ghcr.io/openclaw/openclaw:${version}`,
1264
- configPath: OPENCLAW_CONFIG2
1746
+ stateDir: STATE_DIR_HOST_LINUX,
1747
+ publish
1265
1748
  });
1266
1749
  }
1267
- var gateway_default = defineCommand13({
1750
+ var gateway_default = defineCommand14({
1268
1751
  meta: {
1269
1752
  name: "gateway",
1270
1753
  description: "Manage the OpenClaw gateway daemon (status | restart | update [version])"
1271
1754
  },
1272
1755
  args: {
1273
1756
  stack: { type: "string", description: "Target stack name" },
1274
- channel: { type: "string", description: "Channel for update: stable | dev | <version>" },
1757
+ channel: { type: "string", description: "Version for update (a concrete pin; moving tags are refused)" },
1275
1758
  json: { type: "boolean", description: "Emit JSON (for status)" }
1276
1759
  },
1277
1760
  async run({ args }) {
1278
- const { buildContext } = await import("./context-5LCUG2QR.js");
1761
+ const { buildContext } = await import("./context-7EVC6W45.js");
1279
1762
  const { extractBaseOutputs } = await import("./outputs-DJHBY7EE.js");
1280
- const { acquireSession, drainPool } = await import("./pool-QGFQRMGU.js");
1763
+ const { acquireSession, drainPool } = await import("./pool-PIIKJ4L2.js");
1281
1764
  const [action, versionArg] = args._ ?? [];
1282
1765
  if (!action || !["status", "restart", "update"].includes(action)) {
1283
1766
  failure("Usage: clawops gateway <status | restart | update [version]>");
1284
- process15.exit(2);
1767
+ process16.exit(2);
1285
1768
  }
1286
1769
  const ctx = buildContext(args);
1287
1770
  const stack = await ctx.getStack();
@@ -1296,8 +1779,8 @@ var gateway_default = defineCommand13({
1296
1779
  knownHostsPath: ctx.config.ssh.knownHostsPath
1297
1780
  });
1298
1781
  const abortController = new AbortController();
1299
- process15.on("SIGINT", () => abortController.abort());
1300
- process15.on("SIGTERM", () => abortController.abort());
1782
+ process16.on("SIGINT", () => abortController.abort());
1783
+ process16.on("SIGTERM", () => abortController.abort());
1301
1784
  const { session, release } = await acquireSession({
1302
1785
  host: conn.host,
1303
1786
  port: conn.port,
@@ -1308,17 +1791,29 @@ var gateway_default = defineCommand13({
1308
1791
  });
1309
1792
  try {
1310
1793
  if (action === "status") {
1311
- const statusCmd = `docker inspect openclaw --format '{"status":"{{.State.Status}}","started":"{{.State.StartedAt}}","image":"{{.Config.Image}}"}' 2>/dev/null || echo '{"status":"not running","started":"","image":""}'`;
1312
- const result = await session.exec(statusCmd, abortController.signal);
1794
+ const { inspectContainer } = await import("./docker-3PDO4TS7.js");
1795
+ const inspected = await inspectContainer(
1796
+ session,
1797
+ "openclaw",
1798
+ '{"status":"{{.State.Status}}","started":"{{.State.StartedAt}}","image":"{{.Config.Image}}"}',
1799
+ abortController.signal
1800
+ );
1313
1801
  let status = { status: "unknown", started: "", image: "" };
1314
- try {
1315
- status = JSON.parse(result.stdout.trim());
1316
- } catch {
1802
+ if (inspected.kind === "ok") {
1803
+ try {
1804
+ status = JSON.parse(inspected.value);
1805
+ } catch {
1806
+ }
1807
+ } else if (inspected.kind === "missing") {
1808
+ status = { status: "not running", started: "", image: "" };
1809
+ } else {
1810
+ failure(`Could not ask docker about the gateway: ${inspected.detail}`);
1811
+ process16.exit(1);
1317
1812
  }
1318
1813
  if (args.json) {
1319
1814
  printJson(jsonOk(status));
1320
1815
  } else {
1321
- process15.stdout.write(
1816
+ process16.stdout.write(
1322
1817
  "\n" + renderTable(
1323
1818
  ["Field", "Value"],
1324
1819
  [
@@ -1330,40 +1825,145 @@ var gateway_default = defineCommand13({
1330
1825
  );
1331
1826
  }
1332
1827
  } else if (action === "restart") {
1333
- const imgResult = await session.exec(IMAGE_INSPECT_CMD, abortController.signal);
1828
+ const imgResult = await execPrivileged(session, IMAGE_INSPECT_CMD, abortController.signal);
1334
1829
  const image = imageForRestart(imgResult.stdout);
1335
1830
  if (!image.ok) {
1336
1831
  failure(image.error);
1337
- process15.exit(1);
1832
+ process16.exit(1);
1338
1833
  }
1339
1834
  const version = versionOf(image.value);
1835
+ const pub = await execPrivileged(session, PUBLISH_INSPECT_CMD, abortController.signal);
1836
+ const publish = publishForRestart(pub.stdout);
1340
1837
  const spin = spinner("Restarting gateway...");
1341
- const result = await session.exec(dockerRunCmd2(version), abortController.signal);
1838
+ const result = await execPrivileged(
1839
+ session,
1840
+ dockerRunCmd2(version, publish),
1841
+ abortController.signal
1842
+ );
1342
1843
  spin.stop();
1343
1844
  if (result.code !== 0) {
1344
1845
  failure(`Restart failed: ${result.stderr}`);
1345
- process15.exit(1);
1846
+ process16.exit(1);
1346
1847
  }
1347
1848
  success(`Gateway restarted (${version}).`);
1348
1849
  } else {
1349
- const version = versionArg ?? args.channel ?? "stable";
1850
+ const { guardOpenclawVersion, defaultOpenclawVersion } = await import("./version-guard-XVYW5WFJ.js");
1851
+ const requested = versionArg ?? args.channel;
1852
+ const version = requested ? await guardOpenclawVersion(requested) : await defaultOpenclawVersion();
1350
1853
  const spin = spinner(`Updating gateway to ${version}...`);
1351
- const pullResult = await session.exec(
1352
- `docker pull ghcr.io/openclaw/openclaw:${version}`,
1854
+ const targetImage = `ghcr.io/openclaw/openclaw:${version}`;
1855
+ const pullResult = await execPrivileged(
1856
+ session,
1857
+ `docker pull ${targetImage}`,
1353
1858
  abortController.signal
1354
1859
  );
1355
1860
  if (pullResult.code !== 0) {
1356
1861
  spin.stop();
1357
1862
  failure(`Pull failed: ${pullResult.stderr}`);
1358
- process15.exit(1);
1863
+ process16.exit(1);
1359
1864
  }
1360
- const runResult = await session.exec(dockerRunCmd2(version), abortController.signal);
1361
- spin.stop();
1865
+ spin.text = "Checking state compatibility...";
1866
+ const {
1867
+ snapshotCommand,
1868
+ snapshotPathFrom,
1869
+ preflightCommand,
1870
+ parsePreflight,
1871
+ judgePreflight
1872
+ } = await import("./upgrade-ULD4KFYC.js");
1873
+ const cur = imageForRestart(
1874
+ (await execPrivileged(session, IMAGE_INSPECT_CMD, abortController.signal)).stdout
1875
+ );
1876
+ const snapRepo = `${STATE_DIR_HOST_LINUX}/snapshots`;
1877
+ const snapOut = await execPrivileged(
1878
+ session,
1879
+ snapshotCommand(cur.ok ? cur.value : targetImage, STATE_DIR_HOST_LINUX, snapRepo),
1880
+ abortController.signal
1881
+ );
1882
+ const snapPath = snapshotPathFrom(snapOut.stdout);
1883
+ if (!snapPath) {
1884
+ spin.stop();
1885
+ failure(
1886
+ "Could not snapshot the state database before upgrading, so neither the compatibility check nor a rollback point is available.\n" + (snapOut.stderr || snapOut.stdout).slice(0, 300)
1887
+ );
1888
+ process16.exit(1);
1889
+ }
1890
+ const pre = await execPrivileged(
1891
+ session,
1892
+ preflightCommand(targetImage, STATE_DIR_HOST_LINUX, `${snapPath}/database.sqlite`),
1893
+ abortController.signal
1894
+ );
1895
+ const report = parsePreflight(pre.stdout);
1896
+ const verdict = report ? judgePreflight(report) : { ok: false, reason: `preflight produced no readable report: ${pre.stderr.slice(0, 200)}` };
1897
+ if (!verdict.ok) {
1898
+ spin.stop();
1899
+ failure(`Refusing to upgrade to ${version}: ${verdict.reason}`);
1900
+ info(`A snapshot of the current state was kept at ${snapPath}.`);
1901
+ process16.exit(1);
1902
+ }
1903
+ if (verdict.ok && verdict.note) info(verdict.note);
1904
+ spin.text = `Updating gateway to ${version}...`;
1905
+ const pubU = await execPrivileged(session, PUBLISH_INSPECT_CMD, abortController.signal);
1906
+ const runResult = await execPrivileged(
1907
+ session,
1908
+ dockerRunCmd2(version, publishForRestart(pubU.stdout)),
1909
+ abortController.signal
1910
+ );
1362
1911
  if (runResult.code !== 0) {
1912
+ spin.stop();
1363
1913
  failure(`Start failed: ${runResult.stderr}`);
1364
- process15.exit(1);
1914
+ info(`State snapshot from before the upgrade: ${snapPath}`);
1915
+ process16.exit(1);
1916
+ }
1917
+ spin.text = "Waiting for the gateway to start...";
1918
+ const { probeCommand: probeCommand2, interpretProbe: interpretProbe2 } = await import("./health-5CCOXYGG.js");
1919
+ const { repairCommand, describeOutcome } = await import("./upgrade-ULD4KFYC.js");
1920
+ const publish = publishForRestart(pubU.stdout);
1921
+ const gate = async () => {
1922
+ let last;
1923
+ for (let i = 0; i < 15; i++) {
1924
+ if (abortController.signal.aborted) return { ok: false, reason: "aborted" };
1925
+ const r = await execPrivileged(
1926
+ session,
1927
+ probeCommand2("started", GATEWAY_PORT),
1928
+ abortController.signal
1929
+ );
1930
+ const v = interpretProbe2("started", r.stdout);
1931
+ if (v.ok) return { ok: true };
1932
+ last = v.reason;
1933
+ await new Promise((res) => setTimeout(res, 2e3));
1934
+ }
1935
+ return { ok: false, reason: last ?? "no response" };
1936
+ };
1937
+ const { resolveUpgrade } = await import("./upgrade-ULD4KFYC.js");
1938
+ const outcome = await resolveUpgrade(
1939
+ {
1940
+ gate,
1941
+ repair: async () => {
1942
+ spin.text = "Gateway did not start; attempting one-shot repair...";
1943
+ await execPrivileged(
1944
+ session,
1945
+ repairCommand(targetImage, STATE_DIR_HOST_LINUX),
1946
+ abortController.signal
1947
+ );
1948
+ },
1949
+ run: async (v) => {
1950
+ await execPrivileged(session, dockerRunCmd2(v, publish), abortController.signal);
1951
+ }
1952
+ },
1953
+ {
1954
+ version,
1955
+ previousVersion: cur.ok ? versionOf(cur.value) : void 0,
1956
+ snapshotPath: snapPath
1957
+ }
1958
+ );
1959
+ spin.stop();
1960
+ const message = describeOutcome(outcome, version);
1961
+ if (outcome.kind === "started" || outcome.kind === "repaired") {
1962
+ success(message);
1963
+ } else {
1964
+ failure(message);
1965
+ process16.exit(1);
1365
1966
  }
1366
- success(`Gateway updated to ${version}.`);
1367
1967
  }
1368
1968
  } finally {
1369
1969
  release();
@@ -1373,11 +1973,11 @@ var gateway_default = defineCommand13({
1373
1973
  });
1374
1974
 
1375
1975
  // src/cli/commands/backup.ts
1376
- import { defineCommand as defineCommand14 } from "citty";
1976
+ import { defineCommand as defineCommand15 } from "citty";
1377
1977
  import { createWriteStream } from "fs";
1378
1978
  import { pipeline } from "stream/promises";
1379
- import process16 from "process";
1380
- var backup_default = defineCommand14({
1979
+ import process17 from "process";
1980
+ var backup_default = defineCommand15({
1381
1981
  meta: {
1382
1982
  name: "backup",
1383
1983
  description: "Create an OpenClaw backup (restore is unavailable on this release line)"
@@ -1390,8 +1990,8 @@ var backup_default = defineCommand14({
1390
1990
  yes: { type: "boolean", description: "[restore] Skip confirmation prompt" }
1391
1991
  },
1392
1992
  async run({ args }) {
1393
- const { buildContext } = await import("./context-5LCUG2QR.js");
1394
- const { acquireSession, drainPool } = await import("./pool-QGFQRMGU.js");
1993
+ const { buildContext } = await import("./context-7EVC6W45.js");
1994
+ const { acquireSession, drainPool } = await import("./pool-PIIKJ4L2.js");
1395
1995
  const action = args.action;
1396
1996
  if (action !== "create" && action !== "restore") {
1397
1997
  throw new UsageError(`Unknown action: ${action}. Use "create" or "restore"`);
@@ -1402,7 +2002,7 @@ var backup_default = defineCommand14({
1402
2002
  const state = ctx.localState;
1403
2003
  if (!state) {
1404
2004
  failure("Stack has no state. Run `clawops up` first.");
1405
- process16.exit(4);
2005
+ process17.exit(4);
1406
2006
  }
1407
2007
  conn = {
1408
2008
  host: state.sshHost,
@@ -1420,7 +2020,7 @@ var backup_default = defineCommand14({
1420
2020
  );
1421
2021
  if (!outputs["publicIp"]) {
1422
2022
  failure("Stack has no outputs. Run `clawops up` first.");
1423
- process16.exit(4);
2023
+ process17.exit(4);
1424
2024
  }
1425
2025
  const base = extractBaseOutputs(outputs);
1426
2026
  conn = ctx.adapter.getConnectionInfo({
@@ -1430,8 +2030,8 @@ var backup_default = defineCommand14({
1430
2030
  });
1431
2031
  }
1432
2032
  const abortController = new AbortController();
1433
- process16.on("SIGINT", () => abortController.abort());
1434
- process16.on("SIGTERM", () => abortController.abort());
2033
+ process17.on("SIGINT", () => abortController.abort());
2034
+ process17.on("SIGTERM", () => abortController.abort());
1435
2035
  const { session, release } = await acquireSession({
1436
2036
  host: conn.host,
1437
2037
  port: conn.port,
@@ -1446,7 +2046,8 @@ var backup_default = defineCommand14({
1446
2046
  info(`Writing backup to ${outPath}...`);
1447
2047
  const spin = spinner("Creating backup on remote host...");
1448
2048
  const remoteArchive = "/tmp/clawops-backup.tar.gz";
1449
- const createResult = await session.exec(
2049
+ const createResult = await execPrivileged(
2050
+ session,
1450
2051
  `docker exec openclaw sh -lc 'rm -f ${remoteArchive} && openclaw backup create --output ${remoteArchive} --verify --json'`,
1451
2052
  abortController.signal
1452
2053
  );
@@ -1454,23 +2055,72 @@ var backup_default = defineCommand14({
1454
2055
  spin.stop();
1455
2056
  throw new Error(`Backup failed on the remote host: ${createResult.stderr.slice(0, 300)}`);
1456
2057
  }
1457
- const backupStream = await session.stream(
2058
+ const backupStream = await streamPrivileged(
2059
+ session,
1458
2060
  `docker exec openclaw cat ${remoteArchive}`,
1459
2061
  abortController.signal
1460
2062
  );
1461
2063
  spin.stop();
1462
- const fileStream = createWriteStream(outPath);
2064
+ const fileStream = createWriteStream(outPath, { mode: 384 });
1463
2065
  await pipeline(backupStream, fileStream);
1464
- await session.exec(
2066
+ await execPrivileged(
2067
+ session,
1465
2068
  `docker exec openclaw rm -f ${remoteArchive}`,
1466
2069
  abortController.signal
1467
2070
  );
1468
2071
  success(`Backup saved to ${outPath}`);
1469
- info("The archive contains credentials \u2014 store it accordingly.");
2072
+ info("Saved with mode 0600. The archive contains the state database \u2014 OAuth tokens,");
2073
+ info("secrets and device credentials, unencrypted. Treat it as a credential.");
1470
2074
  } else {
1471
- throw new UsageError(
1472
- "Restore is not available on this clawops release.\nOpenClaw up to 2026.7.1-2 provides `backup create` and `backup verify` only; restore arrived in OpenClaw 2.0 and will be supported by clawops 2.x.\nTo recover manually: copy the archive to the host, then extract it into the gateway container with the gateway stopped."
2075
+ const file = typeof args.file === "string" ? args.file : void 0;
2076
+ if (!file) {
2077
+ throw new UsageError("Usage: clawops backup restore --file <archive.tar.gz>");
2078
+ }
2079
+ const { createReadStream } = await import("fs");
2080
+ const remoteArchive = "/tmp/clawops-restore.tar.gz";
2081
+ const staging = `/tmp/clawops-restored-${Date.now()}`;
2082
+ const spin = spinner("Uploading archive...");
2083
+ const uploadResult = await execPrivilegedWithInput(
2084
+ session,
2085
+ `docker exec -i openclaw sh -c 'cat > ${remoteArchive}'`,
2086
+ createReadStream(file),
2087
+ abortController.signal
2088
+ );
2089
+ if (uploadResult.code !== 0) {
2090
+ spin.stop();
2091
+ throw new Error(`Could not upload the archive: ${uploadResult.stderr.slice(0, 300)}`);
2092
+ }
2093
+ spin.text = "Verifying and restoring to a staging directory...";
2094
+ const restore = await execPrivileged(
2095
+ session,
2096
+ `docker exec openclaw openclaw backup restore ${remoteArchive} --target ${staging} --json`,
2097
+ abortController.signal
1473
2098
  );
2099
+ await execPrivileged(
2100
+ session,
2101
+ `docker exec openclaw rm -f ${remoteArchive}`,
2102
+ abortController.signal
2103
+ );
2104
+ spin.stop();
2105
+ if (restore.code !== 0) {
2106
+ throw new Error(`Restore failed: ${(restore.stderr || restore.stdout).slice(0, 400)}`);
2107
+ }
2108
+ let report = {};
2109
+ try {
2110
+ report = JSON.parse(restore.stdout.trim());
2111
+ } catch {
2112
+ info(restore.stdout);
2113
+ }
2114
+ success(`Archive verified and restored to ${staging} on the host.`);
2115
+ if (report.entryCount !== void 0) info(`${report.entryCount} entries restored.`);
2116
+ for (const w of report.warnings ?? []) warn(w);
2117
+ info("");
2118
+ info("Nothing has been activated. To adopt the restored state:");
2119
+ info(" 1. clawops ssh --command 'sudo docker stop openclaw'");
2120
+ info(` 2. replace the state directory contents with ${staging}`);
2121
+ info(" 3. clawops gateway restart");
2122
+ info("Provider plugins are not carried in the archive; re-run `clawops apply` to");
2123
+ info("reinstall them, or the gateway starts without its model providers.");
1474
2124
  }
1475
2125
  } finally {
1476
2126
  release();
@@ -1480,9 +2130,9 @@ var backup_default = defineCommand14({
1480
2130
  });
1481
2131
 
1482
2132
  // src/cli/commands/stacks.ts
1483
- import { defineCommand as defineCommand15 } from "citty";
1484
- import process17 from "process";
1485
- var stacks_default = defineCommand15({
2133
+ import { defineCommand as defineCommand16 } from "citty";
2134
+ import process18 from "process";
2135
+ var stacks_default = defineCommand16({
1486
2136
  meta: {
1487
2137
  name: "stacks",
1488
2138
  description: "Manage clawops stacks (list | delete <name>)"
@@ -1496,7 +2146,7 @@ var stacks_default = defineCommand15({
1496
2146
  const [action, name] = args._ ?? [];
1497
2147
  if (!action || !["list", "delete"].includes(action)) {
1498
2148
  failure("Usage: clawops stacks <list | delete <name>>");
1499
- process17.exit(2);
2149
+ process18.exit(2);
1500
2150
  }
1501
2151
  if (action === "list") {
1502
2152
  const config2 = requireConfig();
@@ -1519,7 +2169,7 @@ var stacks_default = defineCommand15({
1519
2169
  } else if (rows.length === 0) {
1520
2170
  info("No stacks configured.");
1521
2171
  } else {
1522
- process17.stdout.write(
2172
+ process18.stdout.write(
1523
2173
  "\n" + renderTable(
1524
2174
  ["Name", "Provider", "Region", "State URL"],
1525
2175
  rows
@@ -1531,7 +2181,7 @@ var stacks_default = defineCommand15({
1531
2181
  }
1532
2182
  if (!name) {
1533
2183
  failure("Usage: clawops stacks delete <name>");
1534
- process17.exit(2);
2184
+ process18.exit(2);
1535
2185
  }
1536
2186
  const config = requireConfig();
1537
2187
  if (!(name in config.stacks)) {
@@ -1551,7 +2201,7 @@ var stacks_default = defineCommand15({
1551
2201
  let isDeployed = false;
1552
2202
  if (!args.force) {
1553
2203
  try {
1554
- const { buildContext } = await import("./context-5LCUG2QR.js");
2204
+ const { buildContext } = await import("./context-7EVC6W45.js");
1555
2205
  const ctx = buildContext({ stack: name });
1556
2206
  if (ctx.adapter.name === "local") {
1557
2207
  isDeployed = !!ctx.localState;
@@ -1571,7 +2221,7 @@ var stacks_default = defineCommand15({
1571
2221
  failure(
1572
2222
  `Stack "${name}" is still deployed. Run \`clawops down --stack ${name}\` first to destroy cloud resources, or pass --force to remove from registry only.`
1573
2223
  );
1574
- process17.exit(1);
2224
+ process18.exit(1);
1575
2225
  }
1576
2226
  warn(
1577
2227
  `This removes "${name}" from clawops config only. Cloud resources are NOT destroyed. Run \`clawops destroy --stack ` + name + "` first if you want to remove cloud resources."
@@ -1599,7 +2249,7 @@ var stacks_default = defineCommand15({
1599
2249
  });
1600
2250
  async function confirm(message) {
1601
2251
  const { createInterface: createInterface3 } = await import("readline/promises");
1602
- const rl = createInterface3({ input: process17.stdin, output: process17.stdout });
2252
+ const rl = createInterface3({ input: process18.stdin, output: process18.stdout });
1603
2253
  try {
1604
2254
  const answer = await rl.question(`${message} (y/N) `);
1605
2255
  return answer.trim().toLowerCase() === "y";
@@ -1609,286 +2259,75 @@ async function confirm(message) {
1609
2259
  }
1610
2260
 
1611
2261
  // src/cli/commands/doctor.ts
1612
- import { defineCommand as defineCommand16 } from "citty";
1613
- import process18 from "process";
1614
- import { accessSync, mkdirSync as mkdirSync2, constants } from "fs";
1615
- import path2 from "path";
1616
- var doctor_default = defineCommand16({
2262
+ import { defineCommand as defineCommand17 } from "citty";
2263
+ import process19 from "process";
2264
+ var NAME_COLUMN = 13;
2265
+ var doctor_default = defineCommand17({
1617
2266
  meta: {
1618
2267
  name: "doctor",
1619
2268
  description: "Check system prerequisites, config, SSH keys, and cloud credentials"
1620
2269
  },
1621
2270
  args: {
1622
- stack: { type: "string", description: "Stack name to include remote health checks" }
2271
+ stack: { type: "string", description: "Stack name to include remote health checks" },
2272
+ provider: { type: "string", description: "Check this provider's credentials and account setup, with or without a stack" },
2273
+ "instance-type": { type: "string", description: "Ask account checks about this machine size rather than the provider default" },
2274
+ json: { type: "boolean", description: "Emit the report as JSON" }
1623
2275
  },
1624
2276
  async run({ args }) {
1625
- const { getConfig: getConfig2, getConfigDir: getConfigDir2 } = await import("./store-SDUR52Z5.js");
1626
- process18.stdout.write("\nclawops doctor\n");
1627
- process18.stdout.write("\nRuntime\n");
1628
- const nodeVersion = process18.version;
1629
- const nodeMajor = parseInt(nodeVersion.slice(1).split(".")[0] ?? "0", 10);
1630
- const nodeOk = nodeMajor >= 22;
1631
- if (nodeOk) {
1632
- success(`Node.js ${nodeVersion}`);
1633
- } else {
1634
- failure(`Node.js ${nodeVersion} (requires >=22)`);
1635
- }
1636
- const configDir = getConfigDir2();
1637
- const pulumiHome = path2.join(configDir, ".pulumi");
2277
+ const ac = new AbortController();
2278
+ const abort = () => ac.abort();
2279
+ process19.on("SIGINT", abort);
2280
+ process19.on("SIGTERM", abort);
2281
+ let report;
1638
2282
  try {
1639
- mkdirSync2(pulumiHome, { recursive: true });
1640
- success(`Pulumi home ${pulumiHome}`);
1641
- } catch {
1642
- failure(`Pulumi home ${pulumiHome} (not writable)`);
1643
- }
1644
- process18.stdout.write("\nConfig\n");
1645
- const config = getConfig2();
1646
- if (!config) {
1647
- warn("No config file found \u2014 run `clawops init` to create one");
1648
- } else {
1649
- success(`Config file ${path2.join(configDir, "config.json")}`);
1650
- }
1651
- process18.stdout.write("\nSSH\n");
1652
- if (!config) {
1653
- info("SSH checks skipped \u2014 no config");
1654
- } else {
1655
- const keyPath = config.ssh.keyPath.replace(/^~/, process18.env["HOME"] ?? "~");
1656
- try {
1657
- accessSync(keyPath, constants.R_OK);
1658
- success(`SSH key ${keyPath}`);
1659
- } catch {
1660
- failure(`SSH key ${keyPath} (not found or not readable)`);
1661
- }
1662
- const knownHostsPath = config.ssh.knownHostsPath.replace(/^~/, process18.env["HOME"] ?? "~");
1663
- try {
1664
- accessSync(knownHostsPath, constants.F_OK);
1665
- success(`known_hosts ${knownHostsPath}`);
1666
- } catch {
1667
- warn(`known_hosts ${knownHostsPath} (does not exist \u2014 will be created on first connect)`);
1668
- }
2283
+ report = await runDiagnostics({
2284
+ stack: args.stack,
2285
+ provider: typeof args.provider === "string" ? args.provider : void 0,
2286
+ instanceType: typeof args["instance-type"] === "string" ? args["instance-type"] : void 0,
2287
+ signal: ac.signal
2288
+ });
2289
+ } finally {
2290
+ process19.off("SIGINT", abort);
2291
+ process19.off("SIGTERM", abort);
1669
2292
  }
1670
- process18.stdout.write("\nCredentials\n");
1671
- if (!config) {
1672
- info("Credential checks skipped \u2014 no config");
2293
+ if (args.json) {
2294
+ printJson(jsonOk(report));
1673
2295
  } else {
1674
- const { getProvider } = await import("./providers-2OABPW2E.js");
1675
- await import("./aws-L4RR2LSE.js");
1676
- await import("./gcp-UG6AWPIZ.js");
1677
- await import("./azure-7OXQENQL.js");
1678
- await import("./local-DXBEVZ5C.js");
1679
- const checkedProviders = /* @__PURE__ */ new Set();
1680
- for (const [stackName, stackCfg] of Object.entries(config.stacks)) {
1681
- const providerName = stackCfg.provider;
1682
- if (checkedProviders.has(providerName)) continue;
1683
- checkedProviders.add(providerName);
1684
- if (providerName === "local") {
1685
- success(`local stack "${stackName}" (SSH-only, no cloud credentials required)`);
1686
- continue;
1687
- }
1688
- try {
1689
- const adapter = getProvider(providerName);
1690
- const result = await adapter.validateConfig();
1691
- if (result.ok) {
1692
- success(`${providerName} stack "${stackName}"`);
1693
- } else {
1694
- for (const err of result.errors) {
1695
- failure(`${providerName} stack "${stackName}" \u2014 ${err}`);
1696
- }
1697
- }
1698
- } catch (err) {
1699
- failure(`${providerName} stack "${stackName}" \u2014 ${err instanceof Error ? err.message : String(err)}`);
1700
- }
1701
- }
1702
- if (checkedProviders.size === 0) {
1703
- warn("No stacks configured \u2014 run `clawops init`");
1704
- }
1705
- }
1706
- process18.stdout.write("\nOpenClaw\n");
1707
- try {
1708
- const yaml = await import("./js-yaml-PTEEG4FO.js");
1709
- const { loadVersionSpec, describeRange } = await import("./versions-2UVAT6PQ.js");
1710
- const support = loadVersionSpec(yaml).support;
1711
- success(`Supported range ${describeRange(support)} (recommended ${support.recommended})`);
1712
- if (!support.max) {
1713
- warn("No upper bound declared \u2014 this line would accept any OpenClaw release");
1714
- }
1715
- } catch (e) {
1716
- failure(`Version matrix ${e.message}`);
1717
- }
1718
- if (args.stack) {
1719
- process18.stdout.write("\nRemote health\n");
1720
- if (!config) {
1721
- info("Remote health skipped \u2014 no config");
1722
- } else {
1723
- try {
1724
- const { buildContext } = await import("./context-5LCUG2QR.js");
1725
- const { extractBaseOutputs } = await import("./outputs-DJHBY7EE.js");
1726
- const { acquireSession, drainPool } = await import("./pool-QGFQRMGU.js");
1727
- const ctx = buildContext({ stack: args.stack });
1728
- const stack = await ctx.getStack();
1729
- const outputMap = await stack.outputs();
1730
- const outputs = Object.fromEntries(
1731
- Object.entries(outputMap).map(([k, v]) => [k, v.value])
1732
- );
1733
- const base = extractBaseOutputs(outputs);
1734
- const conn = ctx.adapter.getConnectionInfo({
1735
- ...base,
1736
- privateKeyPath: config.ssh.keyPath,
1737
- knownHostsPath: config.ssh.knownHostsPath
1738
- });
1739
- const ac = new AbortController();
1740
- process18.on("SIGINT", () => ac.abort());
1741
- process18.on("SIGTERM", () => ac.abort());
1742
- const { session, release } = await acquireSession({
1743
- host: conn.host,
1744
- port: conn.port,
1745
- user: conn.user,
1746
- privateKeyPath: conn.privateKeyPath,
1747
- knownHostsPath: conn.knownHostsPath,
1748
- signal: ac.signal
1749
- });
1750
- try {
1751
- const containerResult = await session.exec(
1752
- `docker inspect openclaw --format '{{.State.Status}}' 2>/dev/null || echo 'not found'`,
1753
- ac.signal
1754
- );
1755
- const containerStatus = containerResult.stdout.trim();
1756
- if (containerStatus === "running") {
1757
- success(`Container running`);
1758
- } else {
1759
- failure(`Container ${containerStatus || "unknown"}`);
1760
- }
1761
- const imageResult = await session.exec(
1762
- `docker inspect openclaw --format '{{.Config.Image}}' 2>/dev/null || echo ''`,
1763
- ac.signal
1764
- );
1765
- const image = imageResult.stdout.trim();
1766
- const deployedVersion = image.includes(":") ? image.slice(image.lastIndexOf(":") + 1) : "";
1767
- if (deployedVersion) {
1768
- const yaml = await import("./js-yaml-PTEEG4FO.js");
1769
- const { loadVersionSpec, checkVersion, isMovingTag } = await import("./versions-2UVAT6PQ.js");
1770
- const support = loadVersionSpec(yaml).support;
1771
- if (isMovingTag(deployedVersion)) {
1772
- warn(
1773
- `Deployed ${image} (moving tag \u2014 pin an explicit version; "${deployedVersion}" now resolves to OpenClaw 2.0)`
1774
- );
1775
- } else {
1776
- const check = checkVersion(deployedVersion, support);
1777
- if (check.ok) {
1778
- success(`Deployed OpenClaw ${deployedVersion}`);
1779
- } else {
1780
- failure(`Deployed OpenClaw ${deployedVersion} is not supported by this clawops line`);
1781
- if (check.error.reason === "too-new") {
1782
- warn(
1783
- " This gateway is running OpenClaw 2.0 or later. Session state may already have been lost: 2.0 stores sessions and credentials in SQLite under a directory this clawops line does not mount, so every container restart discards them. Upgrade to clawops 2.x before making changes."
1784
- );
1785
- }
1786
- }
1787
- }
1788
- }
1789
- const healthResult = await session.exec(
1790
- `docker inspect openclaw --format '{{.State.Health.Status}}' 2>/dev/null || echo 'none'`,
1791
- ac.signal
1792
- );
1793
- const healthStatus = healthResult.stdout.trim();
1794
- if (healthStatus === "healthy") {
1795
- success(`Healthcheck healthy`);
1796
- } else if (healthStatus === "none" || healthStatus === "<no value>") {
1797
- info(`Healthcheck no healthcheck configured`);
1798
- } else {
1799
- warn(`Healthcheck ${healthStatus}`);
1800
- }
1801
- const diskResult = await session.exec(
1802
- `df -h /home/clawops 2>/dev/null | awk 'NR==2{print $5" used ("$3" of "$2")"}'`,
1803
- ac.signal
1804
- );
1805
- const diskUsage = diskResult.stdout.trim();
1806
- if (diskUsage) {
1807
- const pctMatch = diskUsage.match(/^(\d+)%/);
1808
- const pct = pctMatch ? parseInt(pctMatch[1], 10) : 0;
1809
- if (pct >= 90) {
1810
- failure(`Disk ${diskUsage}`);
1811
- } else if (pct >= 75) {
1812
- warn(`Disk ${diskUsage}`);
1813
- } else {
1814
- success(`Disk ${diskUsage}`);
1815
- }
1816
- } else {
1817
- warn("Disk unable to determine disk usage");
1818
- }
1819
- const logrotateResult = await session.exec(
1820
- `test -f /etc/logrotate.d/openclaw && echo 'configured' || echo 'not configured'`,
1821
- ac.signal
1822
- );
1823
- const logrotate = logrotateResult.stdout.trim();
1824
- if (logrotate === "configured") {
1825
- success(`Log rotation configured`);
1826
- } else {
1827
- warn(`Log rotation not configured \u2014 logs may grow unbounded`);
1828
- }
1829
- } finally {
1830
- release();
1831
- drainPool();
1832
- }
1833
- } catch (err) {
1834
- failure(`Remote health checks failed: ${err instanceof Error ? err.message : String(err)}`);
1835
- }
1836
- }
1837
- }
1838
- if (args.stack && config) {
1839
- process18.stdout.write("\nHardening\n");
1840
- try {
1841
- const { MODULE_CATALOG, resolveModules, withRemoteExec } = await import("./harden-URPXLSVF.js");
1842
- const { buildContext } = await import("./context-5LCUG2QR.js");
1843
- const { extractBaseOutputs } = await import("./outputs-DJHBY7EE.js");
1844
- const ctx = buildContext({ stack: args.stack });
1845
- const stackObj = await ctx.getStack();
1846
- const outputMap = await stackObj.outputs();
1847
- const outputs = Object.fromEntries(
1848
- Object.entries(outputMap).map(([k, v]) => [k, v.value])
1849
- );
1850
- const base = extractBaseOutputs(outputs);
1851
- const conn = ctx.adapter.getConnectionInfo({
1852
- ...base,
1853
- privateKeyPath: config.ssh.keyPath,
1854
- knownHostsPath: config.ssh.knownHostsPath
1855
- });
1856
- const hardenAc = new AbortController();
1857
- process18.on("SIGINT", () => hardenAc.abort());
1858
- const provider = ctx.adapter.name;
1859
- const modules = resolveModules(MODULE_CATALOG, void 0, provider);
1860
- await withRemoteExec(conn, hardenAc.signal, async (exec) => {
1861
- for (const mod of modules) {
1862
- const result = await mod.check(exec);
1863
- if (result.status === "applied") {
1864
- success(`${mod.label.padEnd(32)} applied`);
1865
- } else if (result.status === "drifted") {
1866
- warn(`${mod.label.padEnd(32)} drifted \u2014 ${result.detail}`);
1867
- } else if (result.status === "missing") {
1868
- info(`${mod.label.padEnd(32)} not applied \u2014 run \`clawops harden\``);
1869
- }
1870
- }
1871
- });
1872
- } catch (err) {
1873
- failure(`Hardening checks failed: ${err instanceof Error ? err.message : String(err)}`);
1874
- }
2296
+ render(report);
1875
2297
  }
1876
- process18.stdout.write("\n");
1877
- if (!nodeOk) {
1878
- process18.stdout.write(
1879
- `Run \`clawops bug\` to open a pre-filled issue at ${REPO_URL}/issues
2298
+ if (!report.ok) {
2299
+ if (!args.json) {
2300
+ process19.stdout.write(
2301
+ `Run \`clawops bug\` to open a pre-filled issue at ${REPO_URL}/issues
1880
2302
 
1881
2303
  `
1882
- );
1883
- process18.exit(1);
2304
+ );
2305
+ }
2306
+ process19.exit(1);
1884
2307
  }
1885
2308
  }
1886
2309
  });
2310
+ function render(report) {
2311
+ process19.stdout.write("\nclawops doctor\n");
2312
+ for (const section of report.sections) {
2313
+ process19.stdout.write(`
2314
+ ${section.title}
2315
+ `);
2316
+ for (const check of section.checks) line(check);
2317
+ }
2318
+ process19.stdout.write("\n");
2319
+ }
2320
+ function line(check) {
2321
+ const label = check.detail ? `${check.name.padEnd(NAME_COLUMN)}${check.name.length >= NAME_COLUMN ? " " : ""}${check.detail}` : check.name;
2322
+ const emit = { pass: success, fail: failure, warn, info }[check.status];
2323
+ emit(label);
2324
+ if (check.remedy) info(` ${check.remedy}`);
2325
+ }
1887
2326
 
1888
2327
  // src/cli/commands/mcp.ts
1889
- import { defineCommand as defineCommand17 } from "citty";
1890
- import process19 from "process";
1891
- var serveCmd = defineCommand17({
2328
+ import { defineCommand as defineCommand18 } from "citty";
2329
+ import process20 from "process";
2330
+ var serveCmd = defineCommand18({
1892
2331
  meta: {
1893
2332
  name: "serve",
1894
2333
  description: "Start the clawops MCP server"
@@ -1903,7 +2342,7 @@ var serveCmd = defineCommand17({
1903
2342
  inspector: { type: "boolean", description: "Enable MCP inspector" }
1904
2343
  },
1905
2344
  async run({ args }) {
1906
- const { serveMcp } = await import("./server-FIZCOLHL.js");
2345
+ const { serveMcp } = await import("./server-GBA7BJGK.js");
1907
2346
  await serveMcp({
1908
2347
  port: args.http ? Number(args.http) : void 0,
1909
2348
  bind: args.bind,
@@ -1915,7 +2354,7 @@ var serveCmd = defineCommand17({
1915
2354
  });
1916
2355
  }
1917
2356
  });
1918
- var installCmd = defineCommand17({
2357
+ var installCmd = defineCommand18({
1919
2358
  meta: {
1920
2359
  name: "install",
1921
2360
  description: "Interactively wire clawops into AI editors (Claude Desktop, Claude Code, Cursor, \u2026)"
@@ -1946,7 +2385,7 @@ var installCmd = defineCommand17({
1946
2385
  const selected = MCP_APPS2.filter((app) => selectedIds.includes(app.id));
1947
2386
  if (selected.length === 0) {
1948
2387
  info("No apps selected. Add this to an app's MCP config manually:");
1949
- process19.stdout.write(JSON.stringify({
2388
+ process20.stdout.write(JSON.stringify({
1950
2389
  mcpServers: { clawops: { command: mcpEntry.command, args: mcpEntry.args } }
1951
2390
  }, null, 2) + "\n\n");
1952
2391
  info("Config file locations:");
@@ -1969,7 +2408,7 @@ var installCmd = defineCommand17({
1969
2408
  }
1970
2409
  }
1971
2410
  });
1972
- var wireCmd = defineCommand17({
2411
+ var wireCmd = defineCommand18({
1973
2412
  meta: {
1974
2413
  name: "wire",
1975
2414
  description: "Wire the gateway AI as an MCP client of clawops (WO-28)"
@@ -1981,26 +2420,26 @@ var wireCmd = defineCommand17({
1981
2420
  rewire: { type: "boolean", description: "Replace an existing clawops entry" }
1982
2421
  },
1983
2422
  async run({ args }) {
1984
- const { buildContext } = await import("./context-5LCUG2QR.js");
1985
- const { acquireSession, drainPool } = await import("./pool-QGFQRMGU.js");
2423
+ const { buildContext } = await import("./context-7EVC6W45.js");
2424
+ const { acquireSession, drainPool } = await import("./pool-PIIKJ4L2.js");
1986
2425
  const { extractBaseOutputs } = await import("./outputs-DJHBY7EE.js");
1987
- const { wireGatewayMcp } = await import("./mcp-wire-7E5JQY47.js");
1988
- const { MCP_HTTP_PORT } = await import("./server-FIZCOLHL.js");
2426
+ const { wireGatewayMcp } = await import("./mcp-wire-3SWBM7D6.js");
2427
+ const { MCP_HTTP_PORT } = await import("./server-GBA7BJGK.js");
1989
2428
  const ac = new AbortController();
1990
- process19.on("SIGINT", () => {
2429
+ process20.on("SIGINT", () => {
1991
2430
  ac.abort();
1992
- process19.exit(130);
2431
+ process20.exit(130);
1993
2432
  });
1994
- process19.on("SIGTERM", () => {
2433
+ process20.on("SIGTERM", () => {
1995
2434
  ac.abort();
1996
- process19.exit(143);
2435
+ process20.exit(143);
1997
2436
  });
1998
2437
  const ctx = buildContext({ stack: args.stack });
1999
2438
  let conn;
2000
2439
  if (ctx.adapter.name === "local") {
2001
2440
  if (!ctx.localState) {
2002
2441
  failure("Stack is not deployed. Run `clawops up` first.");
2003
- process19.exit(1);
2442
+ process20.exit(1);
2004
2443
  }
2005
2444
  const ls = ctx.localState;
2006
2445
  conn = { host: ls.sshHost, port: ls.sshPort, user: ls.sshUser, privateKeyPath: ls.privateKeyPath, knownHostsPath: ls.knownHostsPath };
@@ -2012,7 +2451,7 @@ var wireCmd = defineCommand17({
2012
2451
  );
2013
2452
  if (!outputs["publicIp"]) {
2014
2453
  failure("Stack has no outputs. Run `clawops up` first.");
2015
- process19.exit(1);
2454
+ process20.exit(1);
2016
2455
  }
2017
2456
  const base = extractBaseOutputs(outputs);
2018
2457
  conn = {
@@ -2036,7 +2475,7 @@ var wireCmd = defineCommand17({
2036
2475
  spin.fail("This gateway has no `openclaw mcp add`, so clawops cannot wire it.");
2037
2476
  info("OpenClaw 2026.4.5 ships `openclaw mcp` with only `list` and `serve`.");
2038
2477
  info("Upgrade the gateway to 2026.7.1-2 or later, then re-run this command.");
2039
- process19.exit(1);
2478
+ process20.exit(1);
2040
2479
  }
2041
2480
  if (result.status === "exists") {
2042
2481
  spin.info("The gateway already has a clawops MCP server configured.");
@@ -2049,7 +2488,7 @@ var wireCmd = defineCommand17({
2049
2488
  warn("clawops does not run on the gateway host. Start it where the gateway can reach it:");
2050
2489
  info(` clawops mcp serve --http ${MCP_HTTP_PORT} --bind 0.0.0.0 --token <token>`);
2051
2490
  info(`Then re-run: clawops mcp wire --url ${result.url} --token <token>`);
2052
- process19.exit(1);
2491
+ process20.exit(1);
2053
2492
  }
2054
2493
  spin.succeed(
2055
2494
  result.rewired ? "Re-wired the gateway to clawops \u2014 previous entry replaced." : "Gateway wired to clawops, and the connection was verified."
@@ -2060,14 +2499,14 @@ var wireCmd = defineCommand17({
2060
2499
  } catch (err) {
2061
2500
  spin.fail("Failed to wire gateway MCP client.");
2062
2501
  failure(err instanceof Error ? err.message : String(err));
2063
- process19.exit(1);
2502
+ process20.exit(1);
2064
2503
  } finally {
2065
2504
  release();
2066
2505
  drainPool();
2067
2506
  }
2068
2507
  }
2069
2508
  });
2070
- var mcp_default = defineCommand17({
2509
+ var mcp_default = defineCommand18({
2071
2510
  meta: {
2072
2511
  name: "mcp",
2073
2512
  description: "MCP server operations"
@@ -2081,14 +2520,14 @@ var mcp_default = defineCommand17({
2081
2520
  });
2082
2521
 
2083
2522
  // src/cli/commands/setup.ts
2084
- import { defineCommand as defineCommand18 } from "citty";
2085
- import process20 from "process";
2523
+ import { defineCommand as defineCommand19 } from "citty";
2524
+ import process21 from "process";
2086
2525
  import os from "os";
2087
2526
  import { randomBytes } from "crypto";
2088
- import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, existsSync as existsSync2, mkdirSync as mkdirSync3 } from "fs";
2089
- import { execSync, spawnSync, spawn as spawn2 } from "child_process";
2090
- import path3 from "path";
2091
- var setup_default = defineCommand18({
2527
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, existsSync as existsSync2, mkdirSync as mkdirSync2 } from "fs";
2528
+ import { execSync, spawnSync as spawnSync2, spawn as spawn2 } from "child_process";
2529
+ import path2 from "path";
2530
+ var setup_default = defineCommand19({
2092
2531
  meta: {
2093
2532
  name: "setup",
2094
2533
  description: "Interactive wizard: configure and deploy an OpenClaw stack"
@@ -2103,16 +2542,16 @@ var setup_default = defineCommand18({
2103
2542
  const catalogs = loadCatalogs(yaml);
2104
2543
  const dryRun = Boolean(args["dry-run"]);
2105
2544
  const ac = new AbortController();
2106
- process20.on("SIGINT", () => {
2545
+ process21.on("SIGINT", () => {
2107
2546
  ac.abort();
2108
- process20.exit(130);
2547
+ process21.exit(130);
2109
2548
  });
2110
- process20.on("SIGTERM", () => {
2549
+ process21.on("SIGTERM", () => {
2111
2550
  ac.abort();
2112
- process20.exit(143);
2551
+ process21.exit(143);
2113
2552
  });
2114
- process20.stdout.write("\nWelcome to clawops setup. This wizard will help you deploy OpenClaw\n");
2115
- process20.stdout.write("and connect it to your AI tools. It takes about 2 minutes.\n\n");
2553
+ process21.stdout.write("\nWelcome to clawops setup. This wizard will help you deploy OpenClaw\n");
2554
+ process21.stdout.write("and connect it to your AI tools. It takes about 2 minutes.\n\n");
2116
2555
  const { deploymentType } = await inquirer.prompt([{
2117
2556
  type: "list",
2118
2557
  name: "deploymentType",
@@ -2140,7 +2579,7 @@ var setup_default = defineCommand18({
2140
2579
  ]
2141
2580
  }]);
2142
2581
  provider = answer.provider;
2143
- process20.stdout.write("\n");
2582
+ process21.stdout.write("\n");
2144
2583
  await ensureCloudAuth(provider, inquirer);
2145
2584
  } else {
2146
2585
  provider = "local";
@@ -2196,7 +2635,16 @@ var setup_default = defineCommand18({
2196
2635
  }]);
2197
2636
  localSudoPassword = sudoPass;
2198
2637
  }
2199
- process20.stdout.write("\n");
2638
+ process21.stdout.write("\n");
2639
+ const sshCidrDefault = provider === "local" ? void 0 : await detectSshCidrDefault();
2640
+ const scopeAccount = provider === "local" ? void 0 : await resolveScopeAccount(provider);
2641
+ if (provider !== "local" && provider !== "azure" && !scopeAccount) {
2642
+ const needed = deriveStateBucket(provider, {});
2643
+ if (!needed.ok) {
2644
+ warn(`clawops cannot suggest a name for the state backend: it needs ${needed.needs}.`);
2645
+ info("Type the name of a bucket you already have, or quit, authenticate, and start again.\n");
2646
+ }
2647
+ }
2200
2648
  const stackAnswers = await inquirer.prompt([
2201
2649
  {
2202
2650
  type: "input",
@@ -2218,12 +2666,7 @@ var setup_default = defineCommand18({
2218
2666
  message: "Server size: (bigger = faster, but costs more per month)",
2219
2667
  choices: instanceChoices(provider)
2220
2668
  },
2221
- {
2222
- type: "input",
2223
- name: "stateBucket",
2224
- message: `${stateLabel(provider)} bucket name: (a storage bucket that tracks what's deployed \u2014 create one first if you haven't)`,
2225
- validate: (v) => v.trim() !== "" || "Required \u2014 create a bucket in your cloud console first"
2226
- },
2669
+ stateBucketQuestion(provider, scopeAccount),
2227
2670
  {
2228
2671
  type: "input",
2229
2672
  name: "sshKeyPath",
@@ -2233,8 +2676,13 @@ var setup_default = defineCommand18({
2233
2676
  {
2234
2677
  type: "input",
2235
2678
  name: "sshCidr",
2236
- message: "Restrict SSH access to: (your IP address for security, or 0.0.0.0/0 to allow access from anywhere)",
2237
- default: "0.0.0.0/0"
2679
+ message: sshCidrDefault ? "Restrict SSH access to: (detected your current IP \u2014 press Enter to accept, or enter another CIDR)" : "Restrict SSH access to: (a CIDR such as 203.0.113.4/32 \u2014 your own IP is the usual answer)",
2680
+ // Defaulted to 0.0.0.0/0 until 2.0, so pressing Enter opened SSH to the whole
2681
+ // internet on the path most first-time users take. N10 forbids exactly that
2682
+ // default. When the egress IP cannot be detected there is no default at all and
2683
+ // the answer is required — an unanswerable prompt is better than a wide one.
2684
+ ...sshCidrDefault ? { default: sshCidrDefault } : {},
2685
+ validate: validateCidrAnswer
2238
2686
  }
2239
2687
  ] : [],
2240
2688
  {
@@ -2244,7 +2692,24 @@ var setup_default = defineCommand18({
2244
2692
  default: "latest"
2245
2693
  }
2246
2694
  ]);
2247
- process20.stdout.write("\n");
2695
+ if (provider !== "local") {
2696
+ await runAccountPreflight({
2697
+ provider,
2698
+ region: typeof stackAnswers.region === "string" ? stackAnswers.region : void 0,
2699
+ bucket: stackAnswers.stateBucket,
2700
+ // The size was chosen three questions ago. Azure offers SKU families per subscription,
2701
+ // so asking about the provider default here would clear a size the operator is not
2702
+ // deploying and say nothing about the one they are.
2703
+ instanceType: typeof stackAnswers.instanceSize === "string" ? stackAnswers.instanceSize : void 0,
2704
+ inquirer
2705
+ });
2706
+ }
2707
+ if (provider !== "local" && admitsInternet(stackAnswers.sshCidr)) {
2708
+ warn(
2709
+ "SSH will be reachable from anywhere on the internet. `clawops harden` reports this as a finding; narrowing it to your own IP is the usual fix."
2710
+ );
2711
+ }
2712
+ process21.stdout.write("\n");
2248
2713
  info("Choose the AI model that your OpenClaw agent will use.");
2249
2714
  info("You'll need an API key from your chosen provider (except Bedrock and Ollama).\n");
2250
2715
  const { modelProviderId } = await inquirer.prompt([{
@@ -2256,6 +2721,8 @@ var setup_default = defineCommand18({
2256
2721
  const modelProvider = catalogs.models.find((p) => p.id === modelProviderId);
2257
2722
  const modelConfig = {};
2258
2723
  const secrets = [];
2724
+ let resolvedModelId;
2725
+ let bedrockRegion;
2259
2726
  const { selectedModelId } = await inquirer.prompt([{
2260
2727
  type: "list",
2261
2728
  name: "selectedModelId",
@@ -2273,9 +2740,24 @@ var setup_default = defineCommand18({
2273
2740
  secrets.push(entry);
2274
2741
  modelConfig["apiKey"] = `$secret:${secretName}`;
2275
2742
  } else if (modelProvider.credentialSource === "aws-profile") {
2276
- process20.stdout.write("\n");
2743
+ process21.stdout.write("\n");
2277
2744
  info(modelProvider.iamNote ?? "Bedrock uses your AWS server's IAM role \u2014 no API key needed.");
2278
- info("Make sure your EC2 instance has the AmazonBedrockFullAccess IAM policy attached.\n");
2745
+ bedrockRegion = typeof stackAnswers.region === "string" && stackAnswers.region ? stackAnswers.region : defaultRegion("aws");
2746
+ const { fetchInferenceProfiles, resolveInferenceProfile } = await import("./bedrock-XVASJX2K.js");
2747
+ const listed = await fetchInferenceProfiles(bedrockRegion);
2748
+ if (!listed.ok) {
2749
+ warn(listed.error);
2750
+ info("Continuing with the foundation-model id; Bedrock will refuse it until this is fixed.");
2751
+ } else {
2752
+ const wanted = selectedModel.modelId ?? selectedModel.id;
2753
+ const resolution = resolveInferenceProfile(wanted, listed.profiles, bedrockRegion);
2754
+ if (!resolution.ok) {
2755
+ const { UsageError: UsageError2 } = await import("./errors-OK47MQFD.js");
2756
+ throw new UsageError2(resolution.error);
2757
+ }
2758
+ resolvedModelId = resolution.profileId;
2759
+ info(`Using inference profile ${resolution.profileId} (${resolution.why}).`);
2760
+ }
2279
2761
  } else if (modelProvider.id === "ollama") {
2280
2762
  const { baseUrl } = await inquirer.prompt([{
2281
2763
  type: "input",
@@ -2289,18 +2771,22 @@ var setup_default = defineCommand18({
2289
2771
  }
2290
2772
  modelConfig["baseUrl"] = rewritten;
2291
2773
  }
2292
- const builtModelConfig = {
2293
- provider: modelProviderId,
2294
- ...selectedModel.modelId ? { modelId: selectedModel.modelId } : { model: selectedModel.id },
2295
- ...modelConfig
2296
- };
2297
- process20.stdout.write("\n");
2774
+ const { buildModelsBlock } = await import("./models-AGTQWYDL.js");
2775
+ const builtModelConfig = buildModelsBlock({
2776
+ provider: modelProvider,
2777
+ model: { id: selectedModel.id, modelId: selectedModel.modelId, displayName: selectedModel.displayName },
2778
+ resolvedModelId,
2779
+ apiKeyRef: typeof modelConfig["apiKey"] === "string" ? modelConfig["apiKey"] : void 0,
2780
+ baseUrl: typeof modelConfig["baseUrl"] === "string" ? modelConfig["baseUrl"] : void 0,
2781
+ region: modelProvider.credentialSource === "aws-profile" ? bedrockRegion : void 0
2782
+ });
2783
+ process21.stdout.write("\n");
2298
2784
  info("Use \u2191\u2193 to move, Space to select/deselect, Enter to confirm.");
2299
2785
  const { selectedIntegrationIds } = await inquirer.prompt([{
2300
2786
  type: "checkbox",
2301
2787
  name: "selectedIntegrationIds",
2302
2788
  message: "Which chat integrations would you like to enable?",
2303
- choices: catalogs.integrations.map((integ) => ({
2789
+ choices: wizardChannels(catalogs.integrations).map((integ) => ({
2304
2790
  name: `${integ.displayName} \u2014 ${integ.description}`,
2305
2791
  value: integ.id,
2306
2792
  checked: false
@@ -2310,9 +2796,9 @@ var setup_default = defineCommand18({
2310
2796
  const channelsConfig = {};
2311
2797
  const infraRequired = [];
2312
2798
  for (const integ of catalogs.integrations.filter((i) => selectedIntegrationIds.includes(i.id))) {
2313
- process20.stdout.write("\n");
2799
+ process21.stdout.write("\n");
2314
2800
  info(`Setting up ${integ.displayName}:`);
2315
- const channelConfig = {};
2801
+ const channelConfig = startChannelConfig(integ);
2316
2802
  for (const field of integ.fields) {
2317
2803
  if (field.sensitive && field.envDefault) {
2318
2804
  const entry = await promptSecret(field.envDefault, `${integ.displayName} ${field.label}`, field.envDefault, inquirer);
@@ -2328,6 +2814,15 @@ var setup_default = defineCommand18({
2328
2814
  channelConfig[field.name] = value;
2329
2815
  }
2330
2816
  }
2817
+ if (integ.useEnvSupported === false) {
2818
+ warn(`${integ.displayName} has no non-interactive setup in OpenClaw.`);
2819
+ info(` Run \`openclaw channels add --channel ${integ.channelKey}\` on the host, interactively.`);
2820
+ } else if (integ.plugin?.source === "bundled") {
2821
+ info(`${integ.displayName} ships in the image. Activate it on the host with:`);
2822
+ info(` openclaw channels add --channel ${integ.channelKey} --use-env`);
2823
+ } else if (integ.plugin?.package) {
2824
+ info(`${integ.displayName} needs ${integ.plugin.package}; clawops installs it during apply.`);
2825
+ }
2331
2826
  channelsConfig[integ.channelKey] = channelConfig;
2332
2827
  if (integ.infraRequired) infraRequired.push(integ);
2333
2828
  }
@@ -2346,10 +2841,10 @@ var setup_default = defineCommand18({
2346
2841
  }
2347
2842
  }
2348
2843
  const gatewayToken = randomBytes(24).toString("hex");
2349
- const secretsDir2 = path3.join(os.homedir(), ".clawops", "secrets");
2350
- mkdirSync3(secretsDir2, { recursive: true });
2351
- spawnSync("chmod", ["700", secretsDir2], { stdio: "ignore" });
2352
- const tokenPath = path3.join(secretsDir2, `GATEWAY_TOKEN_${stackAnswers.stackName}`);
2844
+ const secretsDir2 = path2.join(os.homedir(), ".clawops", "secrets");
2845
+ mkdirSync2(secretsDir2, { recursive: true });
2846
+ spawnSync2("chmod", ["700", secretsDir2], { stdio: "ignore" });
2847
+ const tokenPath = path2.join(secretsDir2, `GATEWAY_TOKEN_${stackAnswers.stackName}`);
2353
2848
  writeFileSync3(tokenPath, gatewayToken, { encoding: "utf-8", mode: 384 });
2354
2849
  const openclawConfigOverlay = {
2355
2850
  models: builtModelConfig,
@@ -2360,9 +2855,9 @@ var setup_default = defineCommand18({
2360
2855
  }
2361
2856
  let outputPath;
2362
2857
  if (provider === "local") {
2363
- outputPath = path3.join(outDir, `openclaw-${stackAnswers.stackName}.json`);
2858
+ outputPath = path2.join(outDir, `openclaw-${stackAnswers.stackName}.json`);
2364
2859
  writeFileSync3(outputPath, JSON.stringify(openclawConfigOverlay, null, 2), "utf-8");
2365
- process20.stdout.write("\n");
2860
+ process21.stdout.write("\n");
2366
2861
  success(`Config file saved to ${outputPath}`);
2367
2862
  } else {
2368
2863
  let sshPublicKey = "";
@@ -2370,7 +2865,7 @@ var setup_default = defineCommand18({
2370
2865
  sshPublicKey = readFileSync3(stackAnswers.sshKeyPath ?? "", "utf-8").trim();
2371
2866
  } catch {
2372
2867
  failure(`Cannot read SSH public key at ${stackAnswers.sshKeyPath ?? ""}`);
2373
- process20.exit(1);
2868
+ process21.exit(1);
2374
2869
  }
2375
2870
  const plan = {
2376
2871
  apiVersion: "clawops.dev/v1",
@@ -2392,19 +2887,26 @@ var setup_default = defineCommand18({
2392
2887
  },
2393
2888
  secrets,
2394
2889
  network: {
2395
- allowedSshCidrs: [stackAnswers.sshCidr ?? "0.0.0.0/0"],
2396
- allowedGatewayCidrs: [stackAnswers.sshCidr ?? "0.0.0.0/0"]
2890
+ // No `?? '0.0.0.0/0'`. The prompt requires an answer now, and a fallback that
2891
+ // opened the internet would reintroduce the default it replaced.
2892
+ allowedSshCidrs: [stackAnswers.sshCidr.trim()],
2893
+ // Empty, not the SSH CIDR. Reusing it opened a plaintext HTTP dashboard to
2894
+ // whatever network the operator picked for shell access — two very different
2895
+ // risks answered by one question. The gateway is reached with
2896
+ // `clawops tunnel`; exposing it is an explicit choice (network.publishGateway).
2897
+ allowedGatewayCidrs: [],
2898
+ publishGateway: "loopback"
2397
2899
  },
2398
2900
  ssh: { publicKey: sshPublicKey },
2399
2901
  ...stackAnswers.stateBucket ? { stateBucket: stackAnswers.stateBucket } : {}
2400
2902
  }
2401
2903
  };
2402
- outputPath = path3.join(outDir, `clawops-${stackAnswers.stackName}-plan.json`);
2904
+ outputPath = path2.join(outDir, `clawops-${stackAnswers.stackName}-plan.json`);
2403
2905
  writeFileSync3(outputPath, JSON.stringify(plan, null, 2), "utf-8");
2404
- process20.stdout.write("\n");
2906
+ process21.stdout.write("\n");
2405
2907
  success(`Deployment plan saved to ${outputPath}`);
2406
2908
  }
2407
- process20.stdout.write("\n");
2909
+ process21.stdout.write("\n");
2408
2910
  const mcpEntry = buildMcpEntry();
2409
2911
  if (!mcpEntry.resolved) {
2410
2912
  info("Note: clawops is not installed globally \u2014 AI apps will not be able to start");
@@ -2450,7 +2952,7 @@ var setup_default = defineCommand18({
2450
2952
  }
2451
2953
  }
2452
2954
  if (infraRequired.length > 0) {
2453
- process20.stdout.write("\n");
2955
+ process21.stdout.write("\n");
2454
2956
  info("\u2500\u2500 Action required: webhook registration \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
2455
2957
  info("These integrations need a webhook URL set up in their developer portal:");
2456
2958
  for (const integ of infraRequired) {
@@ -2461,7 +2963,7 @@ var setup_default = defineCommand18({
2461
2963
  }
2462
2964
  }
2463
2965
  if (modelProvider.id === "ollama" && selectedModel.pullSuffix !== void 0) {
2464
- process20.stdout.write("\n");
2966
+ process21.stdout.write("\n");
2465
2967
  info("\u2500\u2500 Action required: download the Ollama model \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
2466
2968
  info("After deployment, run this command on the server (or locally if Ollama is local):");
2467
2969
  info(` ollama pull ${selectedModel.id}${selectedModel.pullSuffix}`);
@@ -2506,8 +3008,8 @@ var setup_default = defineCommand18({
2506
3008
  });
2507
3009
  } else {
2508
3010
  const { getConfigDir: getConfigDir2 } = await import("./store-SDUR52Z5.js");
2509
- const knownHostsPath = path3.join(getConfigDir2(), "known_hosts");
2510
- process20.stdout.write("\n");
3011
+ const knownHostsPath = path2.join(getConfigDir2(), "known_hosts");
3012
+ process21.stdout.write("\n");
2511
3013
  info("To deploy later, run these two commands:");
2512
3014
  info(` clawops init --provider local --host ${localHost} --port ${localPort} --user ${localUser} --key ${localKeyPath} --stack ${stackAnswers.stackName}`);
2513
3015
  info(` clawops up --stack ${stackAnswers.stackName} --config ${outputPath}`);
@@ -2522,18 +3024,18 @@ var setup_default = defineCommand18({
2522
3024
  default: false
2523
3025
  }]);
2524
3026
  if (applyNow) {
2525
- const { applyPlan } = await import("./apply-F3KY2HTB.js");
3027
+ const { applyPlan } = await import("./apply-KYFPWI5U.js");
2526
3028
  const plan = JSON.parse(readFileSync3(outputPath, "utf-8"));
2527
3029
  try {
2528
3030
  await applyPlan(plan, {
2529
- onOutput: (line) => process20.stdout.write(line),
3031
+ onOutput: (line2) => process21.stdout.write(line2),
2530
3032
  signal: ac.signal
2531
3033
  });
2532
3034
  success("Server provisioned and OpenClaw installed.");
2533
3035
  info(`Run: clawops doctor --stack ${stackAnswers.stackName} to verify everything is healthy`);
2534
3036
  } catch (err) {
2535
3037
  failure(err instanceof Error ? err.message : String(err));
2536
- process20.exit(1);
3038
+ process21.exit(1);
2537
3039
  }
2538
3040
  await maybeWireGatewayMcpCloud({ stackName: stackAnswers.stackName, signal: ac.signal, inquirer });
2539
3041
  await maybeHarden({
@@ -2551,11 +3053,11 @@ To deploy later: clawops apply ${outputPath}`);
2551
3053
  });
2552
3054
  async function runLocalDeploy(opts) {
2553
3055
  const { setConfig: setConfig2, getConfig: getConfig2, getConfigDir: getConfigDir2 } = await import("./store-SDUR52Z5.js");
2554
- const { localBootstrap } = await import("./bootstrap-HL4DNUNG.js");
2555
- const { connect } = await import("./ssh-CNJNLM3E.js");
2556
- const { readRemoteConfig, atomicWriteConfig, restartGateway, deepMerge } = await import("./remote-config-QDLN4UMG.js");
3056
+ const { localBootstrap } = await import("./bootstrap-HNGKY6WW.js");
3057
+ const { connect } = await import("./ssh-VAI5DU5M.js");
3058
+ const { readRemoteConfig, atomicWriteConfig, restartGateway, deepMerge } = await import("./remote-config-3HZLUWEA.js");
2557
3059
  const { resolveSecrets } = await import("./secrets-SVZWNAPK.js");
2558
- const knownHostsPath = path3.join(getConfigDir2(), "known_hosts");
3060
+ const knownHostsPath = path2.join(getConfigDir2(), "known_hosts");
2559
3061
  const existing = getConfig2();
2560
3062
  const stackEntry = {
2561
3063
  provider: "local",
@@ -2593,9 +3095,9 @@ async function runLocalDeploy(opts) {
2593
3095
  stackName: opts.stackName,
2594
3096
  sudoPassword: opts.sudoPassword,
2595
3097
  noWait: false,
2596
- onOutput: (line) => {
3098
+ onOutput: (line2) => {
2597
3099
  for (const [pattern, label] of STAGES) {
2598
- if (pattern.test(line)) {
3100
+ if (pattern.test(line2)) {
2599
3101
  spin.text = label;
2600
3102
  break;
2601
3103
  }
@@ -2634,11 +3136,11 @@ async function runLocalDeploy(opts) {
2634
3136
  spin2.fail("Configuration failed");
2635
3137
  throw err;
2636
3138
  }
2637
- const { drainPool } = await import("./pool-QGFQRMGU.js");
3139
+ const { drainPool } = await import("./pool-PIIKJ4L2.js");
2638
3140
  drainPool();
2639
- const tokenPath = path3.join(os.homedir(), ".clawops", "secrets", `GATEWAY_TOKEN_${opts.stackName}`);
3141
+ const tokenPath = path2.join(os.homedir(), ".clawops", "secrets", `GATEWAY_TOKEN_${opts.stackName}`);
2640
3142
  const dashboardUrl = `${state.gatewayUrl}?token=${opts.gatewayToken}`;
2641
- process20.stdout.write("\n");
3143
+ process21.stdout.write("\n");
2642
3144
  success("All done! OpenClaw is running.");
2643
3145
  info(`Open dashboard: ${dashboardUrl}`);
2644
3146
  info(` (or enter the token manually in the "Gateway Token" field: ${opts.gatewayToken})`);
@@ -2693,8 +3195,8 @@ ${cfg.loginHint}`);
2693
3195
  default: true
2694
3196
  }]);
2695
3197
  if (runNow) {
2696
- process20.stdout.write("\n");
2697
- const result = spawnSync(cfg.loginCmd[0], cfg.loginCmd.slice(1), { stdio: "inherit" });
3198
+ process21.stdout.write("\n");
3199
+ const result = spawnSync2(cfg.loginCmd[0], cfg.loginCmd.slice(1), { stdio: "inherit" });
2698
3200
  if (result.error) {
2699
3201
  failure(`Could not launch ${cliName} \u2014 is it installed? (${result.error.message})`);
2700
3202
  info(`Install guide: ${cliInstallUrl(provider)}`);
@@ -2703,10 +3205,10 @@ ${cfg.loginHint}`);
2703
3205
  }
2704
3206
  try {
2705
3207
  const identity = cfg.check();
2706
- process20.stdout.write("\n");
3208
+ process21.stdout.write("\n");
2707
3209
  success(`Authenticated: ${cfg.identity(identity)}`);
2708
3210
  } catch {
2709
- process20.stdout.write("\n");
3211
+ process21.stdout.write("\n");
2710
3212
  failure(`Still not authenticated \u2014 the sign-in may have been cancelled or failed.`);
2711
3213
  info("Re-run `clawops setup` after signing in, or continue and authenticate before running `clawops up`.\n");
2712
3214
  }
@@ -2720,7 +3222,7 @@ function cliInstallUrl(provider) {
2720
3222
  return "https://learn.microsoft.com/en-us/cli/azure/install-azure-cli";
2721
3223
  }
2722
3224
  async function promptSecret(name, label, envDefault, inquirer) {
2723
- const secretsDir2 = path3.join(os.homedir(), ".clawops", "secrets");
3225
+ const secretsDir2 = path2.join(os.homedir(), ".clawops", "secrets");
2724
3226
  const { secretSource } = await inquirer.prompt([{
2725
3227
  type: "list",
2726
3228
  name: "secretSource",
@@ -2738,9 +3240,9 @@ async function promptSecret(name, label, envDefault, inquirer) {
2738
3240
  message: `Paste your ${label}: (input is hidden)`,
2739
3241
  validate: (v) => v.trim() !== "" || "Value cannot be empty"
2740
3242
  }]);
2741
- mkdirSync3(secretsDir2, { recursive: true });
2742
- spawnSync("chmod", ["700", secretsDir2], { stdio: "ignore" });
2743
- const secretPath2 = path3.join(secretsDir2, name);
3243
+ mkdirSync2(secretsDir2, { recursive: true });
3244
+ spawnSync2("chmod", ["700", secretsDir2], { stdio: "ignore" });
3245
+ const secretPath2 = path2.join(secretsDir2, name);
2744
3246
  writeFileSync3(secretPath2, secretValue.trim(), { encoding: "utf-8", mode: 384 });
2745
3247
  success(`Secret saved to ${secretPath2} (chmod 600)`);
2746
3248
  return { name, source: "file", ref: secretPath2 };
@@ -2772,7 +3274,7 @@ var DOCKER_CHECK_CMD = [
2772
3274
  "echo NOT_RUNNING"
2773
3275
  ].join("; ");
2774
3276
  async function checkDockerPreflight(opts) {
2775
- const { acquireSession } = await import("./pool-QGFQRMGU.js");
3277
+ const { acquireSession } = await import("./pool-PIIKJ4L2.js");
2776
3278
  const { ProviderError } = await import("./errors-OK47MQFD.js");
2777
3279
  const spin = spinner("Checking Docker on target host...");
2778
3280
  const timeoutSignal = AbortSignal.timeout(DOCKER_PREFLIGHT_TIMEOUT_MS);
@@ -2787,7 +3289,7 @@ async function checkDockerPreflight(opts) {
2787
3289
  });
2788
3290
  let out = "";
2789
3291
  try {
2790
- const result = await session.exec(DOCKER_CHECK_CMD, execSignal);
3292
+ const result = await execPrivileged(session, DOCKER_CHECK_CMD, execSignal);
2791
3293
  out = result.stdout.trim();
2792
3294
  } catch {
2793
3295
  spin.warn("Docker check timed out \u2014 proceeding anyway. Bootstrap will verify.");
@@ -2819,7 +3321,7 @@ async function checkDockerPreflight(opts) {
2819
3321
  var DOCKER_POLL_MS = 3e3;
2820
3322
  var DOCKER_TIMEOUT_MS = 9e4;
2821
3323
  async function startRemoteDocker(opts, spin) {
2822
- const { acquireSession } = await import("./pool-QGFQRMGU.js");
3324
+ const { acquireSession } = await import("./pool-PIIKJ4L2.js");
2823
3325
  const { ProviderError } = await import("./errors-OK47MQFD.js");
2824
3326
  spin.text = "Starting Docker on the remote server...";
2825
3327
  const { session, release } = await acquireSession({
@@ -2845,7 +3347,8 @@ async function startRemoteDocker(opts, spin) {
2845
3347
  while (Date.now() < deadline) {
2846
3348
  await new Promise((r) => setTimeout(r, DOCKER_POLL_MS));
2847
3349
  spin.text = `Waiting for Docker to start on ${opts.host}...`;
2848
- const check = await session.exec(
3350
+ const check = await execPrivileged(
3351
+ session,
2849
3352
  "docker version >/dev/null 2>&1 && echo OK || echo NOT_RUNNING",
2850
3353
  opts.signal
2851
3354
  );
@@ -2891,7 +3394,7 @@ async function pollUntilDockerReady(label) {
2891
3394
  throw new Error("Docker daemon did not become ready within timeout.");
2892
3395
  }
2893
3396
  async function startDockerAndWait(inquirer) {
2894
- const isLinux = process20.platform === "linux";
3397
+ const isLinux = process21.platform === "linux";
2895
3398
  const { choice } = await inquirer.prompt([{
2896
3399
  type: "list",
2897
3400
  name: "choice",
@@ -2915,7 +3418,7 @@ async function startDockerAndWait(inquirer) {
2915
3418
  await pollUntilDockerReady("Waiting for Docker daemon...");
2916
3419
  } else if (choice === "systemctl") {
2917
3420
  const spin = spinner("Running: sudo systemctl start docker...");
2918
- const res = spawnSync("sudo", ["systemctl", "start", "docker"], { stdio: "pipe" });
3421
+ const res = spawnSync2("sudo", ["systemctl", "start", "docker"], { stdio: "pipe" });
2919
3422
  if (res.status !== 0) {
2920
3423
  spin.fail("systemctl start docker failed.");
2921
3424
  throw new Error("systemctl start docker failed");
@@ -2923,18 +3426,18 @@ async function startDockerAndWait(inquirer) {
2923
3426
  spin.succeed("Docker service started.");
2924
3427
  await pollUntilDockerReady("Waiting for Docker daemon to be reachable...");
2925
3428
  } else if (choice === "desktop") {
2926
- const backendRunning = spawnSync("pgrep", ["-f", "com.docker.backend"], { stdio: "ignore" }).status === 0;
3429
+ const backendRunning = spawnSync2("pgrep", ["-f", "com.docker.backend"], { stdio: "ignore" }).status === 0;
2927
3430
  if (backendRunning) {
2928
3431
  const killSpin = spinner("Docker Desktop is stuck \u2014 force-restarting...");
2929
3432
  for (const pat of ["com.docker.backend", "com.docker.virtualization", "com.docker.build", "Docker Desktop"]) {
2930
- spawnSync("pkill", ["-9", "-f", pat], { stdio: "ignore" });
3433
+ spawnSync2("pkill", ["-9", "-f", pat], { stdio: "ignore" });
2931
3434
  }
2932
3435
  await new Promise((r) => setTimeout(r, 2e3));
2933
3436
  killSpin.succeed("Old Docker processes cleared.");
2934
3437
  }
2935
3438
  const openSpin = spinner("Launching Docker Desktop...");
2936
- let opened = spawnSync("open", ["-a", "Docker"], { stdio: "ignore" });
2937
- if (opened.status !== 0) opened = spawnSync("open", ["/Applications/Docker.app"], { stdio: "ignore" });
3439
+ let opened = spawnSync2("open", ["-a", "Docker"], { stdio: "ignore" });
3440
+ if (opened.status !== 0) opened = spawnSync2("open", ["/Applications/Docker.app"], { stdio: "ignore" });
2938
3441
  if (opened.status !== 0) {
2939
3442
  openSpin.fail("Could not open Docker Desktop \u2014 is it installed in /Applications?");
2940
3443
  throw new Error("Failed to open Docker Desktop");
@@ -2943,7 +3446,7 @@ async function startDockerAndWait(inquirer) {
2943
3446
  await pollUntilDockerReady("Waiting for Docker engine to start...");
2944
3447
  } else if (choice === "colima") {
2945
3448
  const spin = spinner("Running: colima start...");
2946
- const res = spawnSync("colima", ["start"], { stdio: "pipe" });
3449
+ const res = spawnSync2("colima", ["start"], { stdio: "pipe" });
2947
3450
  if (res.status !== 0) {
2948
3451
  spin.fail("colima start failed \u2014 is Colima installed? (brew install colima docker)");
2949
3452
  throw new Error("colima start failed");
@@ -2958,7 +3461,7 @@ async function ensureAuthorizedKey(keyPath, host, inquirer) {
2958
3461
  if (!existsSync2(pubKeyPath)) return;
2959
3462
  const pubKey = readFileSync3(pubKeyPath, "utf-8").trim();
2960
3463
  const isLocal = LOCALHOST_ALIASES.has(host.toLowerCase());
2961
- const authorizedKeysPath = path3.join(os.homedir(), ".ssh", "authorized_keys");
3464
+ const authorizedKeysPath = path2.join(os.homedir(), ".ssh", "authorized_keys");
2962
3465
  if (isLocal) {
2963
3466
  const existing = existsSync2(authorizedKeysPath) ? readFileSync3(authorizedKeysPath, "utf-8") : "";
2964
3467
  if (existing.includes(pubKey)) {
@@ -2973,26 +3476,26 @@ async function ensureAuthorizedKey(keyPath, host, inquirer) {
2973
3476
  default: true
2974
3477
  }]);
2975
3478
  if (addKey) {
2976
- mkdirSync3(path3.join(os.homedir(), ".ssh"), { recursive: true });
3479
+ mkdirSync2(path2.join(os.homedir(), ".ssh"), { recursive: true });
2977
3480
  const entry = existing.endsWith("\n") || existing === "" ? pubKey + "\n" : "\n" + pubKey + "\n";
2978
3481
  writeFileSync3(authorizedKeysPath, existing + entry, { encoding: "utf-8", flag: "a" });
2979
- spawnSync("chmod", ["600", authorizedKeysPath], { stdio: "ignore" });
3482
+ spawnSync2("chmod", ["600", authorizedKeysPath], { stdio: "ignore" });
2980
3483
  success("Public key added to ~/.ssh/authorized_keys.");
2981
3484
  } else {
2982
3485
  info("SSH will likely fail. Add this line to ~/.ssh/authorized_keys manually:");
2983
3486
  info(` ${pubKey}`);
2984
3487
  }
2985
3488
  } else {
2986
- process20.stdout.write("\n");
3489
+ process21.stdout.write("\n");
2987
3490
  info("Make sure this public key is in ~/.ssh/authorized_keys on your server:");
2988
3491
  info(` ${pubKey}`);
2989
3492
  info("(If you just created the server, paste the line above into the server's ~/.ssh/authorized_keys file.)\n");
2990
3493
  }
2991
3494
  }
2992
3495
  async function generateSshKey() {
2993
- const keyPath = path3.join(os.homedir(), ".ssh", "id_ed25519");
2994
- mkdirSync3(path3.join(os.homedir(), ".ssh"), { recursive: true });
2995
- const result = spawnSync("ssh-keygen", ["-t", "ed25519", "-f", keyPath, "-N", "", "-C", "clawops"], {
3496
+ const keyPath = path2.join(os.homedir(), ".ssh", "id_ed25519");
3497
+ mkdirSync2(path2.join(os.homedir(), ".ssh"), { recursive: true });
3498
+ const result = spawnSync2("ssh-keygen", ["-t", "ed25519", "-f", keyPath, "-N", "", "-C", "clawops"], {
2996
3499
  stdio: ["ignore", "pipe", "pipe"]
2997
3500
  });
2998
3501
  if (result.status !== 0) {
@@ -3006,27 +3509,27 @@ async function generateSshKey() {
3006
3509
  function detectSshKey() {
3007
3510
  const candidates = ["id_ed25519", "id_ecdsa", "id_rsa", "id_dsa"];
3008
3511
  for (const name of candidates) {
3009
- const p = path3.join(os.homedir(), ".ssh", name);
3512
+ const p = path2.join(os.homedir(), ".ssh", name);
3010
3513
  if (existsSync2(p)) return p;
3011
3514
  }
3012
- return path3.join(os.homedir(), ".ssh", "id_ed25519");
3515
+ return path2.join(os.homedir(), ".ssh", "id_ed25519");
3013
3516
  }
3014
3517
  function printMcpSnippet() {
3015
- process20.stdout.write("\n");
3016
- process20.stdout.write(JSON.stringify({
3518
+ process21.stdout.write("\n");
3519
+ process21.stdout.write(JSON.stringify({
3017
3520
  mcpServers: {
3018
3521
  clawops: { command: "clawops", args: ["mcp", "serve", "--read-only"] }
3019
3522
  }
3020
3523
  }, null, 2) + "\n\n");
3021
3524
  }
3022
3525
  async function maybeWireGatewayMcp(opts) {
3023
- process20.stdout.write("\n");
3526
+ process21.stdout.write("\n");
3024
3527
  const { wireGateway } = await inquirerPromptWire(opts.inquirer);
3025
3528
  if (!wireGateway) return;
3026
- const { connect } = await import("./ssh-CNJNLM3E.js");
3529
+ const { connect } = await import("./ssh-VAI5DU5M.js");
3027
3530
  const { getConfigDir: getConfigDir2 } = await import("./store-SDUR52Z5.js");
3028
- const { wireGatewayMcp } = await import("./mcp-wire-7E5JQY47.js");
3029
- const knownHostsPath = path3.join(getConfigDir2(), "known_hosts");
3531
+ const { wireGatewayMcp } = await import("./mcp-wire-3SWBM7D6.js");
3532
+ const knownHostsPath = path2.join(getConfigDir2(), "known_hosts");
3030
3533
  const spin = spinner("Wiring gateway MCP client...");
3031
3534
  const session = await connect({
3032
3535
  host: opts.host,
@@ -3040,8 +3543,7 @@ async function maybeWireGatewayMcp(opts) {
3040
3543
  const result = await wireGatewayMcp(session, opts.signal ?? AbortSignal.timeout(3e4));
3041
3544
  if (result.status === "unsupported") {
3042
3545
  spin.warn("This gateway has no `openclaw mcp add`, so nothing was wired.");
3043
- info("OpenClaw 2026.4.5 ships `openclaw mcp` with only `list` and `serve`.");
3044
- info("Upgrade to 2026.7.1-2 and run: clawops mcp wire --stack " + opts.stackName);
3546
+ info("Upgrade OpenClaw to 2026.7.1-2 or later and run: clawops mcp wire --stack " + opts.stackName);
3045
3547
  } else if (result.status === "probe-failed") {
3046
3548
  spin.warn("The gateway could not reach clawops, so nothing was wired.");
3047
3549
  info("clawops does not run on the gateway host. Start it where the gateway can reach it,");
@@ -3062,7 +3564,7 @@ async function maybeWireGatewayMcp(opts) {
3062
3564
  }
3063
3565
  }
3064
3566
  async function maybeWireGatewayMcpCloud(opts) {
3065
- process20.stdout.write("\n");
3567
+ process21.stdout.write("\n");
3066
3568
  const { wireGateway } = await inquirerPromptWire(opts.inquirer);
3067
3569
  if (!wireGateway) return;
3068
3570
  info(`Run this after the server is ready: clawops mcp wire --stack ${opts.stackName}`);
@@ -3078,8 +3580,8 @@ async function inquirerPromptWire(inquirer) {
3078
3580
  function loadCatalogs(yaml) {
3079
3581
  const specDir = resolveSpecDir();
3080
3582
  try {
3081
- const modelsRaw = yaml.load(readFileSync3(path3.join(specDir, "models.yaml"), "utf-8"));
3082
- const integrationsRaw = yaml.load(readFileSync3(path3.join(specDir, "integrations.yaml"), "utf-8"));
3583
+ const modelsRaw = yaml.load(readFileSync3(path2.join(specDir, "models.yaml"), "utf-8"));
3584
+ const integrationsRaw = yaml.load(readFileSync3(path2.join(specDir, "integrations.yaml"), "utf-8"));
3083
3585
  return {
3084
3586
  models: modelsRaw.providers,
3085
3587
  integrations: integrationsRaw.integrations
@@ -3100,6 +3602,21 @@ function defaultRegion(provider) {
3100
3602
  if (provider === "azure") return "eastus";
3101
3603
  return "";
3102
3604
  }
3605
+ function stateBucketQuestion(provider, account) {
3606
+ return {
3607
+ type: "input",
3608
+ name: "stateBucket",
3609
+ message: `${stateLabel(provider)} name: (where clawops records what it deployed \u2014 press Enter to accept)`,
3610
+ default: (answers) => {
3611
+ const derived = deriveStateBucket(provider, {
3612
+ account,
3613
+ region: answers.region ?? defaultRegion(provider)
3614
+ });
3615
+ return derived.ok ? derived.name : void 0;
3616
+ },
3617
+ validate: (v) => validateStateBucket(provider, v)
3618
+ };
3619
+ }
3103
3620
  function stateLabel(provider) {
3104
3621
  if (provider === "aws") return "S3";
3105
3622
  if (provider === "gcp") return "GCS";
@@ -3130,13 +3647,13 @@ function instanceChoices(provider) {
3130
3647
  return table[provider] ?? [];
3131
3648
  }
3132
3649
  async function maybeHarden(opts) {
3133
- const { MODULE_CATALOG, resolveModules, runHardening, formatHardenSummary } = await import("./harden-URPXLSVF.js");
3650
+ const { MODULE_CATALOG, resolveModules, runHardening, formatHardenSummary } = await import("./harden-72JMRT4E.js");
3134
3651
  const { getConfig: getConfig2 } = await import("./store-SDUR52Z5.js");
3135
3652
  const config = getConfig2();
3136
3653
  if (!config) return;
3137
3654
  const availableModules = resolveModules(MODULE_CATALOG, void 0, opts.provider);
3138
3655
  if (availableModules.length === 0) return;
3139
- process20.stdout.write("\n");
3656
+ process21.stdout.write("\n");
3140
3657
  const { selectedIds } = await opts.inquirer.prompt([{
3141
3658
  type: "checkbox",
3142
3659
  name: "selectedIds",
@@ -3152,9 +3669,9 @@ async function maybeHarden(opts) {
3152
3669
  return;
3153
3670
  }
3154
3671
  const modules = availableModules.filter((m) => selectedIds.includes(m.id));
3155
- process20.stdout.write("\nApplying hardening modules...\n\n");
3672
+ process21.stdout.write("\nApplying hardening modules...\n\n");
3156
3673
  try {
3157
- const { buildContext } = await import("./context-5LCUG2QR.js");
3674
+ const { buildContext } = await import("./context-7EVC6W45.js");
3158
3675
  const { extractBaseOutputs } = await import("./outputs-DJHBY7EE.js");
3159
3676
  const ctx = buildContext({ stack: opts.stackName });
3160
3677
  const stackObj = await ctx.getStack();
@@ -3173,11 +3690,11 @@ async function maybeHarden(opts) {
3173
3690
  modules,
3174
3691
  signal: opts.signal,
3175
3692
  onProgress: (r) => {
3176
- process20.stdout.write(` \xB7 ${r.module.label}
3693
+ process21.stdout.write(` \xB7 ${r.module.label}
3177
3694
  `);
3178
3695
  }
3179
3696
  });
3180
- process20.stdout.write("\n" + formatHardenSummary(results));
3697
+ process21.stdout.write("\n" + formatHardenSummary(results));
3181
3698
  const errors = results.filter((r) => r.error);
3182
3699
  if (errors.length > 0) {
3183
3700
  for (const r of errors) fail(`${r.module.label}: ${r.error}`);
@@ -3191,26 +3708,109 @@ async function maybeHarden(opts) {
3191
3708
  info("Run `clawops harden --stack " + opts.stackName + "` to retry.");
3192
3709
  }
3193
3710
  }
3711
+ async function detectSshCidrDefault() {
3712
+ const { detectEgressIp } = await import("./firewall-5VKZ4WBH.js");
3713
+ const result = await detectEgressIp("https://ifconfig.me/ip");
3714
+ if (!result.ok) return void 0;
3715
+ const ip = result.ip.trim();
3716
+ return ip === "" ? void 0 : ip.includes("/") ? ip : `${ip}/32`;
3717
+ }
3718
+ function validateCidrAnswer(value) {
3719
+ const v = value.trim();
3720
+ if (v === "") return "Required \u2014 enter a CIDR such as 203.0.113.4/32";
3721
+ const looksIpv6 = v.includes(":");
3722
+ if (!looksIpv6 && !/^(\d{1,3}\.){3}\d{1,3}\/(3[0-2]|[12]?\d)$/.test(v)) {
3723
+ return `"${v}" is not a CIDR. Use an address and a prefix, e.g. 203.0.113.4/32`;
3724
+ }
3725
+ return true;
3726
+ }
3727
+ function admitsInternet(cidr) {
3728
+ const v = cidr.trim();
3729
+ return v === "0.0.0.0/0" || v === "::/0";
3730
+ }
3731
+ function wizardChannels(integrations) {
3732
+ return integrations.filter((i) => i.wizardSupported !== false);
3733
+ }
3734
+ function startChannelConfig(integ) {
3735
+ return { ...integ.defaults ?? {} };
3736
+ }
3737
+ async function runAccountPreflight(opts) {
3738
+ const { getProvider } = await import("./providers-FW6H6OPF.js");
3739
+ let checks;
3740
+ try {
3741
+ const adapter = getProvider(opts.provider);
3742
+ if (!adapter.preflight) return;
3743
+ checks = await adapter.preflight({
3744
+ region: opts.region,
3745
+ bucket: opts.bucket,
3746
+ instanceType: opts.instanceType
3747
+ });
3748
+ } catch (err) {
3749
+ warn(`Could not check the ${opts.provider} account: ${err instanceof Error ? err.message : String(err)}`);
3750
+ return;
3751
+ }
3752
+ const unanswered = checks.filter((c) => c.unknown);
3753
+ const failed = checks.filter((c) => !c.ok && !c.unknown);
3754
+ if (unanswered.length > 0) {
3755
+ process21.stdout.write("\n");
3756
+ for (const check of unanswered) {
3757
+ warn(`Could not check: ${check.label}`);
3758
+ if (check.detail) info(` ${check.detail}`);
3759
+ }
3760
+ }
3761
+ if (failed.length === 0) {
3762
+ success(
3763
+ unanswered.length > 0 ? `${opts.provider} account is ready, as far as clawops could tell.` : `${opts.provider} account is ready.`
3764
+ );
3765
+ process21.stdout.write("\n");
3766
+ return;
3767
+ }
3768
+ process21.stdout.write("\n");
3769
+ for (const check of failed) {
3770
+ warn(check.label);
3771
+ if (check.detail) info(` ${check.detail}`);
3772
+ if (!check.fix) {
3773
+ continue;
3774
+ }
3775
+ const { apply } = await opts.inquirer.prompt([{
3776
+ type: "confirm",
3777
+ name: "apply",
3778
+ message: `Fix this now? ${check.mutates}`,
3779
+ default: true
3780
+ }]);
3781
+ if (!apply) {
3782
+ info(" Left as is. The deploy will fail until this is resolved.");
3783
+ continue;
3784
+ }
3785
+ try {
3786
+ await check.fix();
3787
+ success(` Done \u2014 ${check.mutates}`);
3788
+ } catch (err) {
3789
+ failure(` Could not fix it: ${err instanceof Error ? err.message : String(err)}`);
3790
+ }
3791
+ }
3792
+ process21.stdout.write("\n");
3793
+ }
3194
3794
 
3195
3795
  // src/cli/commands/secret.ts
3196
- import { defineCommand as defineCommand19 } from "citty";
3197
- import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync4, writeFileSync as writeFileSync4, unlinkSync, mkdirSync as mkdirSync4, statSync } from "fs";
3198
- import { spawnSync as spawnSync2 } from "child_process";
3199
- import path4 from "path";
3796
+ import { defineCommand as defineCommand20 } from "citty";
3797
+ import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync4, writeFileSync as writeFileSync4, unlinkSync, mkdirSync as mkdirSync3, statSync } from "fs";
3798
+ import { spawnSync as spawnSync3 } from "child_process";
3799
+ import path3 from "path";
3200
3800
  import os2 from "os";
3201
- import process21 from "process";
3202
- var SECRETS_DIR = path4.join(os2.homedir(), ".clawops", "secrets");
3801
+ import process22 from "process";
3802
+ var SECRETS_DIR = path3.join(os2.homedir(), ".clawops", "secrets");
3203
3803
  function secretsDir() {
3204
3804
  return SECRETS_DIR;
3205
3805
  }
3206
3806
  function secretPath(name) {
3207
- return path4.join(secretsDir(), name);
3807
+ return path3.join(secretsDir(), name);
3208
3808
  }
3209
3809
  function listSecretNames() {
3210
3810
  if (!existsSync3(secretsDir())) return [];
3211
3811
  return readdirSync(secretsDir()).filter((f) => !f.startsWith("."));
3212
3812
  }
3213
- var listCmd = defineCommand19({
3813
+ var listCmd = defineCommand20({
3214
3814
  meta: { name: "list", description: "List all stored secrets and their status" },
3215
3815
  args: {
3216
3816
  json: { type: "boolean", description: "Emit JSON" }
@@ -3235,7 +3835,7 @@ var listCmd = defineCommand19({
3235
3835
  }
3236
3836
  });
3237
3837
  if (args.json) {
3238
- process21.stdout.write(JSON.stringify(rows, null, 2) + "\n");
3838
+ process22.stdout.write(JSON.stringify(rows, null, 2) + "\n");
3239
3839
  return;
3240
3840
  }
3241
3841
  const tableRows = rows.map((r) => [
@@ -3244,10 +3844,10 @@ var listCmd = defineCommand19({
3244
3844
  r.modified,
3245
3845
  r.path
3246
3846
  ]);
3247
- process21.stdout.write("\n" + renderTable(["Name", "Status", "Modified", "Path"], tableRows) + "\n\n");
3847
+ process22.stdout.write("\n" + renderTable(["Name", "Status", "Modified", "Path"], tableRows) + "\n\n");
3248
3848
  }
3249
3849
  });
3250
- var setCmd = defineCommand19({
3850
+ var setCmd = defineCommand20({
3251
3851
  meta: { name: "set", description: "Create or update a secret" },
3252
3852
  args: {
3253
3853
  value: { type: "string", description: "Secret value (skips interactive prompt)" }
@@ -3256,7 +3856,7 @@ var setCmd = defineCommand19({
3256
3856
  const [name] = args._ ?? [];
3257
3857
  if (!name) {
3258
3858
  failure("Usage: clawops secret set <name>");
3259
- process21.exit(2);
3859
+ process22.exit(2);
3260
3860
  }
3261
3861
  const inquirer = (await import("inquirer")).default;
3262
3862
  let value;
@@ -3271,14 +3871,14 @@ var setCmd = defineCommand19({
3271
3871
  }]);
3272
3872
  value = secretValue;
3273
3873
  }
3274
- mkdirSync4(secretsDir(), { recursive: true });
3275
- spawnSync2("chmod", ["700", secretsDir()], { stdio: "ignore" });
3874
+ mkdirSync3(secretsDir(), { recursive: true });
3875
+ spawnSync3("chmod", ["700", secretsDir()], { stdio: "ignore" });
3276
3876
  writeFileSync4(secretPath(name), value.trim(), { encoding: "utf-8", mode: 384 });
3277
3877
  success(`Secret "${name}" saved to ${secretPath(name)} (chmod 600)`);
3278
3878
  info("Run `clawops secret rotate <name>` to propagate it to a running stack.");
3279
3879
  }
3280
3880
  });
3281
- var deleteCmd = defineCommand19({
3881
+ var deleteCmd = defineCommand20({
3282
3882
  meta: { name: "delete", description: "Remove a stored secret" },
3283
3883
  args: {
3284
3884
  yes: { type: "boolean", description: "Skip confirmation prompt" }
@@ -3287,12 +3887,12 @@ var deleteCmd = defineCommand19({
3287
3887
  const [name] = args._ ?? [];
3288
3888
  if (!name) {
3289
3889
  failure("Usage: clawops secret delete <name>");
3290
- process21.exit(2);
3890
+ process22.exit(2);
3291
3891
  }
3292
3892
  const p = secretPath(name);
3293
3893
  if (!existsSync3(p)) {
3294
3894
  failure(`Secret "${name}" not found at ${p}`);
3295
- process21.exit(1);
3895
+ process22.exit(1);
3296
3896
  }
3297
3897
  const { listOverlays } = await import("./overlay-store-ADZPD7EU.js");
3298
3898
  const referencingStacks = listOverlays().filter((o) => o.secrets.some((s) => s.name === name)).map((o) => o.stackName);
@@ -3317,7 +3917,7 @@ var deleteCmd = defineCommand19({
3317
3917
  success(`Secret "${name}" deleted.`);
3318
3918
  }
3319
3919
  });
3320
- var rotateCmd = defineCommand19({
3920
+ var rotateCmd = defineCommand20({
3321
3921
  meta: { name: "rotate", description: "Update a secret and re-apply config to a running stack" },
3322
3922
  args: {
3323
3923
  stack: { type: "string", description: "Stack name to re-apply (defaults to config default)" },
@@ -3327,7 +3927,7 @@ var rotateCmd = defineCommand19({
3327
3927
  const [name] = args._ ?? [];
3328
3928
  if (!name) {
3329
3929
  failure("Usage: clawops secret rotate <name> [--stack <name>]");
3330
- process21.exit(2);
3930
+ process22.exit(2);
3331
3931
  }
3332
3932
  const inquirer = (await import("inquirer")).default;
3333
3933
  let value;
@@ -3342,8 +3942,8 @@ var rotateCmd = defineCommand19({
3342
3942
  }]);
3343
3943
  value = secretValue;
3344
3944
  }
3345
- mkdirSync4(secretsDir(), { recursive: true });
3346
- spawnSync2("chmod", ["700", secretsDir()], { stdio: "ignore" });
3945
+ mkdirSync3(secretsDir(), { recursive: true });
3946
+ spawnSync3("chmod", ["700", secretsDir()], { stdio: "ignore" });
3347
3947
  writeFileSync4(secretPath(name), value.trim(), { encoding: "utf-8", mode: 384 });
3348
3948
  success(`Secret "${name}" updated.`);
3349
3949
  const { getConfig: getConfig2 } = await import("./store-SDUR52Z5.js");
@@ -3363,10 +3963,10 @@ var rotateCmd = defineCommand19({
3363
3963
  }
3364
3964
  info(`Re-applying config overlay to stack "${targetStack}"\u2026`);
3365
3965
  try {
3366
- const { buildContext } = await import("./context-5LCUG2QR.js");
3367
- const { readRemoteConfig, atomicWriteConfig, restartGateway, deepMerge } = await import("./remote-config-QDLN4UMG.js");
3966
+ const { buildContext } = await import("./context-7EVC6W45.js");
3967
+ const { readRemoteConfig, atomicWriteConfig, restartGateway, deepMerge } = await import("./remote-config-3HZLUWEA.js");
3368
3968
  const { resolveSecrets } = await import("./secrets-SVZWNAPK.js");
3369
- const { acquireSession, drainPool } = await import("./pool-QGFQRMGU.js");
3969
+ const { acquireSession, drainPool } = await import("./pool-PIIKJ4L2.js");
3370
3970
  const { localStateToConnectionInfo } = await import("./state-PRBIIN7I.js");
3371
3971
  const ctx = buildContext({ stack: targetStack });
3372
3972
  let conn;
@@ -3401,7 +4001,7 @@ var rotateCmd = defineCommand19({
3401
4001
  }
3402
4002
  }
3403
4003
  });
3404
- var auditCmd = defineCommand19({
4004
+ var auditCmd = defineCommand20({
3405
4005
  meta: { name: "audit", description: "Report missing secrets and unresolvable $secret: refs" },
3406
4006
  args: {
3407
4007
  json: { type: "boolean", description: "Emit JSON" }
@@ -3430,7 +4030,7 @@ var auditCmd = defineCommand19({
3430
4030
  }
3431
4031
  } else if (s.source === "env") {
3432
4032
  const envVar = s.ref ?? s.name;
3433
- if (!process21.env[envVar]) {
4033
+ if (!process22.env[envVar]) {
3434
4034
  issues.push({ kind: "missing-env", stack: overlay.stackName, secret: s.name, detail: `Env var not set: ${envVar}` });
3435
4035
  }
3436
4036
  } else {
@@ -3439,7 +4039,7 @@ var auditCmd = defineCommand19({
3439
4039
  }
3440
4040
  }
3441
4041
  if (args.json) {
3442
- process21.stdout.write(JSON.stringify({ issues, ok: issues.length === 0 }, null, 2) + "\n");
4042
+ process22.stdout.write(JSON.stringify({ issues, ok: issues.length === 0 }, null, 2) + "\n");
3443
4043
  return;
3444
4044
  }
3445
4045
  if (issues.length === 0) {
@@ -3452,11 +4052,11 @@ var auditCmd = defineCommand19({
3452
4052
  const prefix = issue.stack ? `[${issue.stack}] ` : "";
3453
4053
  warn(`${prefix}${issue.secret}: ${issue.detail}`);
3454
4054
  }
3455
- process21.stdout.write("\n");
4055
+ process22.stdout.write("\n");
3456
4056
  info("Run `clawops secret set <name>` to update a missing or empty secret.");
3457
4057
  }
3458
4058
  });
3459
- var secret_default = defineCommand19({
4059
+ var secret_default = defineCommand20({
3460
4060
  meta: {
3461
4061
  name: "secret",
3462
4062
  description: "Manage clawops secrets (list | set | delete | rotate | audit)"
@@ -3472,7 +4072,7 @@ var secret_default = defineCommand19({
3472
4072
  });
3473
4073
 
3474
4074
  // src/cli/commands/help.ts
3475
- import { defineCommand as defineCommand20 } from "citty";
4075
+ import { defineCommand as defineCommand21 } from "citty";
3476
4076
  var COMMANDS = [
3477
4077
  ["setup", "Interactive first-run wizard \u2014 configure, deploy, and wire AI apps"],
3478
4078
  ["init", "Write config and generate SSH key pair (non-interactive)"],
@@ -3504,7 +4104,7 @@ var GLOBAL_FLAGS = [
3504
4104
  ["--dry-run", "Preview without applying (mutating commands)"],
3505
4105
  ["--yes", "Skip interactive confirmations (CI mode)"]
3506
4106
  ];
3507
- var help_default = defineCommand20({
4107
+ var help_default = defineCommand21({
3508
4108
  meta: {
3509
4109
  name: "help",
3510
4110
  description: "Show available commands and global flags"
@@ -3544,16 +4144,16 @@ ${dim("Run `clawops <command> --help` for command-specific flags.")}
3544
4144
  });
3545
4145
 
3546
4146
  // src/cli/commands/bug.ts
3547
- import { defineCommand as defineCommand21 } from "citty";
3548
- import process22 from "process";
3549
- import { accessSync as accessSync2, constants as constants2 } from "fs";
4147
+ import { defineCommand as defineCommand22 } from "citty";
4148
+ import process23 from "process";
4149
+ import { accessSync, constants } from "fs";
3550
4150
  import { execSync as execSync2 } from "child_process";
3551
4151
  var ISSUES_URL = `${REPO_URL}/issues/new`;
3552
4152
  var BODY_CAP = 2e3;
3553
- var VERSION = true ? "1.7.9" : "dev";
4153
+ var VERSION = true ? "2.0.1" : "dev";
3554
4154
  async function collectContext() {
3555
- const nodeVersion = process22.version;
3556
- const os3 = `${process22.platform} ${process22.arch}`;
4155
+ const nodeVersion = process23.version;
4156
+ const os3 = `${process23.platform} ${process23.arch}`;
3557
4157
  let provider = "unknown";
3558
4158
  let stackCount = 0;
3559
4159
  let sshKey = "unknown";
@@ -3564,9 +4164,9 @@ async function collectContext() {
3564
4164
  const stacks = Object.values(config.stacks);
3565
4165
  stackCount = stacks.length;
3566
4166
  provider = stacks[0]?.provider ?? "none";
3567
- const keyPath = config.ssh.keyPath.replace(/^~/, process22.env["HOME"] ?? "~");
4167
+ const keyPath = config.ssh.keyPath.replace(/^~/, process23.env["HOME"] ?? "~");
3568
4168
  try {
3569
- accessSync2(keyPath, constants2.R_OK);
4169
+ accessSync(keyPath, constants.R_OK);
3570
4170
  sshKey = "present";
3571
4171
  } catch {
3572
4172
  sshKey = "missing";
@@ -3604,14 +4204,14 @@ function buildIssueUrl(title, command, context) {
3604
4204
  }
3605
4205
  function openBrowser2(url) {
3606
4206
  try {
3607
- const cmd = process22.platform === "darwin" ? "open" : "xdg-open";
4207
+ const cmd = process23.platform === "darwin" ? "open" : "xdg-open";
3608
4208
  execSync2(`${cmd} "${url}"`, { stdio: "ignore" });
3609
4209
  return true;
3610
4210
  } catch {
3611
4211
  return false;
3612
4212
  }
3613
4213
  }
3614
- var bug_default = defineCommand21({
4214
+ var bug_default = defineCommand22({
3615
4215
  meta: {
3616
4216
  name: "bug",
3617
4217
  description: "Open a pre-filled GitHub issue with system context from clawops doctor"
@@ -3625,7 +4225,7 @@ var bug_default = defineCommand21({
3625
4225
  let command = "";
3626
4226
  if (!args.json) {
3627
4227
  const { createInterface: createInterface3 } = await import("readline/promises");
3628
- const rl = createInterface3({ input: process22.stdin, output: process22.stdout });
4228
+ const rl = createInterface3({ input: process23.stdin, output: process23.stdout });
3629
4229
  try {
3630
4230
  title = (await rl.question("Describe the issue in one line: ")).trim();
3631
4231
  command = (await rl.question("Which command triggered it? (optional): ")).trim();
@@ -3638,12 +4238,12 @@ var bug_default = defineCommand21({
3638
4238
  printJson(jsonOk({ url }));
3639
4239
  return;
3640
4240
  }
3641
- process22.stdout.write("\n");
4241
+ process23.stdout.write("\n");
3642
4242
  info(`Opening: ${url}`);
3643
- process22.stdout.write("\n");
4243
+ process23.stdout.write("\n");
3644
4244
  const opened = openBrowser2(url);
3645
4245
  if (!opened) {
3646
- process22.stdout.write(`Open this URL to file the issue:
4246
+ process23.stdout.write(`Open this URL to file the issue:
3647
4247
  ${url}
3648
4248
  `);
3649
4249
  }
@@ -3651,9 +4251,9 @@ ${url}
3651
4251
  });
3652
4252
 
3653
4253
  // src/cli/commands/harden.ts
3654
- import { defineCommand as defineCommand22 } from "citty";
3655
- import process23 from "process";
3656
- var harden_default = defineCommand22({
4254
+ import { defineCommand as defineCommand23 } from "citty";
4255
+ import process24 from "process";
4256
+ var harden_default = defineCommand23({
3657
4257
  meta: {
3658
4258
  name: "harden",
3659
4259
  description: "Apply security hardening modules to a deployed stack"
@@ -3667,26 +4267,26 @@ var harden_default = defineCommand22({
3667
4267
  json: { type: "boolean", description: "Emit structured JSON output" }
3668
4268
  },
3669
4269
  async run({ args }) {
3670
- const { MODULE_CATALOG, resolveModules, runHardening, formatHardenSummary } = await import("./harden-URPXLSVF.js");
3671
- const { buildContext } = await import("./context-5LCUG2QR.js");
4270
+ const { MODULE_CATALOG, resolveModules, runHardening, formatHardenSummary } = await import("./harden-72JMRT4E.js");
4271
+ const { buildContext } = await import("./context-7EVC6W45.js");
3672
4272
  const { getConfig: getConfig2 } = await import("./store-SDUR52Z5.js");
3673
4273
  if (args.list) {
3674
- process23.stdout.write("\nAvailable hardening modules:\n\n");
4274
+ process24.stdout.write("\nAvailable hardening modules:\n\n");
3675
4275
  for (const mod of MODULE_CATALOG) {
3676
4276
  const on = mod.defaultOn ? chalk.green("ON ") : chalk.dim("off");
3677
4277
  const providers = mod.providers === "all" ? "all" : mod.providers.join(", ");
3678
- process23.stdout.write(
4278
+ process24.stdout.write(
3679
4279
  ` ${on} ${mod.id.padEnd(28)} ${mod.label} [${providers}]
3680
4280
  `
3681
4281
  );
3682
4282
  }
3683
- process23.stdout.write("\n");
4283
+ process24.stdout.write("\n");
3684
4284
  return;
3685
4285
  }
3686
4286
  const config = getConfig2();
3687
4287
  if (!config) {
3688
4288
  failure("No clawops config found. Run `clawops init` first.");
3689
- process23.exit(1);
4289
+ process24.exit(1);
3690
4290
  }
3691
4291
  const ctx = buildContext({ stack: args.stack });
3692
4292
  const provider = ctx.adapter.name;
@@ -3696,17 +4296,17 @@ var harden_default = defineCommand22({
3696
4296
  return;
3697
4297
  }
3698
4298
  if (!args.json) {
3699
- process23.stdout.write("\nModules to run:\n");
4299
+ process24.stdout.write("\nModules to run:\n");
3700
4300
  for (const mod of modules) {
3701
4301
  const flag = mod.defaultOn ? "" : chalk.yellow(" [opt-in]");
3702
- process23.stdout.write(` \xB7 ${mod.label}${flag}
4302
+ process24.stdout.write(` \xB7 ${mod.label}${flag}
3703
4303
  `);
3704
4304
  }
3705
- process23.stdout.write("\n");
4305
+ process24.stdout.write("\n");
3706
4306
  }
3707
4307
  if (!args["dry-run"] && !args.yes && !args.json) {
3708
4308
  const { createInterface: createInterface3 } = await import("readline/promises");
3709
- const rl = createInterface3({ input: process23.stdin, output: process23.stdout });
4309
+ const rl = createInterface3({ input: process24.stdin, output: process24.stdout });
3710
4310
  const answer = await rl.question("Apply these hardening modules? [y/N] ");
3711
4311
  rl.close();
3712
4312
  if (!answer.toLowerCase().startsWith("y")) {
@@ -3715,8 +4315,8 @@ var harden_default = defineCommand22({
3715
4315
  }
3716
4316
  }
3717
4317
  const ac = new AbortController();
3718
- process23.on("SIGINT", () => ac.abort());
3719
- process23.on("SIGTERM", () => ac.abort());
4318
+ process24.on("SIGINT", () => ac.abort());
4319
+ process24.on("SIGTERM", () => ac.abort());
3720
4320
  const { extractBaseOutputs } = await import("./outputs-DJHBY7EE.js");
3721
4321
  const stackObj = await ctx.getStack();
3722
4322
  const outputMap = await stackObj.outputs();
@@ -3730,7 +4330,7 @@ var harden_default = defineCommand22({
3730
4330
  knownHostsPath: config.ssh.knownHostsPath
3731
4331
  });
3732
4332
  if (!args.json) {
3733
- process23.stdout.write(args["dry-run"] ? "Checking hardening state...\n\n" : "Applying hardening...\n\n");
4333
+ process24.stdout.write(args["dry-run"] ? "Checking hardening state...\n\n" : "Applying hardening...\n\n");
3734
4334
  }
3735
4335
  const results = await runHardening(conn, {
3736
4336
  modules,
@@ -3738,7 +4338,7 @@ var harden_default = defineCommand22({
3738
4338
  signal: ac.signal,
3739
4339
  onProgress: args.json ? void 0 : (r) => {
3740
4340
  const icon = r.error ? chalk.red("\u2717") : r.applyResult?.changed ? chalk.green("\u2713") : chalk.dim("\xB7");
3741
- process23.stdout.write(` ${icon} ${r.module.label}
4341
+ process24.stdout.write(` ${icon} ${r.module.label}
3742
4342
  `);
3743
4343
  }
3744
4344
  });
@@ -3756,14 +4356,14 @@ var harden_default = defineCommand22({
3756
4356
  }));
3757
4357
  return;
3758
4358
  }
3759
- process23.stdout.write("\n" + formatHardenSummary(results));
4359
+ process24.stdout.write("\n" + formatHardenSummary(results));
3760
4360
  const errors = results.filter((r) => r.error);
3761
4361
  if (errors.length > 0) {
3762
- process23.stdout.write("\n");
4362
+ process24.stdout.write("\n");
3763
4363
  for (const r of errors) {
3764
4364
  failure(`${r.module.label}: ${r.error}`);
3765
4365
  }
3766
- process23.exit(1);
4366
+ process24.exit(1);
3767
4367
  }
3768
4368
  if (!args["dry-run"]) {
3769
4369
  success("Hardening complete.");
@@ -3772,26 +4372,26 @@ var harden_default = defineCommand22({
3772
4372
  });
3773
4373
 
3774
4374
  // src/cli/error-handler.ts
3775
- import process24 from "process";
4375
+ import process25 from "process";
3776
4376
  function handleError(err) {
3777
4377
  if (err instanceof ClawopsError) {
3778
4378
  failure(err.message);
3779
- process24.exit(err.exitCode);
4379
+ process25.exit(err.exitCode);
3780
4380
  }
3781
4381
  if (err instanceof Error) {
3782
4382
  failure(`Unexpected error: ${err.message}`);
3783
- if (process24.env["DEBUG"]) {
3784
- process24.stderr.write(err.stack ?? "\n");
4383
+ if (process25.env["DEBUG"]) {
4384
+ process25.stderr.write(err.stack ?? "\n");
3785
4385
  }
3786
- process24.exit(1);
4386
+ process25.exit(1);
3787
4387
  }
3788
4388
  failure(`Unknown error: ${String(err)}`);
3789
- process24.exit(1);
4389
+ process25.exit(1);
3790
4390
  }
3791
4391
 
3792
4392
  // src/cli/index.ts
3793
- var CLI_VERSION = true ? "1.7.9" : "dev";
3794
- var main = defineCommand23({
4393
+ var CLI_VERSION = true ? "2.0.1" : "dev";
4394
+ var main = defineCommand24({
3795
4395
  meta: {
3796
4396
  name: "clawops",
3797
4397
  description: "Deploy and manage self-hosted OpenClaw instances across clouds",
@@ -3819,6 +4419,7 @@ var main = defineCommand23({
3819
4419
  logs: logs_default,
3820
4420
  config: config_default,
3821
4421
  agents: agents_default,
4422
+ migrate: migrate_default,
3822
4423
  gateway: gateway_default,
3823
4424
  backup: backup_default,
3824
4425
  stacks: stacks_default,