@clawops/cli 1.0.0 → 1.2.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 (37) hide show
  1. package/README.md +140 -56
  2. package/dist/apply-KBF7OPWL.js +14 -0
  3. package/dist/{automation-JG2YBEL7.js → automation-RULUSJBW.js} +2 -2
  4. package/dist/{aws-A3323GNM.js → aws-KQBWY43W.js} +12 -10
  5. package/dist/{azure-LNWBK2ZI.js → azure-4EJFVJZL.js} +4 -3
  6. package/dist/{bootstrap-NRIDVS5F.js → bootstrap-UYCOEJQX.js} +48 -12
  7. package/dist/{chunk-LT24GUUO.js → chunk-3QJBNAHW.js} +1 -1
  8. package/dist/{chunk-5XEZAU7V.js → chunk-4U3LTLWZ.js} +6 -83
  9. package/dist/{chunk-PERSDQMT.js → chunk-ACYJBSLJ.js} +7 -7
  10. package/dist/chunk-BOPSG2LI.js +77 -0
  11. package/dist/{chunk-PRYLTCS4.js → chunk-BRPU7AQC.js} +1 -1
  12. package/dist/{chunk-ALSUDYA7.js → chunk-CX5SL5HP.js} +1 -1
  13. package/dist/{chunk-ZSE4QRKE.js → chunk-KGXPLI7W.js} +19 -1
  14. package/dist/chunk-LU63NZD3.js +116 -0
  15. package/dist/chunk-QJ6ERXHN.js +266 -0
  16. package/dist/{chunk-2MJFYG7U.js → chunk-QMXM2SMO.js} +10 -6
  17. package/dist/chunk-UDNZUSKA.js +97 -0
  18. package/dist/chunk-ZVOEQCNW.js +92 -0
  19. package/dist/cli.js +1259 -62
  20. package/dist/context-VUAYRAJY.js +10 -0
  21. package/dist/errors-OK47MQFD.js +21 -0
  22. package/dist/{gcp-OHWCTFLL.js → gcp-SKURG3L6.js} +4 -3
  23. package/dist/generate-WG5VONSE.js +14 -0
  24. package/dist/js-yaml-PTEEG4FO.js +2647 -0
  25. package/dist/{outputs-6DAVEEAZ.js → outputs-DJHBY7EE.js} +2 -2
  26. package/dist/{package-RCGVJCON.js → package-3JZ767DN.js} +8 -3
  27. package/dist/{pool-D4JCUA2V.js → pool-FBFHATDG.js} +3 -2
  28. package/dist/remote-config-JQ77SRC2.js +21 -0
  29. package/dist/secrets-SVZWNAPK.js +7 -0
  30. package/dist/{server-YO4JFQND.js → server-QJRWK2RV.js} +64 -150
  31. package/dist/ssh-IQXNME3D.js +8 -0
  32. package/dist/{store-ARJ2EO6L.js → store-SDUR52Z5.js} +2 -2
  33. package/package.json +6 -3
  34. package/dist/apply-GTVALBAY.js +0 -12
  35. package/dist/chunk-RZE35FQF.js +0 -56
  36. package/dist/context-T52JWL3P.js +0 -10
  37. package/dist/generate-H3D7VQUP.js +0 -14
package/dist/cli.js CHANGED
@@ -1,17 +1,25 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ validateOpenclawConfig
4
+ } from "./chunk-QJ6ERXHN.js";
5
+ import "./chunk-ACYJBSLJ.js";
6
+ import "./chunk-BRPU7AQC.js";
7
+ import "./chunk-ZVOEQCNW.js";
8
+ import "./chunk-4U3LTLWZ.js";
9
+ import "./chunk-UDNZUSKA.js";
2
10
  import {
3
11
  getConfig,
4
12
  getConfigDir,
5
13
  requireConfig,
6
14
  setConfig
7
- } from "./chunk-ALSUDYA7.js";
15
+ } from "./chunk-CX5SL5HP.js";
8
16
  import {
9
17
  ClawopsError,
10
18
  UsageError
11
- } from "./chunk-ZSE4QRKE.js";
19
+ } from "./chunk-KGXPLI7W.js";
12
20
 
13
21
  // src/cli/index.ts
14
- import { defineCommand as defineCommand18, runMain } from "citty";
22
+ import { defineCommand as defineCommand19, runMain } from "citty";
15
23
 
16
24
  // src/cli/commands/init.ts
17
25
  import { defineCommand } from "citty";
@@ -169,6 +177,7 @@ var init_default = defineCommand({
169
177
  // src/cli/commands/up.ts
170
178
  import { defineCommand as defineCommand2 } from "citty";
171
179
  import process3 from "process";
180
+ import { readFileSync } from "fs";
172
181
 
173
182
  // src/output/table.ts
174
183
  function renderTable(headers, rows) {
@@ -199,10 +208,11 @@ var up_default = defineCommand2({
199
208
  "dry-run": { type: "boolean", description: "Preview without applying" },
200
209
  "no-wait": { type: "boolean", description: "Return immediately without waiting for healthy state" },
201
210
  "openclaw-version": { type: "string", description: "semver or 'stable'/'dev'" },
202
- stack: { type: "string", description: "Target stack name" }
211
+ stack: { type: "string", description: "Target stack name" },
212
+ config: { type: "string", description: "Path to openclaw config overlay JSON (local provider only)" }
203
213
  },
204
214
  async run({ args }) {
205
- const { buildContext } = await import("./context-T52JWL3P.js");
215
+ const { buildContext } = await import("./context-VUAYRAJY.js");
206
216
  const ctx = buildContext(args);
207
217
  const openclawVersion = typeof args["openclaw-version"] === "string" ? args["openclaw-version"] : "stable";
208
218
  if (ctx.adapter.name === "local") {
@@ -213,7 +223,7 @@ var up_default = defineCommand2({
213
223
  );
214
224
  }
215
225
  const { localOpts } = stackConfig;
216
- const { localBootstrap } = await import("./bootstrap-NRIDVS5F.js");
226
+ const { localBootstrap } = await import("./bootstrap-UYCOEJQX.js");
217
227
  const abortController = new AbortController();
218
228
  process3.on("SIGINT", () => abortController.abort());
219
229
  process3.on("SIGTERM", () => abortController.abort());
@@ -233,6 +243,18 @@ var up_default = defineCommand2({
233
243
  spin2.succeed(`Host "${localOpts.host}" bootstrapped`);
234
244
  info(`Gateway URL: ${state.gatewayUrl}`);
235
245
  info(`SSH: ${state.sshUser}@${state.sshHost}:${state.sshPort}`);
246
+ if (typeof args.config === "string") {
247
+ await applyLocalConfigOverlay({
248
+ configPath: args.config,
249
+ host: localOpts.host,
250
+ port: localOpts.sshPort,
251
+ user: localOpts.sshUser,
252
+ privateKeyPath: localOpts.sshKeyPath,
253
+ knownHostsPath: ctx.config.ssh.knownHostsPath,
254
+ signal: abortController.signal
255
+ });
256
+ info("Config overlay applied and gateway restarted.");
257
+ }
236
258
  } catch (err) {
237
259
  spin2.fail("Bootstrap failed");
238
260
  throw err;
@@ -293,6 +315,34 @@ var up_default = defineCommand2({
293
315
  }
294
316
  }
295
317
  });
318
+ async function applyLocalConfigOverlay(opts) {
319
+ const { connect } = await import("./ssh-IQXNME3D.js");
320
+ const { resolveSecrets } = await import("./secrets-SVZWNAPK.js");
321
+ const { readRemoteConfig, atomicWriteConfig, restartGateway, deepMerge } = await import("./remote-config-JQ77SRC2.js");
322
+ let overlay;
323
+ try {
324
+ overlay = JSON.parse(readFileSync(opts.configPath, "utf-8"));
325
+ } catch (err) {
326
+ throw new UsageError(`Cannot read config overlay at ${opts.configPath}: ${err.message}`);
327
+ }
328
+ const resolved = resolveSecrets(overlay, []);
329
+ const session = await connect({
330
+ host: opts.host,
331
+ port: opts.port,
332
+ user: opts.user,
333
+ privateKeyPath: opts.privateKeyPath,
334
+ knownHostsPath: opts.knownHostsPath,
335
+ signal: opts.signal
336
+ });
337
+ try {
338
+ const remote = await readRemoteConfig(session, opts.signal);
339
+ const merged = deepMerge(remote, resolved);
340
+ await atomicWriteConfig(session, merged, opts.signal);
341
+ await restartGateway(session, opts.signal);
342
+ } finally {
343
+ session.close();
344
+ }
345
+ }
296
346
 
297
347
  // src/cli/commands/down.ts
298
348
  import { defineCommand as defineCommand3 } from "citty";
@@ -308,7 +358,7 @@ var down_default = defineCommand3({
308
358
  "dry-run": { type: "boolean", description: "Show what would be destroyed without destroying" }
309
359
  },
310
360
  async run({ args }) {
311
- const { buildContext } = await import("./context-T52JWL3P.js");
361
+ const { buildContext } = await import("./context-VUAYRAJY.js");
312
362
  const ctx = buildContext(args);
313
363
  if (args["dry-run"]) {
314
364
  info(`Dry run \u2014 would destroy stack "${ctx.stackName}"`);
@@ -372,7 +422,7 @@ var status_default = defineCommand4({
372
422
  json: { type: "boolean", description: "Emit JSON" }
373
423
  },
374
424
  async run({ args }) {
375
- const { buildContext } = await import("./context-T52JWL3P.js");
425
+ const { buildContext } = await import("./context-VUAYRAJY.js");
376
426
  const ctx = buildContext(args);
377
427
  if (ctx.adapter.name === "local") {
378
428
  const state = ctx.localState;
@@ -397,7 +447,7 @@ var status_default = defineCommand4({
397
447
  process6.stdout.write("\n" + renderTable(["Field", "Value"], rows2) + "\n\n");
398
448
  return;
399
449
  }
400
- const { extractBaseOutputs } = await import("./outputs-6DAVEEAZ.js");
450
+ const { extractBaseOutputs } = await import("./outputs-DJHBY7EE.js");
401
451
  const stack = await ctx.getStack();
402
452
  const outputMap = await stack.outputs();
403
453
  const outputs = Object.fromEntries(
@@ -444,8 +494,8 @@ var plan_default = defineCommand5({
444
494
  out: { type: "string", description: "Write plan JSON to this absolute path (default: stdout)" }
445
495
  },
446
496
  async run({ args }) {
447
- const { buildContext } = await import("./context-T52JWL3P.js");
448
- const { generatePlan } = await import("./generate-H3D7VQUP.js");
497
+ const { buildContext } = await import("./context-VUAYRAJY.js");
498
+ const { generatePlan } = await import("./generate-WG5VONSE.js");
449
499
  const ctx = buildContext(args);
450
500
  if (ctx.adapter.name === "local") {
451
501
  throw new UsageError(
@@ -485,6 +535,18 @@ var plan_default = defineCommand5({
485
535
  } else {
486
536
  process7.stdout.write(planJson + "\n");
487
537
  }
538
+ const { spec, metadata } = plan;
539
+ const sshCidrs = spec.network.allowedSshCidrs.join(", ") || "(none)";
540
+ const gatewayCidrs = spec.network.allowedGatewayCidrs.join(", ") || "(none)";
541
+ process7.stderr.write(
542
+ `
543
+ Plan: ${metadata.name} (${spec.provider}${spec.region ? ` / ${spec.region}` : ""})
544
+ Instance: ${spec.instanceType}
545
+ OpenClaw: ${spec.openclaw.version}
546
+ SSH CIDRs: ${sshCidrs}
547
+ Gateway: ${gatewayCidrs}
548
+ `
549
+ );
488
550
  if (plan.diff) {
489
551
  const { create, update, delete: del, totalChanges } = plan.diff;
490
552
  process7.stderr.write(
@@ -501,7 +563,7 @@ Changes: ${create.length} to create, ${update.length} to update, ${del.length} t
501
563
  process7.stderr.write(renderTable(["Op", "Resource Type", "Name"], rows) + "\n");
502
564
  }
503
565
  } else {
504
- process7.stderr.write("(diff unavailable \u2014 preview could not run against this stack)\n");
566
+ process7.stderr.write("\n(diff unavailable \u2014 preview could not run against this stack)\n");
505
567
  }
506
568
  }
507
569
  });
@@ -509,7 +571,7 @@ Changes: ${create.length} to create, ${update.length} to update, ${del.length} t
509
571
  // src/cli/commands/apply.ts
510
572
  import { defineCommand as defineCommand6 } from "citty";
511
573
  import process8 from "process";
512
- import { readFileSync } from "fs";
574
+ import { readFileSync as readFileSync2 } from "fs";
513
575
  import { isAbsolute as isAbsolute2 } from "path";
514
576
  import { createInterface } from "readline/promises";
515
577
  var apply_default = defineCommand6({
@@ -523,7 +585,7 @@ var apply_default = defineCommand6({
523
585
  },
524
586
  async run({ args }) {
525
587
  const { validatePlan } = await import("./validate-T5M5EHSJ.js");
526
- const { applyPlan } = await import("./apply-GTVALBAY.js");
588
+ const { applyPlan } = await import("./apply-KBF7OPWL.js");
527
589
  const planPath = args._?.[0];
528
590
  if (!planPath) {
529
591
  throw new UsageError("Usage: clawops apply <plan.json>");
@@ -533,7 +595,7 @@ var apply_default = defineCommand6({
533
595
  }
534
596
  let raw;
535
597
  try {
536
- raw = readFileSync(planPath, "utf-8");
598
+ raw = readFileSync2(planPath, "utf-8");
537
599
  } catch {
538
600
  throw new UsageError(`Cannot read plan file: ${planPath}`);
539
601
  }
@@ -592,7 +654,18 @@ ${validation.errors.join("\n")}`);
592
654
  onOutput: (line) => {
593
655
  spin.text = line.trim() || spin.text;
594
656
  },
595
- signal: abortController.signal
657
+ signal: abortController.signal,
658
+ confirmDrift: args.yes ? void 0 : async () => {
659
+ spin.stop();
660
+ const rl = createInterface({ input: process8.stdin, output: process8.stdout });
661
+ const answer = await rl.question("Stack has drifted. Continue anyway? (y/N) ");
662
+ rl.close();
663
+ if (answer.trim().toLowerCase() !== "y") {
664
+ process8.stdout.write("Aborted.\n");
665
+ process8.exit(0);
666
+ }
667
+ spin.start();
668
+ }
596
669
  });
597
670
  spin.succeed(`Stack "${typedPlan.spec.stackName}" applied`);
598
671
  const summaryRows = Object.entries(result.changeSummary).filter(([, count]) => count > 0).map(([op, count]) => [op, String(count)]);
@@ -628,7 +701,7 @@ var destroy_default = defineCommand7({
628
701
  "dry-run": { type: "boolean", description: "Show what would be destroyed without destroying" }
629
702
  },
630
703
  async run({ args }) {
631
- const { buildContext } = await import("./context-T52JWL3P.js");
704
+ const { buildContext } = await import("./context-VUAYRAJY.js");
632
705
  const ctx = buildContext(args);
633
706
  if (ctx.adapter.name === "local") {
634
707
  throw new UsageError(
@@ -695,8 +768,8 @@ var ssh_default = defineCommand8({
695
768
  command: { type: "string", description: "Remote command to run (instead of interactive shell)" }
696
769
  },
697
770
  async run({ args }) {
698
- const { buildContext } = await import("./context-T52JWL3P.js");
699
- const { acquireSession, drainPool } = await import("./pool-D4JCUA2V.js");
771
+ const { buildContext } = await import("./context-VUAYRAJY.js");
772
+ const { acquireSession, drainPool } = await import("./pool-FBFHATDG.js");
700
773
  const ctx = buildContext(args);
701
774
  let conn;
702
775
  if (ctx.adapter.name === "local") {
@@ -713,7 +786,7 @@ var ssh_default = defineCommand8({
713
786
  knownHostsPath: state.knownHostsPath
714
787
  };
715
788
  } else {
716
- const { extractBaseOutputs } = await import("./outputs-6DAVEEAZ.js");
789
+ const { extractBaseOutputs } = await import("./outputs-DJHBY7EE.js");
717
790
  const stack = await ctx.getStack();
718
791
  const outputMap = await stack.outputs();
719
792
  const outputs = Object.fromEntries(
@@ -782,9 +855,9 @@ var tunnel_default = defineCommand9({
782
855
  "no-open": { type: "boolean", description: "Do not open browser after tunnel is ready" }
783
856
  },
784
857
  async run({ args }) {
785
- const { buildContext } = await import("./context-T52JWL3P.js");
786
- const { extractBaseOutputs } = await import("./outputs-6DAVEEAZ.js");
787
- const { acquireSession } = await import("./pool-D4JCUA2V.js");
858
+ const { buildContext } = await import("./context-VUAYRAJY.js");
859
+ const { extractBaseOutputs } = await import("./outputs-DJHBY7EE.js");
860
+ const { acquireSession } = await import("./pool-FBFHATDG.js");
788
861
  const ctx = buildContext(args);
789
862
  const stack = await ctx.getStack();
790
863
  const outputMap = await stack.outputs();
@@ -850,9 +923,9 @@ var logs_default = defineCommand10({
850
923
  since: { type: "string", description: "Show logs since duration (e.g. 5m, 1h)" }
851
924
  },
852
925
  async run({ args }) {
853
- const { buildContext } = await import("./context-T52JWL3P.js");
854
- const { extractBaseOutputs } = await import("./outputs-6DAVEEAZ.js");
855
- const { acquireSession, drainPool } = await import("./pool-D4JCUA2V.js");
926
+ const { buildContext } = await import("./context-VUAYRAJY.js");
927
+ const { extractBaseOutputs } = await import("./outputs-DJHBY7EE.js");
928
+ const { acquireSession, drainPool } = await import("./pool-FBFHATDG.js");
856
929
  const ctx = buildContext(args);
857
930
  const stack = await ctx.getStack();
858
931
  const outputMap = await stack.outputs();
@@ -922,11 +995,11 @@ import { defineCommand as defineCommand11 } from "citty";
922
995
  import process13 from "process";
923
996
  var OPENCLAW_CONFIG = "/home/clawops/openclaw.json";
924
997
  var OPENCLAW_TMP = "/tmp/clawops-config.json.tmp";
925
- function dockerRunCmd(version) {
998
+ function dockerRunCmd(image) {
926
999
  return [
927
1000
  "docker stop openclaw 2>/dev/null || true",
928
1001
  "docker rm openclaw 2>/dev/null || true",
929
- `docker run -d --name openclaw --restart unless-stopped -p 18789:18789 -v ${OPENCLAW_CONFIG}:/app/config.json:ro ghcr.io/openclaw/openclaw:${version}`
1002
+ `docker run -d --name openclaw --restart unless-stopped -p 18789:18789 -v ${OPENCLAW_CONFIG}:/app/config.json:ro ${image}`
930
1003
  ].join(" && ");
931
1004
  }
932
1005
  function getPath(obj, dotKey) {
@@ -967,12 +1040,12 @@ var config_default = defineCommand11({
967
1040
  "dry-run": { type: "boolean", description: "Show what would change without writing" }
968
1041
  },
969
1042
  async run({ args }) {
970
- const { buildContext } = await import("./context-T52JWL3P.js");
971
- const { extractBaseOutputs } = await import("./outputs-6DAVEEAZ.js");
972
- const { acquireSession, drainPool } = await import("./pool-D4JCUA2V.js");
1043
+ const { buildContext } = await import("./context-VUAYRAJY.js");
1044
+ const { extractBaseOutputs } = await import("./outputs-DJHBY7EE.js");
1045
+ const { acquireSession, drainPool } = await import("./pool-FBFHATDG.js");
973
1046
  const [action, key, value] = args._ ?? [];
974
- if (!action || !["get", "set", "unset"].includes(action)) {
975
- failure("Usage: clawops config <get [key] | set key value | unset key>");
1047
+ if (!action || !["get", "set", "unset", "validate"].includes(action)) {
1048
+ failure("Usage: clawops config <get [key] | set key value | unset key | validate>");
976
1049
  process13.exit(2);
977
1050
  }
978
1051
  if (action === "set" && (!key || value === void 0)) {
@@ -1024,6 +1097,17 @@ var config_default = defineCommand11({
1024
1097
  }
1025
1098
  return;
1026
1099
  }
1100
+ if (action === "validate") {
1101
+ const issues = validateOpenclawConfig(cfg);
1102
+ if (issues.length === 0) {
1103
+ success("Config is valid.");
1104
+ } else {
1105
+ for (const issue of issues) warn(issue);
1106
+ failure(`Config has ${issues.length} issue(s).`);
1107
+ process13.exit(1);
1108
+ }
1109
+ return;
1110
+ }
1027
1111
  if (action === "set") {
1028
1112
  let parsedValue = value;
1029
1113
  try {
@@ -1054,8 +1138,8 @@ var config_default = defineCommand11({
1054
1138
  `docker inspect openclaw --format '{{.Config.Image}}' 2>/dev/null || echo 'ghcr.io/openclaw/openclaw:stable'`,
1055
1139
  abortController.signal
1056
1140
  );
1057
- const version = imgResult.stdout.trim().split(":")[1] ?? "stable";
1058
- const restartResult = await session.exec(dockerRunCmd(version), abortController.signal);
1141
+ const image = imgResult.stdout.trim();
1142
+ const restartResult = await session.exec(dockerRunCmd(image), abortController.signal);
1059
1143
  if (restartResult.code !== 0) {
1060
1144
  failure(`Restart failed: ${restartResult.stderr}`);
1061
1145
  process13.exit(1);
@@ -1082,9 +1166,9 @@ var agents_default = defineCommand12({
1082
1166
  json: { type: "boolean", description: "Emit JSON (for list)" }
1083
1167
  },
1084
1168
  async run({ args }) {
1085
- const { buildContext } = await import("./context-T52JWL3P.js");
1086
- const { extractBaseOutputs } = await import("./outputs-6DAVEEAZ.js");
1087
- const { acquireSession, drainPool } = await import("./pool-D4JCUA2V.js");
1169
+ const { buildContext } = await import("./context-VUAYRAJY.js");
1170
+ const { extractBaseOutputs } = await import("./outputs-DJHBY7EE.js");
1171
+ const { acquireSession, drainPool } = await import("./pool-FBFHATDG.js");
1088
1172
  const [action, name] = args._ ?? [];
1089
1173
  if (!action || !["list", "restart", "logs"].includes(action)) {
1090
1174
  failure("Usage: clawops agents <list | restart [name] | logs <name>>");
@@ -1186,9 +1270,9 @@ var gateway_default = defineCommand13({
1186
1270
  json: { type: "boolean", description: "Emit JSON (for status)" }
1187
1271
  },
1188
1272
  async run({ args }) {
1189
- const { buildContext } = await import("./context-T52JWL3P.js");
1190
- const { extractBaseOutputs } = await import("./outputs-6DAVEEAZ.js");
1191
- const { acquireSession, drainPool } = await import("./pool-D4JCUA2V.js");
1273
+ const { buildContext } = await import("./context-VUAYRAJY.js");
1274
+ const { extractBaseOutputs } = await import("./outputs-DJHBY7EE.js");
1275
+ const { acquireSession, drainPool } = await import("./pool-FBFHATDG.js");
1192
1276
  const [action, versionArg] = args._ ?? [];
1193
1277
  if (!action || !["status", "restart", "update"].includes(action)) {
1194
1278
  failure("Usage: clawops gateway <status | restart | update [version]>");
@@ -1299,8 +1383,8 @@ var backup_default = defineCommand14({
1299
1383
  yes: { type: "boolean", description: "[restore] Skip confirmation prompt" }
1300
1384
  },
1301
1385
  async run({ args }) {
1302
- const { buildContext } = await import("./context-T52JWL3P.js");
1303
- const { acquireSession, drainPool } = await import("./pool-D4JCUA2V.js");
1386
+ const { buildContext } = await import("./context-VUAYRAJY.js");
1387
+ const { acquireSession, drainPool } = await import("./pool-FBFHATDG.js");
1304
1388
  const action = args.action;
1305
1389
  if (action !== "create" && action !== "restore") {
1306
1390
  throw new UsageError(`Unknown action: ${action}. Use "create" or "restore"`);
@@ -1321,7 +1405,7 @@ var backup_default = defineCommand14({
1321
1405
  knownHostsPath: state.knownHostsPath
1322
1406
  };
1323
1407
  } else {
1324
- const { extractBaseOutputs } = await import("./outputs-6DAVEEAZ.js");
1408
+ const { extractBaseOutputs } = await import("./outputs-DJHBY7EE.js");
1325
1409
  const stack = await ctx.getStack();
1326
1410
  const outputMap = await stack.outputs();
1327
1411
  const outputs = Object.fromEntries(
@@ -1514,8 +1598,11 @@ var doctor_default = defineCommand16({
1514
1598
  name: "doctor",
1515
1599
  description: "Check system prerequisites, config, SSH keys, and cloud credentials"
1516
1600
  },
1517
- async run() {
1518
- const { getConfig: getConfig2, getConfigDir: getConfigDir2 } = await import("./store-ARJ2EO6L.js");
1601
+ args: {
1602
+ stack: { type: "string", description: "Stack name to include remote health checks" }
1603
+ },
1604
+ async run({ args }) {
1605
+ const { getConfig: getConfig2, getConfigDir: getConfigDir2 } = await import("./store-SDUR52Z5.js");
1519
1606
  process18.stdout.write("\nclawops doctor\n");
1520
1607
  process18.stdout.write("\nRuntime\n");
1521
1608
  const nodeVersion = process18.version;
@@ -1565,9 +1652,9 @@ var doctor_default = defineCommand16({
1565
1652
  info("Credential checks skipped \u2014 no config");
1566
1653
  } else {
1567
1654
  const { getProvider } = await import("./providers-2OABPW2E.js");
1568
- await import("./aws-A3323GNM.js");
1569
- await import("./gcp-OHWCTFLL.js");
1570
- await import("./azure-LNWBK2ZI.js");
1655
+ await import("./aws-KQBWY43W.js");
1656
+ await import("./gcp-SKURG3L6.js");
1657
+ await import("./azure-4EJFVJZL.js");
1571
1658
  await import("./local-DXBEVZ5C.js");
1572
1659
  const checkedProviders = /* @__PURE__ */ new Set();
1573
1660
  for (const [stackName, stackCfg] of Object.entries(config.stacks)) {
@@ -1596,6 +1683,98 @@ var doctor_default = defineCommand16({
1596
1683
  warn("No stacks configured \u2014 run `clawops init`");
1597
1684
  }
1598
1685
  }
1686
+ if (args.stack) {
1687
+ process18.stdout.write("\nRemote health\n");
1688
+ if (!config) {
1689
+ info("Remote health skipped \u2014 no config");
1690
+ } else {
1691
+ try {
1692
+ const { buildContext } = await import("./context-VUAYRAJY.js");
1693
+ const { extractBaseOutputs } = await import("./outputs-DJHBY7EE.js");
1694
+ const { acquireSession, drainPool } = await import("./pool-FBFHATDG.js");
1695
+ const ctx = buildContext({ stack: args.stack });
1696
+ const stack = await ctx.getStack();
1697
+ const outputMap = await stack.outputs();
1698
+ const outputs = Object.fromEntries(
1699
+ Object.entries(outputMap).map(([k, v]) => [k, v.value])
1700
+ );
1701
+ const base = extractBaseOutputs(outputs);
1702
+ const conn = ctx.adapter.getConnectionInfo({
1703
+ ...base,
1704
+ privateKeyPath: config.ssh.keyPath,
1705
+ knownHostsPath: config.ssh.knownHostsPath
1706
+ });
1707
+ const ac = new AbortController();
1708
+ process18.on("SIGINT", () => ac.abort());
1709
+ process18.on("SIGTERM", () => ac.abort());
1710
+ const { session, release } = await acquireSession({
1711
+ host: conn.host,
1712
+ port: conn.port,
1713
+ user: conn.user,
1714
+ privateKeyPath: conn.privateKeyPath,
1715
+ knownHostsPath: conn.knownHostsPath,
1716
+ signal: ac.signal
1717
+ });
1718
+ try {
1719
+ const containerResult = await session.exec(
1720
+ `docker inspect openclaw --format '{{.State.Status}}' 2>/dev/null || echo 'not found'`,
1721
+ ac.signal
1722
+ );
1723
+ const containerStatus = containerResult.stdout.trim();
1724
+ if (containerStatus === "running") {
1725
+ success(`Container running`);
1726
+ } else {
1727
+ failure(`Container ${containerStatus || "unknown"}`);
1728
+ }
1729
+ const healthResult = await session.exec(
1730
+ `docker inspect openclaw --format '{{.State.Health.Status}}' 2>/dev/null || echo 'none'`,
1731
+ ac.signal
1732
+ );
1733
+ const healthStatus = healthResult.stdout.trim();
1734
+ if (healthStatus === "healthy") {
1735
+ success(`Healthcheck healthy`);
1736
+ } else if (healthStatus === "none" || healthStatus === "<no value>") {
1737
+ info(`Healthcheck no healthcheck configured`);
1738
+ } else {
1739
+ warn(`Healthcheck ${healthStatus}`);
1740
+ }
1741
+ const diskResult = await session.exec(
1742
+ `df -h /home/clawops 2>/dev/null | awk 'NR==2{print $5" used ("$3" of "$2")"}'`,
1743
+ ac.signal
1744
+ );
1745
+ const diskUsage = diskResult.stdout.trim();
1746
+ if (diskUsage) {
1747
+ const pctMatch = diskUsage.match(/^(\d+)%/);
1748
+ const pct = pctMatch ? parseInt(pctMatch[1], 10) : 0;
1749
+ if (pct >= 90) {
1750
+ failure(`Disk ${diskUsage}`);
1751
+ } else if (pct >= 75) {
1752
+ warn(`Disk ${diskUsage}`);
1753
+ } else {
1754
+ success(`Disk ${diskUsage}`);
1755
+ }
1756
+ } else {
1757
+ warn("Disk unable to determine disk usage");
1758
+ }
1759
+ const logrotateResult = await session.exec(
1760
+ `test -f /etc/logrotate.d/openclaw && echo 'configured' || echo 'not configured'`,
1761
+ ac.signal
1762
+ );
1763
+ const logrotate = logrotateResult.stdout.trim();
1764
+ if (logrotate === "configured") {
1765
+ success(`Log rotation configured`);
1766
+ } else {
1767
+ warn(`Log rotation not configured \u2014 logs may grow unbounded`);
1768
+ }
1769
+ } finally {
1770
+ release();
1771
+ drainPool();
1772
+ }
1773
+ } catch (err) {
1774
+ failure(`Remote health checks failed: ${err instanceof Error ? err.message : String(err)}`);
1775
+ }
1776
+ }
1777
+ }
1599
1778
  process18.stdout.write("\n");
1600
1779
  if (!nodeOk) process18.exit(1);
1601
1780
  }
@@ -1603,7 +1782,7 @@ var doctor_default = defineCommand16({
1603
1782
 
1604
1783
  // src/cli/commands/mcp.ts
1605
1784
  import { defineCommand as defineCommand17 } from "citty";
1606
- import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3 } from "fs";
1785
+ import { existsSync as existsSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3 } from "fs";
1607
1786
  import path3 from "path";
1608
1787
  import os from "os";
1609
1788
  var mcp_default = defineCommand17({
@@ -1635,7 +1814,7 @@ var mcp_default = defineCommand17({
1635
1814
  }
1636
1815
  return;
1637
1816
  }
1638
- const { serveMcp } = await import("./server-YO4JFQND.js");
1817
+ const { serveMcp } = await import("./server-QJRWK2RV.js");
1639
1818
  await serveMcp({
1640
1819
  port: args.http ? Number(args.http) : void 0,
1641
1820
  bind: args.bind,
@@ -1653,13 +1832,14 @@ var CLAWOPS_ENTRY = {
1653
1832
  };
1654
1833
  function getConfigPath(client) {
1655
1834
  const home = os.homedir();
1835
+ const isLinux = process.platform === "linux";
1656
1836
  switch (client) {
1657
1837
  case "claude":
1658
- return path3.join(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
1838
+ return isLinux ? path3.join(home, ".config", "Claude", "claude_desktop_config.json") : path3.join(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
1659
1839
  case "cursor":
1660
1840
  return path3.join(home, ".cursor", "mcp.json");
1661
1841
  case "vscode":
1662
- return path3.join(home, "Library", "Application Support", "Code", "User", "mcp.json");
1842
+ return isLinux ? path3.join(home, ".config", "Code", "User", "mcp.json") : path3.join(home, "Library", "Application Support", "Code", "User", "mcp.json");
1663
1843
  case "windsurf":
1664
1844
  return path3.join(home, ".codeium", "windsurf", "mcp_config.json");
1665
1845
  case "zed":
@@ -1675,7 +1855,7 @@ function installMcp(client) {
1675
1855
  let config = {};
1676
1856
  if (existsSync2(configPath)) {
1677
1857
  try {
1678
- config = JSON.parse(readFileSync2(configPath, "utf-8"));
1858
+ config = JSON.parse(readFileSync3(configPath, "utf-8"));
1679
1859
  } catch {
1680
1860
  }
1681
1861
  }
@@ -1691,26 +1871,1042 @@ function installMcp(client) {
1691
1871
  writeFileSync3(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
1692
1872
  }
1693
1873
 
1694
- // src/cli/error-handler.ts
1874
+ // src/cli/commands/setup.ts
1875
+ import { defineCommand as defineCommand18 } from "citty";
1695
1876
  import process19 from "process";
1877
+ import os2 from "os";
1878
+ import { randomBytes } from "crypto";
1879
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync4, existsSync as existsSync3, mkdirSync as mkdirSync4 } from "fs";
1880
+ import { execSync, spawnSync, spawn as spawn2 } from "child_process";
1881
+ import path4 from "path";
1882
+ import { fileURLToPath } from "url";
1883
+ var setup_default = defineCommand18({
1884
+ meta: {
1885
+ name: "setup",
1886
+ description: "Interactive wizard: configure and deploy an OpenClaw stack"
1887
+ },
1888
+ args: {
1889
+ "dry-run": { type: "boolean", description: "Generate plan/config without applying" },
1890
+ "output-dir": { type: "string", description: "Directory to write generated files (skips prompt)" }
1891
+ },
1892
+ async run({ args }) {
1893
+ const inquirer = (await import("inquirer")).default;
1894
+ const yaml = await import("./js-yaml-PTEEG4FO.js");
1895
+ const catalogs = loadCatalogs(yaml);
1896
+ const dryRun = Boolean(args["dry-run"]);
1897
+ const ac = new AbortController();
1898
+ process19.on("SIGINT", () => {
1899
+ ac.abort();
1900
+ process19.exit(130);
1901
+ });
1902
+ process19.on("SIGTERM", () => {
1903
+ ac.abort();
1904
+ process19.exit(143);
1905
+ });
1906
+ process19.stdout.write("\nWelcome to clawops setup. This wizard will help you deploy OpenClaw\n");
1907
+ process19.stdout.write("and connect it to your AI tools. It takes about 2 minutes.\n\n");
1908
+ const { deploymentType } = await inquirer.prompt([{
1909
+ type: "list",
1910
+ name: "deploymentType",
1911
+ message: "Where would you like to run OpenClaw?",
1912
+ choices: [
1913
+ { name: "Cloud (AWS, GCP, or Azure) \u2014 deploy to paid cloud hosting", value: "cloud" },
1914
+ { name: "Deploy to a local or existing server \u2014 connect over SSH (Linux or macOS)", value: "local" }
1915
+ ]
1916
+ }]);
1917
+ let provider;
1918
+ let localHost = "";
1919
+ let localUser = os2.userInfo().username;
1920
+ let localKeyPath = detectSshKey();
1921
+ let localPort = 22;
1922
+ let localSudoPassword = "";
1923
+ if (deploymentType === "cloud") {
1924
+ const answer = await inquirer.prompt([{
1925
+ type: "list",
1926
+ name: "provider",
1927
+ message: "Which cloud service would you like to use?",
1928
+ choices: [
1929
+ { name: "Amazon Web Services (AWS) \u2014 most popular, widest region coverage", value: "aws" },
1930
+ { name: "Google Cloud (GCP) \u2014 good pricing, strong in AI workloads", value: "gcp" },
1931
+ { name: "Microsoft Azure \u2014 best if your team already uses Microsoft", value: "azure" }
1932
+ ]
1933
+ }]);
1934
+ provider = answer.provider;
1935
+ process19.stdout.write("\n");
1936
+ await ensureCloudAuth(provider, inquirer);
1937
+ } else {
1938
+ provider = "local";
1939
+ info("We'll connect to your server over SSH to install and configure OpenClaw.\n");
1940
+ if (!existsSync3(localKeyPath)) {
1941
+ info(`No SSH key found at ${localKeyPath}.`);
1942
+ const { genKey } = await inquirer.prompt([{
1943
+ type: "confirm",
1944
+ name: "genKey",
1945
+ message: "Generate a new SSH key pair now? (recommended if you don't have one)",
1946
+ default: true
1947
+ }]);
1948
+ if (genKey) {
1949
+ localKeyPath = await generateSshKey();
1950
+ }
1951
+ }
1952
+ const localAnswers = await inquirer.prompt([
1953
+ {
1954
+ type: "input",
1955
+ name: "host",
1956
+ message: "Server address: (IP address or hostname, e.g. 192.168.1.100 or myserver.example.com)",
1957
+ validate: (v) => v.trim() !== "" || "Please enter a server address"
1958
+ },
1959
+ {
1960
+ type: "number",
1961
+ name: "port",
1962
+ message: "SSH port: (almost always 22 \u2014 press Enter to accept)",
1963
+ default: 22
1964
+ },
1965
+ {
1966
+ type: "input",
1967
+ name: "user",
1968
+ message: 'SSH username: (e.g. "ubuntu" on AWS, "ec2-user" on Amazon Linux, or your local username for localhost)',
1969
+ default: localUser
1970
+ },
1971
+ {
1972
+ type: "input",
1973
+ name: "keyPath",
1974
+ message: `SSH private key file: (the key file on your computer used to log in \u2014 e.g. ~/.ssh/id_ed25519)`,
1975
+ default: localKeyPath,
1976
+ validate: (v) => existsSync3(v.trim()) || `File not found: ${v.trim()}`
1977
+ }
1978
+ ]);
1979
+ localHost = localAnswers.host;
1980
+ localPort = localAnswers.port;
1981
+ localUser = localAnswers.user;
1982
+ localKeyPath = localAnswers.keyPath;
1983
+ await ensureAuthorizedKey(localKeyPath, localHost, inquirer);
1984
+ const { sudoPass } = await inquirer.prompt([{
1985
+ type: "password",
1986
+ name: "sudoPass",
1987
+ message: `Sudo password for ${localUser}@${localHost}: (press Enter to skip if passwordless sudo is configured)`
1988
+ }]);
1989
+ localSudoPassword = sudoPass;
1990
+ }
1991
+ process19.stdout.write("\n");
1992
+ const stackAnswers = await inquirer.prompt([
1993
+ {
1994
+ type: "input",
1995
+ name: "stackName",
1996
+ message: 'Deployment name: (a short label for this install, e.g. "prod", "home", "team-dev")',
1997
+ default: "prod",
1998
+ validate: (v) => /^[a-z][a-z0-9-]{0,62}$/.test(v) || "Use lowercase letters, numbers, and hyphens only"
1999
+ },
2000
+ ...provider !== "local" ? [
2001
+ {
2002
+ type: "input",
2003
+ name: "region",
2004
+ message: `Server region: (the geographic area where your server will live \u2014 pick closest to your users)`,
2005
+ default: defaultRegion(provider)
2006
+ },
2007
+ {
2008
+ type: "list",
2009
+ name: "instanceSize",
2010
+ message: "Server size: (bigger = faster, but costs more per month)",
2011
+ choices: instanceChoices(provider)
2012
+ },
2013
+ {
2014
+ type: "input",
2015
+ name: "stateBucket",
2016
+ message: `${stateLabel(provider)} bucket name: (a storage bucket that tracks what's deployed \u2014 create one first if you haven't)`,
2017
+ validate: (v) => v.trim() !== "" || "Required \u2014 create a bucket in your cloud console first"
2018
+ },
2019
+ {
2020
+ type: "input",
2021
+ name: "sshKeyPath",
2022
+ message: "SSH public key file: (the .pub file that goes with your private key \u2014 used to access the new server)",
2023
+ default: `${detectSshKey()}.pub`
2024
+ },
2025
+ {
2026
+ type: "input",
2027
+ name: "sshCidr",
2028
+ message: "Restrict SSH access to: (your IP address for security, or 0.0.0.0/0 to allow access from anywhere)",
2029
+ default: "0.0.0.0/0"
2030
+ }
2031
+ ] : [],
2032
+ {
2033
+ type: "input",
2034
+ name: "openclawVersion",
2035
+ message: 'OpenClaw version: ("latest" for the latest release, or a specific version like "2026.4")',
2036
+ default: "latest"
2037
+ }
2038
+ ]);
2039
+ process19.stdout.write("\n");
2040
+ info("Choose the AI model that your OpenClaw agent will use.");
2041
+ info("You'll need an API key from your chosen provider (except Bedrock and Ollama).\n");
2042
+ const { modelProviderId } = await inquirer.prompt([{
2043
+ type: "list",
2044
+ name: "modelProviderId",
2045
+ message: "Which AI provider do you want to use?",
2046
+ choices: catalogs.models.map((p) => ({ name: p.displayName, value: p.id }))
2047
+ }]);
2048
+ const modelProvider = catalogs.models.find((p) => p.id === modelProviderId);
2049
+ const modelConfig = {};
2050
+ const secrets = [];
2051
+ const { selectedModelId } = await inquirer.prompt([{
2052
+ type: "list",
2053
+ name: "selectedModelId",
2054
+ message: `Which ${modelProvider.displayName} model should OpenClaw use?`,
2055
+ choices: modelProvider.models.map((m) => ({
2056
+ name: m.family ? `[${m.family}] ${m.displayName}` : m.displayName,
2057
+ value: m.id
2058
+ })),
2059
+ default: modelProvider.defaultModel
2060
+ }]);
2061
+ const selectedModel = modelProvider.models.find((m) => m.id === selectedModelId);
2062
+ if (modelProvider.credentialSource === "api-key") {
2063
+ const secretName = `${modelProviderId.toUpperCase()}_API_KEY`;
2064
+ const entry = await promptSecret(secretName, `${modelProvider.displayName} API key`, modelProvider.envDefault ?? "API_KEY", inquirer);
2065
+ secrets.push(entry);
2066
+ modelConfig["apiKey"] = `$secret:${secretName}`;
2067
+ } else if (modelProvider.credentialSource === "aws-profile") {
2068
+ process19.stdout.write("\n");
2069
+ info(modelProvider.iamNote ?? "Bedrock uses your AWS server's IAM role \u2014 no API key needed.");
2070
+ info("Make sure your EC2 instance has the AmazonBedrockFullAccess IAM policy attached.\n");
2071
+ } else if (modelProvider.id === "ollama") {
2072
+ const { baseUrl } = await inquirer.prompt([{
2073
+ type: "input",
2074
+ name: "baseUrl",
2075
+ message: "Ollama address: (the URL where Ollama is running \u2014 usually http://localhost:11434)",
2076
+ default: modelProvider.baseUrlDefault ?? "http://localhost:11434"
2077
+ }]);
2078
+ modelConfig["baseUrl"] = baseUrl;
2079
+ }
2080
+ const builtModelConfig = {
2081
+ provider: modelProviderId,
2082
+ ...selectedModel.modelId ? { modelId: selectedModel.modelId } : { model: selectedModel.id },
2083
+ ...modelConfig
2084
+ };
2085
+ process19.stdout.write("\n");
2086
+ info("Use \u2191\u2193 to move, Space to select/deselect, Enter to confirm.");
2087
+ const { selectedIntegrationIds } = await inquirer.prompt([{
2088
+ type: "checkbox",
2089
+ name: "selectedIntegrationIds",
2090
+ message: "Which chat integrations would you like to enable?",
2091
+ choices: catalogs.integrations.map((integ) => ({
2092
+ name: `${integ.displayName} \u2014 ${integ.description}`,
2093
+ value: integ.id,
2094
+ checked: false
2095
+ })),
2096
+ pageSize: catalogs.integrations.length + 1
2097
+ }]);
2098
+ const channelsConfig = {};
2099
+ const infraRequired = [];
2100
+ for (const integ of catalogs.integrations.filter((i) => selectedIntegrationIds.includes(i.id))) {
2101
+ process19.stdout.write("\n");
2102
+ info(`Setting up ${integ.displayName}:`);
2103
+ const channelConfig = {};
2104
+ for (const field of integ.fields) {
2105
+ if (field.sensitive && field.envDefault) {
2106
+ const entry = await promptSecret(field.envDefault, `${integ.displayName} ${field.label}`, field.envDefault, inquirer);
2107
+ secrets.push(entry);
2108
+ channelConfig[field.name] = `$secret:${field.envDefault}`;
2109
+ } else {
2110
+ const { value } = await inquirer.prompt([{
2111
+ type: "input",
2112
+ name: "value",
2113
+ message: `${field.label}: ${field.description}`,
2114
+ default: ""
2115
+ }]);
2116
+ channelConfig[field.name] = value;
2117
+ }
2118
+ }
2119
+ channelsConfig[integ.channelKey] = channelConfig;
2120
+ if (integ.infraRequired) infraRequired.push(integ);
2121
+ }
2122
+ let outDir = typeof args["output-dir"] === "string" ? args["output-dir"] : null;
2123
+ if (outDir === null) {
2124
+ if (provider !== "local") {
2125
+ const { outputDir } = await inquirer.prompt([{
2126
+ type: "input",
2127
+ name: "outputDir",
2128
+ message: "Where should the plan file be saved? (press Enter to save in the current folder)",
2129
+ default: "."
2130
+ }]);
2131
+ outDir = outputDir;
2132
+ } else {
2133
+ outDir = ".";
2134
+ }
2135
+ }
2136
+ const gatewayToken = randomBytes(24).toString("hex");
2137
+ const secretsDir = path4.join(os2.homedir(), ".clawops", "secrets");
2138
+ mkdirSync4(secretsDir, { recursive: true });
2139
+ spawnSync("chmod", ["700", secretsDir], { stdio: "ignore" });
2140
+ const tokenPath = path4.join(secretsDir, `GATEWAY_TOKEN_${stackAnswers.stackName}`);
2141
+ writeFileSync4(tokenPath, gatewayToken, { encoding: "utf-8", mode: 384 });
2142
+ const openclawConfigOverlay = {
2143
+ models: builtModelConfig,
2144
+ gateway: { auth: { mode: "token", token: gatewayToken } }
2145
+ };
2146
+ if (Object.keys(channelsConfig).length > 0) {
2147
+ openclawConfigOverlay["channels"] = channelsConfig;
2148
+ }
2149
+ let outputPath;
2150
+ if (provider === "local") {
2151
+ outputPath = path4.join(outDir, `openclaw-${stackAnswers.stackName}.json`);
2152
+ writeFileSync4(outputPath, JSON.stringify(openclawConfigOverlay, null, 2), "utf-8");
2153
+ process19.stdout.write("\n");
2154
+ success(`Config file saved to ${outputPath}`);
2155
+ } else {
2156
+ let sshPublicKey = "";
2157
+ try {
2158
+ sshPublicKey = readFileSync4(stackAnswers.sshKeyPath ?? "", "utf-8").trim();
2159
+ } catch {
2160
+ failure(`Cannot read SSH public key at ${stackAnswers.sshKeyPath ?? ""}`);
2161
+ process19.exit(1);
2162
+ }
2163
+ const plan = {
2164
+ apiVersion: "clawops.dev/v1",
2165
+ kind: "DeployPlan",
2166
+ metadata: {
2167
+ name: stackAnswers.stackName,
2168
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
2169
+ generator: "clawops-setup"
2170
+ },
2171
+ spec: {
2172
+ provider,
2173
+ region: stackAnswers.region,
2174
+ stackName: stackAnswers.stackName,
2175
+ instanceType: stackAnswers.instanceSize,
2176
+ openclaw: {
2177
+ version: stackAnswers.openclawVersion,
2178
+ config: openclawConfigOverlay,
2179
+ ...Object.keys(channelsConfig).length > 0 ? { channels: channelsConfig } : {}
2180
+ },
2181
+ secrets,
2182
+ network: {
2183
+ allowedSshCidrs: [stackAnswers.sshCidr ?? "0.0.0.0/0"],
2184
+ allowedGatewayCidrs: [stackAnswers.sshCidr ?? "0.0.0.0/0"]
2185
+ },
2186
+ ssh: { publicKey: sshPublicKey },
2187
+ ...stackAnswers.stateBucket ? { stateBucket: stackAnswers.stateBucket } : {}
2188
+ }
2189
+ };
2190
+ outputPath = path4.join(outDir, `clawops-${stackAnswers.stackName}-plan.json`);
2191
+ writeFileSync4(outputPath, JSON.stringify(plan, null, 2), "utf-8");
2192
+ process19.stdout.write("\n");
2193
+ success(`Deployment plan saved to ${outputPath}`);
2194
+ }
2195
+ process19.stdout.write("\n");
2196
+ const mcpEntry = buildMcpEntry();
2197
+ if (!mcpEntry.resolved) {
2198
+ info("Note: clawops is not installed globally \u2014 AI apps will not be able to start");
2199
+ info("the MCP server until you install it:");
2200
+ info(" npm install -g @clawops/cli");
2201
+ info("The config will be written now; re-run setup after installing to update the path.\n");
2202
+ }
2203
+ info("Use \u2191\u2193 to move, Space to select/deselect, Enter to confirm.");
2204
+ const appChoices = MCP_APPS.map((app) => ({
2205
+ name: app.isInstalled() ? app.name : `${app.name} (not detected \u2014 config will be written anyway)`,
2206
+ value: app.id,
2207
+ checked: app.isInstalled()
2208
+ }));
2209
+ const { selectedAppIds } = await inquirer.prompt([{
2210
+ type: "checkbox",
2211
+ name: "selectedAppIds",
2212
+ message: "Connect clawops to your AI apps? (space to toggle, enter to skip)",
2213
+ choices: appChoices,
2214
+ pageSize: MCP_APPS.length + 1
2215
+ }]);
2216
+ const selectedApps = MCP_APPS.filter((app) => selectedAppIds.includes(app.id));
2217
+ if (selectedApps.length > 0) {
2218
+ for (const app of selectedApps) {
2219
+ const cfgPath = app.configPath();
2220
+ try {
2221
+ let existing = {};
2222
+ if (existsSync3(cfgPath)) {
2223
+ existing = JSON.parse(readFileSync4(cfgPath, "utf-8"));
2224
+ }
2225
+ const mcpServers = existing["mcpServers"] ?? {};
2226
+ mcpServers["clawops"] = { command: mcpEntry.command, args: mcpEntry.args };
2227
+ existing["mcpServers"] = mcpServers;
2228
+ mkdirSync4(path4.dirname(cfgPath), { recursive: true });
2229
+ writeFileSync4(cfgPath, JSON.stringify(existing, null, 2), "utf-8");
2230
+ success(`${app.name} configured (${cfgPath})`);
2231
+ } catch (err) {
2232
+ failure(`Could not configure ${app.name}: ${err.message}`);
2233
+ info("Add this to its MCP config manually:");
2234
+ printMcpSnippet();
2235
+ }
2236
+ }
2237
+ const needsRestart = selectedApps.filter((app) => app.id !== "claude-code");
2238
+ if (needsRestart.length > 0) {
2239
+ info(`Restart ${needsRestart.map((a) => a.name).join(", ")} to load the clawops tool.`);
2240
+ }
2241
+ } else {
2242
+ info("To connect clawops to an AI app later, add this to its MCP config:");
2243
+ printMcpSnippet();
2244
+ info("Config paths:");
2245
+ for (const app of MCP_APPS) {
2246
+ info(` ${app.name.padEnd(18)} ${app.configPath()}`);
2247
+ }
2248
+ }
2249
+ if (infraRequired.length > 0) {
2250
+ process19.stdout.write("\n");
2251
+ 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");
2252
+ info("These integrations need a webhook URL set up in their developer portal:");
2253
+ for (const integ of infraRequired) {
2254
+ info(`
2255
+ ${integ.displayName}`);
2256
+ if (integ.infraNote) info(` ${integ.infraNote.trim()}`);
2257
+ if (integ.setupUrl) info(` Guide: ${integ.setupUrl}`);
2258
+ }
2259
+ }
2260
+ if (modelProvider.id === "ollama" && selectedModel.pullSuffix !== void 0) {
2261
+ process19.stdout.write("\n");
2262
+ 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");
2263
+ info("After deployment, run this command on the server (or locally if Ollama is local):");
2264
+ info(` ollama pull ${selectedModel.id}${selectedModel.pullSuffix}`);
2265
+ }
2266
+ if (dryRun) return;
2267
+ if (provider === "local") {
2268
+ const { deployNow } = await inquirer.prompt([{
2269
+ type: "confirm",
2270
+ name: "deployNow",
2271
+ message: "Initialize and deploy the server now? (connects over SSH \u2014 takes 2\u20135 min)",
2272
+ default: true
2273
+ }]);
2274
+ if (deployNow) {
2275
+ await runLocalDeploy({
2276
+ stackName: stackAnswers.stackName,
2277
+ host: localHost,
2278
+ port: localPort,
2279
+ user: localUser,
2280
+ keyPath: localKeyPath,
2281
+ sudoPassword: localSudoPassword || void 0,
2282
+ openclawVersion: stackAnswers.openclawVersion,
2283
+ overlay: openclawConfigOverlay,
2284
+ secrets,
2285
+ gatewayToken,
2286
+ signal: ac.signal,
2287
+ inquirer
2288
+ });
2289
+ } else {
2290
+ const { getConfigDir: getConfigDir2 } = await import("./store-SDUR52Z5.js");
2291
+ const knownHostsPath = path4.join(getConfigDir2(), "known_hosts");
2292
+ process19.stdout.write("\n");
2293
+ info("To deploy later, run these two commands:");
2294
+ info(` clawops init --provider local --host ${localHost} --port ${localPort} --user ${localUser} --key ${localKeyPath} --stack ${stackAnswers.stackName}`);
2295
+ info(` clawops up --stack ${stackAnswers.stackName} --config ${outputPath}`);
2296
+ info(`
2297
+ (SSH host verification will be saved to ${knownHostsPath})`);
2298
+ }
2299
+ } else {
2300
+ const { applyNow } = await inquirer.prompt([{
2301
+ type: "confirm",
2302
+ name: "applyNow",
2303
+ message: "Provision the cloud server now? (creates the server and installs OpenClaw \u2014 takes 3\u20135 minutes)",
2304
+ default: false
2305
+ }]);
2306
+ if (applyNow) {
2307
+ const { applyPlan } = await import("./apply-KBF7OPWL.js");
2308
+ const plan = JSON.parse(readFileSync4(outputPath, "utf-8"));
2309
+ try {
2310
+ await applyPlan(plan, {
2311
+ onOutput: (line) => process19.stdout.write(line),
2312
+ signal: ac.signal
2313
+ });
2314
+ success("Server provisioned and OpenClaw installed.");
2315
+ info(`Run: clawops doctor --stack ${stackAnswers.stackName} to verify everything is healthy`);
2316
+ } catch (err) {
2317
+ failure(err instanceof Error ? err.message : String(err));
2318
+ process19.exit(1);
2319
+ }
2320
+ } else {
2321
+ info(`
2322
+ To deploy later: clawops apply ${outputPath}`);
2323
+ }
2324
+ }
2325
+ }
2326
+ });
2327
+ async function runLocalDeploy(opts) {
2328
+ const { setConfig: setConfig2, getConfig: getConfig2, getConfigDir: getConfigDir2 } = await import("./store-SDUR52Z5.js");
2329
+ const { localBootstrap } = await import("./bootstrap-UYCOEJQX.js");
2330
+ const { connect } = await import("./ssh-IQXNME3D.js");
2331
+ const { readRemoteConfig, atomicWriteConfig, restartGateway, deepMerge } = await import("./remote-config-JQ77SRC2.js");
2332
+ const { resolveSecrets } = await import("./secrets-SVZWNAPK.js");
2333
+ const knownHostsPath = path4.join(getConfigDir2(), "known_hosts");
2334
+ const existing = getConfig2();
2335
+ const stackEntry = {
2336
+ provider: "local",
2337
+ stateUrl: "file://~/.clawops/state",
2338
+ credentialsRef: { source: "file", envVars: [] },
2339
+ localOpts: { host: opts.host, sshUser: opts.user, sshPort: opts.port, sshKeyPath: opts.keyPath }
2340
+ };
2341
+ const newConfig = existing ? { ...existing, defaults: { stack: opts.stackName, provider: "local" }, stacks: { ...existing.stacks, [opts.stackName]: stackEntry } } : {
2342
+ version: 1,
2343
+ defaults: { stack: opts.stackName, provider: "local" },
2344
+ stacks: { [opts.stackName]: stackEntry },
2345
+ ssh: { keyPath: opts.keyPath, knownHostsPath }
2346
+ };
2347
+ setConfig2(newConfig);
2348
+ success(`Deployment "${opts.stackName}" registered (~/.clawops/config.json)`);
2349
+ await checkDockerPreflight({ host: opts.host, port: opts.port, user: opts.user, keyPath: opts.keyPath, knownHostsPath, signal: opts.signal, inquirer: opts.inquirer });
2350
+ const STAGES = [
2351
+ [/apt-get update|dnf|yum|apk update/i, "Updating package lists..."],
2352
+ [/apt-get install|dnf install|yum install|apk add/i, "Installing dependencies..."],
2353
+ [/docker-ce|containerd|docker\.io|Install Docker/i, "Installing Docker..."],
2354
+ [/Pulling from|docker pull|Pull complete|Already exists/i, "Pulling OpenClaw image..."],
2355
+ [/systemctl|rc-update|docker run|ExecStart/i, "Starting OpenClaw service..."],
2356
+ [/bootstrap complete/i, "Waiting for OpenClaw to start..."]
2357
+ ];
2358
+ const spin = spinner(`Connecting to ${opts.host}...`);
2359
+ let state;
2360
+ try {
2361
+ state = await localBootstrap({
2362
+ host: opts.host,
2363
+ port: opts.port,
2364
+ user: opts.user,
2365
+ privateKeyPath: opts.keyPath,
2366
+ knownHostsPath,
2367
+ openclawVersion: opts.openclawVersion,
2368
+ stackName: opts.stackName,
2369
+ sudoPassword: opts.sudoPassword,
2370
+ noWait: false,
2371
+ onOutput: (line) => {
2372
+ for (const [pattern, label] of STAGES) {
2373
+ if (pattern.test(line)) {
2374
+ spin.text = label;
2375
+ break;
2376
+ }
2377
+ }
2378
+ },
2379
+ signal: opts.signal
2380
+ });
2381
+ spin.succeed(`OpenClaw installed on ${opts.host}`);
2382
+ } catch (err) {
2383
+ spin.fail("Installation failed");
2384
+ throw err;
2385
+ }
2386
+ const spin2 = spinner("Applying your configuration...");
2387
+ try {
2388
+ const session = await connect({
2389
+ host: opts.host,
2390
+ port: opts.port,
2391
+ user: opts.user,
2392
+ privateKeyPath: opts.keyPath,
2393
+ knownHostsPath,
2394
+ signal: opts.signal
2395
+ });
2396
+ try {
2397
+ const remote = await readRemoteConfig(session, opts.signal);
2398
+ const resolved = resolveSecrets(opts.overlay, opts.secrets);
2399
+ const merged = deepMerge(remote, resolved);
2400
+ await atomicWriteConfig(session, merged, opts.signal);
2401
+ await restartGateway(session, opts.signal);
2402
+ } finally {
2403
+ session.close();
2404
+ }
2405
+ spin2.succeed("Configuration applied");
2406
+ } catch (err) {
2407
+ spin2.fail("Configuration failed");
2408
+ throw err;
2409
+ }
2410
+ const { drainPool } = await import("./pool-FBFHATDG.js");
2411
+ drainPool();
2412
+ const tokenPath = path4.join(os2.homedir(), ".clawops", "secrets", `GATEWAY_TOKEN_${opts.stackName}`);
2413
+ const dashboardUrl = `${state.gatewayUrl}?token=${opts.gatewayToken}`;
2414
+ process19.stdout.write("\n");
2415
+ success("All done! OpenClaw is running.");
2416
+ info(`Open dashboard: ${dashboardUrl}`);
2417
+ info(` (or enter the token manually in the "Gateway Token" field: ${opts.gatewayToken})`);
2418
+ info(`SSH access: ${state.sshUser}@${state.sshHost}:${state.sshPort}`);
2419
+ info(`Token saved to ${tokenPath}`);
2420
+ info(`
2421
+ Run clawops doctor --stack ${opts.stackName} to check everything is healthy`);
2422
+ }
2423
+ var CLOUD_AUTH = {
2424
+ aws: {
2425
+ check: () => execSync("aws sts get-caller-identity --query Account --output text", { stdio: ["ignore", "pipe", "ignore"] }).toString().trim(),
2426
+ identity: (out) => `AWS account ${out}`,
2427
+ loginCmd: ["aws", "configure"],
2428
+ loginHint: "This will walk you through entering your Access Key ID and Secret."
2429
+ },
2430
+ gcp: {
2431
+ check: () => {
2432
+ const acct = execSync("gcloud config get-value account 2>/dev/null", { stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
2433
+ if (!acct || acct === "(unset)") throw new Error("not authenticated");
2434
+ return acct;
2435
+ },
2436
+ identity: (out) => `GCP account ${out}`,
2437
+ loginCmd: ["gcloud", "auth", "login"],
2438
+ loginHint: "This will open a browser window to sign in to your Google account."
2439
+ },
2440
+ azure: {
2441
+ check: () => execSync("az account show --query user.name --output tsv", { stdio: ["ignore", "pipe", "ignore"] }).toString().trim(),
2442
+ identity: (out) => `Azure user ${out}`,
2443
+ loginCmd: ["az", "login"],
2444
+ loginHint: "This will open a browser window to sign in to your Azure account."
2445
+ }
2446
+ };
2447
+ async function ensureCloudAuth(provider, inquirer) {
2448
+ const cfg = CLOUD_AUTH[provider];
2449
+ try {
2450
+ const identity = cfg.check();
2451
+ success(`Authenticated: ${cfg.identity(identity)}`);
2452
+ return;
2453
+ } catch {
2454
+ }
2455
+ const cliName = cfg.loginCmd[0];
2456
+ failure(`Not signed in to ${provider.toUpperCase()}. You need to authenticate before deploying.`);
2457
+ info(`
2458
+ To authenticate, run:
2459
+ ${cfg.loginCmd.join(" ")}
2460
+ ${cfg.loginHint}`);
2461
+ const { runNow } = await inquirer.prompt([{
2462
+ type: "confirm",
2463
+ name: "runNow",
2464
+ message: `Run \`${cfg.loginCmd.join(" ")}\` now?`,
2465
+ default: true
2466
+ }]);
2467
+ if (runNow) {
2468
+ process19.stdout.write("\n");
2469
+ const result = spawnSync(cfg.loginCmd[0], cfg.loginCmd.slice(1), { stdio: "inherit" });
2470
+ if (result.error) {
2471
+ failure(`Could not launch ${cliName} \u2014 is it installed? (${result.error.message})`);
2472
+ info(`Install guide: ${cliInstallUrl(provider)}`);
2473
+ info("Re-run `clawops setup` after installing and signing in.\n");
2474
+ return;
2475
+ }
2476
+ try {
2477
+ const identity = cfg.check();
2478
+ process19.stdout.write("\n");
2479
+ success(`Authenticated: ${cfg.identity(identity)}`);
2480
+ } catch {
2481
+ process19.stdout.write("\n");
2482
+ failure(`Still not authenticated \u2014 the sign-in may have been cancelled or failed.`);
2483
+ info("Re-run `clawops setup` after signing in, or continue and authenticate before running `clawops up`.\n");
2484
+ }
2485
+ } else {
2486
+ info("You can authenticate later, but `clawops up` will fail until you do.\n");
2487
+ }
2488
+ }
2489
+ function cliInstallUrl(provider) {
2490
+ if (provider === "aws") return "https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html";
2491
+ if (provider === "gcp") return "https://cloud.google.com/sdk/docs/install";
2492
+ return "https://learn.microsoft.com/en-us/cli/azure/install-azure-cli";
2493
+ }
2494
+ async function promptSecret(name, label, envDefault, inquirer) {
2495
+ const secretsDir = path4.join(os2.homedir(), ".clawops", "secrets");
2496
+ const { secretSource } = await inquirer.prompt([{
2497
+ type: "list",
2498
+ name: "secretSource",
2499
+ message: `Where is your ${label}?`,
2500
+ choices: [
2501
+ { name: `Paste it here \u2014 saved to ~/.clawops/secrets/${name}`, value: "paste" },
2502
+ { name: `In an environment variable (e.g. ${envDefault})`, value: "env" },
2503
+ { name: "In a file on this computer", value: "file" }
2504
+ ]
2505
+ }]);
2506
+ if (secretSource === "paste") {
2507
+ const { secretValue } = await inquirer.prompt([{
2508
+ type: "password",
2509
+ name: "secretValue",
2510
+ message: `Paste your ${label}: (input is hidden)`,
2511
+ validate: (v) => v.trim() !== "" || "Value cannot be empty"
2512
+ }]);
2513
+ mkdirSync4(secretsDir, { recursive: true });
2514
+ spawnSync("chmod", ["700", secretsDir], { stdio: "ignore" });
2515
+ const secretPath = path4.join(secretsDir, name);
2516
+ writeFileSync4(secretPath, secretValue.trim(), { encoding: "utf-8", mode: 384 });
2517
+ success(`Secret saved to ${secretPath} (chmod 600)`);
2518
+ return { name, source: "file", ref: secretPath };
2519
+ }
2520
+ if (secretSource === "env") {
2521
+ const { envVar } = await inquirer.prompt([{
2522
+ type: "input",
2523
+ name: "envVar",
2524
+ message: `Environment variable name: (the variable that holds your ${label})`,
2525
+ default: envDefault
2526
+ }]);
2527
+ return { name, source: "env", ref: envVar };
2528
+ }
2529
+ const { filePath } = await inquirer.prompt([{
2530
+ type: "input",
2531
+ name: "filePath",
2532
+ message: `File path: (full path to the file containing your ${label})`,
2533
+ validate: (v) => existsSync3(v.trim()) || `File not found: ${v.trim()}`
2534
+ }]);
2535
+ return { name, source: "file", ref: filePath };
2536
+ }
2537
+ var DOCKER_PREFLIGHT_TIMEOUT_MS = 1e4;
2538
+ var DOCKER_CHECK_CMD = [
2539
+ 'export PATH="/usr/local/bin:/opt/homebrew/bin:/Applications/Docker.app/Contents/Resources/bin:$PATH"',
2540
+ "if ! command -v docker >/dev/null 2>&1; then echo NOT_INSTALLED; exit 0; fi",
2541
+ "if docker version >/dev/null 2>&1; then echo OK; exit 0; fi",
2542
+ 'if DOCKER_HOST="unix://${HOME}/.docker/run/docker.sock" docker version >/dev/null 2>&1; then echo OK; exit 0; fi',
2543
+ 'if DOCKER_HOST="unix://${HOME}/.colima/default/docker.sock" docker version >/dev/null 2>&1; then echo OK; exit 0; fi',
2544
+ "echo NOT_RUNNING"
2545
+ ].join("; ");
2546
+ async function checkDockerPreflight(opts) {
2547
+ const { acquireSession } = await import("./pool-FBFHATDG.js");
2548
+ const { ProviderError } = await import("./errors-OK47MQFD.js");
2549
+ const spin = spinner("Checking Docker on target host...");
2550
+ const timeoutSignal = AbortSignal.timeout(DOCKER_PREFLIGHT_TIMEOUT_MS);
2551
+ const execSignal = opts.signal ? AbortSignal.any([opts.signal, timeoutSignal]) : timeoutSignal;
2552
+ const { session, release } = await acquireSession({
2553
+ host: opts.host,
2554
+ port: opts.port,
2555
+ user: opts.user,
2556
+ privateKeyPath: opts.keyPath,
2557
+ knownHostsPath: opts.knownHostsPath,
2558
+ signal: opts.signal
2559
+ });
2560
+ let out = "";
2561
+ try {
2562
+ const result = await session.exec(DOCKER_CHECK_CMD, execSignal);
2563
+ out = result.stdout.trim();
2564
+ } catch {
2565
+ spin.warn("Docker check timed out \u2014 proceeding anyway. Bootstrap will verify.");
2566
+ return;
2567
+ } finally {
2568
+ release();
2569
+ }
2570
+ if (out === "OK") {
2571
+ spin.succeed("Docker is installed and running.");
2572
+ return;
2573
+ }
2574
+ if (out === "NOT_RUNNING") {
2575
+ if (!LOCALHOST_ALIASES.has(opts.host.toLowerCase())) {
2576
+ await startRemoteDocker(opts, spin);
2577
+ return;
2578
+ }
2579
+ spin.warn("Docker is installed but not running.");
2580
+ await startDockerAndWait(opts.inquirer);
2581
+ return;
2582
+ }
2583
+ spin.fail("Docker not found on the target host.");
2584
+ info("Install Docker with one of:");
2585
+ info(" Docker Desktop (GUI): https://www.docker.com/products/docker-desktop/");
2586
+ info(" Homebrew: brew install --cask docker");
2587
+ info(" Colima (FOSS): brew install colima docker && colima start");
2588
+ info("Then re-run: clawops setup");
2589
+ throw new ProviderError("Docker is not installed on the target host.");
2590
+ }
2591
+ var DOCKER_POLL_MS = 3e3;
2592
+ var DOCKER_TIMEOUT_MS = 9e4;
2593
+ async function startRemoteDocker(opts, spin) {
2594
+ const { acquireSession } = await import("./pool-FBFHATDG.js");
2595
+ const { ProviderError } = await import("./errors-OK47MQFD.js");
2596
+ spin.text = "Starting Docker on the remote server...";
2597
+ const { session, release } = await acquireSession({
2598
+ host: opts.host,
2599
+ port: opts.port,
2600
+ user: opts.user,
2601
+ privateKeyPath: opts.keyPath,
2602
+ knownHostsPath: opts.knownHostsPath,
2603
+ signal: opts.signal
2604
+ });
2605
+ try {
2606
+ const startResult = await session.exec(
2607
+ "sudo systemctl start docker 2>/dev/null || sudo service docker start 2>/dev/null",
2608
+ opts.signal
2609
+ );
2610
+ if (startResult.code !== 0) {
2611
+ spin.fail("Could not start Docker on the remote server.");
2612
+ throw new ProviderError(
2613
+ "Start Docker manually (sudo systemctl start docker) and re-run: clawops setup"
2614
+ );
2615
+ }
2616
+ const deadline = Date.now() + DOCKER_TIMEOUT_MS;
2617
+ while (Date.now() < deadline) {
2618
+ await new Promise((r) => setTimeout(r, DOCKER_POLL_MS));
2619
+ spin.text = `Waiting for Docker to start on ${opts.host}...`;
2620
+ const check = await session.exec(
2621
+ "docker version >/dev/null 2>&1 && echo OK || echo NOT_RUNNING",
2622
+ opts.signal
2623
+ );
2624
+ if (check.stdout.trim() === "OK") {
2625
+ spin.succeed("Docker started on the remote server.");
2626
+ return;
2627
+ }
2628
+ }
2629
+ spin.fail("Docker did not start within 90s on the remote server.");
2630
+ throw new ProviderError(
2631
+ "Docker did not become ready. Start it manually and re-run: clawops setup"
2632
+ );
2633
+ } finally {
2634
+ release();
2635
+ }
2636
+ }
2637
+ function dockerIsReachable() {
2638
+ try {
2639
+ execSync(
2640
+ 'docker version >/dev/null 2>&1 || DOCKER_HOST="unix://$HOME/.docker/run/docker.sock" docker version >/dev/null 2>&1 || DOCKER_HOST="unix://$HOME/.colima/default/docker.sock" docker version >/dev/null 2>&1',
2641
+ { stdio: "ignore" }
2642
+ );
2643
+ return true;
2644
+ } catch {
2645
+ return false;
2646
+ }
2647
+ }
2648
+ async function pollUntilDockerReady(label) {
2649
+ const spin = spinner(label);
2650
+ const deadline = Date.now() + DOCKER_TIMEOUT_MS;
2651
+ let elapsed = 0;
2652
+ while (Date.now() < deadline) {
2653
+ await new Promise((r) => setTimeout(r, DOCKER_POLL_MS));
2654
+ elapsed += DOCKER_POLL_MS;
2655
+ spin.text = `${label} (${Math.round(elapsed / 1e3)}s)`;
2656
+ if (dockerIsReachable()) {
2657
+ spin.succeed("Docker is running.");
2658
+ return;
2659
+ }
2660
+ }
2661
+ spin.fail("Docker did not start within 90s.");
2662
+ info("Check Docker Desktop for errors, then re-run: clawops setup");
2663
+ throw new Error("Docker daemon did not become ready within timeout.");
2664
+ }
2665
+ async function startDockerAndWait(inquirer) {
2666
+ const isLinux = process19.platform === "linux";
2667
+ const { choice } = await inquirer.prompt([{
2668
+ type: "list",
2669
+ name: "choice",
2670
+ message: "How would you like to start Docker?",
2671
+ choices: [
2672
+ ...isLinux ? [{ name: "Start daemon directly (sudo dockerd)", value: "dockerd" }] : [],
2673
+ ...isLinux ? [{ name: "systemctl (sudo systemctl start docker)", value: "systemctl" }] : [],
2674
+ ...!isLinux ? [{ name: "Docker Desktop (open -a Docker)", value: "desktop" }] : [],
2675
+ { name: "Colima (colima start)", value: "colima" },
2676
+ { name: "Skip \u2014 I'll start it myself and re-run clawops setup", value: "skip" }
2677
+ ]
2678
+ }]);
2679
+ if (choice === "skip") {
2680
+ info("Re-run `clawops setup` once Docker is running.");
2681
+ throw new Error("Docker not running \u2014 user chose to exit.");
2682
+ }
2683
+ if (choice === "dockerd") {
2684
+ info("Starting dockerd in the background (you may be prompted for your password)...");
2685
+ const child = spawn2("sudo", ["dockerd"], { detached: true, stdio: "ignore" });
2686
+ child.unref();
2687
+ await pollUntilDockerReady("Waiting for Docker daemon...");
2688
+ } else if (choice === "systemctl") {
2689
+ const spin = spinner("Running: sudo systemctl start docker...");
2690
+ const res = spawnSync("sudo", ["systemctl", "start", "docker"], { stdio: "pipe" });
2691
+ if (res.status !== 0) {
2692
+ spin.fail("systemctl start docker failed.");
2693
+ throw new Error("systemctl start docker failed");
2694
+ }
2695
+ spin.succeed("Docker service started.");
2696
+ await pollUntilDockerReady("Waiting for Docker daemon to be reachable...");
2697
+ } else if (choice === "desktop") {
2698
+ const backendRunning = spawnSync("pgrep", ["-f", "com.docker.backend"], { stdio: "ignore" }).status === 0;
2699
+ if (backendRunning) {
2700
+ const killSpin = spinner("Docker Desktop is stuck \u2014 force-restarting...");
2701
+ for (const pat of ["com.docker.backend", "com.docker.virtualization", "com.docker.build", "Docker Desktop"]) {
2702
+ spawnSync("pkill", ["-9", "-f", pat], { stdio: "ignore" });
2703
+ }
2704
+ await new Promise((r) => setTimeout(r, 2e3));
2705
+ killSpin.succeed("Old Docker processes cleared.");
2706
+ }
2707
+ const openSpin = spinner("Launching Docker Desktop...");
2708
+ let opened = spawnSync("open", ["-a", "Docker"], { stdio: "ignore" });
2709
+ if (opened.status !== 0) opened = spawnSync("open", ["/Applications/Docker.app"], { stdio: "ignore" });
2710
+ if (opened.status !== 0) {
2711
+ openSpin.fail("Could not open Docker Desktop \u2014 is it installed in /Applications?");
2712
+ throw new Error("Failed to open Docker Desktop");
2713
+ }
2714
+ openSpin.succeed("Docker Desktop launched.");
2715
+ await pollUntilDockerReady("Waiting for Docker engine to start...");
2716
+ } else if (choice === "colima") {
2717
+ const spin = spinner("Running: colima start...");
2718
+ const res = spawnSync("colima", ["start"], { stdio: "pipe" });
2719
+ if (res.status !== 0) {
2720
+ spin.fail("colima start failed \u2014 is Colima installed? (brew install colima docker)");
2721
+ throw new Error("colima start failed");
2722
+ }
2723
+ spin.succeed("Colima started.");
2724
+ await pollUntilDockerReady("Waiting for Docker daemon to be reachable...");
2725
+ }
2726
+ }
2727
+ var LOCALHOST_ALIASES = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1"]);
2728
+ async function ensureAuthorizedKey(keyPath, host, inquirer) {
2729
+ const pubKeyPath = `${keyPath}.pub`;
2730
+ if (!existsSync3(pubKeyPath)) return;
2731
+ const pubKey = readFileSync4(pubKeyPath, "utf-8").trim();
2732
+ const isLocal = LOCALHOST_ALIASES.has(host.toLowerCase());
2733
+ const authorizedKeysPath = path4.join(os2.homedir(), ".ssh", "authorized_keys");
2734
+ if (isLocal) {
2735
+ const existing = existsSync3(authorizedKeysPath) ? readFileSync4(authorizedKeysPath, "utf-8") : "";
2736
+ if (existing.includes(pubKey)) {
2737
+ success("SSH public key is already in authorized_keys.");
2738
+ return;
2739
+ }
2740
+ info("\nConnecting to localhost requires your public key to be in ~/.ssh/authorized_keys.");
2741
+ const { addKey } = await inquirer.prompt([{
2742
+ type: "confirm",
2743
+ name: "addKey",
2744
+ message: "Add your public key to ~/.ssh/authorized_keys now? (required for SSH to work)",
2745
+ default: true
2746
+ }]);
2747
+ if (addKey) {
2748
+ mkdirSync4(path4.join(os2.homedir(), ".ssh"), { recursive: true });
2749
+ const entry = existing.endsWith("\n") || existing === "" ? pubKey + "\n" : "\n" + pubKey + "\n";
2750
+ writeFileSync4(authorizedKeysPath, existing + entry, { encoding: "utf-8", flag: "a" });
2751
+ spawnSync("chmod", ["600", authorizedKeysPath], { stdio: "ignore" });
2752
+ success("Public key added to ~/.ssh/authorized_keys.");
2753
+ } else {
2754
+ info("SSH will likely fail. Add this line to ~/.ssh/authorized_keys manually:");
2755
+ info(` ${pubKey}`);
2756
+ }
2757
+ } else {
2758
+ process19.stdout.write("\n");
2759
+ info("Make sure this public key is in ~/.ssh/authorized_keys on your server:");
2760
+ info(` ${pubKey}`);
2761
+ info("(If you just created the server, paste the line above into the server's ~/.ssh/authorized_keys file.)\n");
2762
+ }
2763
+ }
2764
+ async function generateSshKey() {
2765
+ const keyPath = path4.join(os2.homedir(), ".ssh", "id_ed25519");
2766
+ mkdirSync4(path4.join(os2.homedir(), ".ssh"), { recursive: true });
2767
+ const result = spawnSync("ssh-keygen", ["-t", "ed25519", "-f", keyPath, "-N", "", "-C", "clawops"], {
2768
+ stdio: ["ignore", "pipe", "pipe"]
2769
+ });
2770
+ if (result.status !== 0) {
2771
+ throw new Error(`ssh-keygen failed: ${result.stderr?.toString().trim()}`);
2772
+ }
2773
+ success(`SSH key pair generated: ${keyPath}`);
2774
+ info(`Public key (add this to your server's ~/.ssh/authorized_keys if needed):
2775
+ ${readFileSync4(`${keyPath}.pub`, "utf-8").trim()}`);
2776
+ return keyPath;
2777
+ }
2778
+ function detectSshKey() {
2779
+ const candidates = ["id_ed25519", "id_ecdsa", "id_rsa", "id_dsa"];
2780
+ for (const name of candidates) {
2781
+ const p = path4.join(os2.homedir(), ".ssh", name);
2782
+ if (existsSync3(p)) return p;
2783
+ }
2784
+ return path4.join(os2.homedir(), ".ssh", "id_ed25519");
2785
+ }
2786
+ function buildMcpEntry() {
2787
+ try {
2788
+ const bin = execSync("which clawops", {
2789
+ encoding: "utf-8",
2790
+ stdio: ["ignore", "pipe", "ignore"]
2791
+ }).trim();
2792
+ if (bin) return { command: bin, args: ["mcp", "serve", "--read-only"], resolved: true };
2793
+ } catch {
2794
+ }
2795
+ return { command: "clawops", args: ["mcp", "serve", "--read-only"], resolved: false };
2796
+ }
2797
+ var MCP_APPS = [
2798
+ {
2799
+ id: "claude-desktop",
2800
+ name: "Claude Desktop",
2801
+ configPath: () => process19.platform === "darwin" ? path4.join(os2.homedir(), "Library", "Application Support", "Claude", "claude_desktop_config.json") : path4.join(os2.homedir(), ".config", "Claude", "claude_desktop_config.json"),
2802
+ isInstalled: () => existsSync3(
2803
+ process19.platform === "darwin" ? path4.join(os2.homedir(), "Library", "Application Support", "Claude") : path4.join(os2.homedir(), ".config", "Claude")
2804
+ )
2805
+ },
2806
+ {
2807
+ id: "claude-code",
2808
+ name: "Claude Code",
2809
+ configPath: () => path4.join(os2.homedir(), ".claude.json"),
2810
+ isInstalled: () => {
2811
+ if (existsSync3(path4.join(os2.homedir(), ".claude.json"))) return true;
2812
+ try {
2813
+ execSync("claude --version", { stdio: "ignore" });
2814
+ return true;
2815
+ } catch {
2816
+ return false;
2817
+ }
2818
+ }
2819
+ },
2820
+ {
2821
+ id: "cursor",
2822
+ name: "Cursor",
2823
+ configPath: () => path4.join(os2.homedir(), ".cursor", "mcp.json"),
2824
+ isInstalled: () => existsSync3(path4.join(os2.homedir(), ".cursor"))
2825
+ },
2826
+ {
2827
+ id: "windsurf",
2828
+ name: "Windsurf",
2829
+ configPath: () => path4.join(os2.homedir(), ".codeium", "windsurf", "mcp_config.json"),
2830
+ isInstalled: () => existsSync3(path4.join(os2.homedir(), ".codeium", "windsurf"))
2831
+ }
2832
+ ];
2833
+ function printMcpSnippet() {
2834
+ process19.stdout.write("\n");
2835
+ process19.stdout.write(JSON.stringify({
2836
+ mcpServers: {
2837
+ clawops: { command: "clawops", args: ["mcp", "serve", "--read-only"] }
2838
+ }
2839
+ }, null, 2) + "\n\n");
2840
+ }
2841
+ function loadCatalogs(yaml) {
2842
+ const specDir = path4.join(path4.dirname(fileURLToPath(import.meta.url)), "../../../spec");
2843
+ try {
2844
+ const modelsRaw = yaml.load(readFileSync4(path4.join(specDir, "models.yaml"), "utf-8"));
2845
+ const integrationsRaw = yaml.load(readFileSync4(path4.join(specDir, "integrations.yaml"), "utf-8"));
2846
+ return {
2847
+ models: modelsRaw.providers,
2848
+ integrations: integrationsRaw.integrations
2849
+ };
2850
+ } catch (err) {
2851
+ throw new Error(`Cannot load wizard catalogs from spec/: ${err.message}`);
2852
+ }
2853
+ }
2854
+ function defaultRegion(provider) {
2855
+ if (provider === "aws") return "us-east-1";
2856
+ if (provider === "gcp") return "us-central1";
2857
+ if (provider === "azure") return "eastus";
2858
+ return "";
2859
+ }
2860
+ function stateLabel(provider) {
2861
+ if (provider === "aws") return "S3";
2862
+ if (provider === "gcp") return "GCS";
2863
+ if (provider === "azure") return "Azure Blob Storage";
2864
+ return "State";
2865
+ }
2866
+ function instanceChoices(provider) {
2867
+ const table = {
2868
+ aws: [
2869
+ { name: "micro \u2014 1 CPU, 1 GB RAM (~$8/mo) \u2014 light personal use", value: "micro" },
2870
+ { name: "small \u2014 2 CPU, 2 GB RAM (~$17/mo) \u2014 recommended for most users", value: "small" },
2871
+ { name: "medium \u2014 2 CPU, 4 GB RAM (~$33/mo) \u2014 teams or heavy usage", value: "medium" },
2872
+ { name: "large \u2014 2 CPU, 8 GB RAM (~$67/mo) \u2014 high traffic or multiple agents", value: "large" }
2873
+ ],
2874
+ gcp: [
2875
+ { name: "micro \u2014 2 CPU, 1 GB RAM (~$7/mo) \u2014 light personal use", value: "micro" },
2876
+ { name: "small \u2014 2 CPU, 8 GB RAM (~$49/mo) \u2014 recommended for most users", value: "small" },
2877
+ { name: "medium \u2014 4 CPU, 16 GB RAM (~$97/mo) \u2014 teams or heavy usage", value: "medium" },
2878
+ { name: "large \u2014 8 CPU, 32 GB RAM (~$194/mo) \u2014 high traffic or multiple agents", value: "large" }
2879
+ ],
2880
+ azure: [
2881
+ { name: "micro \u2014 1 CPU, 1 GB RAM (~$8/mo) \u2014 light personal use", value: "micro" },
2882
+ { name: "small \u2014 2 CPU, 4 GB RAM (~$35/mo) \u2014 recommended for most users", value: "small" },
2883
+ { name: "medium \u2014 4 CPU, 8 GB RAM (~$70/mo) \u2014 teams or heavy usage", value: "medium" },
2884
+ { name: "large \u2014 8 CPU, 16 GB RAM (~$140/mo) \u2014 high traffic or multiple agents", value: "large" }
2885
+ ]
2886
+ };
2887
+ return table[provider] ?? [];
2888
+ }
2889
+
2890
+ // src/cli/error-handler.ts
2891
+ import process20 from "process";
1696
2892
  function handleError(err) {
1697
2893
  if (err instanceof ClawopsError) {
1698
2894
  failure(err.message);
1699
- process19.exit(err.exitCode);
2895
+ process20.exit(err.exitCode);
1700
2896
  }
1701
2897
  if (err instanceof Error) {
1702
2898
  failure(`Unexpected error: ${err.message}`);
1703
- if (process19.env["DEBUG"]) {
1704
- process19.stderr.write(err.stack ?? "\n");
2899
+ if (process20.env["DEBUG"]) {
2900
+ process20.stderr.write(err.stack ?? "\n");
1705
2901
  }
1706
- process19.exit(1);
2902
+ process20.exit(1);
1707
2903
  }
1708
2904
  failure(`Unknown error: ${String(err)}`);
1709
- process19.exit(1);
2905
+ process20.exit(1);
1710
2906
  }
1711
2907
 
1712
2908
  // src/cli/index.ts
1713
- var main = defineCommand18({
2909
+ var main = defineCommand19({
1714
2910
  meta: {
1715
2911
  name: "clawops",
1716
2912
  description: "Deploy and manage self-hosted OpenClaw instances across clouds",
@@ -1742,7 +2938,8 @@ var main = defineCommand18({
1742
2938
  backup: backup_default,
1743
2939
  stacks: stacks_default,
1744
2940
  doctor: doctor_default,
1745
- mcp: mcp_default
2941
+ mcp: mcp_default,
2942
+ setup: setup_default
1746
2943
  }
1747
2944
  });
1748
2945
  try {