@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.
- package/README.md +140 -56
- package/dist/apply-KBF7OPWL.js +14 -0
- package/dist/{automation-JG2YBEL7.js → automation-RULUSJBW.js} +2 -2
- package/dist/{aws-A3323GNM.js → aws-KQBWY43W.js} +12 -10
- package/dist/{azure-LNWBK2ZI.js → azure-4EJFVJZL.js} +4 -3
- package/dist/{bootstrap-NRIDVS5F.js → bootstrap-UYCOEJQX.js} +48 -12
- package/dist/{chunk-LT24GUUO.js → chunk-3QJBNAHW.js} +1 -1
- package/dist/{chunk-5XEZAU7V.js → chunk-4U3LTLWZ.js} +6 -83
- package/dist/{chunk-PERSDQMT.js → chunk-ACYJBSLJ.js} +7 -7
- package/dist/chunk-BOPSG2LI.js +77 -0
- package/dist/{chunk-PRYLTCS4.js → chunk-BRPU7AQC.js} +1 -1
- package/dist/{chunk-ALSUDYA7.js → chunk-CX5SL5HP.js} +1 -1
- package/dist/{chunk-ZSE4QRKE.js → chunk-KGXPLI7W.js} +19 -1
- package/dist/chunk-LU63NZD3.js +116 -0
- package/dist/chunk-QJ6ERXHN.js +266 -0
- package/dist/{chunk-2MJFYG7U.js → chunk-QMXM2SMO.js} +10 -6
- package/dist/chunk-UDNZUSKA.js +97 -0
- package/dist/chunk-ZVOEQCNW.js +92 -0
- package/dist/cli.js +1259 -62
- package/dist/context-VUAYRAJY.js +10 -0
- package/dist/errors-OK47MQFD.js +21 -0
- package/dist/{gcp-OHWCTFLL.js → gcp-SKURG3L6.js} +4 -3
- package/dist/generate-WG5VONSE.js +14 -0
- package/dist/js-yaml-PTEEG4FO.js +2647 -0
- package/dist/{outputs-6DAVEEAZ.js → outputs-DJHBY7EE.js} +2 -2
- package/dist/{package-RCGVJCON.js → package-3JZ767DN.js} +8 -3
- package/dist/{pool-D4JCUA2V.js → pool-FBFHATDG.js} +3 -2
- package/dist/remote-config-JQ77SRC2.js +21 -0
- package/dist/secrets-SVZWNAPK.js +7 -0
- package/dist/{server-YO4JFQND.js → server-QJRWK2RV.js} +64 -150
- package/dist/ssh-IQXNME3D.js +8 -0
- package/dist/{store-ARJ2EO6L.js → store-SDUR52Z5.js} +2 -2
- package/package.json +6 -3
- package/dist/apply-GTVALBAY.js +0 -12
- package/dist/chunk-RZE35FQF.js +0 -56
- package/dist/context-T52JWL3P.js +0 -10
- 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-
|
|
15
|
+
} from "./chunk-CX5SL5HP.js";
|
|
8
16
|
import {
|
|
9
17
|
ClawopsError,
|
|
10
18
|
UsageError
|
|
11
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-KGXPLI7W.js";
|
|
12
20
|
|
|
13
21
|
// src/cli/index.ts
|
|
14
|
-
import { defineCommand as
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
448
|
-
const { generatePlan } = await import("./generate-
|
|
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-
|
|
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 =
|
|
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-
|
|
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-
|
|
699
|
-
const { acquireSession, drainPool } = await import("./pool-
|
|
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-
|
|
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-
|
|
786
|
-
const { extractBaseOutputs } = await import("./outputs-
|
|
787
|
-
const { acquireSession } = await import("./pool-
|
|
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-
|
|
854
|
-
const { extractBaseOutputs } = await import("./outputs-
|
|
855
|
-
const { acquireSession, drainPool } = await import("./pool-
|
|
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(
|
|
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
|
|
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-
|
|
971
|
-
const { extractBaseOutputs } = await import("./outputs-
|
|
972
|
-
const { acquireSession, drainPool } = await import("./pool-
|
|
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
|
|
1058
|
-
const restartResult = await session.exec(dockerRunCmd(
|
|
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-
|
|
1086
|
-
const { extractBaseOutputs } = await import("./outputs-
|
|
1087
|
-
const { acquireSession, drainPool } = await import("./pool-
|
|
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-
|
|
1190
|
-
const { extractBaseOutputs } = await import("./outputs-
|
|
1191
|
-
const { acquireSession, drainPool } = await import("./pool-
|
|
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-
|
|
1303
|
-
const { acquireSession, drainPool } = await import("./pool-
|
|
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-
|
|
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
|
-
|
|
1518
|
-
|
|
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-
|
|
1569
|
-
await import("./gcp-
|
|
1570
|
-
await import("./azure-
|
|
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
|
|
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-
|
|
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(
|
|
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/
|
|
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
|
-
|
|
2895
|
+
process20.exit(err.exitCode);
|
|
1700
2896
|
}
|
|
1701
2897
|
if (err instanceof Error) {
|
|
1702
2898
|
failure(`Unexpected error: ${err.message}`);
|
|
1703
|
-
if (
|
|
1704
|
-
|
|
2899
|
+
if (process20.env["DEBUG"]) {
|
|
2900
|
+
process20.stderr.write(err.stack ?? "\n");
|
|
1705
2901
|
}
|
|
1706
|
-
|
|
2902
|
+
process20.exit(1);
|
|
1707
2903
|
}
|
|
1708
2904
|
failure(`Unknown error: ${String(err)}`);
|
|
1709
|
-
|
|
2905
|
+
process20.exit(1);
|
|
1710
2906
|
}
|
|
1711
2907
|
|
|
1712
2908
|
// src/cli/index.ts
|
|
1713
|
-
var main =
|
|
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 {
|