@clawops/cli 1.1.0 → 1.2.1

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