@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.
- package/README.md +115 -44
- 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-7ZS2GLFJ.js → bootstrap-UYCOEJQX.js} +48 -12
- package/dist/{chunk-LT24GUUO.js → chunk-3QJBNAHW.js} +1 -1
- package/dist/{chunk-BA6MDFZK.js → chunk-4U3LTLWZ.js} +2 -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-OIGTOLB3.js +99 -0
- package/dist/chunk-QJ6ERXHN.js +266 -0
- package/dist/{chunk-D4UAHAKI.js → chunk-SSB6SGPQ.js} +10 -6
- package/dist/chunk-UDNZUSKA.js +97 -0
- package/dist/chunk-ZVOEQCNW.js +92 -0
- package/dist/cli.js +1340 -115
- 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-PZPLG3ZM.js +14 -0
- package/dist/js-yaml-PTEEG4FO.js +2647 -0
- package/dist/mcp-apps-KY44ED3Y.js +11 -0
- package/dist/{outputs-6DAVEEAZ.js → outputs-DJHBY7EE.js} +2 -2
- package/dist/{package-FA6UOG2I.js → package-SE3M4NJA.js} +1 -1
- package/dist/{pool-FAVU7WCW.js → pool-FBFHATDG.js} +3 -2
- package/dist/remote-config-JQ77SRC2.js +21 -0
- package/dist/secrets-SVZWNAPK.js +7 -0
- package/dist/{server-V5ZFL76Y.js → server-4DNHLGEG.js} +64 -150
- package/dist/ssh-IQXNME3D.js +8 -0
- package/dist/{store-ARJ2EO6L.js → store-SDUR52Z5.js} +2 -2
- package/package.json +1 -1
- package/dist/apply-DPS23GLL.js +0 -12
- package/dist/chunk-UCIVU24B.js +0 -56
- package/dist/context-T52JWL3P.js +0 -10
- 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-
|
|
20
|
+
} from "./chunk-CX5SL5HP.js";
|
|
8
21
|
import {
|
|
9
22
|
ClawopsError,
|
|
10
23
|
UsageError
|
|
11
|
-
} from "./chunk-
|
|
24
|
+
} from "./chunk-KGXPLI7W.js";
|
|
12
25
|
|
|
13
26
|
// src/cli/index.ts
|
|
14
|
-
import { defineCommand as
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
448
|
-
const { generatePlan } = await import("./generate-
|
|
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-
|
|
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 =
|
|
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-
|
|
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-
|
|
699
|
-
const { acquireSession, drainPool } = await import("./pool-
|
|
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-
|
|
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-
|
|
786
|
-
const { extractBaseOutputs } = await import("./outputs-
|
|
787
|
-
const { acquireSession } = await import("./pool-
|
|
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-
|
|
854
|
-
const { extractBaseOutputs } = await import("./outputs-
|
|
855
|
-
const { acquireSession, drainPool } = await import("./pool-
|
|
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(
|
|
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
|
|
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-
|
|
971
|
-
const { extractBaseOutputs } = await import("./outputs-
|
|
972
|
-
const { acquireSession, drainPool } = await import("./pool-
|
|
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
|
|
1058
|
-
const restartResult = await session.exec(dockerRunCmd(
|
|
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-
|
|
1086
|
-
const { extractBaseOutputs } = await import("./outputs-
|
|
1087
|
-
const { acquireSession, drainPool } = await import("./pool-
|
|
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-
|
|
1190
|
-
const { extractBaseOutputs } = await import("./outputs-
|
|
1191
|
-
const { acquireSession, drainPool } = await import("./pool-
|
|
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-
|
|
1303
|
-
const { acquireSession, drainPool } = await import("./pool-
|
|
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-
|
|
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
|
-
|
|
1518
|
-
|
|
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-
|
|
1569
|
-
await import("./gcp-
|
|
1570
|
-
await import("./azure-
|
|
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
|
|
1607
|
-
|
|
1608
|
-
import os from "os";
|
|
1609
|
-
var mcp_default = defineCommand17({
|
|
1790
|
+
import process19 from "process";
|
|
1791
|
+
var serveCmd = defineCommand17({
|
|
1610
1792
|
meta: {
|
|
1611
|
-
name: "
|
|
1612
|
-
description: "
|
|
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
|
|
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
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
}
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
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
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
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
|
-
|
|
1677
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
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
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
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
|
-
|
|
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
|
|
2919
|
+
import process21 from "process";
|
|
1697
2920
|
function handleError(err) {
|
|
1698
2921
|
if (err instanceof ClawopsError) {
|
|
1699
2922
|
failure(err.message);
|
|
1700
|
-
|
|
2923
|
+
process21.exit(err.exitCode);
|
|
1701
2924
|
}
|
|
1702
2925
|
if (err instanceof Error) {
|
|
1703
2926
|
failure(`Unexpected error: ${err.message}`);
|
|
1704
|
-
if (
|
|
1705
|
-
|
|
2927
|
+
if (process21.env["DEBUG"]) {
|
|
2928
|
+
process21.stderr.write(err.stack ?? "\n");
|
|
1706
2929
|
}
|
|
1707
|
-
|
|
2930
|
+
process21.exit(1);
|
|
1708
2931
|
}
|
|
1709
2932
|
failure(`Unknown error: ${String(err)}`);
|
|
1710
|
-
|
|
2933
|
+
process21.exit(1);
|
|
1711
2934
|
}
|
|
1712
2935
|
|
|
1713
2936
|
// src/cli/index.ts
|
|
1714
|
-
var main =
|
|
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 {
|