@clawops/cli 1.7.9 → 2.0.0

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 (57) hide show
  1. package/README.md +412 -70
  2. package/dist/{apply-F3KY2HTB.js → apply-AM5HERE7.js} +8 -6
  3. package/dist/{aws-L4RR2LSE.js → aws-AMON7DA7.js} +20 -9
  4. package/dist/{azure-7OXQENQL.js → azure-T4G6OBJQ.js} +19 -8
  5. package/dist/{bootstrap-HL4DNUNG.js → bootstrap-DMRPWJGN.js} +28 -19
  6. package/dist/bootstrap.sh.tmpl +51 -31
  7. package/dist/channels-KBUR42IU.js +53 -0
  8. package/dist/{chunk-DQDAWNGB.js → chunk-3WYA2QDM.js} +1 -1
  9. package/dist/chunk-6TIYVHPX.js +152 -0
  10. package/dist/{chunk-3XK66DKK.js → chunk-AHAAEXWC.js} +41 -0
  11. package/dist/{chunk-GZN7PKP4.js → chunk-AX74VXTV.js} +292 -94
  12. package/dist/{chunk-C5X375RO.js → chunk-CKSPROD5.js} +59 -15
  13. package/dist/{chunk-UKTP332Z.js → chunk-GAZPPSKU.js} +6 -7
  14. package/dist/{chunk-FNS56UQY.js → chunk-GYV4KNW7.js} +319 -252
  15. package/dist/{chunk-PVUVI35Y.js → chunk-JW5A7E3X.js} +5 -1
  16. package/dist/chunk-LT4XMO5D.js +50 -0
  17. package/dist/{chunk-P5LKWRPL.js → chunk-LU2V3LXA.js} +3 -3
  18. package/dist/chunk-N44K3LHE.js +140 -0
  19. package/dist/chunk-NUDIHZEH.js +55 -0
  20. package/dist/chunk-SBLQH2TJ.js +56 -0
  21. package/dist/chunk-UFRO4H5W.js +228 -0
  22. package/dist/{chunk-A4QR4TXW.js → chunk-WHJMKDV7.js} +24 -4
  23. package/dist/chunk-YR3O2I3S.js +101 -0
  24. package/dist/cli.js +866 -577
  25. package/dist/config-validate-K2N6XLDX.js +76 -0
  26. package/dist/{context-5LCUG2QR.js → context-KHKWJJMC.js} +1 -1
  27. package/dist/{firewall-XQGO7HWK.js → firewall-T3NTPZJD.js} +10 -0
  28. package/dist/{gcp-UG6AWPIZ.js → gcp-2JBTD7ZM.js} +19 -8
  29. package/dist/{generate-OAGYT3VH.js → generate-2XV2GSP2.js} +4 -4
  30. package/dist/{harden-URPXLSVF.js → harden-PCDK3UQ5.js} +48 -11
  31. package/dist/health-5CCOXYGG.js +11 -0
  32. package/dist/logs-P7YCKVF4.js +15 -0
  33. package/dist/{mcp-wire-7E5JQY47.js → mcp-wire-BFEY2Y6H.js} +23 -17
  34. package/dist/{package-5AR4ZPK4.js → package-3HUW57XH.js} +3 -3
  35. package/dist/plugins-DTST3OER.js +49 -0
  36. package/dist/{pool-QGFQRMGU.js → pool-EX4ZTA5P.js} +2 -2
  37. package/dist/privileged-JFA6LLTV.js +17 -0
  38. package/dist/{remote-config-QDLN4UMG.js → remote-config-Z6IIZR3D.js} +3 -3
  39. package/dist/runtime-LZ7NA53V.js +42 -0
  40. package/dist/{server-FIZCOLHL.js → server-A5AB67GQ.js} +16 -12
  41. package/dist/spec-path-KQOOIQCA.js +9 -0
  42. package/dist/{ssh-CNJNLM3E.js → ssh-B3NZAU4I.js} +1 -1
  43. package/dist/upgrade-ULD4KFYC.js +88 -0
  44. package/dist/validate-BOQRWX4F.js +14 -0
  45. package/dist/{version-guard-VUCNZXQS.js → version-guard-GPSTCPT7.js} +3 -3
  46. package/dist/{versions-2UVAT6PQ.js → versions-W5LRQCAJ.js} +2 -2
  47. package/package.json +3 -3
  48. package/spec/deploy-plan.schema.json +114 -25
  49. package/spec/integrations.yaml +135 -28
  50. package/spec/mcp-tools.yaml +68 -35
  51. package/spec/models.yaml +9 -0
  52. package/spec/openclaw-2.0.config.schema.json +1 -0
  53. package/spec/openclaw-versions.yaml +116 -27
  54. package/dist/chunk-35WVQY3X.js +0 -122
  55. package/dist/chunk-6X45DDGH.js +0 -42
  56. package/dist/chunk-SEZMVVAS.js +0 -156
  57. package/dist/validate-CFQ4FH2T.js +0 -10
package/dist/cli.js CHANGED
@@ -6,21 +6,26 @@ import {
6
6
  } from "./chunk-OIGTOLB3.js";
7
7
  import {
8
8
  monitor_default,
9
- validateOpenclawConfig
10
- } from "./chunk-FNS56UQY.js";
11
- import "./chunk-DQDAWNGB.js";
9
+ runDiagnostics
10
+ } from "./chunk-GYV4KNW7.js";
12
11
  import {
13
- IMAGE_INSPECT_CMD,
14
- gatewayRunCommand,
15
- imageForRestart,
16
- versionOf
17
- } from "./chunk-SEZMVVAS.js";
18
- import "./chunk-P5LKWRPL.js";
19
- import "./chunk-EI7CQQ7D.js";
12
+ GATEWAY_LOGS_PROBE,
13
+ chooseLogSource,
14
+ containerLogsCommand,
15
+ gatewayLogsCommand
16
+ } from "./chunk-SBLQH2TJ.js";
17
+ import {
18
+ interpretProbe,
19
+ probeCommand
20
+ } from "./chunk-LT4XMO5D.js";
21
+ import {
22
+ execPrivileged,
23
+ execPrivilegedWithInput,
24
+ streamPrivileged
25
+ } from "./chunk-NUDIHZEH.js";
20
26
  import {
21
27
  resolveSpecDir
22
- } from "./chunk-PVUVI35Y.js";
23
- import "./chunk-3XK66DKK.js";
28
+ } from "./chunk-JW5A7E3X.js";
24
29
  import {
25
30
  REPO_URL,
26
31
  chalk,
@@ -31,7 +36,6 @@ import {
31
36
  success,
32
37
  warn
33
38
  } from "./chunk-Q7NQY5HV.js";
34
- import "./chunk-A2I76FTA.js";
35
39
  import {
36
40
  getConfig,
37
41
  getConfigDir,
@@ -42,9 +46,23 @@ import {
42
46
  ClawopsError,
43
47
  UsageError
44
48
  } from "./chunk-KGXPLI7W.js";
49
+ import {
50
+ CONFIG_FILENAME,
51
+ CONTAINER_UID,
52
+ GATEWAY_PORT,
53
+ IMAGE_INSPECT_CMD,
54
+ PUBLISH_INSPECT_CMD,
55
+ STATE_DIR_HOST_LINUX,
56
+ configPathForOS,
57
+ gatewayRunCommand,
58
+ imageForRestart,
59
+ publishForRestart,
60
+ versionOf
61
+ } from "./chunk-6TIYVHPX.js";
62
+ import "./chunk-EI7CQQ7D.js";
45
63
 
46
64
  // src/cli/index.ts
47
- import { defineCommand as defineCommand23, runMain } from "citty";
65
+ import { defineCommand as defineCommand24, runMain } from "citty";
48
66
 
49
67
  // src/cli/commands/init.ts
50
68
  import { defineCommand } from "citty";
@@ -213,11 +231,12 @@ var up_default = defineCommand2({
213
231
  "no-wait": { type: "boolean", description: "Return immediately without waiting for healthy state" },
214
232
  "openclaw-version": { type: "string", description: "OpenClaw release (e.g. 2026.7.1-2). Moving tags are resolved and range-checked" },
215
233
  stack: { type: "string", description: "Target stack name" },
216
- config: { type: "string", description: "Path to openclaw config overlay JSON (local provider only)" }
234
+ config: { type: "string", description: "Path to openclaw config overlay JSON (local provider only)" },
235
+ "gateway-port": { type: "string", description: `Host port to publish the gateway on (default ${GATEWAY_PORT}, local provider only \u2014 cloud stacks set spec.network.gatewayPort in the plan)` }
217
236
  },
218
237
  async run({ args }) {
219
- const { buildContext } = await import("./context-5LCUG2QR.js");
220
- const { guardOpenclawVersion, defaultOpenclawVersion } = await import("./version-guard-VUCNZXQS.js");
238
+ const { buildContext } = await import("./context-KHKWJJMC.js");
239
+ const { guardOpenclawVersion, defaultOpenclawVersion } = await import("./version-guard-GPSTCPT7.js");
221
240
  const ctx = buildContext(args);
222
241
  const requestedVersion = typeof args["openclaw-version"] === "string" ? args["openclaw-version"] : await defaultOpenclawVersion();
223
242
  const openclawVersion = await guardOpenclawVersion(requestedVersion);
@@ -229,7 +248,8 @@ var up_default = defineCommand2({
229
248
  );
230
249
  }
231
250
  const { localOpts } = stackConfig;
232
- const { localBootstrap } = await import("./bootstrap-HL4DNUNG.js");
251
+ const { localBootstrap } = await import("./bootstrap-DMRPWJGN.js");
252
+ const gatewayPort = parseGatewayPort(args["gateway-port"]);
233
253
  const abortController = new AbortController();
234
254
  process3.on("SIGINT", () => abortController.abort());
235
255
  process3.on("SIGTERM", () => abortController.abort());
@@ -243,6 +263,7 @@ var up_default = defineCommand2({
243
263
  knownHostsPath: ctx.config.ssh.knownHostsPath,
244
264
  openclawVersion,
245
265
  stackName: ctx.stackName,
266
+ gatewayPort,
246
267
  noWait: Boolean(args["no-wait"]),
247
268
  signal: abortController.signal
248
269
  });
@@ -322,9 +343,9 @@ var up_default = defineCommand2({
322
343
  }
323
344
  });
324
345
  async function applyLocalConfigOverlay(opts) {
325
- const { connect } = await import("./ssh-CNJNLM3E.js");
346
+ const { connect } = await import("./ssh-B3NZAU4I.js");
326
347
  const { resolveSecrets } = await import("./secrets-SVZWNAPK.js");
327
- const { readRemoteConfig, atomicWriteConfig, restartGateway, deepMerge } = await import("./remote-config-QDLN4UMG.js");
348
+ const { readRemoteConfig, atomicWriteConfig, restartGateway, deepMerge } = await import("./remote-config-Z6IIZR3D.js");
328
349
  let overlay;
329
350
  try {
330
351
  overlay = JSON.parse(readFileSync(opts.configPath, "utf-8"));
@@ -349,6 +370,14 @@ async function applyLocalConfigOverlay(opts) {
349
370
  session.close();
350
371
  }
351
372
  }
373
+ function parseGatewayPort(raw) {
374
+ if (typeof raw !== "string" || raw.trim() === "") return void 0;
375
+ const port = Number(raw);
376
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
377
+ throw new UsageError(`--gateway-port must be a port number between 1 and 65535, got "${raw}"`);
378
+ }
379
+ return port;
380
+ }
352
381
 
353
382
  // src/cli/commands/down.ts
354
383
  import { defineCommand as defineCommand3 } from "citty";
@@ -364,7 +393,7 @@ var down_default = defineCommand3({
364
393
  "dry-run": { type: "boolean", description: "Show what would be destroyed without destroying" }
365
394
  },
366
395
  async run({ args }) {
367
- const { buildContext } = await import("./context-5LCUG2QR.js");
396
+ const { buildContext } = await import("./context-KHKWJJMC.js");
368
397
  const ctx = buildContext(args);
369
398
  if (args["dry-run"]) {
370
399
  info(`Dry run \u2014 would destroy stack "${ctx.stackName}"`);
@@ -428,7 +457,7 @@ var status_default = defineCommand4({
428
457
  json: { type: "boolean", description: "Emit JSON" }
429
458
  },
430
459
  async run({ args }) {
431
- const { buildContext } = await import("./context-5LCUG2QR.js");
460
+ const { buildContext } = await import("./context-KHKWJJMC.js");
432
461
  const ctx = buildContext(args);
433
462
  if (ctx.adapter.name === "local") {
434
463
  const state = ctx.localState;
@@ -500,8 +529,8 @@ var plan_default = defineCommand5({
500
529
  out: { type: "string", description: "Write plan JSON to this absolute path (default: stdout)" }
501
530
  },
502
531
  async run({ args }) {
503
- const { buildContext } = await import("./context-5LCUG2QR.js");
504
- const { generatePlan } = await import("./generate-OAGYT3VH.js");
532
+ const { buildContext } = await import("./context-KHKWJJMC.js");
533
+ const { generatePlan } = await import("./generate-2XV2GSP2.js");
505
534
  const ctx = buildContext(args);
506
535
  if (ctx.adapter.name === "local") {
507
536
  throw new UsageError(
@@ -544,6 +573,7 @@ var plan_default = defineCommand5({
544
573
  const { spec, metadata } = plan;
545
574
  const sshCidrs = spec.network.allowedSshCidrs.join(", ") || "(none)";
546
575
  const gatewayCidrs = spec.network.allowedGatewayCidrs.join(", ") || "(none)";
576
+ const publish = spec.network.publishGateway ?? "loopback";
547
577
  process7.stderr.write(
548
578
  `
549
579
  Plan: ${metadata.name} (${spec.provider}${spec.region ? ` / ${spec.region}` : ""})
@@ -551,6 +581,7 @@ Plan: ${metadata.name} (${spec.provider}${spec.region ? ` / ${spec.region}` : "
551
581
  OpenClaw: ${spec.openclaw.version}
552
582
  SSH CIDRs: ${sshCidrs}
553
583
  Gateway: ${gatewayCidrs}
584
+ 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
585
  `
555
586
  );
556
587
  if (plan.diff) {
@@ -590,8 +621,8 @@ var apply_default = defineCommand6({
590
621
  "dry-run": { type: "boolean", description: "Validate plan and show diff without applying" }
591
622
  },
592
623
  async run({ args }) {
593
- const { validatePlan } = await import("./validate-CFQ4FH2T.js");
594
- const { applyPlan } = await import("./apply-F3KY2HTB.js");
624
+ const { validatePlan } = await import("./validate-BOQRWX4F.js");
625
+ const { applyPlan } = await import("./apply-AM5HERE7.js");
595
626
  const planPath = args._?.[0];
596
627
  if (!planPath) {
597
628
  throw new UsageError("Usage: clawops apply <plan.json>");
@@ -657,8 +688,8 @@ ${validation.errors.join("\n")}`);
657
688
  const spin = spinner(`Applying plan for stack "${typedPlan.spec.stackName}"\u2026`);
658
689
  try {
659
690
  const result = await applyPlan(typedPlan, {
660
- onOutput: (line) => {
661
- spin.text = line.trim() || spin.text;
691
+ onOutput: (line2) => {
692
+ spin.text = line2.trim() || spin.text;
662
693
  },
663
694
  signal: abortController.signal,
664
695
  confirmDrift: args.yes ? void 0 : async () => {
@@ -707,7 +738,7 @@ var destroy_default = defineCommand7({
707
738
  "dry-run": { type: "boolean", description: "Show what would be destroyed without destroying" }
708
739
  },
709
740
  async run({ args }) {
710
- const { buildContext } = await import("./context-5LCUG2QR.js");
741
+ const { buildContext } = await import("./context-KHKWJJMC.js");
711
742
  const ctx = buildContext(args);
712
743
  if (ctx.adapter.name === "local") {
713
744
  throw new UsageError(
@@ -774,8 +805,8 @@ var ssh_default = defineCommand8({
774
805
  command: { type: "string", description: "Remote command to run (instead of interactive shell)" }
775
806
  },
776
807
  async run({ args }) {
777
- const { buildContext } = await import("./context-5LCUG2QR.js");
778
- const { acquireSession, drainPool } = await import("./pool-QGFQRMGU.js");
808
+ const { buildContext } = await import("./context-KHKWJJMC.js");
809
+ const { acquireSession, drainPool } = await import("./pool-EX4ZTA5P.js");
779
810
  const ctx = buildContext(args);
780
811
  let conn;
781
812
  if (ctx.adapter.name === "local") {
@@ -861,9 +892,9 @@ var tunnel_default = defineCommand9({
861
892
  "no-open": { type: "boolean", description: "Do not open browser after tunnel is ready" }
862
893
  },
863
894
  async run({ args }) {
864
- const { buildContext } = await import("./context-5LCUG2QR.js");
895
+ const { buildContext } = await import("./context-KHKWJJMC.js");
865
896
  const { extractBaseOutputs } = await import("./outputs-DJHBY7EE.js");
866
- const { acquireSession } = await import("./pool-QGFQRMGU.js");
897
+ const { acquireSession } = await import("./pool-EX4ZTA5P.js");
867
898
  const ctx = buildContext(args);
868
899
  const stack = await ctx.getStack();
869
900
  const outputMap = await stack.outputs();
@@ -926,12 +957,13 @@ var logs_default = defineCommand10({
926
957
  stack: { type: "string", description: "Target stack name" },
927
958
  follow: { type: "boolean", alias: "f", description: "Follow log output" },
928
959
  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)" }
960
+ since: { type: "string", description: "Show logs since duration (e.g. 5m, 1h). Reads container output \u2014 the gateway log command has no time filter" },
961
+ json: { type: "boolean", description: "Emit the gateway's structured JSON log lines" }
930
962
  },
931
963
  async run({ args }) {
932
- const { buildContext } = await import("./context-5LCUG2QR.js");
964
+ const { buildContext } = await import("./context-KHKWJJMC.js");
933
965
  const { extractBaseOutputs } = await import("./outputs-DJHBY7EE.js");
934
- const { acquireSession, drainPool } = await import("./pool-QGFQRMGU.js");
966
+ const { acquireSession, drainPool } = await import("./pool-EX4ZTA5P.js");
935
967
  const ctx = buildContext(args);
936
968
  const stack = await ctx.getStack();
937
969
  const outputMap = await stack.outputs();
@@ -950,18 +982,7 @@ var logs_default = defineCommand10({
950
982
  });
951
983
  const tailLines = typeof args.tail === "string" ? parseInt(args.tail, 10) : 100;
952
984
  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(" ");
985
+ const since = typeof args.since === "string" ? args.since : void 0;
965
986
  const abortController = new AbortController();
966
987
  process12.on("SIGINT", () => abortController.abort());
967
988
  process12.on("SIGTERM", () => abortController.abort());
@@ -976,8 +997,13 @@ var logs_default = defineCommand10({
976
997
  });
977
998
  spin.stop();
978
999
  try {
1000
+ const probe = since ? { stdout: "" } : await execPrivileged(session, GATEWAY_LOGS_PROBE, abortController.signal);
1001
+ const choice = chooseLogSource({ since, gatewayReachable: probe.stdout.trim() === "ok" });
1002
+ const opts = { tail: tailLines, follow, since, json: Boolean(args.json) };
1003
+ const command = choice.source === "gateway" ? gatewayLogsCommand(opts) : containerLogsCommand(opts);
1004
+ info(`Logs: ${choice.source} \u2014 ${choice.reason}`);
979
1005
  if (follow) {
980
- const logStream = await session.stream(command, abortController.signal);
1006
+ const logStream = await streamPrivileged(session, command, abortController.signal);
981
1007
  logStream.pipe(process12.stdout);
982
1008
  await new Promise((resolve) => {
983
1009
  logStream.on("end", resolve);
@@ -985,7 +1011,7 @@ var logs_default = defineCommand10({
985
1011
  abortController.signal.addEventListener("abort", () => resolve(), { once: true });
986
1012
  });
987
1013
  } else {
988
- const result = await session.exec(command, abortController.signal);
1014
+ const result = await execPrivileged(session, command, abortController.signal);
989
1015
  process12.stdout.write(result.stdout);
990
1016
  if (result.stderr) process12.stderr.write(result.stderr);
991
1017
  }
@@ -999,10 +1025,9 @@ var logs_default = defineCommand10({
999
1025
  // src/cli/commands/config.ts
1000
1026
  import { defineCommand as defineCommand11 } from "citty";
1001
1027
  import process13 from "process";
1002
- var OPENCLAW_CONFIG = "/home/clawops/openclaw.json";
1003
- var OPENCLAW_TMP = "/tmp/clawops-config.json.tmp";
1028
+ var OPENCLAW_CONFIG = configPathForOS("Linux");
1004
1029
  function dockerRunCmd(image) {
1005
- return gatewayRunCommand({ image, configPath: OPENCLAW_CONFIG });
1030
+ return gatewayRunCommand({ image, stateDir: STATE_DIR_HOST_LINUX });
1006
1031
  }
1007
1032
  function getPath(obj, dotKey) {
1008
1033
  return dotKey.split(".").reduce((cur, k) => {
@@ -1042,9 +1067,9 @@ var config_default = defineCommand11({
1042
1067
  "dry-run": { type: "boolean", description: "Show what would change without writing" }
1043
1068
  },
1044
1069
  async run({ args }) {
1045
- const { buildContext } = await import("./context-5LCUG2QR.js");
1070
+ const { buildContext } = await import("./context-KHKWJJMC.js");
1046
1071
  const { extractBaseOutputs } = await import("./outputs-DJHBY7EE.js");
1047
- const { acquireSession, drainPool } = await import("./pool-QGFQRMGU.js");
1072
+ const { acquireSession, drainPool } = await import("./pool-EX4ZTA5P.js");
1048
1073
  const [action, key, value] = args._ ?? [];
1049
1074
  if (!action || !["get", "set", "unset", "validate"].includes(action)) {
1050
1075
  failure("Usage: clawops config <get [key] | set key value | unset key | validate>");
@@ -1100,12 +1125,19 @@ var config_default = defineCommand11({
1100
1125
  return;
1101
1126
  }
1102
1127
  if (action === "validate") {
1103
- const issues = validateOpenclawConfig(cfg);
1104
- if (issues.length === 0) {
1105
- success("Config is valid.");
1128
+ const { validateConfig } = await import("./config-validate-K2N6XLDX.js");
1129
+ const yaml = await import("./js-yaml-PTEEG4FO.js");
1130
+ const { loadVersionSpec } = await import("./versions-W5LRQCAJ.js");
1131
+ const spec = loadVersionSpec(yaml);
1132
+ const { errors, warnings } = await validateConfig(cfg, {
1133
+ schemaCapturedFrom: spec.runtime?.configSchemaCapturedFrom
1134
+ });
1135
+ for (const w of warnings) warn(w);
1136
+ if (errors.length === 0) {
1137
+ success(warnings.length === 0 ? "Config is valid." : "Config is valid, with notes.");
1106
1138
  } else {
1107
- for (const issue of issues) warn(issue);
1108
- failure(`Config has ${issues.length} issue(s).`);
1139
+ for (const e of errors) failure(e);
1140
+ failure(`Config has ${errors.length} error(s).`);
1109
1141
  process13.exit(1);
1110
1142
  }
1111
1143
  return;
@@ -1125,23 +1157,23 @@ var config_default = defineCommand11({
1125
1157
  process13.stdout.write(JSON.stringify(cfg, null, 2) + "\n");
1126
1158
  return;
1127
1159
  }
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}`);
1160
+ const { atomicWriteConfig } = await import("./remote-config-Z6IIZR3D.js");
1161
+ try {
1162
+ await atomicWriteConfig(session, cfg, abortController.signal);
1163
+ } catch (err) {
1164
+ failure(err.message);
1134
1165
  process13.exit(1);
1135
1166
  }
1136
1167
  success(`config ${action}: ${key ?? "(all)"}`);
1137
1168
  if (args.restart) {
1138
1169
  info("Restarting gateway...");
1139
- const imgResult = await session.exec(
1170
+ const imgResult = await execPrivileged(
1171
+ session,
1140
1172
  `docker inspect openclaw --format '{{.Config.Image}}' 2>/dev/null || echo 'ghcr.io/openclaw/openclaw:stable'`,
1141
1173
  abortController.signal
1142
1174
  );
1143
1175
  const image = imgResult.stdout.trim();
1144
- const restartResult = await session.exec(dockerRunCmd(image), abortController.signal);
1176
+ const restartResult = await execPrivileged(session, dockerRunCmd(image), abortController.signal);
1145
1177
  if (restartResult.code !== 0) {
1146
1178
  failure(`Restart failed: ${restartResult.stderr}`);
1147
1179
  process13.exit(1);
@@ -1161,19 +1193,27 @@ import process14 from "process";
1161
1193
  var agents_default = defineCommand12({
1162
1194
  meta: {
1163
1195
  name: "agents",
1164
- description: "Manage OpenClaw agents (list | restart [name] | logs <name>)"
1196
+ description: "Manage OpenClaw agents (list | logs <name>)"
1165
1197
  },
1166
1198
  args: {
1167
1199
  stack: { type: "string", description: "Target stack name" },
1168
- json: { type: "boolean", description: "Emit JSON (for list)" }
1200
+ json: { type: "boolean", description: "Emit JSON (for list and logs)" },
1201
+ limit: { type: "string", description: "Max activity records for `logs` (default 50)" },
1202
+ cursor: { type: "string", description: "Continue from a previous `logs` result cursor" }
1169
1203
  },
1170
1204
  async run({ args }) {
1171
- const { buildContext } = await import("./context-5LCUG2QR.js");
1205
+ const { buildContext } = await import("./context-KHKWJJMC.js");
1172
1206
  const { extractBaseOutputs } = await import("./outputs-DJHBY7EE.js");
1173
- const { acquireSession, drainPool } = await import("./pool-QGFQRMGU.js");
1207
+ const { acquireSession, drainPool } = await import("./pool-EX4ZTA5P.js");
1174
1208
  const [action, name] = args._ ?? [];
1175
- if (!action || !["list", "restart", "logs"].includes(action)) {
1176
- failure("Usage: clawops agents <list | restart [name] | logs <name>>");
1209
+ if (action === "restart") {
1210
+ failure(
1211
+ "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."
1212
+ );
1213
+ process14.exit(2);
1214
+ }
1215
+ if (!action || !["list", "logs"].includes(action)) {
1216
+ failure("Usage: clawops agents <list | logs <name>>");
1177
1217
  process14.exit(2);
1178
1218
  }
1179
1219
  if (action === "logs" && !name) {
@@ -1205,15 +1245,24 @@ var agents_default = defineCommand12({
1205
1245
  });
1206
1246
  try {
1207
1247
  if (action === "list") {
1208
- const result = await session.exec(
1209
- "docker exec openclaw openclaw agents list --json 2>/dev/null || echo '[]'",
1248
+ const result = await execPrivileged(
1249
+ session,
1250
+ "docker exec openclaw openclaw agents list --json",
1210
1251
  abortController.signal
1211
1252
  );
1212
- let agents = [];
1253
+ if (result.code !== 0) {
1254
+ const why = result.stderr.trim() || result.stdout.trim() || `exit ${result.code}`;
1255
+ failure(`Cannot list agents: ${why}`);
1256
+ process14.exitCode = 1;
1257
+ return;
1258
+ }
1259
+ let agents;
1213
1260
  try {
1214
- agents = JSON.parse(result.stdout.trim());
1261
+ agents = JSON.parse(result.stdout.trim() || "[]");
1215
1262
  } catch {
1216
- agents = [];
1263
+ failure(`Cannot list agents: unexpected output from OpenClaw: ${result.stdout.trim().slice(0, 200)}`);
1264
+ process14.exitCode = 1;
1265
+ return;
1217
1266
  }
1218
1267
  if (args.json) {
1219
1268
  printJson(jsonOk(agents));
@@ -1227,25 +1276,24 @@ var agents_default = defineCommand12({
1227
1276
  ) + "\n\n"
1228
1277
  );
1229
1278
  }
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
1279
  } else {
1239
- const logStream = await session.stream(
1240
- `docker exec -t openclaw openclaw agents logs ${name} --follow`,
1280
+ const { agentAuditCommand } = await import("./logs-P7YCKVF4.js");
1281
+ const limit = typeof args.limit === "string" ? parseInt(args.limit, 10) : 50;
1282
+ const result = await execPrivileged(
1283
+ session,
1284
+ agentAuditCommand({ agentId: name, limit, cursor: typeof args.cursor === "string" ? args.cursor : void 0 }),
1241
1285
  abortController.signal
1242
1286
  );
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
- });
1287
+ if (result.code !== 0) {
1288
+ failure(`Cannot read activity for "${name}": ${result.stderr.trim() || result.stdout.trim() || `exit ${result.code}`}`);
1289
+ process14.exitCode = 1;
1290
+ return;
1291
+ }
1292
+ if (args.json) {
1293
+ process14.stdout.write(result.stdout);
1294
+ } else {
1295
+ renderAgentRuns(name, result.stdout);
1296
+ }
1249
1297
  }
1250
1298
  } finally {
1251
1299
  release();
@@ -1253,35 +1301,285 @@ var agents_default = defineCommand12({
1253
1301
  }
1254
1302
  }
1255
1303
  });
1304
+ function renderAgentRuns(agentId, stdout) {
1305
+ let page;
1306
+ try {
1307
+ page = JSON.parse(stdout.trim() || "{}");
1308
+ } catch {
1309
+ failure(`Cannot read activity for "${agentId}": unexpected output: ${stdout.trim().slice(0, 200)}`);
1310
+ process14.exitCode = 1;
1311
+ return;
1312
+ }
1313
+ const records = page.records ?? [];
1314
+ if (records.length === 0) {
1315
+ info(`No recorded activity for agent "${agentId}".`);
1316
+ return;
1317
+ }
1318
+ process14.stdout.write(
1319
+ "\n" + renderTable(
1320
+ ["When", "Status", "Detail"],
1321
+ records.map((r) => [r.at ?? "\u2014", r.status ?? "\u2014", r.summary ?? r.message ?? "\u2014"])
1322
+ ) + "\n"
1323
+ );
1324
+ if (page.cursor) {
1325
+ info(`More records: clawops agents logs ${agentId} --cursor ${page.cursor}`);
1326
+ }
1327
+ process14.stdout.write("\n");
1328
+ }
1256
1329
 
1257
- // src/cli/commands/gateway.ts
1330
+ // src/cli/commands/migrate.ts
1258
1331
  import { defineCommand as defineCommand13 } from "citty";
1259
1332
  import process15 from "process";
1260
- var OPENCLAW_CONFIG2 = "/home/clawops/openclaw.json";
1261
- function dockerRunCmd2(version) {
1333
+
1334
+ // src/openclaw/migrate.ts
1335
+ async function migrate(steps) {
1336
+ const sourceImage = await steps.inspectSource();
1337
+ if (!sourceImage) {
1338
+ return {
1339
+ kind: "nothing-to-migrate",
1340
+ 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`."
1341
+ };
1342
+ }
1343
+ const identityBefore = await steps.deviceId();
1344
+ const backup = await steps.backup();
1345
+ if (!backup.ok) {
1346
+ return {
1347
+ kind: "refused",
1348
+ reason: `Refusing to migrate without a verified backup: ${backup.detail}
1349
+ This replaces the running container, and the state it extracts is the only copy.`
1350
+ };
1351
+ }
1352
+ const extracted = await steps.extract();
1353
+ if (!extracted.ok) {
1354
+ return {
1355
+ kind: "failed",
1356
+ reason: "Could not extract state from the running container; it has not been touched.",
1357
+ backupDetail: backup.detail
1358
+ };
1359
+ }
1360
+ await steps.chown();
1361
+ await steps.removeSource();
1362
+ await steps.writeConfig();
1363
+ await steps.start();
1364
+ let restarts = 0;
1365
+ let gate = await steps.gate();
1366
+ if (!gate.ok) {
1367
+ await steps.start();
1368
+ restarts = 1;
1369
+ gate = await steps.gate();
1370
+ }
1371
+ if (!gate.ok) {
1372
+ return {
1373
+ kind: "failed",
1374
+ reason: gate.reason ?? "the gateway did not start after migration",
1375
+ backupDetail: backup.detail
1376
+ };
1377
+ }
1378
+ const identityAfter = await steps.deviceId();
1379
+ const identity = identityBefore === void 0 || identityAfter === void 0 ? "unknown" : identityBefore === identityAfter ? "preserved" : "changed";
1380
+ return { kind: "migrated", entries: extracted.entries, identity, restarts };
1381
+ }
1382
+ function describeMigration(o) {
1383
+ switch (o.kind) {
1384
+ case "migrated": {
1385
+ const lines = [
1386
+ `Migrated to the 2.0 runtime contract. Carried over: ${o.entries.join(", ") || "(nothing)"}.`
1387
+ ];
1388
+ if (o.restarts > 0) {
1389
+ lines.push(
1390
+ "The gateway needed a second start to finish its state-schema migration, which is expected for a 1.x database."
1391
+ );
1392
+ }
1393
+ if (o.identity === "preserved") {
1394
+ lines.push("Device identity preserved \u2014 paired devices do not need re-pairing.");
1395
+ } else if (o.identity === "changed") {
1396
+ lines.push(
1397
+ "Device identity CHANGED. Paired devices and nodes must be re-paired \u2014 check `openclaw devices list` on the host for what was paired."
1398
+ );
1399
+ } else {
1400
+ lines.push(
1401
+ "Device identity could not be compared, so pairings may or may not have carried over. Check `openclaw devices list` before relying on them."
1402
+ );
1403
+ }
1404
+ lines.push("Your 1.x config was NOT applied \u2014 it never took effect on 1.x either. Review it");
1405
+ lines.push("and set what you need with `clawops config set`.");
1406
+ return lines.join("\n");
1407
+ }
1408
+ case "nothing-to-migrate":
1409
+ return o.reason;
1410
+ case "refused":
1411
+ return o.reason;
1412
+ case "failed":
1413
+ return `Migration failed: ${o.reason}
1414
+ A verified backup was taken first: ${o.backupDetail}
1415
+ Restore it with \`clawops backup restore --file <archive>\` before retrying.`;
1416
+ }
1417
+ }
1418
+
1419
+ // src/cli/commands/migrate.ts
1420
+ var SYNTHESISED_CONFIG = JSON.stringify({
1421
+ meta: { lastTouchedVersion: "2026.9.2" },
1422
+ gateway: { mode: "local", port: GATEWAY_PORT, auth: { mode: "token" } },
1423
+ models: {},
1424
+ channels: {}
1425
+ });
1426
+ var migrate_default = defineCommand13({
1427
+ meta: {
1428
+ name: "migrate",
1429
+ description: "Move a 1.x OpenClaw deployment onto the 2.0 runtime contract"
1430
+ },
1431
+ args: {
1432
+ stack: { type: "string", description: "Target stack name" },
1433
+ "openclaw-version": { type: "string", description: "OpenClaw 2.x version to migrate to" },
1434
+ yes: { type: "boolean", description: "Skip the confirmation prompt" }
1435
+ },
1436
+ async run({ args }) {
1437
+ const { buildContext } = await import("./context-KHKWJJMC.js");
1438
+ const { extractBaseOutputs } = await import("./outputs-DJHBY7EE.js");
1439
+ const { acquireSession, drainPool } = await import("./pool-EX4ZTA5P.js");
1440
+ const { guardOpenclawVersion, defaultOpenclawVersion } = await import("./version-guard-GPSTCPT7.js");
1441
+ const requested = typeof args["openclaw-version"] === "string" ? args["openclaw-version"] : void 0;
1442
+ const target = requested ? await guardOpenclawVersion(requested) : await defaultOpenclawVersion();
1443
+ const targetImage = `ghcr.io/openclaw/openclaw:${target}`;
1444
+ const ctx = buildContext(args);
1445
+ const stack = await ctx.getStack();
1446
+ const outputs = Object.fromEntries(
1447
+ Object.entries(await stack.outputs()).map(([k, v]) => [k, v.value])
1448
+ );
1449
+ const conn = ctx.adapter.getConnectionInfo({
1450
+ ...extractBaseOutputs(outputs),
1451
+ privateKeyPath: ctx.config.ssh.keyPath,
1452
+ knownHostsPath: ctx.config.ssh.knownHostsPath
1453
+ });
1454
+ const ac = new AbortController();
1455
+ process15.on("SIGINT", () => ac.abort());
1456
+ const { session, release } = await acquireSession({
1457
+ host: conn.host,
1458
+ port: conn.port,
1459
+ user: conn.user,
1460
+ privateKeyPath: conn.privateKeyPath,
1461
+ knownHostsPath: conn.knownHostsPath,
1462
+ signal: ac.signal
1463
+ });
1464
+ const spin = spinner("Inspecting the deployment...");
1465
+ const run = (cmd) => execPrivileged(session, cmd, ac.signal);
1466
+ const archive = "/tmp/clawops-premigration.tar.gz";
1467
+ const configPath = `${STATE_DIR_HOST_LINUX}/${CONFIG_FILENAME}`;
1468
+ const steps = {
1469
+ inspectSource: async () => {
1470
+ const r = await run(`docker inspect openclaw --format '{{.Config.Image}}'`);
1471
+ const image = r.stdout.trim();
1472
+ return r.code === 0 && image.includes(":") ? image : void 0;
1473
+ },
1474
+ backup: async () => {
1475
+ spin.text = "Taking a verified backup...";
1476
+ const r = await run(
1477
+ `docker exec openclaw openclaw backup create --output ${archive} --verify --json`
1478
+ );
1479
+ return r.code === 0 ? { ok: true, detail: `${archive} on the host (verified)` } : { ok: false, detail: (r.stderr || r.stdout).slice(0, 200) };
1480
+ },
1481
+ extract: async () => {
1482
+ spin.text = "Extracting state from the running container...";
1483
+ const mk = await run(`mkdir -p ${STATE_DIR_HOST_LINUX}`);
1484
+ if (mk.code !== 0) return { ok: false, entries: [] };
1485
+ const cp = await run(`docker cp openclaw:/home/node/.openclaw/. ${STATE_DIR_HOST_LINUX}/`);
1486
+ if (cp.code !== 0) return { ok: false, entries: [] };
1487
+ const ls = await run(`ls ${STATE_DIR_HOST_LINUX}`);
1488
+ return { ok: true, entries: ls.stdout.trim().split(/\s+/).filter(Boolean) };
1489
+ },
1490
+ chown: async () => {
1491
+ await run(`chown -R ${CONTAINER_UID}:${CONTAINER_UID} ${STATE_DIR_HOST_LINUX}`);
1492
+ },
1493
+ removeSource: async () => {
1494
+ spin.text = "Stopping the 1.x container...";
1495
+ await run("docker stop openclaw 2>/dev/null || true");
1496
+ await run("docker rm openclaw 2>/dev/null || true");
1497
+ },
1498
+ writeConfig: async () => {
1499
+ const b64 = Buffer.from(SYNTHESISED_CONFIG, "utf-8").toString("base64");
1500
+ await run(
1501
+ `echo '${b64}' | base64 -d > ${configPath} && chown ${CONTAINER_UID}:${CONTAINER_UID} ${configPath}`
1502
+ );
1503
+ },
1504
+ start: async () => {
1505
+ spin.text = "Starting the 2.0 gateway...";
1506
+ await run(gatewayRunCommand({ image: targetImage, stateDir: STATE_DIR_HOST_LINUX }));
1507
+ },
1508
+ gate: async () => {
1509
+ spin.text = "Waiting for the gateway to start...";
1510
+ let last;
1511
+ for (let i = 0; i < 15; i++) {
1512
+ if (ac.signal.aborted) return { ok: false, reason: "aborted" };
1513
+ const r = await run(probeCommand("started", GATEWAY_PORT));
1514
+ const v = interpretProbe("started", r.stdout);
1515
+ if (v.ok) return { ok: true };
1516
+ last = v.reason;
1517
+ await new Promise((res) => setTimeout(res, 2e3));
1518
+ }
1519
+ return { ok: false, reason: last ?? "no response" };
1520
+ },
1521
+ deviceId: async () => {
1522
+ const file = await run(
1523
+ `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`
1524
+ );
1525
+ try {
1526
+ const parsed = JSON.parse(file.stdout.trim());
1527
+ return parsed.deviceId;
1528
+ } catch {
1529
+ return void 0;
1530
+ }
1531
+ }
1532
+ };
1533
+ try {
1534
+ const outcome = await migrate(steps);
1535
+ spin.stop();
1536
+ const report = describeMigration(outcome);
1537
+ if (outcome.kind === "migrated") {
1538
+ success(report.split("\n")[0]);
1539
+ for (const line2 of report.split("\n").slice(1)) {
1540
+ if (/CHANGED|could not be compared/.test(line2)) warn(line2);
1541
+ else info(line2);
1542
+ }
1543
+ info(`Pre-migration backup: ${archive}`);
1544
+ } else {
1545
+ failure(report);
1546
+ process15.exit(outcome.kind === "nothing-to-migrate" ? 0 : 1);
1547
+ }
1548
+ } finally {
1549
+ release();
1550
+ drainPool();
1551
+ }
1552
+ }
1553
+ });
1554
+
1555
+ // src/cli/commands/gateway.ts
1556
+ import { defineCommand as defineCommand14 } from "citty";
1557
+ import process16 from "process";
1558
+ function dockerRunCmd2(version, publish = "loopback") {
1262
1559
  return gatewayRunCommand({
1263
1560
  image: `ghcr.io/openclaw/openclaw:${version}`,
1264
- configPath: OPENCLAW_CONFIG2
1561
+ stateDir: STATE_DIR_HOST_LINUX,
1562
+ publish
1265
1563
  });
1266
1564
  }
1267
- var gateway_default = defineCommand13({
1565
+ var gateway_default = defineCommand14({
1268
1566
  meta: {
1269
1567
  name: "gateway",
1270
1568
  description: "Manage the OpenClaw gateway daemon (status | restart | update [version])"
1271
1569
  },
1272
1570
  args: {
1273
1571
  stack: { type: "string", description: "Target stack name" },
1274
- channel: { type: "string", description: "Channel for update: stable | dev | <version>" },
1572
+ channel: { type: "string", description: "Version for update (a concrete pin; moving tags are refused)" },
1275
1573
  json: { type: "boolean", description: "Emit JSON (for status)" }
1276
1574
  },
1277
1575
  async run({ args }) {
1278
- const { buildContext } = await import("./context-5LCUG2QR.js");
1576
+ const { buildContext } = await import("./context-KHKWJJMC.js");
1279
1577
  const { extractBaseOutputs } = await import("./outputs-DJHBY7EE.js");
1280
- const { acquireSession, drainPool } = await import("./pool-QGFQRMGU.js");
1578
+ const { acquireSession, drainPool } = await import("./pool-EX4ZTA5P.js");
1281
1579
  const [action, versionArg] = args._ ?? [];
1282
1580
  if (!action || !["status", "restart", "update"].includes(action)) {
1283
1581
  failure("Usage: clawops gateway <status | restart | update [version]>");
1284
- process15.exit(2);
1582
+ process16.exit(2);
1285
1583
  }
1286
1584
  const ctx = buildContext(args);
1287
1585
  const stack = await ctx.getStack();
@@ -1296,8 +1594,8 @@ var gateway_default = defineCommand13({
1296
1594
  knownHostsPath: ctx.config.ssh.knownHostsPath
1297
1595
  });
1298
1596
  const abortController = new AbortController();
1299
- process15.on("SIGINT", () => abortController.abort());
1300
- process15.on("SIGTERM", () => abortController.abort());
1597
+ process16.on("SIGINT", () => abortController.abort());
1598
+ process16.on("SIGTERM", () => abortController.abort());
1301
1599
  const { session, release } = await acquireSession({
1302
1600
  host: conn.host,
1303
1601
  port: conn.port,
@@ -1318,7 +1616,7 @@ var gateway_default = defineCommand13({
1318
1616
  if (args.json) {
1319
1617
  printJson(jsonOk(status));
1320
1618
  } else {
1321
- process15.stdout.write(
1619
+ process16.stdout.write(
1322
1620
  "\n" + renderTable(
1323
1621
  ["Field", "Value"],
1324
1622
  [
@@ -1330,40 +1628,145 @@ var gateway_default = defineCommand13({
1330
1628
  );
1331
1629
  }
1332
1630
  } else if (action === "restart") {
1333
- const imgResult = await session.exec(IMAGE_INSPECT_CMD, abortController.signal);
1631
+ const imgResult = await execPrivileged(session, IMAGE_INSPECT_CMD, abortController.signal);
1334
1632
  const image = imageForRestart(imgResult.stdout);
1335
1633
  if (!image.ok) {
1336
1634
  failure(image.error);
1337
- process15.exit(1);
1635
+ process16.exit(1);
1338
1636
  }
1339
1637
  const version = versionOf(image.value);
1638
+ const pub = await execPrivileged(session, PUBLISH_INSPECT_CMD, abortController.signal);
1639
+ const publish = publishForRestart(pub.stdout);
1340
1640
  const spin = spinner("Restarting gateway...");
1341
- const result = await session.exec(dockerRunCmd2(version), abortController.signal);
1641
+ const result = await execPrivileged(
1642
+ session,
1643
+ dockerRunCmd2(version, publish),
1644
+ abortController.signal
1645
+ );
1342
1646
  spin.stop();
1343
1647
  if (result.code !== 0) {
1344
1648
  failure(`Restart failed: ${result.stderr}`);
1345
- process15.exit(1);
1649
+ process16.exit(1);
1346
1650
  }
1347
1651
  success(`Gateway restarted (${version}).`);
1348
1652
  } else {
1349
- const version = versionArg ?? args.channel ?? "stable";
1653
+ const { guardOpenclawVersion, defaultOpenclawVersion } = await import("./version-guard-GPSTCPT7.js");
1654
+ const requested = versionArg ?? args.channel;
1655
+ const version = requested ? await guardOpenclawVersion(requested) : await defaultOpenclawVersion();
1350
1656
  const spin = spinner(`Updating gateway to ${version}...`);
1351
- const pullResult = await session.exec(
1352
- `docker pull ghcr.io/openclaw/openclaw:${version}`,
1657
+ const targetImage = `ghcr.io/openclaw/openclaw:${version}`;
1658
+ const pullResult = await execPrivileged(
1659
+ session,
1660
+ `docker pull ${targetImage}`,
1353
1661
  abortController.signal
1354
1662
  );
1355
1663
  if (pullResult.code !== 0) {
1356
1664
  spin.stop();
1357
1665
  failure(`Pull failed: ${pullResult.stderr}`);
1358
- process15.exit(1);
1666
+ process16.exit(1);
1359
1667
  }
1360
- const runResult = await session.exec(dockerRunCmd2(version), abortController.signal);
1361
- spin.stop();
1668
+ spin.text = "Checking state compatibility...";
1669
+ const {
1670
+ snapshotCommand,
1671
+ snapshotPathFrom,
1672
+ preflightCommand,
1673
+ parsePreflight,
1674
+ judgePreflight
1675
+ } = await import("./upgrade-ULD4KFYC.js");
1676
+ const cur = imageForRestart(
1677
+ (await execPrivileged(session, IMAGE_INSPECT_CMD, abortController.signal)).stdout
1678
+ );
1679
+ const snapRepo = `${STATE_DIR_HOST_LINUX}/snapshots`;
1680
+ const snapOut = await execPrivileged(
1681
+ session,
1682
+ snapshotCommand(cur.ok ? cur.value : targetImage, STATE_DIR_HOST_LINUX, snapRepo),
1683
+ abortController.signal
1684
+ );
1685
+ const snapPath = snapshotPathFrom(snapOut.stdout);
1686
+ if (!snapPath) {
1687
+ spin.stop();
1688
+ failure(
1689
+ "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)
1690
+ );
1691
+ process16.exit(1);
1692
+ }
1693
+ const pre = await execPrivileged(
1694
+ session,
1695
+ preflightCommand(targetImage, STATE_DIR_HOST_LINUX, `${snapPath}/database.sqlite`),
1696
+ abortController.signal
1697
+ );
1698
+ const report = parsePreflight(pre.stdout);
1699
+ const verdict = report ? judgePreflight(report) : { ok: false, reason: `preflight produced no readable report: ${pre.stderr.slice(0, 200)}` };
1700
+ if (!verdict.ok) {
1701
+ spin.stop();
1702
+ failure(`Refusing to upgrade to ${version}: ${verdict.reason}`);
1703
+ info(`A snapshot of the current state was kept at ${snapPath}.`);
1704
+ process16.exit(1);
1705
+ }
1706
+ if (verdict.ok && verdict.note) info(verdict.note);
1707
+ spin.text = `Updating gateway to ${version}...`;
1708
+ const pubU = await execPrivileged(session, PUBLISH_INSPECT_CMD, abortController.signal);
1709
+ const runResult = await execPrivileged(
1710
+ session,
1711
+ dockerRunCmd2(version, publishForRestart(pubU.stdout)),
1712
+ abortController.signal
1713
+ );
1362
1714
  if (runResult.code !== 0) {
1715
+ spin.stop();
1363
1716
  failure(`Start failed: ${runResult.stderr}`);
1364
- process15.exit(1);
1717
+ info(`State snapshot from before the upgrade: ${snapPath}`);
1718
+ process16.exit(1);
1719
+ }
1720
+ spin.text = "Waiting for the gateway to start...";
1721
+ const { probeCommand: probeCommand2, interpretProbe: interpretProbe2 } = await import("./health-5CCOXYGG.js");
1722
+ const { repairCommand, describeOutcome } = await import("./upgrade-ULD4KFYC.js");
1723
+ const publish = publishForRestart(pubU.stdout);
1724
+ const gate = async () => {
1725
+ let last;
1726
+ for (let i = 0; i < 15; i++) {
1727
+ if (abortController.signal.aborted) return { ok: false, reason: "aborted" };
1728
+ const r = await execPrivileged(
1729
+ session,
1730
+ probeCommand2("started", GATEWAY_PORT),
1731
+ abortController.signal
1732
+ );
1733
+ const v = interpretProbe2("started", r.stdout);
1734
+ if (v.ok) return { ok: true };
1735
+ last = v.reason;
1736
+ await new Promise((res) => setTimeout(res, 2e3));
1737
+ }
1738
+ return { ok: false, reason: last ?? "no response" };
1739
+ };
1740
+ const { resolveUpgrade } = await import("./upgrade-ULD4KFYC.js");
1741
+ const outcome = await resolveUpgrade(
1742
+ {
1743
+ gate,
1744
+ repair: async () => {
1745
+ spin.text = "Gateway did not start; attempting one-shot repair...";
1746
+ await execPrivileged(
1747
+ session,
1748
+ repairCommand(targetImage, STATE_DIR_HOST_LINUX),
1749
+ abortController.signal
1750
+ );
1751
+ },
1752
+ run: async (v) => {
1753
+ await execPrivileged(session, dockerRunCmd2(v, publish), abortController.signal);
1754
+ }
1755
+ },
1756
+ {
1757
+ version,
1758
+ previousVersion: cur.ok ? versionOf(cur.value) : void 0,
1759
+ snapshotPath: snapPath
1760
+ }
1761
+ );
1762
+ spin.stop();
1763
+ const message = describeOutcome(outcome, version);
1764
+ if (outcome.kind === "started" || outcome.kind === "repaired") {
1765
+ success(message);
1766
+ } else {
1767
+ failure(message);
1768
+ process16.exit(1);
1365
1769
  }
1366
- success(`Gateway updated to ${version}.`);
1367
1770
  }
1368
1771
  } finally {
1369
1772
  release();
@@ -1373,11 +1776,11 @@ var gateway_default = defineCommand13({
1373
1776
  });
1374
1777
 
1375
1778
  // src/cli/commands/backup.ts
1376
- import { defineCommand as defineCommand14 } from "citty";
1779
+ import { defineCommand as defineCommand15 } from "citty";
1377
1780
  import { createWriteStream } from "fs";
1378
1781
  import { pipeline } from "stream/promises";
1379
- import process16 from "process";
1380
- var backup_default = defineCommand14({
1782
+ import process17 from "process";
1783
+ var backup_default = defineCommand15({
1381
1784
  meta: {
1382
1785
  name: "backup",
1383
1786
  description: "Create an OpenClaw backup (restore is unavailable on this release line)"
@@ -1390,8 +1793,8 @@ var backup_default = defineCommand14({
1390
1793
  yes: { type: "boolean", description: "[restore] Skip confirmation prompt" }
1391
1794
  },
1392
1795
  async run({ args }) {
1393
- const { buildContext } = await import("./context-5LCUG2QR.js");
1394
- const { acquireSession, drainPool } = await import("./pool-QGFQRMGU.js");
1796
+ const { buildContext } = await import("./context-KHKWJJMC.js");
1797
+ const { acquireSession, drainPool } = await import("./pool-EX4ZTA5P.js");
1395
1798
  const action = args.action;
1396
1799
  if (action !== "create" && action !== "restore") {
1397
1800
  throw new UsageError(`Unknown action: ${action}. Use "create" or "restore"`);
@@ -1402,7 +1805,7 @@ var backup_default = defineCommand14({
1402
1805
  const state = ctx.localState;
1403
1806
  if (!state) {
1404
1807
  failure("Stack has no state. Run `clawops up` first.");
1405
- process16.exit(4);
1808
+ process17.exit(4);
1406
1809
  }
1407
1810
  conn = {
1408
1811
  host: state.sshHost,
@@ -1420,7 +1823,7 @@ var backup_default = defineCommand14({
1420
1823
  );
1421
1824
  if (!outputs["publicIp"]) {
1422
1825
  failure("Stack has no outputs. Run `clawops up` first.");
1423
- process16.exit(4);
1826
+ process17.exit(4);
1424
1827
  }
1425
1828
  const base = extractBaseOutputs(outputs);
1426
1829
  conn = ctx.adapter.getConnectionInfo({
@@ -1430,8 +1833,8 @@ var backup_default = defineCommand14({
1430
1833
  });
1431
1834
  }
1432
1835
  const abortController = new AbortController();
1433
- process16.on("SIGINT", () => abortController.abort());
1434
- process16.on("SIGTERM", () => abortController.abort());
1836
+ process17.on("SIGINT", () => abortController.abort());
1837
+ process17.on("SIGTERM", () => abortController.abort());
1435
1838
  const { session, release } = await acquireSession({
1436
1839
  host: conn.host,
1437
1840
  port: conn.port,
@@ -1446,7 +1849,8 @@ var backup_default = defineCommand14({
1446
1849
  info(`Writing backup to ${outPath}...`);
1447
1850
  const spin = spinner("Creating backup on remote host...");
1448
1851
  const remoteArchive = "/tmp/clawops-backup.tar.gz";
1449
- const createResult = await session.exec(
1852
+ const createResult = await execPrivileged(
1853
+ session,
1450
1854
  `docker exec openclaw sh -lc 'rm -f ${remoteArchive} && openclaw backup create --output ${remoteArchive} --verify --json'`,
1451
1855
  abortController.signal
1452
1856
  );
@@ -1454,23 +1858,72 @@ var backup_default = defineCommand14({
1454
1858
  spin.stop();
1455
1859
  throw new Error(`Backup failed on the remote host: ${createResult.stderr.slice(0, 300)}`);
1456
1860
  }
1457
- const backupStream = await session.stream(
1861
+ const backupStream = await streamPrivileged(
1862
+ session,
1458
1863
  `docker exec openclaw cat ${remoteArchive}`,
1459
1864
  abortController.signal
1460
1865
  );
1461
1866
  spin.stop();
1462
- const fileStream = createWriteStream(outPath);
1867
+ const fileStream = createWriteStream(outPath, { mode: 384 });
1463
1868
  await pipeline(backupStream, fileStream);
1464
- await session.exec(
1869
+ await execPrivileged(
1870
+ session,
1465
1871
  `docker exec openclaw rm -f ${remoteArchive}`,
1466
1872
  abortController.signal
1467
1873
  );
1468
1874
  success(`Backup saved to ${outPath}`);
1469
- info("The archive contains credentials \u2014 store it accordingly.");
1875
+ info("Saved with mode 0600. The archive contains the state database \u2014 OAuth tokens,");
1876
+ info("secrets and device credentials, unencrypted. Treat it as a credential.");
1470
1877
  } 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."
1878
+ const file = typeof args.file === "string" ? args.file : void 0;
1879
+ if (!file) {
1880
+ throw new UsageError("Usage: clawops backup restore --file <archive.tar.gz>");
1881
+ }
1882
+ const { createReadStream } = await import("fs");
1883
+ const remoteArchive = "/tmp/clawops-restore.tar.gz";
1884
+ const staging = `/tmp/clawops-restored-${Date.now()}`;
1885
+ const spin = spinner("Uploading archive...");
1886
+ const uploadResult = await execPrivilegedWithInput(
1887
+ session,
1888
+ `docker exec -i openclaw sh -c 'cat > ${remoteArchive}'`,
1889
+ createReadStream(file),
1890
+ abortController.signal
1891
+ );
1892
+ if (uploadResult.code !== 0) {
1893
+ spin.stop();
1894
+ throw new Error(`Could not upload the archive: ${uploadResult.stderr.slice(0, 300)}`);
1895
+ }
1896
+ spin.text = "Verifying and restoring to a staging directory...";
1897
+ const restore = await execPrivileged(
1898
+ session,
1899
+ `docker exec openclaw openclaw backup restore ${remoteArchive} --target ${staging} --json`,
1900
+ abortController.signal
1473
1901
  );
1902
+ await execPrivileged(
1903
+ session,
1904
+ `docker exec openclaw rm -f ${remoteArchive}`,
1905
+ abortController.signal
1906
+ );
1907
+ spin.stop();
1908
+ if (restore.code !== 0) {
1909
+ throw new Error(`Restore failed: ${(restore.stderr || restore.stdout).slice(0, 400)}`);
1910
+ }
1911
+ let report = {};
1912
+ try {
1913
+ report = JSON.parse(restore.stdout.trim());
1914
+ } catch {
1915
+ info(restore.stdout);
1916
+ }
1917
+ success(`Archive verified and restored to ${staging} on the host.`);
1918
+ if (report.entryCount !== void 0) info(`${report.entryCount} entries restored.`);
1919
+ for (const w of report.warnings ?? []) warn(w);
1920
+ info("");
1921
+ info("Nothing has been activated. To adopt the restored state:");
1922
+ info(" 1. clawops ssh --command 'sudo docker stop openclaw'");
1923
+ info(` 2. replace the state directory contents with ${staging}`);
1924
+ info(" 3. clawops gateway restart");
1925
+ info("Provider plugins are not carried in the archive; re-run `clawops apply` to");
1926
+ info("reinstall them, or the gateway starts without its model providers.");
1474
1927
  }
1475
1928
  } finally {
1476
1929
  release();
@@ -1480,9 +1933,9 @@ var backup_default = defineCommand14({
1480
1933
  });
1481
1934
 
1482
1935
  // src/cli/commands/stacks.ts
1483
- import { defineCommand as defineCommand15 } from "citty";
1484
- import process17 from "process";
1485
- var stacks_default = defineCommand15({
1936
+ import { defineCommand as defineCommand16 } from "citty";
1937
+ import process18 from "process";
1938
+ var stacks_default = defineCommand16({
1486
1939
  meta: {
1487
1940
  name: "stacks",
1488
1941
  description: "Manage clawops stacks (list | delete <name>)"
@@ -1496,7 +1949,7 @@ var stacks_default = defineCommand15({
1496
1949
  const [action, name] = args._ ?? [];
1497
1950
  if (!action || !["list", "delete"].includes(action)) {
1498
1951
  failure("Usage: clawops stacks <list | delete <name>>");
1499
- process17.exit(2);
1952
+ process18.exit(2);
1500
1953
  }
1501
1954
  if (action === "list") {
1502
1955
  const config2 = requireConfig();
@@ -1519,7 +1972,7 @@ var stacks_default = defineCommand15({
1519
1972
  } else if (rows.length === 0) {
1520
1973
  info("No stacks configured.");
1521
1974
  } else {
1522
- process17.stdout.write(
1975
+ process18.stdout.write(
1523
1976
  "\n" + renderTable(
1524
1977
  ["Name", "Provider", "Region", "State URL"],
1525
1978
  rows
@@ -1531,7 +1984,7 @@ var stacks_default = defineCommand15({
1531
1984
  }
1532
1985
  if (!name) {
1533
1986
  failure("Usage: clawops stacks delete <name>");
1534
- process17.exit(2);
1987
+ process18.exit(2);
1535
1988
  }
1536
1989
  const config = requireConfig();
1537
1990
  if (!(name in config.stacks)) {
@@ -1551,7 +2004,7 @@ var stacks_default = defineCommand15({
1551
2004
  let isDeployed = false;
1552
2005
  if (!args.force) {
1553
2006
  try {
1554
- const { buildContext } = await import("./context-5LCUG2QR.js");
2007
+ const { buildContext } = await import("./context-KHKWJJMC.js");
1555
2008
  const ctx = buildContext({ stack: name });
1556
2009
  if (ctx.adapter.name === "local") {
1557
2010
  isDeployed = !!ctx.localState;
@@ -1571,7 +2024,7 @@ var stacks_default = defineCommand15({
1571
2024
  failure(
1572
2025
  `Stack "${name}" is still deployed. Run \`clawops down --stack ${name}\` first to destroy cloud resources, or pass --force to remove from registry only.`
1573
2026
  );
1574
- process17.exit(1);
2027
+ process18.exit(1);
1575
2028
  }
1576
2029
  warn(
1577
2030
  `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 +2052,7 @@ var stacks_default = defineCommand15({
1599
2052
  });
1600
2053
  async function confirm(message) {
1601
2054
  const { createInterface: createInterface3 } = await import("readline/promises");
1602
- const rl = createInterface3({ input: process17.stdin, output: process17.stdout });
2055
+ const rl = createInterface3({ input: process18.stdin, output: process18.stdout });
1603
2056
  try {
1604
2057
  const answer = await rl.question(`${message} (y/N) `);
1605
2058
  return answer.trim().toLowerCase() === "y";
@@ -1609,286 +2062,68 @@ async function confirm(message) {
1609
2062
  }
1610
2063
 
1611
2064
  // 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({
2065
+ import { defineCommand as defineCommand17 } from "citty";
2066
+ import process19 from "process";
2067
+ var NAME_COLUMN = 13;
2068
+ var doctor_default = defineCommand17({
1617
2069
  meta: {
1618
2070
  name: "doctor",
1619
2071
  description: "Check system prerequisites, config, SSH keys, and cloud credentials"
1620
2072
  },
1621
2073
  args: {
1622
- stack: { type: "string", description: "Stack name to include remote health checks" }
2074
+ stack: { type: "string", description: "Stack name to include remote health checks" },
2075
+ json: { type: "boolean", description: "Emit the report as JSON" }
1623
2076
  },
1624
2077
  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");
2078
+ const ac = new AbortController();
2079
+ const abort = () => ac.abort();
2080
+ process19.on("SIGINT", abort);
2081
+ process19.on("SIGTERM", abort);
2082
+ let report;
1638
2083
  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
- }
2084
+ report = await runDiagnostics({ stack: args.stack, signal: ac.signal });
2085
+ } finally {
2086
+ process19.off("SIGINT", abort);
2087
+ process19.off("SIGTERM", abort);
1669
2088
  }
1670
- process18.stdout.write("\nCredentials\n");
1671
- if (!config) {
1672
- info("Credential checks skipped \u2014 no config");
2089
+ if (args.json) {
2090
+ printJson(jsonOk(report));
1673
2091
  } 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
- }
2092
+ render(report);
1875
2093
  }
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
2094
+ if (!report.ok) {
2095
+ if (!args.json) {
2096
+ process19.stdout.write(
2097
+ `Run \`clawops bug\` to open a pre-filled issue at ${REPO_URL}/issues
1880
2098
 
1881
2099
  `
1882
- );
1883
- process18.exit(1);
2100
+ );
2101
+ }
2102
+ process19.exit(1);
1884
2103
  }
1885
2104
  }
1886
2105
  });
2106
+ function render(report) {
2107
+ process19.stdout.write("\nclawops doctor\n");
2108
+ for (const section of report.sections) {
2109
+ process19.stdout.write(`
2110
+ ${section.title}
2111
+ `);
2112
+ for (const check of section.checks) line(check);
2113
+ }
2114
+ process19.stdout.write("\n");
2115
+ }
2116
+ function line(check) {
2117
+ const label = check.detail ? `${check.name.padEnd(NAME_COLUMN)}${check.detail}` : check.name;
2118
+ const emit = { pass: success, fail: failure, warn, info }[check.status];
2119
+ emit(label);
2120
+ if (check.remedy) info(` ${check.remedy}`);
2121
+ }
1887
2122
 
1888
2123
  // src/cli/commands/mcp.ts
1889
- import { defineCommand as defineCommand17 } from "citty";
1890
- import process19 from "process";
1891
- var serveCmd = defineCommand17({
2124
+ import { defineCommand as defineCommand18 } from "citty";
2125
+ import process20 from "process";
2126
+ var serveCmd = defineCommand18({
1892
2127
  meta: {
1893
2128
  name: "serve",
1894
2129
  description: "Start the clawops MCP server"
@@ -1903,7 +2138,7 @@ var serveCmd = defineCommand17({
1903
2138
  inspector: { type: "boolean", description: "Enable MCP inspector" }
1904
2139
  },
1905
2140
  async run({ args }) {
1906
- const { serveMcp } = await import("./server-FIZCOLHL.js");
2141
+ const { serveMcp } = await import("./server-A5AB67GQ.js");
1907
2142
  await serveMcp({
1908
2143
  port: args.http ? Number(args.http) : void 0,
1909
2144
  bind: args.bind,
@@ -1915,7 +2150,7 @@ var serveCmd = defineCommand17({
1915
2150
  });
1916
2151
  }
1917
2152
  });
1918
- var installCmd = defineCommand17({
2153
+ var installCmd = defineCommand18({
1919
2154
  meta: {
1920
2155
  name: "install",
1921
2156
  description: "Interactively wire clawops into AI editors (Claude Desktop, Claude Code, Cursor, \u2026)"
@@ -1946,7 +2181,7 @@ var installCmd = defineCommand17({
1946
2181
  const selected = MCP_APPS2.filter((app) => selectedIds.includes(app.id));
1947
2182
  if (selected.length === 0) {
1948
2183
  info("No apps selected. Add this to an app's MCP config manually:");
1949
- process19.stdout.write(JSON.stringify({
2184
+ process20.stdout.write(JSON.stringify({
1950
2185
  mcpServers: { clawops: { command: mcpEntry.command, args: mcpEntry.args } }
1951
2186
  }, null, 2) + "\n\n");
1952
2187
  info("Config file locations:");
@@ -1969,7 +2204,7 @@ var installCmd = defineCommand17({
1969
2204
  }
1970
2205
  }
1971
2206
  });
1972
- var wireCmd = defineCommand17({
2207
+ var wireCmd = defineCommand18({
1973
2208
  meta: {
1974
2209
  name: "wire",
1975
2210
  description: "Wire the gateway AI as an MCP client of clawops (WO-28)"
@@ -1981,26 +2216,26 @@ var wireCmd = defineCommand17({
1981
2216
  rewire: { type: "boolean", description: "Replace an existing clawops entry" }
1982
2217
  },
1983
2218
  async run({ args }) {
1984
- const { buildContext } = await import("./context-5LCUG2QR.js");
1985
- const { acquireSession, drainPool } = await import("./pool-QGFQRMGU.js");
2219
+ const { buildContext } = await import("./context-KHKWJJMC.js");
2220
+ const { acquireSession, drainPool } = await import("./pool-EX4ZTA5P.js");
1986
2221
  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");
2222
+ const { wireGatewayMcp } = await import("./mcp-wire-BFEY2Y6H.js");
2223
+ const { MCP_HTTP_PORT } = await import("./server-A5AB67GQ.js");
1989
2224
  const ac = new AbortController();
1990
- process19.on("SIGINT", () => {
2225
+ process20.on("SIGINT", () => {
1991
2226
  ac.abort();
1992
- process19.exit(130);
2227
+ process20.exit(130);
1993
2228
  });
1994
- process19.on("SIGTERM", () => {
2229
+ process20.on("SIGTERM", () => {
1995
2230
  ac.abort();
1996
- process19.exit(143);
2231
+ process20.exit(143);
1997
2232
  });
1998
2233
  const ctx = buildContext({ stack: args.stack });
1999
2234
  let conn;
2000
2235
  if (ctx.adapter.name === "local") {
2001
2236
  if (!ctx.localState) {
2002
2237
  failure("Stack is not deployed. Run `clawops up` first.");
2003
- process19.exit(1);
2238
+ process20.exit(1);
2004
2239
  }
2005
2240
  const ls = ctx.localState;
2006
2241
  conn = { host: ls.sshHost, port: ls.sshPort, user: ls.sshUser, privateKeyPath: ls.privateKeyPath, knownHostsPath: ls.knownHostsPath };
@@ -2012,7 +2247,7 @@ var wireCmd = defineCommand17({
2012
2247
  );
2013
2248
  if (!outputs["publicIp"]) {
2014
2249
  failure("Stack has no outputs. Run `clawops up` first.");
2015
- process19.exit(1);
2250
+ process20.exit(1);
2016
2251
  }
2017
2252
  const base = extractBaseOutputs(outputs);
2018
2253
  conn = {
@@ -2036,7 +2271,7 @@ var wireCmd = defineCommand17({
2036
2271
  spin.fail("This gateway has no `openclaw mcp add`, so clawops cannot wire it.");
2037
2272
  info("OpenClaw 2026.4.5 ships `openclaw mcp` with only `list` and `serve`.");
2038
2273
  info("Upgrade the gateway to 2026.7.1-2 or later, then re-run this command.");
2039
- process19.exit(1);
2274
+ process20.exit(1);
2040
2275
  }
2041
2276
  if (result.status === "exists") {
2042
2277
  spin.info("The gateway already has a clawops MCP server configured.");
@@ -2049,7 +2284,7 @@ var wireCmd = defineCommand17({
2049
2284
  warn("clawops does not run on the gateway host. Start it where the gateway can reach it:");
2050
2285
  info(` clawops mcp serve --http ${MCP_HTTP_PORT} --bind 0.0.0.0 --token <token>`);
2051
2286
  info(`Then re-run: clawops mcp wire --url ${result.url} --token <token>`);
2052
- process19.exit(1);
2287
+ process20.exit(1);
2053
2288
  }
2054
2289
  spin.succeed(
2055
2290
  result.rewired ? "Re-wired the gateway to clawops \u2014 previous entry replaced." : "Gateway wired to clawops, and the connection was verified."
@@ -2060,14 +2295,14 @@ var wireCmd = defineCommand17({
2060
2295
  } catch (err) {
2061
2296
  spin.fail("Failed to wire gateway MCP client.");
2062
2297
  failure(err instanceof Error ? err.message : String(err));
2063
- process19.exit(1);
2298
+ process20.exit(1);
2064
2299
  } finally {
2065
2300
  release();
2066
2301
  drainPool();
2067
2302
  }
2068
2303
  }
2069
2304
  });
2070
- var mcp_default = defineCommand17({
2305
+ var mcp_default = defineCommand18({
2071
2306
  meta: {
2072
2307
  name: "mcp",
2073
2308
  description: "MCP server operations"
@@ -2081,14 +2316,14 @@ var mcp_default = defineCommand17({
2081
2316
  });
2082
2317
 
2083
2318
  // src/cli/commands/setup.ts
2084
- import { defineCommand as defineCommand18 } from "citty";
2085
- import process20 from "process";
2319
+ import { defineCommand as defineCommand19 } from "citty";
2320
+ import process21 from "process";
2086
2321
  import os from "os";
2087
2322
  import { randomBytes } from "crypto";
2088
- import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, existsSync as existsSync2, mkdirSync as mkdirSync3 } from "fs";
2323
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, existsSync as existsSync2, mkdirSync as mkdirSync2 } from "fs";
2089
2324
  import { execSync, spawnSync, spawn as spawn2 } from "child_process";
2090
- import path3 from "path";
2091
- var setup_default = defineCommand18({
2325
+ import path2 from "path";
2326
+ var setup_default = defineCommand19({
2092
2327
  meta: {
2093
2328
  name: "setup",
2094
2329
  description: "Interactive wizard: configure and deploy an OpenClaw stack"
@@ -2103,16 +2338,16 @@ var setup_default = defineCommand18({
2103
2338
  const catalogs = loadCatalogs(yaml);
2104
2339
  const dryRun = Boolean(args["dry-run"]);
2105
2340
  const ac = new AbortController();
2106
- process20.on("SIGINT", () => {
2341
+ process21.on("SIGINT", () => {
2107
2342
  ac.abort();
2108
- process20.exit(130);
2343
+ process21.exit(130);
2109
2344
  });
2110
- process20.on("SIGTERM", () => {
2345
+ process21.on("SIGTERM", () => {
2111
2346
  ac.abort();
2112
- process20.exit(143);
2347
+ process21.exit(143);
2113
2348
  });
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");
2349
+ process21.stdout.write("\nWelcome to clawops setup. This wizard will help you deploy OpenClaw\n");
2350
+ process21.stdout.write("and connect it to your AI tools. It takes about 2 minutes.\n\n");
2116
2351
  const { deploymentType } = await inquirer.prompt([{
2117
2352
  type: "list",
2118
2353
  name: "deploymentType",
@@ -2140,7 +2375,7 @@ var setup_default = defineCommand18({
2140
2375
  ]
2141
2376
  }]);
2142
2377
  provider = answer.provider;
2143
- process20.stdout.write("\n");
2378
+ process21.stdout.write("\n");
2144
2379
  await ensureCloudAuth(provider, inquirer);
2145
2380
  } else {
2146
2381
  provider = "local";
@@ -2196,7 +2431,8 @@ var setup_default = defineCommand18({
2196
2431
  }]);
2197
2432
  localSudoPassword = sudoPass;
2198
2433
  }
2199
- process20.stdout.write("\n");
2434
+ process21.stdout.write("\n");
2435
+ const sshCidrDefault = provider === "local" ? void 0 : await detectSshCidrDefault();
2200
2436
  const stackAnswers = await inquirer.prompt([
2201
2437
  {
2202
2438
  type: "input",
@@ -2233,8 +2469,13 @@ var setup_default = defineCommand18({
2233
2469
  {
2234
2470
  type: "input",
2235
2471
  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"
2472
+ 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)",
2473
+ // Defaulted to 0.0.0.0/0 until 2.0, so pressing Enter opened SSH to the whole
2474
+ // internet on the path most first-time users take. N10 forbids exactly that
2475
+ // default. When the egress IP cannot be detected there is no default at all and
2476
+ // the answer is required — an unanswerable prompt is better than a wide one.
2477
+ ...sshCidrDefault ? { default: sshCidrDefault } : {},
2478
+ validate: validateCidrAnswer
2238
2479
  }
2239
2480
  ] : [],
2240
2481
  {
@@ -2244,7 +2485,12 @@ var setup_default = defineCommand18({
2244
2485
  default: "latest"
2245
2486
  }
2246
2487
  ]);
2247
- process20.stdout.write("\n");
2488
+ if (provider !== "local" && admitsInternet(stackAnswers.sshCidr)) {
2489
+ warn(
2490
+ "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."
2491
+ );
2492
+ }
2493
+ process21.stdout.write("\n");
2248
2494
  info("Choose the AI model that your OpenClaw agent will use.");
2249
2495
  info("You'll need an API key from your chosen provider (except Bedrock and Ollama).\n");
2250
2496
  const { modelProviderId } = await inquirer.prompt([{
@@ -2273,7 +2519,7 @@ var setup_default = defineCommand18({
2273
2519
  secrets.push(entry);
2274
2520
  modelConfig["apiKey"] = `$secret:${secretName}`;
2275
2521
  } else if (modelProvider.credentialSource === "aws-profile") {
2276
- process20.stdout.write("\n");
2522
+ process21.stdout.write("\n");
2277
2523
  info(modelProvider.iamNote ?? "Bedrock uses your AWS server's IAM role \u2014 no API key needed.");
2278
2524
  info("Make sure your EC2 instance has the AmazonBedrockFullAccess IAM policy attached.\n");
2279
2525
  } else if (modelProvider.id === "ollama") {
@@ -2294,13 +2540,13 @@ var setup_default = defineCommand18({
2294
2540
  ...selectedModel.modelId ? { modelId: selectedModel.modelId } : { model: selectedModel.id },
2295
2541
  ...modelConfig
2296
2542
  };
2297
- process20.stdout.write("\n");
2543
+ process21.stdout.write("\n");
2298
2544
  info("Use \u2191\u2193 to move, Space to select/deselect, Enter to confirm.");
2299
2545
  const { selectedIntegrationIds } = await inquirer.prompt([{
2300
2546
  type: "checkbox",
2301
2547
  name: "selectedIntegrationIds",
2302
2548
  message: "Which chat integrations would you like to enable?",
2303
- choices: catalogs.integrations.map((integ) => ({
2549
+ choices: wizardChannels(catalogs.integrations).map((integ) => ({
2304
2550
  name: `${integ.displayName} \u2014 ${integ.description}`,
2305
2551
  value: integ.id,
2306
2552
  checked: false
@@ -2310,9 +2556,9 @@ var setup_default = defineCommand18({
2310
2556
  const channelsConfig = {};
2311
2557
  const infraRequired = [];
2312
2558
  for (const integ of catalogs.integrations.filter((i) => selectedIntegrationIds.includes(i.id))) {
2313
- process20.stdout.write("\n");
2559
+ process21.stdout.write("\n");
2314
2560
  info(`Setting up ${integ.displayName}:`);
2315
- const channelConfig = {};
2561
+ const channelConfig = startChannelConfig(integ);
2316
2562
  for (const field of integ.fields) {
2317
2563
  if (field.sensitive && field.envDefault) {
2318
2564
  const entry = await promptSecret(field.envDefault, `${integ.displayName} ${field.label}`, field.envDefault, inquirer);
@@ -2328,6 +2574,15 @@ var setup_default = defineCommand18({
2328
2574
  channelConfig[field.name] = value;
2329
2575
  }
2330
2576
  }
2577
+ if (integ.useEnvSupported === false) {
2578
+ warn(`${integ.displayName} has no non-interactive setup in OpenClaw.`);
2579
+ info(` Run \`openclaw channels add --channel ${integ.channelKey}\` on the host, interactively.`);
2580
+ } else if (integ.plugin?.source === "bundled") {
2581
+ info(`${integ.displayName} ships in the image. Activate it on the host with:`);
2582
+ info(` openclaw channels add --channel ${integ.channelKey} --use-env`);
2583
+ } else if (integ.plugin?.package) {
2584
+ info(`${integ.displayName} needs ${integ.plugin.package}; clawops installs it during apply.`);
2585
+ }
2331
2586
  channelsConfig[integ.channelKey] = channelConfig;
2332
2587
  if (integ.infraRequired) infraRequired.push(integ);
2333
2588
  }
@@ -2346,10 +2601,10 @@ var setup_default = defineCommand18({
2346
2601
  }
2347
2602
  }
2348
2603
  const gatewayToken = randomBytes(24).toString("hex");
2349
- const secretsDir2 = path3.join(os.homedir(), ".clawops", "secrets");
2350
- mkdirSync3(secretsDir2, { recursive: true });
2604
+ const secretsDir2 = path2.join(os.homedir(), ".clawops", "secrets");
2605
+ mkdirSync2(secretsDir2, { recursive: true });
2351
2606
  spawnSync("chmod", ["700", secretsDir2], { stdio: "ignore" });
2352
- const tokenPath = path3.join(secretsDir2, `GATEWAY_TOKEN_${stackAnswers.stackName}`);
2607
+ const tokenPath = path2.join(secretsDir2, `GATEWAY_TOKEN_${stackAnswers.stackName}`);
2353
2608
  writeFileSync3(tokenPath, gatewayToken, { encoding: "utf-8", mode: 384 });
2354
2609
  const openclawConfigOverlay = {
2355
2610
  models: builtModelConfig,
@@ -2360,9 +2615,9 @@ var setup_default = defineCommand18({
2360
2615
  }
2361
2616
  let outputPath;
2362
2617
  if (provider === "local") {
2363
- outputPath = path3.join(outDir, `openclaw-${stackAnswers.stackName}.json`);
2618
+ outputPath = path2.join(outDir, `openclaw-${stackAnswers.stackName}.json`);
2364
2619
  writeFileSync3(outputPath, JSON.stringify(openclawConfigOverlay, null, 2), "utf-8");
2365
- process20.stdout.write("\n");
2620
+ process21.stdout.write("\n");
2366
2621
  success(`Config file saved to ${outputPath}`);
2367
2622
  } else {
2368
2623
  let sshPublicKey = "";
@@ -2370,7 +2625,7 @@ var setup_default = defineCommand18({
2370
2625
  sshPublicKey = readFileSync3(stackAnswers.sshKeyPath ?? "", "utf-8").trim();
2371
2626
  } catch {
2372
2627
  failure(`Cannot read SSH public key at ${stackAnswers.sshKeyPath ?? ""}`);
2373
- process20.exit(1);
2628
+ process21.exit(1);
2374
2629
  }
2375
2630
  const plan = {
2376
2631
  apiVersion: "clawops.dev/v1",
@@ -2392,19 +2647,26 @@ var setup_default = defineCommand18({
2392
2647
  },
2393
2648
  secrets,
2394
2649
  network: {
2395
- allowedSshCidrs: [stackAnswers.sshCidr ?? "0.0.0.0/0"],
2396
- allowedGatewayCidrs: [stackAnswers.sshCidr ?? "0.0.0.0/0"]
2650
+ // No `?? '0.0.0.0/0'`. The prompt requires an answer now, and a fallback that
2651
+ // opened the internet would reintroduce the default it replaced.
2652
+ allowedSshCidrs: [stackAnswers.sshCidr.trim()],
2653
+ // Empty, not the SSH CIDR. Reusing it opened a plaintext HTTP dashboard to
2654
+ // whatever network the operator picked for shell access — two very different
2655
+ // risks answered by one question. The gateway is reached with
2656
+ // `clawops tunnel`; exposing it is an explicit choice (network.publishGateway).
2657
+ allowedGatewayCidrs: [],
2658
+ publishGateway: "loopback"
2397
2659
  },
2398
2660
  ssh: { publicKey: sshPublicKey },
2399
2661
  ...stackAnswers.stateBucket ? { stateBucket: stackAnswers.stateBucket } : {}
2400
2662
  }
2401
2663
  };
2402
- outputPath = path3.join(outDir, `clawops-${stackAnswers.stackName}-plan.json`);
2664
+ outputPath = path2.join(outDir, `clawops-${stackAnswers.stackName}-plan.json`);
2403
2665
  writeFileSync3(outputPath, JSON.stringify(plan, null, 2), "utf-8");
2404
- process20.stdout.write("\n");
2666
+ process21.stdout.write("\n");
2405
2667
  success(`Deployment plan saved to ${outputPath}`);
2406
2668
  }
2407
- process20.stdout.write("\n");
2669
+ process21.stdout.write("\n");
2408
2670
  const mcpEntry = buildMcpEntry();
2409
2671
  if (!mcpEntry.resolved) {
2410
2672
  info("Note: clawops is not installed globally \u2014 AI apps will not be able to start");
@@ -2450,7 +2712,7 @@ var setup_default = defineCommand18({
2450
2712
  }
2451
2713
  }
2452
2714
  if (infraRequired.length > 0) {
2453
- process20.stdout.write("\n");
2715
+ process21.stdout.write("\n");
2454
2716
  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
2717
  info("These integrations need a webhook URL set up in their developer portal:");
2456
2718
  for (const integ of infraRequired) {
@@ -2461,7 +2723,7 @@ var setup_default = defineCommand18({
2461
2723
  }
2462
2724
  }
2463
2725
  if (modelProvider.id === "ollama" && selectedModel.pullSuffix !== void 0) {
2464
- process20.stdout.write("\n");
2726
+ process21.stdout.write("\n");
2465
2727
  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
2728
  info("After deployment, run this command on the server (or locally if Ollama is local):");
2467
2729
  info(` ollama pull ${selectedModel.id}${selectedModel.pullSuffix}`);
@@ -2506,8 +2768,8 @@ var setup_default = defineCommand18({
2506
2768
  });
2507
2769
  } else {
2508
2770
  const { getConfigDir: getConfigDir2 } = await import("./store-SDUR52Z5.js");
2509
- const knownHostsPath = path3.join(getConfigDir2(), "known_hosts");
2510
- process20.stdout.write("\n");
2771
+ const knownHostsPath = path2.join(getConfigDir2(), "known_hosts");
2772
+ process21.stdout.write("\n");
2511
2773
  info("To deploy later, run these two commands:");
2512
2774
  info(` clawops init --provider local --host ${localHost} --port ${localPort} --user ${localUser} --key ${localKeyPath} --stack ${stackAnswers.stackName}`);
2513
2775
  info(` clawops up --stack ${stackAnswers.stackName} --config ${outputPath}`);
@@ -2522,18 +2784,18 @@ var setup_default = defineCommand18({
2522
2784
  default: false
2523
2785
  }]);
2524
2786
  if (applyNow) {
2525
- const { applyPlan } = await import("./apply-F3KY2HTB.js");
2787
+ const { applyPlan } = await import("./apply-AM5HERE7.js");
2526
2788
  const plan = JSON.parse(readFileSync3(outputPath, "utf-8"));
2527
2789
  try {
2528
2790
  await applyPlan(plan, {
2529
- onOutput: (line) => process20.stdout.write(line),
2791
+ onOutput: (line2) => process21.stdout.write(line2),
2530
2792
  signal: ac.signal
2531
2793
  });
2532
2794
  success("Server provisioned and OpenClaw installed.");
2533
2795
  info(`Run: clawops doctor --stack ${stackAnswers.stackName} to verify everything is healthy`);
2534
2796
  } catch (err) {
2535
2797
  failure(err instanceof Error ? err.message : String(err));
2536
- process20.exit(1);
2798
+ process21.exit(1);
2537
2799
  }
2538
2800
  await maybeWireGatewayMcpCloud({ stackName: stackAnswers.stackName, signal: ac.signal, inquirer });
2539
2801
  await maybeHarden({
@@ -2551,11 +2813,11 @@ To deploy later: clawops apply ${outputPath}`);
2551
2813
  });
2552
2814
  async function runLocalDeploy(opts) {
2553
2815
  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");
2816
+ const { localBootstrap } = await import("./bootstrap-DMRPWJGN.js");
2817
+ const { connect } = await import("./ssh-B3NZAU4I.js");
2818
+ const { readRemoteConfig, atomicWriteConfig, restartGateway, deepMerge } = await import("./remote-config-Z6IIZR3D.js");
2557
2819
  const { resolveSecrets } = await import("./secrets-SVZWNAPK.js");
2558
- const knownHostsPath = path3.join(getConfigDir2(), "known_hosts");
2820
+ const knownHostsPath = path2.join(getConfigDir2(), "known_hosts");
2559
2821
  const existing = getConfig2();
2560
2822
  const stackEntry = {
2561
2823
  provider: "local",
@@ -2593,9 +2855,9 @@ async function runLocalDeploy(opts) {
2593
2855
  stackName: opts.stackName,
2594
2856
  sudoPassword: opts.sudoPassword,
2595
2857
  noWait: false,
2596
- onOutput: (line) => {
2858
+ onOutput: (line2) => {
2597
2859
  for (const [pattern, label] of STAGES) {
2598
- if (pattern.test(line)) {
2860
+ if (pattern.test(line2)) {
2599
2861
  spin.text = label;
2600
2862
  break;
2601
2863
  }
@@ -2634,11 +2896,11 @@ async function runLocalDeploy(opts) {
2634
2896
  spin2.fail("Configuration failed");
2635
2897
  throw err;
2636
2898
  }
2637
- const { drainPool } = await import("./pool-QGFQRMGU.js");
2899
+ const { drainPool } = await import("./pool-EX4ZTA5P.js");
2638
2900
  drainPool();
2639
- const tokenPath = path3.join(os.homedir(), ".clawops", "secrets", `GATEWAY_TOKEN_${opts.stackName}`);
2901
+ const tokenPath = path2.join(os.homedir(), ".clawops", "secrets", `GATEWAY_TOKEN_${opts.stackName}`);
2640
2902
  const dashboardUrl = `${state.gatewayUrl}?token=${opts.gatewayToken}`;
2641
- process20.stdout.write("\n");
2903
+ process21.stdout.write("\n");
2642
2904
  success("All done! OpenClaw is running.");
2643
2905
  info(`Open dashboard: ${dashboardUrl}`);
2644
2906
  info(` (or enter the token manually in the "Gateway Token" field: ${opts.gatewayToken})`);
@@ -2693,7 +2955,7 @@ ${cfg.loginHint}`);
2693
2955
  default: true
2694
2956
  }]);
2695
2957
  if (runNow) {
2696
- process20.stdout.write("\n");
2958
+ process21.stdout.write("\n");
2697
2959
  const result = spawnSync(cfg.loginCmd[0], cfg.loginCmd.slice(1), { stdio: "inherit" });
2698
2960
  if (result.error) {
2699
2961
  failure(`Could not launch ${cliName} \u2014 is it installed? (${result.error.message})`);
@@ -2703,10 +2965,10 @@ ${cfg.loginHint}`);
2703
2965
  }
2704
2966
  try {
2705
2967
  const identity = cfg.check();
2706
- process20.stdout.write("\n");
2968
+ process21.stdout.write("\n");
2707
2969
  success(`Authenticated: ${cfg.identity(identity)}`);
2708
2970
  } catch {
2709
- process20.stdout.write("\n");
2971
+ process21.stdout.write("\n");
2710
2972
  failure(`Still not authenticated \u2014 the sign-in may have been cancelled or failed.`);
2711
2973
  info("Re-run `clawops setup` after signing in, or continue and authenticate before running `clawops up`.\n");
2712
2974
  }
@@ -2720,7 +2982,7 @@ function cliInstallUrl(provider) {
2720
2982
  return "https://learn.microsoft.com/en-us/cli/azure/install-azure-cli";
2721
2983
  }
2722
2984
  async function promptSecret(name, label, envDefault, inquirer) {
2723
- const secretsDir2 = path3.join(os.homedir(), ".clawops", "secrets");
2985
+ const secretsDir2 = path2.join(os.homedir(), ".clawops", "secrets");
2724
2986
  const { secretSource } = await inquirer.prompt([{
2725
2987
  type: "list",
2726
2988
  name: "secretSource",
@@ -2738,9 +3000,9 @@ async function promptSecret(name, label, envDefault, inquirer) {
2738
3000
  message: `Paste your ${label}: (input is hidden)`,
2739
3001
  validate: (v) => v.trim() !== "" || "Value cannot be empty"
2740
3002
  }]);
2741
- mkdirSync3(secretsDir2, { recursive: true });
3003
+ mkdirSync2(secretsDir2, { recursive: true });
2742
3004
  spawnSync("chmod", ["700", secretsDir2], { stdio: "ignore" });
2743
- const secretPath2 = path3.join(secretsDir2, name);
3005
+ const secretPath2 = path2.join(secretsDir2, name);
2744
3006
  writeFileSync3(secretPath2, secretValue.trim(), { encoding: "utf-8", mode: 384 });
2745
3007
  success(`Secret saved to ${secretPath2} (chmod 600)`);
2746
3008
  return { name, source: "file", ref: secretPath2 };
@@ -2772,7 +3034,7 @@ var DOCKER_CHECK_CMD = [
2772
3034
  "echo NOT_RUNNING"
2773
3035
  ].join("; ");
2774
3036
  async function checkDockerPreflight(opts) {
2775
- const { acquireSession } = await import("./pool-QGFQRMGU.js");
3037
+ const { acquireSession } = await import("./pool-EX4ZTA5P.js");
2776
3038
  const { ProviderError } = await import("./errors-OK47MQFD.js");
2777
3039
  const spin = spinner("Checking Docker on target host...");
2778
3040
  const timeoutSignal = AbortSignal.timeout(DOCKER_PREFLIGHT_TIMEOUT_MS);
@@ -2787,7 +3049,7 @@ async function checkDockerPreflight(opts) {
2787
3049
  });
2788
3050
  let out = "";
2789
3051
  try {
2790
- const result = await session.exec(DOCKER_CHECK_CMD, execSignal);
3052
+ const result = await execPrivileged(session, DOCKER_CHECK_CMD, execSignal);
2791
3053
  out = result.stdout.trim();
2792
3054
  } catch {
2793
3055
  spin.warn("Docker check timed out \u2014 proceeding anyway. Bootstrap will verify.");
@@ -2819,7 +3081,7 @@ async function checkDockerPreflight(opts) {
2819
3081
  var DOCKER_POLL_MS = 3e3;
2820
3082
  var DOCKER_TIMEOUT_MS = 9e4;
2821
3083
  async function startRemoteDocker(opts, spin) {
2822
- const { acquireSession } = await import("./pool-QGFQRMGU.js");
3084
+ const { acquireSession } = await import("./pool-EX4ZTA5P.js");
2823
3085
  const { ProviderError } = await import("./errors-OK47MQFD.js");
2824
3086
  spin.text = "Starting Docker on the remote server...";
2825
3087
  const { session, release } = await acquireSession({
@@ -2845,7 +3107,8 @@ async function startRemoteDocker(opts, spin) {
2845
3107
  while (Date.now() < deadline) {
2846
3108
  await new Promise((r) => setTimeout(r, DOCKER_POLL_MS));
2847
3109
  spin.text = `Waiting for Docker to start on ${opts.host}...`;
2848
- const check = await session.exec(
3110
+ const check = await execPrivileged(
3111
+ session,
2849
3112
  "docker version >/dev/null 2>&1 && echo OK || echo NOT_RUNNING",
2850
3113
  opts.signal
2851
3114
  );
@@ -2891,7 +3154,7 @@ async function pollUntilDockerReady(label) {
2891
3154
  throw new Error("Docker daemon did not become ready within timeout.");
2892
3155
  }
2893
3156
  async function startDockerAndWait(inquirer) {
2894
- const isLinux = process20.platform === "linux";
3157
+ const isLinux = process21.platform === "linux";
2895
3158
  const { choice } = await inquirer.prompt([{
2896
3159
  type: "list",
2897
3160
  name: "choice",
@@ -2958,7 +3221,7 @@ async function ensureAuthorizedKey(keyPath, host, inquirer) {
2958
3221
  if (!existsSync2(pubKeyPath)) return;
2959
3222
  const pubKey = readFileSync3(pubKeyPath, "utf-8").trim();
2960
3223
  const isLocal = LOCALHOST_ALIASES.has(host.toLowerCase());
2961
- const authorizedKeysPath = path3.join(os.homedir(), ".ssh", "authorized_keys");
3224
+ const authorizedKeysPath = path2.join(os.homedir(), ".ssh", "authorized_keys");
2962
3225
  if (isLocal) {
2963
3226
  const existing = existsSync2(authorizedKeysPath) ? readFileSync3(authorizedKeysPath, "utf-8") : "";
2964
3227
  if (existing.includes(pubKey)) {
@@ -2973,7 +3236,7 @@ async function ensureAuthorizedKey(keyPath, host, inquirer) {
2973
3236
  default: true
2974
3237
  }]);
2975
3238
  if (addKey) {
2976
- mkdirSync3(path3.join(os.homedir(), ".ssh"), { recursive: true });
3239
+ mkdirSync2(path2.join(os.homedir(), ".ssh"), { recursive: true });
2977
3240
  const entry = existing.endsWith("\n") || existing === "" ? pubKey + "\n" : "\n" + pubKey + "\n";
2978
3241
  writeFileSync3(authorizedKeysPath, existing + entry, { encoding: "utf-8", flag: "a" });
2979
3242
  spawnSync("chmod", ["600", authorizedKeysPath], { stdio: "ignore" });
@@ -2983,15 +3246,15 @@ async function ensureAuthorizedKey(keyPath, host, inquirer) {
2983
3246
  info(` ${pubKey}`);
2984
3247
  }
2985
3248
  } else {
2986
- process20.stdout.write("\n");
3249
+ process21.stdout.write("\n");
2987
3250
  info("Make sure this public key is in ~/.ssh/authorized_keys on your server:");
2988
3251
  info(` ${pubKey}`);
2989
3252
  info("(If you just created the server, paste the line above into the server's ~/.ssh/authorized_keys file.)\n");
2990
3253
  }
2991
3254
  }
2992
3255
  async function generateSshKey() {
2993
- const keyPath = path3.join(os.homedir(), ".ssh", "id_ed25519");
2994
- mkdirSync3(path3.join(os.homedir(), ".ssh"), { recursive: true });
3256
+ const keyPath = path2.join(os.homedir(), ".ssh", "id_ed25519");
3257
+ mkdirSync2(path2.join(os.homedir(), ".ssh"), { recursive: true });
2995
3258
  const result = spawnSync("ssh-keygen", ["-t", "ed25519", "-f", keyPath, "-N", "", "-C", "clawops"], {
2996
3259
  stdio: ["ignore", "pipe", "pipe"]
2997
3260
  });
@@ -3006,27 +3269,27 @@ async function generateSshKey() {
3006
3269
  function detectSshKey() {
3007
3270
  const candidates = ["id_ed25519", "id_ecdsa", "id_rsa", "id_dsa"];
3008
3271
  for (const name of candidates) {
3009
- const p = path3.join(os.homedir(), ".ssh", name);
3272
+ const p = path2.join(os.homedir(), ".ssh", name);
3010
3273
  if (existsSync2(p)) return p;
3011
3274
  }
3012
- return path3.join(os.homedir(), ".ssh", "id_ed25519");
3275
+ return path2.join(os.homedir(), ".ssh", "id_ed25519");
3013
3276
  }
3014
3277
  function printMcpSnippet() {
3015
- process20.stdout.write("\n");
3016
- process20.stdout.write(JSON.stringify({
3278
+ process21.stdout.write("\n");
3279
+ process21.stdout.write(JSON.stringify({
3017
3280
  mcpServers: {
3018
3281
  clawops: { command: "clawops", args: ["mcp", "serve", "--read-only"] }
3019
3282
  }
3020
3283
  }, null, 2) + "\n\n");
3021
3284
  }
3022
3285
  async function maybeWireGatewayMcp(opts) {
3023
- process20.stdout.write("\n");
3286
+ process21.stdout.write("\n");
3024
3287
  const { wireGateway } = await inquirerPromptWire(opts.inquirer);
3025
3288
  if (!wireGateway) return;
3026
- const { connect } = await import("./ssh-CNJNLM3E.js");
3289
+ const { connect } = await import("./ssh-B3NZAU4I.js");
3027
3290
  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");
3291
+ const { wireGatewayMcp } = await import("./mcp-wire-BFEY2Y6H.js");
3292
+ const knownHostsPath = path2.join(getConfigDir2(), "known_hosts");
3030
3293
  const spin = spinner("Wiring gateway MCP client...");
3031
3294
  const session = await connect({
3032
3295
  host: opts.host,
@@ -3040,8 +3303,7 @@ async function maybeWireGatewayMcp(opts) {
3040
3303
  const result = await wireGatewayMcp(session, opts.signal ?? AbortSignal.timeout(3e4));
3041
3304
  if (result.status === "unsupported") {
3042
3305
  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);
3306
+ info("Upgrade OpenClaw to 2026.7.1-2 or later and run: clawops mcp wire --stack " + opts.stackName);
3045
3307
  } else if (result.status === "probe-failed") {
3046
3308
  spin.warn("The gateway could not reach clawops, so nothing was wired.");
3047
3309
  info("clawops does not run on the gateway host. Start it where the gateway can reach it,");
@@ -3062,7 +3324,7 @@ async function maybeWireGatewayMcp(opts) {
3062
3324
  }
3063
3325
  }
3064
3326
  async function maybeWireGatewayMcpCloud(opts) {
3065
- process20.stdout.write("\n");
3327
+ process21.stdout.write("\n");
3066
3328
  const { wireGateway } = await inquirerPromptWire(opts.inquirer);
3067
3329
  if (!wireGateway) return;
3068
3330
  info(`Run this after the server is ready: clawops mcp wire --stack ${opts.stackName}`);
@@ -3078,8 +3340,8 @@ async function inquirerPromptWire(inquirer) {
3078
3340
  function loadCatalogs(yaml) {
3079
3341
  const specDir = resolveSpecDir();
3080
3342
  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"));
3343
+ const modelsRaw = yaml.load(readFileSync3(path2.join(specDir, "models.yaml"), "utf-8"));
3344
+ const integrationsRaw = yaml.load(readFileSync3(path2.join(specDir, "integrations.yaml"), "utf-8"));
3083
3345
  return {
3084
3346
  models: modelsRaw.providers,
3085
3347
  integrations: integrationsRaw.integrations
@@ -3130,13 +3392,13 @@ function instanceChoices(provider) {
3130
3392
  return table[provider] ?? [];
3131
3393
  }
3132
3394
  async function maybeHarden(opts) {
3133
- const { MODULE_CATALOG, resolveModules, runHardening, formatHardenSummary } = await import("./harden-URPXLSVF.js");
3395
+ const { MODULE_CATALOG, resolveModules, runHardening, formatHardenSummary } = await import("./harden-PCDK3UQ5.js");
3134
3396
  const { getConfig: getConfig2 } = await import("./store-SDUR52Z5.js");
3135
3397
  const config = getConfig2();
3136
3398
  if (!config) return;
3137
3399
  const availableModules = resolveModules(MODULE_CATALOG, void 0, opts.provider);
3138
3400
  if (availableModules.length === 0) return;
3139
- process20.stdout.write("\n");
3401
+ process21.stdout.write("\n");
3140
3402
  const { selectedIds } = await opts.inquirer.prompt([{
3141
3403
  type: "checkbox",
3142
3404
  name: "selectedIds",
@@ -3152,9 +3414,9 @@ async function maybeHarden(opts) {
3152
3414
  return;
3153
3415
  }
3154
3416
  const modules = availableModules.filter((m) => selectedIds.includes(m.id));
3155
- process20.stdout.write("\nApplying hardening modules...\n\n");
3417
+ process21.stdout.write("\nApplying hardening modules...\n\n");
3156
3418
  try {
3157
- const { buildContext } = await import("./context-5LCUG2QR.js");
3419
+ const { buildContext } = await import("./context-KHKWJJMC.js");
3158
3420
  const { extractBaseOutputs } = await import("./outputs-DJHBY7EE.js");
3159
3421
  const ctx = buildContext({ stack: opts.stackName });
3160
3422
  const stackObj = await ctx.getStack();
@@ -3173,11 +3435,11 @@ async function maybeHarden(opts) {
3173
3435
  modules,
3174
3436
  signal: opts.signal,
3175
3437
  onProgress: (r) => {
3176
- process20.stdout.write(` \xB7 ${r.module.label}
3438
+ process21.stdout.write(` \xB7 ${r.module.label}
3177
3439
  `);
3178
3440
  }
3179
3441
  });
3180
- process20.stdout.write("\n" + formatHardenSummary(results));
3442
+ process21.stdout.write("\n" + formatHardenSummary(results));
3181
3443
  const errors = results.filter((r) => r.error);
3182
3444
  if (errors.length > 0) {
3183
3445
  for (const r of errors) fail(`${r.module.label}: ${r.error}`);
@@ -3191,26 +3453,52 @@ async function maybeHarden(opts) {
3191
3453
  info("Run `clawops harden --stack " + opts.stackName + "` to retry.");
3192
3454
  }
3193
3455
  }
3456
+ async function detectSshCidrDefault() {
3457
+ const { detectEgressIp } = await import("./firewall-T3NTPZJD.js");
3458
+ const result = await detectEgressIp("https://ifconfig.me");
3459
+ if (!result.ok) return void 0;
3460
+ const ip = result.ip.trim();
3461
+ return ip === "" ? void 0 : ip.includes("/") ? ip : `${ip}/32`;
3462
+ }
3463
+ function validateCidrAnswer(value) {
3464
+ const v = value.trim();
3465
+ if (v === "") return "Required \u2014 enter a CIDR such as 203.0.113.4/32";
3466
+ const looksIpv6 = v.includes(":");
3467
+ if (!looksIpv6 && !/^(\d{1,3}\.){3}\d{1,3}\/(3[0-2]|[12]?\d)$/.test(v)) {
3468
+ return `"${v}" is not a CIDR. Use an address and a prefix, e.g. 203.0.113.4/32`;
3469
+ }
3470
+ return true;
3471
+ }
3472
+ function admitsInternet(cidr) {
3473
+ const v = cidr.trim();
3474
+ return v === "0.0.0.0/0" || v === "::/0";
3475
+ }
3476
+ function wizardChannels(integrations) {
3477
+ return integrations.filter((i) => i.wizardSupported !== false);
3478
+ }
3479
+ function startChannelConfig(integ) {
3480
+ return { ...integ.defaults ?? {} };
3481
+ }
3194
3482
 
3195
3483
  // 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";
3484
+ import { defineCommand as defineCommand20 } from "citty";
3485
+ import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync4, writeFileSync as writeFileSync4, unlinkSync, mkdirSync as mkdirSync3, statSync } from "fs";
3198
3486
  import { spawnSync as spawnSync2 } from "child_process";
3199
- import path4 from "path";
3487
+ import path3 from "path";
3200
3488
  import os2 from "os";
3201
- import process21 from "process";
3202
- var SECRETS_DIR = path4.join(os2.homedir(), ".clawops", "secrets");
3489
+ import process22 from "process";
3490
+ var SECRETS_DIR = path3.join(os2.homedir(), ".clawops", "secrets");
3203
3491
  function secretsDir() {
3204
3492
  return SECRETS_DIR;
3205
3493
  }
3206
3494
  function secretPath(name) {
3207
- return path4.join(secretsDir(), name);
3495
+ return path3.join(secretsDir(), name);
3208
3496
  }
3209
3497
  function listSecretNames() {
3210
3498
  if (!existsSync3(secretsDir())) return [];
3211
3499
  return readdirSync(secretsDir()).filter((f) => !f.startsWith("."));
3212
3500
  }
3213
- var listCmd = defineCommand19({
3501
+ var listCmd = defineCommand20({
3214
3502
  meta: { name: "list", description: "List all stored secrets and their status" },
3215
3503
  args: {
3216
3504
  json: { type: "boolean", description: "Emit JSON" }
@@ -3235,7 +3523,7 @@ var listCmd = defineCommand19({
3235
3523
  }
3236
3524
  });
3237
3525
  if (args.json) {
3238
- process21.stdout.write(JSON.stringify(rows, null, 2) + "\n");
3526
+ process22.stdout.write(JSON.stringify(rows, null, 2) + "\n");
3239
3527
  return;
3240
3528
  }
3241
3529
  const tableRows = rows.map((r) => [
@@ -3244,10 +3532,10 @@ var listCmd = defineCommand19({
3244
3532
  r.modified,
3245
3533
  r.path
3246
3534
  ]);
3247
- process21.stdout.write("\n" + renderTable(["Name", "Status", "Modified", "Path"], tableRows) + "\n\n");
3535
+ process22.stdout.write("\n" + renderTable(["Name", "Status", "Modified", "Path"], tableRows) + "\n\n");
3248
3536
  }
3249
3537
  });
3250
- var setCmd = defineCommand19({
3538
+ var setCmd = defineCommand20({
3251
3539
  meta: { name: "set", description: "Create or update a secret" },
3252
3540
  args: {
3253
3541
  value: { type: "string", description: "Secret value (skips interactive prompt)" }
@@ -3256,7 +3544,7 @@ var setCmd = defineCommand19({
3256
3544
  const [name] = args._ ?? [];
3257
3545
  if (!name) {
3258
3546
  failure("Usage: clawops secret set <name>");
3259
- process21.exit(2);
3547
+ process22.exit(2);
3260
3548
  }
3261
3549
  const inquirer = (await import("inquirer")).default;
3262
3550
  let value;
@@ -3271,14 +3559,14 @@ var setCmd = defineCommand19({
3271
3559
  }]);
3272
3560
  value = secretValue;
3273
3561
  }
3274
- mkdirSync4(secretsDir(), { recursive: true });
3562
+ mkdirSync3(secretsDir(), { recursive: true });
3275
3563
  spawnSync2("chmod", ["700", secretsDir()], { stdio: "ignore" });
3276
3564
  writeFileSync4(secretPath(name), value.trim(), { encoding: "utf-8", mode: 384 });
3277
3565
  success(`Secret "${name}" saved to ${secretPath(name)} (chmod 600)`);
3278
3566
  info("Run `clawops secret rotate <name>` to propagate it to a running stack.");
3279
3567
  }
3280
3568
  });
3281
- var deleteCmd = defineCommand19({
3569
+ var deleteCmd = defineCommand20({
3282
3570
  meta: { name: "delete", description: "Remove a stored secret" },
3283
3571
  args: {
3284
3572
  yes: { type: "boolean", description: "Skip confirmation prompt" }
@@ -3287,12 +3575,12 @@ var deleteCmd = defineCommand19({
3287
3575
  const [name] = args._ ?? [];
3288
3576
  if (!name) {
3289
3577
  failure("Usage: clawops secret delete <name>");
3290
- process21.exit(2);
3578
+ process22.exit(2);
3291
3579
  }
3292
3580
  const p = secretPath(name);
3293
3581
  if (!existsSync3(p)) {
3294
3582
  failure(`Secret "${name}" not found at ${p}`);
3295
- process21.exit(1);
3583
+ process22.exit(1);
3296
3584
  }
3297
3585
  const { listOverlays } = await import("./overlay-store-ADZPD7EU.js");
3298
3586
  const referencingStacks = listOverlays().filter((o) => o.secrets.some((s) => s.name === name)).map((o) => o.stackName);
@@ -3317,7 +3605,7 @@ var deleteCmd = defineCommand19({
3317
3605
  success(`Secret "${name}" deleted.`);
3318
3606
  }
3319
3607
  });
3320
- var rotateCmd = defineCommand19({
3608
+ var rotateCmd = defineCommand20({
3321
3609
  meta: { name: "rotate", description: "Update a secret and re-apply config to a running stack" },
3322
3610
  args: {
3323
3611
  stack: { type: "string", description: "Stack name to re-apply (defaults to config default)" },
@@ -3327,7 +3615,7 @@ var rotateCmd = defineCommand19({
3327
3615
  const [name] = args._ ?? [];
3328
3616
  if (!name) {
3329
3617
  failure("Usage: clawops secret rotate <name> [--stack <name>]");
3330
- process21.exit(2);
3618
+ process22.exit(2);
3331
3619
  }
3332
3620
  const inquirer = (await import("inquirer")).default;
3333
3621
  let value;
@@ -3342,7 +3630,7 @@ var rotateCmd = defineCommand19({
3342
3630
  }]);
3343
3631
  value = secretValue;
3344
3632
  }
3345
- mkdirSync4(secretsDir(), { recursive: true });
3633
+ mkdirSync3(secretsDir(), { recursive: true });
3346
3634
  spawnSync2("chmod", ["700", secretsDir()], { stdio: "ignore" });
3347
3635
  writeFileSync4(secretPath(name), value.trim(), { encoding: "utf-8", mode: 384 });
3348
3636
  success(`Secret "${name}" updated.`);
@@ -3363,10 +3651,10 @@ var rotateCmd = defineCommand19({
3363
3651
  }
3364
3652
  info(`Re-applying config overlay to stack "${targetStack}"\u2026`);
3365
3653
  try {
3366
- const { buildContext } = await import("./context-5LCUG2QR.js");
3367
- const { readRemoteConfig, atomicWriteConfig, restartGateway, deepMerge } = await import("./remote-config-QDLN4UMG.js");
3654
+ const { buildContext } = await import("./context-KHKWJJMC.js");
3655
+ const { readRemoteConfig, atomicWriteConfig, restartGateway, deepMerge } = await import("./remote-config-Z6IIZR3D.js");
3368
3656
  const { resolveSecrets } = await import("./secrets-SVZWNAPK.js");
3369
- const { acquireSession, drainPool } = await import("./pool-QGFQRMGU.js");
3657
+ const { acquireSession, drainPool } = await import("./pool-EX4ZTA5P.js");
3370
3658
  const { localStateToConnectionInfo } = await import("./state-PRBIIN7I.js");
3371
3659
  const ctx = buildContext({ stack: targetStack });
3372
3660
  let conn;
@@ -3401,7 +3689,7 @@ var rotateCmd = defineCommand19({
3401
3689
  }
3402
3690
  }
3403
3691
  });
3404
- var auditCmd = defineCommand19({
3692
+ var auditCmd = defineCommand20({
3405
3693
  meta: { name: "audit", description: "Report missing secrets and unresolvable $secret: refs" },
3406
3694
  args: {
3407
3695
  json: { type: "boolean", description: "Emit JSON" }
@@ -3430,7 +3718,7 @@ var auditCmd = defineCommand19({
3430
3718
  }
3431
3719
  } else if (s.source === "env") {
3432
3720
  const envVar = s.ref ?? s.name;
3433
- if (!process21.env[envVar]) {
3721
+ if (!process22.env[envVar]) {
3434
3722
  issues.push({ kind: "missing-env", stack: overlay.stackName, secret: s.name, detail: `Env var not set: ${envVar}` });
3435
3723
  }
3436
3724
  } else {
@@ -3439,7 +3727,7 @@ var auditCmd = defineCommand19({
3439
3727
  }
3440
3728
  }
3441
3729
  if (args.json) {
3442
- process21.stdout.write(JSON.stringify({ issues, ok: issues.length === 0 }, null, 2) + "\n");
3730
+ process22.stdout.write(JSON.stringify({ issues, ok: issues.length === 0 }, null, 2) + "\n");
3443
3731
  return;
3444
3732
  }
3445
3733
  if (issues.length === 0) {
@@ -3452,11 +3740,11 @@ var auditCmd = defineCommand19({
3452
3740
  const prefix = issue.stack ? `[${issue.stack}] ` : "";
3453
3741
  warn(`${prefix}${issue.secret}: ${issue.detail}`);
3454
3742
  }
3455
- process21.stdout.write("\n");
3743
+ process22.stdout.write("\n");
3456
3744
  info("Run `clawops secret set <name>` to update a missing or empty secret.");
3457
3745
  }
3458
3746
  });
3459
- var secret_default = defineCommand19({
3747
+ var secret_default = defineCommand20({
3460
3748
  meta: {
3461
3749
  name: "secret",
3462
3750
  description: "Manage clawops secrets (list | set | delete | rotate | audit)"
@@ -3472,7 +3760,7 @@ var secret_default = defineCommand19({
3472
3760
  });
3473
3761
 
3474
3762
  // src/cli/commands/help.ts
3475
- import { defineCommand as defineCommand20 } from "citty";
3763
+ import { defineCommand as defineCommand21 } from "citty";
3476
3764
  var COMMANDS = [
3477
3765
  ["setup", "Interactive first-run wizard \u2014 configure, deploy, and wire AI apps"],
3478
3766
  ["init", "Write config and generate SSH key pair (non-interactive)"],
@@ -3504,7 +3792,7 @@ var GLOBAL_FLAGS = [
3504
3792
  ["--dry-run", "Preview without applying (mutating commands)"],
3505
3793
  ["--yes", "Skip interactive confirmations (CI mode)"]
3506
3794
  ];
3507
- var help_default = defineCommand20({
3795
+ var help_default = defineCommand21({
3508
3796
  meta: {
3509
3797
  name: "help",
3510
3798
  description: "Show available commands and global flags"
@@ -3544,16 +3832,16 @@ ${dim("Run `clawops <command> --help` for command-specific flags.")}
3544
3832
  });
3545
3833
 
3546
3834
  // 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";
3835
+ import { defineCommand as defineCommand22 } from "citty";
3836
+ import process23 from "process";
3837
+ import { accessSync, constants } from "fs";
3550
3838
  import { execSync as execSync2 } from "child_process";
3551
3839
  var ISSUES_URL = `${REPO_URL}/issues/new`;
3552
3840
  var BODY_CAP = 2e3;
3553
- var VERSION = true ? "1.7.9" : "dev";
3841
+ var VERSION = true ? "2.0.0" : "dev";
3554
3842
  async function collectContext() {
3555
- const nodeVersion = process22.version;
3556
- const os3 = `${process22.platform} ${process22.arch}`;
3843
+ const nodeVersion = process23.version;
3844
+ const os3 = `${process23.platform} ${process23.arch}`;
3557
3845
  let provider = "unknown";
3558
3846
  let stackCount = 0;
3559
3847
  let sshKey = "unknown";
@@ -3564,9 +3852,9 @@ async function collectContext() {
3564
3852
  const stacks = Object.values(config.stacks);
3565
3853
  stackCount = stacks.length;
3566
3854
  provider = stacks[0]?.provider ?? "none";
3567
- const keyPath = config.ssh.keyPath.replace(/^~/, process22.env["HOME"] ?? "~");
3855
+ const keyPath = config.ssh.keyPath.replace(/^~/, process23.env["HOME"] ?? "~");
3568
3856
  try {
3569
- accessSync2(keyPath, constants2.R_OK);
3857
+ accessSync(keyPath, constants.R_OK);
3570
3858
  sshKey = "present";
3571
3859
  } catch {
3572
3860
  sshKey = "missing";
@@ -3604,14 +3892,14 @@ function buildIssueUrl(title, command, context) {
3604
3892
  }
3605
3893
  function openBrowser2(url) {
3606
3894
  try {
3607
- const cmd = process22.platform === "darwin" ? "open" : "xdg-open";
3895
+ const cmd = process23.platform === "darwin" ? "open" : "xdg-open";
3608
3896
  execSync2(`${cmd} "${url}"`, { stdio: "ignore" });
3609
3897
  return true;
3610
3898
  } catch {
3611
3899
  return false;
3612
3900
  }
3613
3901
  }
3614
- var bug_default = defineCommand21({
3902
+ var bug_default = defineCommand22({
3615
3903
  meta: {
3616
3904
  name: "bug",
3617
3905
  description: "Open a pre-filled GitHub issue with system context from clawops doctor"
@@ -3625,7 +3913,7 @@ var bug_default = defineCommand21({
3625
3913
  let command = "";
3626
3914
  if (!args.json) {
3627
3915
  const { createInterface: createInterface3 } = await import("readline/promises");
3628
- const rl = createInterface3({ input: process22.stdin, output: process22.stdout });
3916
+ const rl = createInterface3({ input: process23.stdin, output: process23.stdout });
3629
3917
  try {
3630
3918
  title = (await rl.question("Describe the issue in one line: ")).trim();
3631
3919
  command = (await rl.question("Which command triggered it? (optional): ")).trim();
@@ -3638,12 +3926,12 @@ var bug_default = defineCommand21({
3638
3926
  printJson(jsonOk({ url }));
3639
3927
  return;
3640
3928
  }
3641
- process22.stdout.write("\n");
3929
+ process23.stdout.write("\n");
3642
3930
  info(`Opening: ${url}`);
3643
- process22.stdout.write("\n");
3931
+ process23.stdout.write("\n");
3644
3932
  const opened = openBrowser2(url);
3645
3933
  if (!opened) {
3646
- process22.stdout.write(`Open this URL to file the issue:
3934
+ process23.stdout.write(`Open this URL to file the issue:
3647
3935
  ${url}
3648
3936
  `);
3649
3937
  }
@@ -3651,9 +3939,9 @@ ${url}
3651
3939
  });
3652
3940
 
3653
3941
  // src/cli/commands/harden.ts
3654
- import { defineCommand as defineCommand22 } from "citty";
3655
- import process23 from "process";
3656
- var harden_default = defineCommand22({
3942
+ import { defineCommand as defineCommand23 } from "citty";
3943
+ import process24 from "process";
3944
+ var harden_default = defineCommand23({
3657
3945
  meta: {
3658
3946
  name: "harden",
3659
3947
  description: "Apply security hardening modules to a deployed stack"
@@ -3667,26 +3955,26 @@ var harden_default = defineCommand22({
3667
3955
  json: { type: "boolean", description: "Emit structured JSON output" }
3668
3956
  },
3669
3957
  async run({ args }) {
3670
- const { MODULE_CATALOG, resolveModules, runHardening, formatHardenSummary } = await import("./harden-URPXLSVF.js");
3671
- const { buildContext } = await import("./context-5LCUG2QR.js");
3958
+ const { MODULE_CATALOG, resolveModules, runHardening, formatHardenSummary } = await import("./harden-PCDK3UQ5.js");
3959
+ const { buildContext } = await import("./context-KHKWJJMC.js");
3672
3960
  const { getConfig: getConfig2 } = await import("./store-SDUR52Z5.js");
3673
3961
  if (args.list) {
3674
- process23.stdout.write("\nAvailable hardening modules:\n\n");
3962
+ process24.stdout.write("\nAvailable hardening modules:\n\n");
3675
3963
  for (const mod of MODULE_CATALOG) {
3676
3964
  const on = mod.defaultOn ? chalk.green("ON ") : chalk.dim("off");
3677
3965
  const providers = mod.providers === "all" ? "all" : mod.providers.join(", ");
3678
- process23.stdout.write(
3966
+ process24.stdout.write(
3679
3967
  ` ${on} ${mod.id.padEnd(28)} ${mod.label} [${providers}]
3680
3968
  `
3681
3969
  );
3682
3970
  }
3683
- process23.stdout.write("\n");
3971
+ process24.stdout.write("\n");
3684
3972
  return;
3685
3973
  }
3686
3974
  const config = getConfig2();
3687
3975
  if (!config) {
3688
3976
  failure("No clawops config found. Run `clawops init` first.");
3689
- process23.exit(1);
3977
+ process24.exit(1);
3690
3978
  }
3691
3979
  const ctx = buildContext({ stack: args.stack });
3692
3980
  const provider = ctx.adapter.name;
@@ -3696,17 +3984,17 @@ var harden_default = defineCommand22({
3696
3984
  return;
3697
3985
  }
3698
3986
  if (!args.json) {
3699
- process23.stdout.write("\nModules to run:\n");
3987
+ process24.stdout.write("\nModules to run:\n");
3700
3988
  for (const mod of modules) {
3701
3989
  const flag = mod.defaultOn ? "" : chalk.yellow(" [opt-in]");
3702
- process23.stdout.write(` \xB7 ${mod.label}${flag}
3990
+ process24.stdout.write(` \xB7 ${mod.label}${flag}
3703
3991
  `);
3704
3992
  }
3705
- process23.stdout.write("\n");
3993
+ process24.stdout.write("\n");
3706
3994
  }
3707
3995
  if (!args["dry-run"] && !args.yes && !args.json) {
3708
3996
  const { createInterface: createInterface3 } = await import("readline/promises");
3709
- const rl = createInterface3({ input: process23.stdin, output: process23.stdout });
3997
+ const rl = createInterface3({ input: process24.stdin, output: process24.stdout });
3710
3998
  const answer = await rl.question("Apply these hardening modules? [y/N] ");
3711
3999
  rl.close();
3712
4000
  if (!answer.toLowerCase().startsWith("y")) {
@@ -3715,8 +4003,8 @@ var harden_default = defineCommand22({
3715
4003
  }
3716
4004
  }
3717
4005
  const ac = new AbortController();
3718
- process23.on("SIGINT", () => ac.abort());
3719
- process23.on("SIGTERM", () => ac.abort());
4006
+ process24.on("SIGINT", () => ac.abort());
4007
+ process24.on("SIGTERM", () => ac.abort());
3720
4008
  const { extractBaseOutputs } = await import("./outputs-DJHBY7EE.js");
3721
4009
  const stackObj = await ctx.getStack();
3722
4010
  const outputMap = await stackObj.outputs();
@@ -3730,7 +4018,7 @@ var harden_default = defineCommand22({
3730
4018
  knownHostsPath: config.ssh.knownHostsPath
3731
4019
  });
3732
4020
  if (!args.json) {
3733
- process23.stdout.write(args["dry-run"] ? "Checking hardening state...\n\n" : "Applying hardening...\n\n");
4021
+ process24.stdout.write(args["dry-run"] ? "Checking hardening state...\n\n" : "Applying hardening...\n\n");
3734
4022
  }
3735
4023
  const results = await runHardening(conn, {
3736
4024
  modules,
@@ -3738,7 +4026,7 @@ var harden_default = defineCommand22({
3738
4026
  signal: ac.signal,
3739
4027
  onProgress: args.json ? void 0 : (r) => {
3740
4028
  const icon = r.error ? chalk.red("\u2717") : r.applyResult?.changed ? chalk.green("\u2713") : chalk.dim("\xB7");
3741
- process23.stdout.write(` ${icon} ${r.module.label}
4029
+ process24.stdout.write(` ${icon} ${r.module.label}
3742
4030
  `);
3743
4031
  }
3744
4032
  });
@@ -3756,14 +4044,14 @@ var harden_default = defineCommand22({
3756
4044
  }));
3757
4045
  return;
3758
4046
  }
3759
- process23.stdout.write("\n" + formatHardenSummary(results));
4047
+ process24.stdout.write("\n" + formatHardenSummary(results));
3760
4048
  const errors = results.filter((r) => r.error);
3761
4049
  if (errors.length > 0) {
3762
- process23.stdout.write("\n");
4050
+ process24.stdout.write("\n");
3763
4051
  for (const r of errors) {
3764
4052
  failure(`${r.module.label}: ${r.error}`);
3765
4053
  }
3766
- process23.exit(1);
4054
+ process24.exit(1);
3767
4055
  }
3768
4056
  if (!args["dry-run"]) {
3769
4057
  success("Hardening complete.");
@@ -3772,26 +4060,26 @@ var harden_default = defineCommand22({
3772
4060
  });
3773
4061
 
3774
4062
  // src/cli/error-handler.ts
3775
- import process24 from "process";
4063
+ import process25 from "process";
3776
4064
  function handleError(err) {
3777
4065
  if (err instanceof ClawopsError) {
3778
4066
  failure(err.message);
3779
- process24.exit(err.exitCode);
4067
+ process25.exit(err.exitCode);
3780
4068
  }
3781
4069
  if (err instanceof Error) {
3782
4070
  failure(`Unexpected error: ${err.message}`);
3783
- if (process24.env["DEBUG"]) {
3784
- process24.stderr.write(err.stack ?? "\n");
4071
+ if (process25.env["DEBUG"]) {
4072
+ process25.stderr.write(err.stack ?? "\n");
3785
4073
  }
3786
- process24.exit(1);
4074
+ process25.exit(1);
3787
4075
  }
3788
4076
  failure(`Unknown error: ${String(err)}`);
3789
- process24.exit(1);
4077
+ process25.exit(1);
3790
4078
  }
3791
4079
 
3792
4080
  // src/cli/index.ts
3793
- var CLI_VERSION = true ? "1.7.9" : "dev";
3794
- var main = defineCommand23({
4081
+ var CLI_VERSION = true ? "2.0.0" : "dev";
4082
+ var main = defineCommand24({
3795
4083
  meta: {
3796
4084
  name: "clawops",
3797
4085
  description: "Deploy and manage self-hosted OpenClaw instances across clouds",
@@ -3819,6 +4107,7 @@ var main = defineCommand23({
3819
4107
  logs: logs_default,
3820
4108
  config: config_default,
3821
4109
  agents: agents_default,
4110
+ migrate: migrate_default,
3822
4111
  gateway: gateway_default,
3823
4112
  backup: backup_default,
3824
4113
  stacks: stacks_default,