@clawops/cli 0.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/dist/cli.js ADDED
@@ -0,0 +1,1752 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ getConfig,
4
+ getConfigDir,
5
+ requireConfig,
6
+ setConfig
7
+ } from "./chunk-ALSUDYA7.js";
8
+ import {
9
+ ClawopsError,
10
+ UsageError
11
+ } from "./chunk-ZSE4QRKE.js";
12
+
13
+ // src/cli/index.ts
14
+ import { defineCommand as defineCommand18, runMain } from "citty";
15
+
16
+ // src/cli/commands/init.ts
17
+ import { defineCommand } from "citty";
18
+ import { generateKeyPairSync } from "crypto";
19
+ import { writeFileSync, mkdirSync, existsSync } from "fs";
20
+ import path from "path";
21
+ import process2 from "process";
22
+
23
+ // src/output/human.ts
24
+ import chalk from "chalk";
25
+ import ora from "ora";
26
+ function success(msg) {
27
+ console.log(chalk.green("\u2713") + " " + msg);
28
+ }
29
+ function failure(msg) {
30
+ console.error(chalk.red("\u2717") + " " + msg);
31
+ }
32
+ function warn(msg) {
33
+ console.warn(chalk.yellow("\u26A0") + " " + msg);
34
+ }
35
+ function info(msg) {
36
+ console.log(chalk.blue("\u2139") + " " + msg);
37
+ }
38
+ function spinner(text) {
39
+ return ora(text).start();
40
+ }
41
+
42
+ // src/cli/commands/init.ts
43
+ var SUPPORTED_PROVIDERS = ["gcp", "aws", "azure", "local"];
44
+ var PROVIDER_DEFAULTS = {
45
+ gcp: { region: "us-central1", credEnv: "GOOGLE_APPLICATION_CREDENTIALS", stateScheme: "gs://" },
46
+ aws: { region: "us-east-1", credEnv: "AWS_PROFILE", stateScheme: "s3://" },
47
+ azure: { region: "eastus", credEnv: "AZURE_CLIENT_ID", stateScheme: "azblob://" }
48
+ };
49
+ var init_default = defineCommand({
50
+ meta: {
51
+ name: "init",
52
+ description: "Initialise clawops: choose provider, configure state backend, generate SSH key"
53
+ },
54
+ args: {
55
+ provider: { type: "string", description: "Cloud provider (gcp|aws|azure|local)" },
56
+ state: { type: "string", description: "State backend URL (e.g. gs://my-bucket/clawops)" },
57
+ region: { type: "string", description: "Cloud region (defaults per provider)" },
58
+ stack: { type: "string", description: 'Stack name (default: "default")' },
59
+ "non-interactive": { type: "boolean", description: "Suppress all prompts; requires --provider" },
60
+ force: { type: "boolean", description: "Overwrite existing config without prompting" },
61
+ // local-specific
62
+ host: { type: "string", description: "[local] Hostname or IP of the target machine" },
63
+ "ssh-user": { type: "string", description: "[local] SSH login user (default: root)" },
64
+ "ssh-port": { type: "string", description: "[local] SSH port (default: 22)" },
65
+ "key-path": { type: "string", description: "[local] Path to an existing SSH private key" }
66
+ },
67
+ async run({ args }) {
68
+ const nonInteractive = Boolean(args["non-interactive"]);
69
+ const providerArg = typeof args.provider === "string" ? args.provider : null;
70
+ const stackName = typeof args.stack === "string" ? args.stack : "default";
71
+ const forceOverwrite = Boolean(args.force);
72
+ if (nonInteractive && !providerArg) {
73
+ throw new UsageError(
74
+ "--non-interactive requires --provider. Example: clawops init --provider gcp --non-interactive"
75
+ );
76
+ }
77
+ const provider = providerArg ?? "gcp";
78
+ if (!SUPPORTED_PROVIDERS.includes(provider)) {
79
+ throw new UsageError(
80
+ `Unsupported provider: ${provider}. Supported: ${SUPPORTED_PROVIDERS.join(", ")}`
81
+ );
82
+ }
83
+ const existing = getConfig();
84
+ if (existing && !forceOverwrite && !nonInteractive) {
85
+ failure(
86
+ `Config already exists at ${path.join(getConfigDir(), "config.json")}. Use --force to overwrite.`
87
+ );
88
+ process2.exit(1);
89
+ }
90
+ const configDir = getConfigDir();
91
+ mkdirSync(configDir, { recursive: true });
92
+ const keyPath = typeof args["key-path"] === "string" ? args["key-path"] : path.join(configDir, "id_ed25519");
93
+ const knownHostsPath = path.join(configDir, "known_hosts");
94
+ if (!existsSync(keyPath)) {
95
+ if (typeof args["key-path"] === "string") {
96
+ throw new UsageError(`SSH key not found at ${keyPath}`);
97
+ }
98
+ info("Generating SSH key pair...");
99
+ const { privateKey } = generateKeyPairSync("ed25519", {
100
+ privateKeyEncoding: { type: "pkcs8", format: "pem" },
101
+ publicKeyEncoding: { type: "spki", format: "pem" }
102
+ });
103
+ writeFileSync(keyPath, privateKey, { mode: 384 });
104
+ success(`SSH private key written to ${keyPath}`);
105
+ } else {
106
+ info(`Using existing SSH key at ${keyPath}`);
107
+ }
108
+ if (!existsSync(knownHostsPath)) {
109
+ writeFileSync(knownHostsPath, "", "utf-8");
110
+ }
111
+ let config;
112
+ if (provider === "local") {
113
+ const host = typeof args.host === "string" ? args.host : "";
114
+ if (!host) {
115
+ throw new UsageError("--host is required for the local provider");
116
+ }
117
+ const sshUser = typeof args["ssh-user"] === "string" ? args["ssh-user"] : "root";
118
+ const sshPort = typeof args["ssh-port"] === "string" ? parseInt(args["ssh-port"], 10) : 22;
119
+ config = {
120
+ version: 1,
121
+ defaults: { stack: stackName, provider },
122
+ stacks: {
123
+ [stackName]: {
124
+ provider,
125
+ stateUrl: "file://~/.clawops/state",
126
+ credentialsRef: { source: "file", envVars: [] },
127
+ localOpts: { host, sshUser, sshPort, sshKeyPath: keyPath }
128
+ }
129
+ },
130
+ ssh: { keyPath, knownHostsPath }
131
+ };
132
+ } else {
133
+ const defaults = PROVIDER_DEFAULTS[provider];
134
+ const region = typeof args.region === "string" ? args.region : defaults.region;
135
+ const stateUrl = typeof args.state === "string" ? args.state : `${defaults.stateScheme}CHANGEME/clawops`;
136
+ config = {
137
+ version: 1,
138
+ defaults: { stack: stackName, provider },
139
+ stacks: {
140
+ [stackName]: {
141
+ provider,
142
+ stateUrl,
143
+ region,
144
+ credentialsRef: { source: "env", envVars: [defaults.credEnv] }
145
+ }
146
+ },
147
+ ssh: { keyPath, knownHostsPath }
148
+ };
149
+ if (stateUrl.includes("CHANGEME")) {
150
+ process2.stdout.write("\n");
151
+ info(
152
+ `Update stateUrl in the config to a real state backend before running \`clawops up\`.
153
+ Example: clawops init --provider ${provider} --state ${defaults.stateScheme}your-bucket/clawops`
154
+ );
155
+ }
156
+ process2.stdout.write("\n");
157
+ success(`Provider: ${provider} Region: ${region} Stack: ${stackName}`);
158
+ setConfig(config);
159
+ success(`Config written to ${path.join(configDir, "config.json")}`);
160
+ return;
161
+ }
162
+ setConfig(config);
163
+ success(`Config written to ${path.join(configDir, "config.json")}`);
164
+ process2.stdout.write("\n");
165
+ success(`Provider: ${provider} Stack: ${stackName}`);
166
+ }
167
+ });
168
+
169
+ // src/cli/commands/up.ts
170
+ import { defineCommand as defineCommand2 } from "citty";
171
+ import process3 from "process";
172
+
173
+ // src/output/table.ts
174
+ function renderTable(headers, rows) {
175
+ if (headers.length === 0) return "";
176
+ const widths = headers.map((h, i) => {
177
+ const cellMax = rows.reduce((max, row) => Math.max(max, (row[i] ?? "").length), 0);
178
+ return Math.max(h.length, cellMax);
179
+ });
180
+ const pad = (s, w) => s + " ".repeat(w - s.length);
181
+ const sep = " ";
182
+ const header = headers.map((h, i) => pad(h, widths[i] ?? h.length)).join(sep);
183
+ const divider = widths.map((w) => "-".repeat(w)).join(sep);
184
+ const body = rows.map((row) => row.map((cell, i) => pad(cell, widths[i] ?? cell.length)).join(sep)).join("\n");
185
+ return [header, divider, ...body ? [body] : []].join("\n");
186
+ }
187
+
188
+ // src/cli/commands/up.ts
189
+ var VALID_INSTANCE_TYPES = ["micro", "small", "medium", "large", "gpu"];
190
+ var up_default = defineCommand2({
191
+ meta: {
192
+ name: "up",
193
+ description: "Provision and deploy an OpenClaw stack"
194
+ },
195
+ args: {
196
+ provider: { type: "string", description: "Cloud provider (gcp|aws|azure|local)" },
197
+ region: { type: "string", description: "Cloud region" },
198
+ "instance-type": { type: "string", description: "Instance size alias (micro|small|medium|large|gpu)" },
199
+ "dry-run": { type: "boolean", description: "Preview without applying" },
200
+ "no-wait": { type: "boolean", description: "Return immediately without waiting for healthy state" },
201
+ "openclaw-version": { type: "string", description: "semver or 'stable'/'dev'" },
202
+ stack: { type: "string", description: "Target stack name" }
203
+ },
204
+ async run({ args }) {
205
+ const { buildContext } = await import("./context-T52JWL3P.js");
206
+ const ctx = buildContext(args);
207
+ const openclawVersion = typeof args["openclaw-version"] === "string" ? args["openclaw-version"] : "stable";
208
+ if (ctx.adapter.name === "local") {
209
+ const stackConfig = ctx.config.stacks[ctx.stackName];
210
+ if (!stackConfig?.localOpts) {
211
+ throw new UsageError(
212
+ `Stack "${ctx.stackName}" has no localOpts. Run \`clawops init --provider local --host <HOST>\` first.`
213
+ );
214
+ }
215
+ const { localOpts } = stackConfig;
216
+ const { localBootstrap } = await import("./bootstrap-NRIDVS5F.js");
217
+ const abortController = new AbortController();
218
+ process3.on("SIGINT", () => abortController.abort());
219
+ process3.on("SIGTERM", () => abortController.abort());
220
+ const spin2 = spinner(`Bootstrapping local host "${localOpts.host}"...`);
221
+ try {
222
+ const state = await localBootstrap({
223
+ host: localOpts.host,
224
+ port: localOpts.sshPort,
225
+ user: localOpts.sshUser,
226
+ privateKeyPath: localOpts.sshKeyPath,
227
+ knownHostsPath: ctx.config.ssh.knownHostsPath,
228
+ openclawVersion,
229
+ stackName: ctx.stackName,
230
+ noWait: Boolean(args["no-wait"]),
231
+ signal: abortController.signal
232
+ });
233
+ spin2.succeed(`Host "${localOpts.host}" bootstrapped`);
234
+ info(`Gateway URL: ${state.gatewayUrl}`);
235
+ info(`SSH: ${state.sshUser}@${state.sshHost}:${state.sshPort}`);
236
+ } catch (err) {
237
+ spin2.fail("Bootstrap failed");
238
+ throw err;
239
+ }
240
+ return;
241
+ }
242
+ const instanceAlias = typeof args["instance-type"] === "string" ? args["instance-type"] : "small";
243
+ if (!VALID_INSTANCE_TYPES.includes(instanceAlias)) {
244
+ throw new UsageError(
245
+ `Invalid --instance-type: ${instanceAlias}. Valid values: ${VALID_INSTANCE_TYPES.join(", ")}`
246
+ );
247
+ }
248
+ const isDryRun = Boolean(args["dry-run"]);
249
+ const validation = await ctx.adapter.validateConfig();
250
+ if (!validation.ok) {
251
+ for (const e of validation.errors) failure(e);
252
+ process3.exit(3);
253
+ }
254
+ const stack = await ctx.getStack();
255
+ const region = typeof args.region === "string" ? args.region : ctx.adapter.defaultRegion();
256
+ const instanceType = ctx.adapter.normalizeInstanceType(
257
+ instanceAlias
258
+ );
259
+ await stack.setConfig("region", { value: region });
260
+ await stack.setConfig("instanceType", { value: instanceType });
261
+ await stack.setConfig("openclawVersion", { value: openclawVersion });
262
+ if (isDryRun) {
263
+ info("Previewing changes (--dry-run)...");
264
+ const preview = await stack.preview({ onOutput: (out) => process3.stdout.write(out) });
265
+ process3.stdout.write("\n");
266
+ if (preview.changeSummary) {
267
+ const rows = Object.entries(preview.changeSummary).filter(([, count]) => count > 0).map(([op, count]) => [op, String(count)]);
268
+ if (rows.length > 0) {
269
+ process3.stdout.write(renderTable(["Operation", "Count"], rows) + "\n");
270
+ }
271
+ }
272
+ success("Preview complete (no resources changed)");
273
+ return;
274
+ }
275
+ const spin = spinner(`Deploying stack "${ctx.stackName}"...`);
276
+ try {
277
+ const result = await stack.up({
278
+ onOutput: (out) => {
279
+ spin.text = out.trim() || spin.text;
280
+ }
281
+ });
282
+ spin.succeed(`Stack "${ctx.stackName}" deployed`);
283
+ const outputs = result.outputs;
284
+ if (outputs["publicIp"]) {
285
+ info(`Public IP: ${outputs["publicIp"].value}`);
286
+ }
287
+ if (outputs["gatewayUrl"]) {
288
+ info(`Gateway URL: ${outputs["gatewayUrl"].value}`);
289
+ }
290
+ } catch (err) {
291
+ spin.fail("Deployment failed");
292
+ throw err;
293
+ }
294
+ }
295
+ });
296
+
297
+ // src/cli/commands/down.ts
298
+ import { defineCommand as defineCommand3 } from "citty";
299
+ import process4 from "process";
300
+ var down_default = defineCommand3({
301
+ meta: {
302
+ name: "down",
303
+ description: "Destroy all provisioned resources for a stack"
304
+ },
305
+ args: {
306
+ stack: { type: "string", description: "Target stack name" },
307
+ yes: { type: "boolean", description: "Skip confirmation prompt" },
308
+ "dry-run": { type: "boolean", description: "Show what would be destroyed without destroying" }
309
+ },
310
+ async run({ args }) {
311
+ const { buildContext } = await import("./context-T52JWL3P.js");
312
+ const ctx = buildContext(args);
313
+ if (args["dry-run"]) {
314
+ info(`Dry run \u2014 would destroy stack "${ctx.stackName}"`);
315
+ try {
316
+ const stack2 = await ctx.getStack();
317
+ const outputMap = await stack2.outputs();
318
+ const rows = Object.entries(outputMap).map(([k, v]) => [k, String(v.value ?? "")]);
319
+ if (rows.length > 0) {
320
+ process4.stdout.write("\nCurrent outputs that would be lost:\n");
321
+ process4.stdout.write(renderTable(["Output", "Value"], rows) + "\n");
322
+ }
323
+ } catch {
324
+ }
325
+ process4.stdout.write("\nPass --yes to proceed with destruction.\n");
326
+ return;
327
+ }
328
+ const confirmed = Boolean(args.yes);
329
+ if (!confirmed) {
330
+ failure(
331
+ `This will destroy all resources in stack "${ctx.stackName}". Pass --yes to confirm.`
332
+ );
333
+ process4.exit(1);
334
+ }
335
+ info(`Destroying stack "${ctx.stackName}"...`);
336
+ const stack = await ctx.getStack();
337
+ const spin = spinner("Running destroy...");
338
+ try {
339
+ await stack.destroy({ onOutput: (out) => {
340
+ spin.text = out.trim() || spin.text;
341
+ } });
342
+ spin.succeed(`Stack "${ctx.stackName}" destroyed`);
343
+ success("All resources have been removed.");
344
+ } catch (err) {
345
+ spin.fail("Destroy failed");
346
+ throw err;
347
+ }
348
+ }
349
+ });
350
+
351
+ // src/cli/commands/status.ts
352
+ import { defineCommand as defineCommand4 } from "citty";
353
+ import process6 from "process";
354
+
355
+ // src/output/json.ts
356
+ import process5 from "process";
357
+ function printJson(output) {
358
+ process5.stdout.write(JSON.stringify(output, null, 2) + "\n");
359
+ }
360
+ function jsonOk(data) {
361
+ return { ok: true, data };
362
+ }
363
+
364
+ // src/cli/commands/status.ts
365
+ var status_default = defineCommand4({
366
+ meta: {
367
+ name: "status",
368
+ description: "Show current stack status: outputs, region, provisioned time"
369
+ },
370
+ args: {
371
+ stack: { type: "string", description: "Target stack name" },
372
+ json: { type: "boolean", description: "Emit JSON" }
373
+ },
374
+ async run({ args }) {
375
+ const { buildContext } = await import("./context-T52JWL3P.js");
376
+ const ctx = buildContext(args);
377
+ if (ctx.adapter.name === "local") {
378
+ const state = ctx.localState;
379
+ if (Boolean(args.json)) {
380
+ if (state) {
381
+ printJson(jsonOk({ stack: ctx.stackName, ...state }));
382
+ } else {
383
+ printJson(jsonOk({ stack: ctx.stackName, status: "not bootstrapped" }));
384
+ }
385
+ return;
386
+ }
387
+ const rows2 = [["Stack", ctx.stackName]];
388
+ if (state) {
389
+ rows2.push(["Provider", "local"]);
390
+ rows2.push(["Host", state.sshHost]);
391
+ rows2.push(["SSH", `${state.sshUser}@${state.sshHost}:${state.sshPort}`]);
392
+ rows2.push(["Gateway URL", state.gatewayUrl]);
393
+ rows2.push(["Bootstrapped", state.provisionedAt]);
394
+ } else {
395
+ rows2.push(["Status", "not bootstrapped (run `clawops up`)"]);
396
+ }
397
+ process6.stdout.write("\n" + renderTable(["Field", "Value"], rows2) + "\n\n");
398
+ return;
399
+ }
400
+ const { extractBaseOutputs } = await import("./outputs-6DAVEEAZ.js");
401
+ const stack = await ctx.getStack();
402
+ const outputMap = await stack.outputs();
403
+ const outputs = Object.fromEntries(
404
+ Object.entries(outputMap).map(([k, v]) => [k, v.value])
405
+ );
406
+ if (Boolean(args.json)) {
407
+ printJson(jsonOk({ stack: ctx.stackName, ...outputs }));
408
+ return;
409
+ }
410
+ const rows = [];
411
+ try {
412
+ const base = extractBaseOutputs(outputs);
413
+ rows.push(["Stack", ctx.stackName]);
414
+ rows.push(["Provider", ctx.config.stacks[ctx.stackName]?.provider ?? "\u2014"]);
415
+ rows.push(["Region", base.region]);
416
+ rows.push(["Public IP", base.publicIp]);
417
+ rows.push(["Gateway URL", base.gatewayUrl]);
418
+ rows.push(["SSH", `${base.sshUser}@${base.sshHost}:${base.sshPort}`]);
419
+ rows.push(["Provisioned", base.provisionedAt]);
420
+ } catch {
421
+ rows.push(["Stack", ctx.stackName]);
422
+ rows.push(["Status", "not deployed (run `clawops up`)"]);
423
+ }
424
+ process6.stdout.write("\n" + renderTable(["Field", "Value"], rows) + "\n\n");
425
+ }
426
+ });
427
+
428
+ // src/cli/commands/plan.ts
429
+ import { defineCommand as defineCommand5 } from "citty";
430
+ import process7 from "process";
431
+ import { writeFileSync as writeFileSync2 } from "fs";
432
+ import { isAbsolute } from "path";
433
+ var plan_default = defineCommand5({
434
+ meta: {
435
+ name: "plan",
436
+ description: "Generate a Maker deploy plan without applying it"
437
+ },
438
+ args: {
439
+ provider: { type: "string", description: "Cloud provider (aws|gcp|azure)" },
440
+ stack: { type: "string", description: "Target stack name" },
441
+ region: { type: "string", description: "Cloud region" },
442
+ "instance-type": { type: "string", description: "Instance size alias (micro|small|medium|large|gpu)" },
443
+ "openclaw-version": { type: "string", description: "semver or 'stable'/'dev'" },
444
+ out: { type: "string", description: "Write plan JSON to this absolute path (default: stdout)" }
445
+ },
446
+ async run({ args }) {
447
+ const { buildContext } = await import("./context-T52JWL3P.js");
448
+ const { generatePlan } = await import("./generate-Y6O465VB.js");
449
+ const ctx = buildContext(args);
450
+ if (ctx.adapter.name === "local") {
451
+ throw new UsageError(
452
+ "plan/apply is not supported for the local provider. Use `clawops up` directly."
453
+ );
454
+ }
455
+ const provider = ctx.adapter.name;
456
+ const outPath = typeof args.out === "string" ? args.out : void 0;
457
+ if (outPath && !isAbsolute(outPath)) {
458
+ throw new UsageError("--out path must be absolute (R7). Use an absolute path like /tmp/plan.json.");
459
+ }
460
+ const abortController = new AbortController();
461
+ process7.on("SIGINT", () => abortController.abort());
462
+ process7.on("SIGTERM", () => abortController.abort());
463
+ const spin = spinner("Generating plan\u2026");
464
+ let plan;
465
+ try {
466
+ plan = await generatePlan(
467
+ {
468
+ stackName: ctx.stackName,
469
+ provider,
470
+ region: typeof args.region === "string" ? args.region : void 0,
471
+ instanceType: typeof args["instance-type"] === "string" ? args["instance-type"] : void 0,
472
+ openclawVersion: typeof args["openclaw-version"] === "string" ? args["openclaw-version"] : void 0
473
+ },
474
+ { signal: abortController.signal }
475
+ );
476
+ spin.succeed("Plan generated");
477
+ } catch (err) {
478
+ spin.fail("Plan generation failed");
479
+ throw err;
480
+ }
481
+ const planJson = JSON.stringify(plan, null, 2);
482
+ if (outPath) {
483
+ writeFileSync2(outPath, planJson + "\n", "utf-8");
484
+ success(`Plan written to ${outPath}`);
485
+ } else {
486
+ process7.stdout.write(planJson + "\n");
487
+ }
488
+ if (plan.diff) {
489
+ const { create, update, delete: del, totalChanges } = plan.diff;
490
+ process7.stderr.write(
491
+ `
492
+ Changes: ${create.length} to create, ${update.length} to update, ${del.length} to delete (${totalChanges} total)
493
+ `
494
+ );
495
+ const rows = [
496
+ ...create.map((r) => ["+", r.type, r.name ?? ""]),
497
+ ...update.map((r) => ["~", r.resource.type, r.resource.name ?? ""]),
498
+ ...del.map((r) => ["-", r.type, r.name ?? ""])
499
+ ];
500
+ if (rows.length > 0) {
501
+ process7.stderr.write(renderTable(["Op", "Resource Type", "Name"], rows) + "\n");
502
+ }
503
+ } else {
504
+ process7.stderr.write("(diff unavailable \u2014 preview could not run against this stack)\n");
505
+ }
506
+ }
507
+ });
508
+
509
+ // src/cli/commands/apply.ts
510
+ import { defineCommand as defineCommand6 } from "citty";
511
+ import process8 from "process";
512
+ import { readFileSync } from "fs";
513
+ import { isAbsolute as isAbsolute2 } from "path";
514
+ import { createInterface } from "readline/promises";
515
+ var apply_default = defineCommand6({
516
+ meta: {
517
+ name: "apply",
518
+ description: "Apply a Maker plan JSON produced by `clawops plan`"
519
+ },
520
+ args: {
521
+ yes: { type: "boolean", description: "Skip confirmation prompt" },
522
+ "dry-run": { type: "boolean", description: "Validate plan and show diff without applying" }
523
+ },
524
+ async run({ args }) {
525
+ const { validatePlan } = await import("./validate-T5M5EHSJ.js");
526
+ const { applyPlan } = await import("./apply-GTVALBAY.js");
527
+ const planPath = args._?.[0];
528
+ if (!planPath) {
529
+ throw new UsageError("Usage: clawops apply <plan.json>");
530
+ }
531
+ if (!isAbsolute2(planPath)) {
532
+ throw new UsageError(`Plan path must be absolute (R7). Got: ${planPath}`);
533
+ }
534
+ let raw;
535
+ try {
536
+ raw = readFileSync(planPath, "utf-8");
537
+ } catch {
538
+ throw new UsageError(`Cannot read plan file: ${planPath}`);
539
+ }
540
+ let plan;
541
+ try {
542
+ plan = JSON.parse(raw);
543
+ } catch {
544
+ throw new UsageError(`Plan file is not valid JSON: ${planPath}`);
545
+ }
546
+ const validation = validatePlan(plan);
547
+ if (!validation.ok) {
548
+ throw new UsageError(`Invalid plan:
549
+ ${validation.errors.join("\n")}`);
550
+ }
551
+ const typedPlan = plan;
552
+ if (typedPlan.spec.provider === "local") {
553
+ throw new UsageError(
554
+ "plan/apply is not supported for the local provider. Use `clawops up` directly."
555
+ );
556
+ }
557
+ info(`Applying plan: ${planPath}`);
558
+ if (typedPlan.diff) {
559
+ const { create, update, delete: del } = typedPlan.diff;
560
+ process8.stdout.write(
561
+ ` ${create.length} to create, ${update.length} to update, ${del.length} to delete
562
+ `
563
+ );
564
+ const rows = [
565
+ ...create.map((r) => ["+", r.type, r.name ?? ""]),
566
+ ...update.map((r) => ["~", r.resource.type, r.resource.name ?? ""]),
567
+ ...del.map((r) => ["-", r.type, r.name ?? ""])
568
+ ];
569
+ if (rows.length > 0) {
570
+ process8.stdout.write(renderTable(["Op", "Resource Type", "Name"], rows) + "\n");
571
+ }
572
+ }
573
+ if (args["dry-run"]) {
574
+ info("Dry run \u2014 plan is valid. Pass --yes (without --dry-run) to apply.");
575
+ return;
576
+ }
577
+ if (!args.yes) {
578
+ const rl = createInterface({ input: process8.stdin, output: process8.stdout });
579
+ const answer = await rl.question("Continue? (y/N) ");
580
+ rl.close();
581
+ if (answer.trim().toLowerCase() !== "y") {
582
+ process8.stdout.write("Aborted.\n");
583
+ process8.exit(0);
584
+ }
585
+ }
586
+ const abortController = new AbortController();
587
+ process8.on("SIGINT", () => abortController.abort());
588
+ process8.on("SIGTERM", () => abortController.abort());
589
+ const spin = spinner(`Applying plan for stack "${typedPlan.spec.stackName}"\u2026`);
590
+ try {
591
+ const result = await applyPlan(typedPlan, {
592
+ onOutput: (line) => {
593
+ spin.text = line.trim() || spin.text;
594
+ },
595
+ signal: abortController.signal
596
+ });
597
+ spin.succeed(`Stack "${typedPlan.spec.stackName}" applied`);
598
+ const summaryRows = Object.entries(result.changeSummary).filter(([, count]) => count > 0).map(([op, count]) => [op, String(count)]);
599
+ if (summaryRows.length > 0) {
600
+ process8.stdout.write(renderTable(["Operation", "Count"], summaryRows) + "\n");
601
+ }
602
+ if (result.outputs["gatewayUrl"]) {
603
+ info(`Gateway URL: ${result.outputs["gatewayUrl"]}`);
604
+ }
605
+ if (result.outputs["publicIp"]) {
606
+ info(`Public IP: ${result.outputs["publicIp"]}`);
607
+ }
608
+ success(`Done in ${(result.durationMs / 1e3).toFixed(1)}s`);
609
+ } catch (err) {
610
+ spin.fail("Apply failed");
611
+ throw err;
612
+ }
613
+ }
614
+ });
615
+
616
+ // src/cli/commands/destroy.ts
617
+ import { defineCommand as defineCommand7 } from "citty";
618
+ import process9 from "process";
619
+ import { createInterface as createInterface2 } from "readline/promises";
620
+ var destroy_default = defineCommand7({
621
+ meta: {
622
+ name: "destroy",
623
+ description: "Destroy all resources in a stack (irreversible)"
624
+ },
625
+ args: {
626
+ stack: { type: "string", description: "Target stack name" },
627
+ yes: { type: "boolean", description: "Skip confirmation prompt" },
628
+ "dry-run": { type: "boolean", description: "Show what would be destroyed without destroying" }
629
+ },
630
+ async run({ args }) {
631
+ const { buildContext } = await import("./context-T52JWL3P.js");
632
+ const ctx = buildContext(args);
633
+ if (ctx.adapter.name === "local") {
634
+ throw new UsageError(
635
+ "Local provider stacks cannot be destroyed via `clawops destroy`. Use `clawops down --yes` to remove a local stack."
636
+ );
637
+ }
638
+ const stack = await ctx.getStack();
639
+ if (args["dry-run"]) {
640
+ info(`Dry run \u2014 would destroy stack "${ctx.stackName}" (${ctx.adapter.name})`);
641
+ try {
642
+ const outputMap = await stack.outputs();
643
+ const rows = Object.entries(outputMap).map(([k, v]) => [k, String(v.value ?? "")]);
644
+ if (rows.length > 0) {
645
+ process9.stdout.write("\nCurrent outputs that would be lost:\n");
646
+ process9.stdout.write(renderTable(["Output", "Value"], rows) + "\n");
647
+ }
648
+ } catch {
649
+ }
650
+ process9.stdout.write(`
651
+ Pass --yes to proceed with destruction.
652
+ `);
653
+ return;
654
+ }
655
+ if (!args.yes) {
656
+ const rl = createInterface2({ input: process9.stdin, output: process9.stdout });
657
+ const answer = await rl.question(
658
+ `Destroy stack "${ctx.stackName}"? This is irreversible. (y/N) `
659
+ );
660
+ rl.close();
661
+ if (answer.trim().toLowerCase() !== "y") {
662
+ process9.stdout.write("Aborted.\n");
663
+ process9.exit(0);
664
+ }
665
+ }
666
+ const abortController = new AbortController();
667
+ process9.on("SIGINT", () => abortController.abort());
668
+ process9.on("SIGTERM", () => abortController.abort());
669
+ const spin = spinner(`Destroying stack "${ctx.stackName}"\u2026`);
670
+ try {
671
+ await stack.destroy({
672
+ onOutput: (out) => {
673
+ spin.text = out.trim() || spin.text;
674
+ }
675
+ });
676
+ spin.succeed(`Stack "${ctx.stackName}" destroyed`);
677
+ success("All resources have been removed.");
678
+ } catch (err) {
679
+ spin.fail("Destroy failed");
680
+ throw err;
681
+ }
682
+ }
683
+ });
684
+
685
+ // src/cli/commands/ssh.ts
686
+ import { defineCommand as defineCommand8 } from "citty";
687
+ import process10 from "process";
688
+ var ssh_default = defineCommand8({
689
+ meta: {
690
+ name: "ssh",
691
+ description: "Open an SSH session to the stack instance"
692
+ },
693
+ args: {
694
+ stack: { type: "string", description: "Target stack name" },
695
+ command: { type: "string", description: "Remote command to run (instead of interactive shell)" }
696
+ },
697
+ async run({ args }) {
698
+ const { buildContext } = await import("./context-T52JWL3P.js");
699
+ const { acquireSession, drainPool } = await import("./pool-D4JCUA2V.js");
700
+ const ctx = buildContext(args);
701
+ let conn;
702
+ if (ctx.adapter.name === "local") {
703
+ const state = ctx.localState;
704
+ if (!state) {
705
+ failure("Stack has no state. Run `clawops up` first.");
706
+ process10.exit(4);
707
+ }
708
+ conn = {
709
+ host: state.sshHost,
710
+ port: state.sshPort,
711
+ user: state.sshUser,
712
+ privateKeyPath: state.privateKeyPath,
713
+ knownHostsPath: state.knownHostsPath
714
+ };
715
+ } else {
716
+ const { extractBaseOutputs } = await import("./outputs-6DAVEEAZ.js");
717
+ const stack = await ctx.getStack();
718
+ const outputMap = await stack.outputs();
719
+ const outputs = Object.fromEntries(
720
+ Object.entries(outputMap).map(([k, v]) => [k, v.value])
721
+ );
722
+ if (!outputs["publicIp"]) {
723
+ failure("Stack has no outputs. Run `clawops up` first.");
724
+ process10.exit(4);
725
+ }
726
+ const base = extractBaseOutputs(outputs);
727
+ conn = ctx.adapter.getConnectionInfo({
728
+ ...base,
729
+ privateKeyPath: ctx.config.ssh.keyPath,
730
+ knownHostsPath: ctx.config.ssh.knownHostsPath
731
+ });
732
+ }
733
+ const remoteCommand = typeof args.command === "string" ? args.command : null;
734
+ const abortController = new AbortController();
735
+ process10.on("SIGINT", () => abortController.abort());
736
+ process10.on("SIGTERM", () => abortController.abort());
737
+ const spin = spinner("Connecting...");
738
+ const { session, release } = await acquireSession({
739
+ host: conn.host,
740
+ port: conn.port,
741
+ user: conn.user,
742
+ privateKeyPath: conn.privateKeyPath,
743
+ knownHostsPath: conn.knownHostsPath,
744
+ signal: abortController.signal
745
+ });
746
+ spin.stop();
747
+ try {
748
+ if (remoteCommand) {
749
+ const result = await session.exec(remoteCommand, abortController.signal);
750
+ process10.stdout.write(result.stdout);
751
+ if (result.stderr) process10.stderr.write(result.stderr);
752
+ process10.exit(result.code);
753
+ } else {
754
+ const shellStream = await session.stream("bash -l", abortController.signal);
755
+ shellStream.pipe(process10.stdout);
756
+ process10.stdin.pipe(shellStream);
757
+ await new Promise((resolve) => {
758
+ shellStream.on("end", resolve);
759
+ shellStream.on("close", resolve);
760
+ abortController.signal.addEventListener("abort", () => resolve(), { once: true });
761
+ });
762
+ }
763
+ } finally {
764
+ release();
765
+ drainPool();
766
+ }
767
+ }
768
+ });
769
+
770
+ // src/cli/commands/tunnel.ts
771
+ import { defineCommand as defineCommand9 } from "citty";
772
+ import { spawn } from "child_process";
773
+ import process11 from "process";
774
+ var tunnel_default = defineCommand9({
775
+ meta: {
776
+ name: "tunnel",
777
+ description: "Forward the OpenClaw gateway port to localhost"
778
+ },
779
+ args: {
780
+ stack: { type: "string", description: "Target stack name" },
781
+ port: { type: "string", description: "Local port to listen on (default: same as gateway)" },
782
+ "no-open": { type: "boolean", description: "Do not open browser after tunnel is ready" }
783
+ },
784
+ async run({ args }) {
785
+ const { buildContext } = await import("./context-T52JWL3P.js");
786
+ const { extractBaseOutputs } = await import("./outputs-6DAVEEAZ.js");
787
+ const { acquireSession } = await import("./pool-D4JCUA2V.js");
788
+ const ctx = buildContext(args);
789
+ const stack = await ctx.getStack();
790
+ const outputMap = await stack.outputs();
791
+ const outputs = Object.fromEntries(
792
+ Object.entries(outputMap).map(([k, v]) => [k, v.value])
793
+ );
794
+ const base = extractBaseOutputs(outputs);
795
+ const gatewayUrl = new URL(base.gatewayUrl);
796
+ const remotePort = parseInt(gatewayUrl.port, 10) || 443;
797
+ const localPort = args.port ? parseInt(String(args.port), 10) : remotePort;
798
+ const conn = ctx.adapter.getConnectionInfo({
799
+ ...base,
800
+ privateKeyPath: ctx.config.ssh.keyPath,
801
+ knownHostsPath: ctx.config.ssh.knownHostsPath
802
+ });
803
+ const abortController = new AbortController();
804
+ process11.on("SIGINT", () => abortController.abort());
805
+ process11.on("SIGTERM", () => abortController.abort());
806
+ const spin = spinner("Connecting...");
807
+ const { session, release } = await acquireSession({
808
+ host: conn.host,
809
+ port: conn.port,
810
+ user: conn.user,
811
+ privateKeyPath: conn.privateKeyPath,
812
+ knownHostsPath: conn.knownHostsPath,
813
+ signal: abortController.signal
814
+ });
815
+ spin.stop();
816
+ try {
817
+ const handle = await session.tunnel(localPort, "localhost", remotePort, abortController.signal);
818
+ const localUrl = `http://localhost:${handle.localPort}`;
819
+ success(`Tunnel ready: ${localUrl}`);
820
+ process11.stdout.write("Press Ctrl+C to close.\n");
821
+ if (!args["no-open"]) {
822
+ openBrowser(localUrl);
823
+ }
824
+ await new Promise((resolve) => {
825
+ abortController.signal.addEventListener("abort", () => resolve(), { once: true });
826
+ });
827
+ handle.close();
828
+ } finally {
829
+ release();
830
+ }
831
+ }
832
+ });
833
+ function openBrowser(url) {
834
+ const [cmd, ...cmdArgs] = process11.platform === "darwin" ? ["open", url] : process11.platform === "win32" ? ["cmd", "/c", "start", url] : ["xdg-open", url];
835
+ spawn(cmd, cmdArgs, { detached: true, stdio: "ignore" }).unref();
836
+ }
837
+
838
+ // src/cli/commands/logs.ts
839
+ import { defineCommand as defineCommand10 } from "citty";
840
+ import process12 from "process";
841
+ var logs_default = defineCommand10({
842
+ meta: {
843
+ name: "logs",
844
+ description: "Stream gateway logs from the remote instance"
845
+ },
846
+ args: {
847
+ stack: { type: "string", description: "Target stack name" },
848
+ follow: { type: "boolean", alias: "f", description: "Follow log output" },
849
+ tail: { type: "string", description: "Number of lines to show from end (default: 100)" },
850
+ since: { type: "string", description: "Show logs since duration (e.g. 5m, 1h)" }
851
+ },
852
+ async run({ args }) {
853
+ const { buildContext } = await import("./context-T52JWL3P.js");
854
+ const { extractBaseOutputs } = await import("./outputs-6DAVEEAZ.js");
855
+ const { acquireSession, drainPool } = await import("./pool-D4JCUA2V.js");
856
+ const ctx = buildContext(args);
857
+ const stack = await ctx.getStack();
858
+ const outputMap = await stack.outputs();
859
+ const outputs = Object.fromEntries(
860
+ Object.entries(outputMap).map(([k, v]) => [k, v.value])
861
+ );
862
+ if (!outputs["publicIp"]) {
863
+ failure("Stack has no outputs. Run `clawops up` first.");
864
+ process12.exit(4);
865
+ }
866
+ const base = extractBaseOutputs(outputs);
867
+ const conn = ctx.adapter.getConnectionInfo({
868
+ ...base,
869
+ privateKeyPath: ctx.config.ssh.keyPath,
870
+ knownHostsPath: ctx.config.ssh.knownHostsPath
871
+ });
872
+ const tailLines = typeof args.tail === "string" ? parseInt(args.tail, 10) : 100;
873
+ const follow = Boolean(args.follow);
874
+ const sinceFlag = typeof args.since === "string" ? `--since "${args.since}"` : "";
875
+ const followFlag = follow ? "-f" : "";
876
+ const command = [
877
+ "journalctl -u openclaw",
878
+ `-n ${tailLines}`,
879
+ followFlag,
880
+ sinceFlag,
881
+ "2>/dev/null",
882
+ "|| docker logs openclaw",
883
+ `-n ${tailLines}`,
884
+ follow ? "-f" : ""
885
+ ].filter(Boolean).join(" ");
886
+ const abortController = new AbortController();
887
+ process12.on("SIGINT", () => abortController.abort());
888
+ process12.on("SIGTERM", () => abortController.abort());
889
+ const spin = spinner("Connecting...");
890
+ const { session, release } = await acquireSession({
891
+ host: conn.host,
892
+ port: conn.port,
893
+ user: conn.user,
894
+ privateKeyPath: conn.privateKeyPath,
895
+ knownHostsPath: conn.knownHostsPath,
896
+ signal: abortController.signal
897
+ });
898
+ spin.stop();
899
+ try {
900
+ if (follow) {
901
+ const logStream = await session.stream(command, abortController.signal);
902
+ logStream.pipe(process12.stdout);
903
+ await new Promise((resolve) => {
904
+ logStream.on("end", resolve);
905
+ logStream.on("close", resolve);
906
+ abortController.signal.addEventListener("abort", () => resolve(), { once: true });
907
+ });
908
+ } else {
909
+ const result = await session.exec(command, abortController.signal);
910
+ process12.stdout.write(result.stdout);
911
+ if (result.stderr) process12.stderr.write(result.stderr);
912
+ }
913
+ } finally {
914
+ release();
915
+ if (!follow) drainPool();
916
+ }
917
+ }
918
+ });
919
+
920
+ // src/cli/commands/config.ts
921
+ import { defineCommand as defineCommand11 } from "citty";
922
+ import process13 from "process";
923
+ var OPENCLAW_CONFIG = "/home/clawops/openclaw.json";
924
+ var OPENCLAW_TMP = "/tmp/clawops-config.json.tmp";
925
+ function dockerRunCmd(version) {
926
+ return [
927
+ "docker stop openclaw 2>/dev/null || true",
928
+ "docker rm openclaw 2>/dev/null || true",
929
+ `docker run -d --name openclaw --restart unless-stopped -p 18789:18789 -v ${OPENCLAW_CONFIG}:/app/config.json:ro ghcr.io/openclaw/openclaw:${version}`
930
+ ].join(" && ");
931
+ }
932
+ function getPath(obj, dotKey) {
933
+ return dotKey.split(".").reduce((cur, k) => {
934
+ if (cur !== null && typeof cur === "object") return cur[k];
935
+ return void 0;
936
+ }, obj);
937
+ }
938
+ function setPath(obj, dotKey, value) {
939
+ const keys = dotKey.split(".");
940
+ let cur = obj;
941
+ for (let i = 0; i < keys.length - 1; i++) {
942
+ const k = keys[i];
943
+ if (typeof cur[k] !== "object" || cur[k] === null) cur[k] = {};
944
+ cur = cur[k];
945
+ }
946
+ cur[keys[keys.length - 1]] = value;
947
+ }
948
+ function deletePath(obj, dotKey) {
949
+ const keys = dotKey.split(".");
950
+ let cur = obj;
951
+ for (let i = 0; i < keys.length - 1; i++) {
952
+ const k = keys[i];
953
+ if (typeof cur[k] !== "object" || cur[k] === null) return;
954
+ cur = cur[k];
955
+ }
956
+ delete cur[keys[keys.length - 1]];
957
+ }
958
+ var config_default = defineCommand11({
959
+ meta: {
960
+ name: "config",
961
+ description: "Manage OpenClaw gateway configuration (get | set | unset)"
962
+ },
963
+ args: {
964
+ stack: { type: "string", description: "Target stack name" },
965
+ restart: { type: "boolean", description: "Restart gateway after set/unset" },
966
+ json: { type: "boolean", description: "Emit JSON (for get)" },
967
+ "dry-run": { type: "boolean", description: "Show what would change without writing" }
968
+ },
969
+ async run({ args }) {
970
+ const { buildContext } = await import("./context-T52JWL3P.js");
971
+ const { extractBaseOutputs } = await import("./outputs-6DAVEEAZ.js");
972
+ const { acquireSession, drainPool } = await import("./pool-D4JCUA2V.js");
973
+ 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>");
976
+ process13.exit(2);
977
+ }
978
+ if (action === "set" && (!key || value === void 0)) {
979
+ failure("Usage: clawops config set <key> <value>");
980
+ process13.exit(2);
981
+ }
982
+ if (action === "unset" && !key) {
983
+ failure("Usage: clawops config unset <key>");
984
+ process13.exit(2);
985
+ }
986
+ const ctx = buildContext(args);
987
+ const stack = await ctx.getStack();
988
+ const outputMap = await stack.outputs();
989
+ const outputs = Object.fromEntries(
990
+ Object.entries(outputMap).map(([k, v]) => [k, v.value])
991
+ );
992
+ const base = extractBaseOutputs(outputs);
993
+ const conn = ctx.adapter.getConnectionInfo({
994
+ ...base,
995
+ privateKeyPath: ctx.config.ssh.keyPath,
996
+ knownHostsPath: ctx.config.ssh.knownHostsPath
997
+ });
998
+ const abortController = new AbortController();
999
+ process13.on("SIGINT", () => abortController.abort());
1000
+ process13.on("SIGTERM", () => abortController.abort());
1001
+ const { session, release } = await acquireSession({
1002
+ host: conn.host,
1003
+ port: conn.port,
1004
+ user: conn.user,
1005
+ privateKeyPath: conn.privateKeyPath,
1006
+ knownHostsPath: conn.knownHostsPath,
1007
+ signal: abortController.signal
1008
+ });
1009
+ try {
1010
+ const readResult = await session.exec(`cat ${OPENCLAW_CONFIG}`, abortController.signal);
1011
+ let cfg;
1012
+ try {
1013
+ cfg = JSON.parse(readResult.stdout);
1014
+ } catch {
1015
+ failure(`Cannot parse ${OPENCLAW_CONFIG}: ${readResult.stderr || readResult.stdout}`);
1016
+ process13.exit(1);
1017
+ }
1018
+ if (action === "get") {
1019
+ const result = key ? getPath(cfg, key) : cfg;
1020
+ if (args.json) {
1021
+ printJson(jsonOk(result));
1022
+ } else {
1023
+ process13.stdout.write(JSON.stringify(result, null, 2) + "\n");
1024
+ }
1025
+ return;
1026
+ }
1027
+ if (action === "set") {
1028
+ let parsedValue = value;
1029
+ try {
1030
+ parsedValue = JSON.parse(value);
1031
+ } catch {
1032
+ }
1033
+ setPath(cfg, key, parsedValue);
1034
+ } else {
1035
+ deletePath(cfg, key);
1036
+ }
1037
+ if (args["dry-run"]) {
1038
+ info(`Dry run \u2014 would write to ${OPENCLAW_CONFIG}:`);
1039
+ process13.stdout.write(JSON.stringify(cfg, null, 2) + "\n");
1040
+ return;
1041
+ }
1042
+ const json = JSON.stringify(cfg, null, 2);
1043
+ const b64 = Buffer.from(json, "utf-8").toString("base64");
1044
+ const writeCmd = `echo '${b64}' | base64 -d > ${OPENCLAW_TMP} && mv ${OPENCLAW_TMP} ${OPENCLAW_CONFIG} && chown clawops:clawops ${OPENCLAW_CONFIG}`;
1045
+ const writeResult = await session.exec(writeCmd, abortController.signal);
1046
+ if (writeResult.code !== 0) {
1047
+ failure(`Failed to write config: ${writeResult.stderr}`);
1048
+ process13.exit(1);
1049
+ }
1050
+ success(`config ${action}: ${key ?? "(all)"}`);
1051
+ if (args.restart) {
1052
+ info("Restarting gateway...");
1053
+ const imgResult = await session.exec(
1054
+ `docker inspect openclaw --format '{{.Config.Image}}' 2>/dev/null || echo 'ghcr.io/openclaw/openclaw:stable'`,
1055
+ abortController.signal
1056
+ );
1057
+ const version = imgResult.stdout.trim().split(":")[1] ?? "stable";
1058
+ const restartResult = await session.exec(dockerRunCmd(version), abortController.signal);
1059
+ if (restartResult.code !== 0) {
1060
+ failure(`Restart failed: ${restartResult.stderr}`);
1061
+ process13.exit(1);
1062
+ }
1063
+ success("Gateway restarted.");
1064
+ }
1065
+ } finally {
1066
+ release();
1067
+ drainPool();
1068
+ }
1069
+ }
1070
+ });
1071
+
1072
+ // src/cli/commands/agents.ts
1073
+ import { defineCommand as defineCommand12 } from "citty";
1074
+ import process14 from "process";
1075
+ var agents_default = defineCommand12({
1076
+ meta: {
1077
+ name: "agents",
1078
+ description: "Manage OpenClaw agents (list | restart [name] | logs <name>)"
1079
+ },
1080
+ args: {
1081
+ stack: { type: "string", description: "Target stack name" },
1082
+ json: { type: "boolean", description: "Emit JSON (for list)" }
1083
+ },
1084
+ async run({ args }) {
1085
+ const { buildContext } = await import("./context-T52JWL3P.js");
1086
+ const { extractBaseOutputs } = await import("./outputs-6DAVEEAZ.js");
1087
+ const { acquireSession, drainPool } = await import("./pool-D4JCUA2V.js");
1088
+ const [action, name] = args._ ?? [];
1089
+ if (!action || !["list", "restart", "logs"].includes(action)) {
1090
+ failure("Usage: clawops agents <list | restart [name] | logs <name>>");
1091
+ process14.exit(2);
1092
+ }
1093
+ if (action === "logs" && !name) {
1094
+ failure("Usage: clawops agents logs <name>");
1095
+ process14.exit(2);
1096
+ }
1097
+ const ctx = buildContext(args);
1098
+ const stack = await ctx.getStack();
1099
+ const outputMap = await stack.outputs();
1100
+ const outputs = Object.fromEntries(
1101
+ Object.entries(outputMap).map(([k, v]) => [k, v.value])
1102
+ );
1103
+ const base = extractBaseOutputs(outputs);
1104
+ const conn = ctx.adapter.getConnectionInfo({
1105
+ ...base,
1106
+ privateKeyPath: ctx.config.ssh.keyPath,
1107
+ knownHostsPath: ctx.config.ssh.knownHostsPath
1108
+ });
1109
+ const abortController = new AbortController();
1110
+ process14.on("SIGINT", () => abortController.abort());
1111
+ process14.on("SIGTERM", () => abortController.abort());
1112
+ const { session, release } = await acquireSession({
1113
+ host: conn.host,
1114
+ port: conn.port,
1115
+ user: conn.user,
1116
+ privateKeyPath: conn.privateKeyPath,
1117
+ knownHostsPath: conn.knownHostsPath,
1118
+ signal: abortController.signal
1119
+ });
1120
+ try {
1121
+ if (action === "list") {
1122
+ const result = await session.exec(
1123
+ "docker exec openclaw openclaw agents list --json 2>/dev/null || echo '[]'",
1124
+ abortController.signal
1125
+ );
1126
+ let agents = [];
1127
+ try {
1128
+ agents = JSON.parse(result.stdout.trim());
1129
+ } catch {
1130
+ agents = [];
1131
+ }
1132
+ if (args.json) {
1133
+ printJson(jsonOk(agents));
1134
+ } else if (agents.length === 0) {
1135
+ info("No agents running.");
1136
+ } else {
1137
+ process14.stdout.write(
1138
+ "\n" + renderTable(
1139
+ ["Name", "Status"],
1140
+ agents.map((a) => [a.name ?? "\u2014", a.status ?? "\u2014"])
1141
+ ) + "\n\n"
1142
+ );
1143
+ }
1144
+ } else if (action === "restart") {
1145
+ const cmd = name ? `docker exec openclaw openclaw agents restart ${name}` : "docker exec openclaw openclaw agents restart";
1146
+ const result = await session.exec(cmd, abortController.signal);
1147
+ if (result.code !== 0) {
1148
+ failure(`Restart failed: ${result.stderr}`);
1149
+ process14.exit(1);
1150
+ }
1151
+ success(name ? `Agent '${name}' restarted.` : "All agents restarted.");
1152
+ } else {
1153
+ const logStream = await session.stream(
1154
+ `docker exec -t openclaw openclaw agents logs ${name} --follow`,
1155
+ abortController.signal
1156
+ );
1157
+ logStream.pipe(process14.stdout);
1158
+ await new Promise((resolve) => {
1159
+ logStream.on("end", resolve);
1160
+ logStream.on("close", resolve);
1161
+ abortController.signal.addEventListener("abort", () => resolve(), { once: true });
1162
+ });
1163
+ }
1164
+ } finally {
1165
+ release();
1166
+ if (action !== "logs") drainPool();
1167
+ }
1168
+ }
1169
+ });
1170
+
1171
+ // src/cli/commands/gateway.ts
1172
+ import { defineCommand as defineCommand13 } from "citty";
1173
+ import process15 from "process";
1174
+ var OPENCLAW_CONFIG2 = "/home/clawops/openclaw.json";
1175
+ function dockerRunCmd2(version) {
1176
+ return `docker stop openclaw 2>/dev/null || true && docker rm openclaw 2>/dev/null || true && docker run -d --name openclaw --restart unless-stopped -p 18789:18789 -v ${OPENCLAW_CONFIG2}:/app/config.json:ro ghcr.io/openclaw/openclaw:${version}`;
1177
+ }
1178
+ var gateway_default = defineCommand13({
1179
+ meta: {
1180
+ name: "gateway",
1181
+ description: "Manage the OpenClaw gateway daemon (status | restart | update [version])"
1182
+ },
1183
+ args: {
1184
+ stack: { type: "string", description: "Target stack name" },
1185
+ channel: { type: "string", description: "Channel for update: stable | dev | <version>" },
1186
+ json: { type: "boolean", description: "Emit JSON (for status)" }
1187
+ },
1188
+ async run({ args }) {
1189
+ const { buildContext } = await import("./context-T52JWL3P.js");
1190
+ const { extractBaseOutputs } = await import("./outputs-6DAVEEAZ.js");
1191
+ const { acquireSession, drainPool } = await import("./pool-D4JCUA2V.js");
1192
+ const [action, versionArg] = args._ ?? [];
1193
+ if (!action || !["status", "restart", "update"].includes(action)) {
1194
+ failure("Usage: clawops gateway <status | restart | update [version]>");
1195
+ process15.exit(2);
1196
+ }
1197
+ const ctx = buildContext(args);
1198
+ const stack = await ctx.getStack();
1199
+ const outputMap = await stack.outputs();
1200
+ const outputs = Object.fromEntries(
1201
+ Object.entries(outputMap).map(([k, v]) => [k, v.value])
1202
+ );
1203
+ const base = extractBaseOutputs(outputs);
1204
+ const conn = ctx.adapter.getConnectionInfo({
1205
+ ...base,
1206
+ privateKeyPath: ctx.config.ssh.keyPath,
1207
+ knownHostsPath: ctx.config.ssh.knownHostsPath
1208
+ });
1209
+ const abortController = new AbortController();
1210
+ process15.on("SIGINT", () => abortController.abort());
1211
+ process15.on("SIGTERM", () => abortController.abort());
1212
+ const { session, release } = await acquireSession({
1213
+ host: conn.host,
1214
+ port: conn.port,
1215
+ user: conn.user,
1216
+ privateKeyPath: conn.privateKeyPath,
1217
+ knownHostsPath: conn.knownHostsPath,
1218
+ signal: abortController.signal
1219
+ });
1220
+ try {
1221
+ if (action === "status") {
1222
+ const statusCmd = `docker inspect openclaw --format '{"status":"{{.State.Status}}","started":"{{.State.StartedAt}}","image":"{{.Config.Image}}"}' 2>/dev/null || echo '{"status":"not running","started":"","image":""}'`;
1223
+ const result = await session.exec(statusCmd, abortController.signal);
1224
+ let status = { status: "unknown", started: "", image: "" };
1225
+ try {
1226
+ status = JSON.parse(result.stdout.trim());
1227
+ } catch {
1228
+ }
1229
+ if (args.json) {
1230
+ printJson(jsonOk(status));
1231
+ } else {
1232
+ process15.stdout.write(
1233
+ "\n" + renderTable(
1234
+ ["Field", "Value"],
1235
+ [
1236
+ ["Status", status.status],
1237
+ ["Started", status.started],
1238
+ ["Image", status.image]
1239
+ ]
1240
+ ) + "\n\n"
1241
+ );
1242
+ }
1243
+ } else if (action === "restart") {
1244
+ const imgResult = await session.exec(
1245
+ `docker inspect openclaw --format '{{.Config.Image}}' 2>/dev/null || echo 'ghcr.io/openclaw/openclaw:stable'`,
1246
+ abortController.signal
1247
+ );
1248
+ const version = imgResult.stdout.trim().split(":")[1] ?? "stable";
1249
+ const spin = spinner("Restarting gateway...");
1250
+ const result = await session.exec(dockerRunCmd2(version), abortController.signal);
1251
+ spin.stop();
1252
+ if (result.code !== 0) {
1253
+ failure(`Restart failed: ${result.stderr}`);
1254
+ process15.exit(1);
1255
+ }
1256
+ success(`Gateway restarted (${version}).`);
1257
+ } else {
1258
+ const version = versionArg ?? args.channel ?? "stable";
1259
+ const spin = spinner(`Updating gateway to ${version}...`);
1260
+ const pullResult = await session.exec(
1261
+ `docker pull ghcr.io/openclaw/openclaw:${version}`,
1262
+ abortController.signal
1263
+ );
1264
+ if (pullResult.code !== 0) {
1265
+ spin.stop();
1266
+ failure(`Pull failed: ${pullResult.stderr}`);
1267
+ process15.exit(1);
1268
+ }
1269
+ const runResult = await session.exec(dockerRunCmd2(version), abortController.signal);
1270
+ spin.stop();
1271
+ if (runResult.code !== 0) {
1272
+ failure(`Start failed: ${runResult.stderr}`);
1273
+ process15.exit(1);
1274
+ }
1275
+ success(`Gateway updated to ${version}.`);
1276
+ }
1277
+ } finally {
1278
+ release();
1279
+ drainPool();
1280
+ }
1281
+ }
1282
+ });
1283
+
1284
+ // src/cli/commands/backup.ts
1285
+ import { defineCommand as defineCommand14 } from "citty";
1286
+ import { createWriteStream, createReadStream } from "fs";
1287
+ import { pipeline } from "stream/promises";
1288
+ import process16 from "process";
1289
+ var backup_default = defineCommand14({
1290
+ meta: {
1291
+ name: "backup",
1292
+ description: "Create or restore an OpenClaw backup (create | restore)"
1293
+ },
1294
+ args: {
1295
+ action: { type: "positional", description: "Action: create | restore", required: true },
1296
+ out: { type: "string", description: "[create] Local path to write the backup archive" },
1297
+ file: { type: "string", description: "[restore] Local backup archive to restore from" },
1298
+ stack: { type: "string", description: "Target stack name" },
1299
+ yes: { type: "boolean", description: "[restore] Skip confirmation prompt" }
1300
+ },
1301
+ async run({ args }) {
1302
+ const { buildContext } = await import("./context-T52JWL3P.js");
1303
+ const { acquireSession, drainPool } = await import("./pool-D4JCUA2V.js");
1304
+ const action = args.action;
1305
+ if (action !== "create" && action !== "restore") {
1306
+ throw new UsageError(`Unknown action: ${action}. Use "create" or "restore"`);
1307
+ }
1308
+ const ctx = buildContext(args);
1309
+ let conn;
1310
+ if (ctx.adapter.name === "local") {
1311
+ const state = ctx.localState;
1312
+ if (!state) {
1313
+ failure("Stack has no state. Run `clawops up` first.");
1314
+ process16.exit(4);
1315
+ }
1316
+ conn = {
1317
+ host: state.sshHost,
1318
+ port: state.sshPort,
1319
+ user: state.sshUser,
1320
+ privateKeyPath: state.privateKeyPath,
1321
+ knownHostsPath: state.knownHostsPath
1322
+ };
1323
+ } else {
1324
+ const { extractBaseOutputs } = await import("./outputs-6DAVEEAZ.js");
1325
+ const stack = await ctx.getStack();
1326
+ const outputMap = await stack.outputs();
1327
+ const outputs = Object.fromEntries(
1328
+ Object.entries(outputMap).map(([k, v]) => [k, v.value])
1329
+ );
1330
+ if (!outputs["publicIp"]) {
1331
+ failure("Stack has no outputs. Run `clawops up` first.");
1332
+ process16.exit(4);
1333
+ }
1334
+ const base = extractBaseOutputs(outputs);
1335
+ conn = ctx.adapter.getConnectionInfo({
1336
+ ...base,
1337
+ privateKeyPath: ctx.config.ssh.keyPath,
1338
+ knownHostsPath: ctx.config.ssh.knownHostsPath
1339
+ });
1340
+ }
1341
+ const abortController = new AbortController();
1342
+ process16.on("SIGINT", () => abortController.abort());
1343
+ process16.on("SIGTERM", () => abortController.abort());
1344
+ const { session, release } = await acquireSession({
1345
+ host: conn.host,
1346
+ port: conn.port,
1347
+ user: conn.user,
1348
+ privateKeyPath: conn.privateKeyPath,
1349
+ knownHostsPath: conn.knownHostsPath,
1350
+ signal: abortController.signal
1351
+ });
1352
+ try {
1353
+ if (action === "create") {
1354
+ const outPath = typeof args.out === "string" ? args.out : `openclaw-backup-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.tar.gz`;
1355
+ info(`Writing backup to ${outPath}...`);
1356
+ const spin = spinner("Creating backup on remote host...");
1357
+ const backupStream = await session.stream(
1358
+ "docker exec openclaw openclaw-ctl backup create --stdout",
1359
+ abortController.signal
1360
+ );
1361
+ spin.stop();
1362
+ const fileStream = createWriteStream(outPath);
1363
+ await pipeline(backupStream, fileStream);
1364
+ success(`Backup saved to ${outPath}`);
1365
+ } else {
1366
+ const filePath = typeof args.file === "string" ? args.file : null;
1367
+ if (!filePath) {
1368
+ throw new UsageError("--file is required for backup restore");
1369
+ }
1370
+ if (!args.yes) {
1371
+ const { createInterface: createInterface3 } = await import("readline/promises");
1372
+ const rl = createInterface3({ input: process16.stdin, output: process16.stdout });
1373
+ try {
1374
+ const answer = await rl.question(
1375
+ `Restore backup from "${filePath}" to stack "${ctx.stackName}"? This will overwrite existing data. (y/N) `
1376
+ );
1377
+ if (answer.trim().toLowerCase() !== "y") {
1378
+ info("Restore cancelled.");
1379
+ return;
1380
+ }
1381
+ } finally {
1382
+ rl.close();
1383
+ }
1384
+ }
1385
+ info(`Restoring from ${filePath}...`);
1386
+ const spin = spinner("Transferring and restoring backup...");
1387
+ const restoreStream = await session.stream(
1388
+ "docker exec -i openclaw openclaw-ctl backup restore --stdin",
1389
+ abortController.signal
1390
+ );
1391
+ const fileStream = createReadStream(filePath);
1392
+ await pipeline(fileStream, restoreStream);
1393
+ spin.stop();
1394
+ success("Backup restored successfully");
1395
+ }
1396
+ } finally {
1397
+ release();
1398
+ drainPool();
1399
+ }
1400
+ }
1401
+ });
1402
+
1403
+ // src/cli/commands/stacks.ts
1404
+ import { defineCommand as defineCommand15 } from "citty";
1405
+ import process17 from "process";
1406
+ var stacks_default = defineCommand15({
1407
+ meta: {
1408
+ name: "stacks",
1409
+ description: "Manage clawops stacks (list | delete <name>)"
1410
+ },
1411
+ args: {
1412
+ json: { type: "boolean", description: "Emit JSON (for list)" },
1413
+ yes: { type: "boolean", description: "Skip confirmation prompt on delete" },
1414
+ force: { type: "boolean", description: "Allow deleting the default stack" }
1415
+ },
1416
+ async run({ args }) {
1417
+ const [action, name] = args._ ?? [];
1418
+ if (!action || !["list", "delete"].includes(action)) {
1419
+ failure("Usage: clawops stacks <list | delete <name>>");
1420
+ process17.exit(2);
1421
+ }
1422
+ if (action === "list") {
1423
+ const config2 = requireConfig();
1424
+ const defaultStack = config2.defaults.stack;
1425
+ const rows = Object.entries(config2.stacks).map(([n, s]) => [
1426
+ n === defaultStack ? `${n} *` : n,
1427
+ s.provider,
1428
+ s.region ?? "\u2014",
1429
+ s.stateUrl
1430
+ ]);
1431
+ if (args.json) {
1432
+ const data = Object.entries(config2.stacks).map(([n, s]) => ({
1433
+ name: n,
1434
+ provider: s.provider,
1435
+ region: s.region ?? null,
1436
+ stateUrl: s.stateUrl,
1437
+ isDefault: n === defaultStack
1438
+ }));
1439
+ printJson(jsonOk({ stacks: data, default: defaultStack }));
1440
+ } else if (rows.length === 0) {
1441
+ info("No stacks configured.");
1442
+ } else {
1443
+ process17.stdout.write(
1444
+ "\n" + renderTable(
1445
+ ["Name", "Provider", "Region", "State URL"],
1446
+ rows
1447
+ ) + "\n\n"
1448
+ );
1449
+ info("* = default stack");
1450
+ }
1451
+ return;
1452
+ }
1453
+ if (!name) {
1454
+ failure("Usage: clawops stacks delete <name>");
1455
+ process17.exit(2);
1456
+ }
1457
+ const config = requireConfig();
1458
+ if (!(name in config.stacks)) {
1459
+ throw new UsageError(`Stack "${name}" not found in config.`);
1460
+ }
1461
+ const stackNames = Object.keys(config.stacks);
1462
+ if (stackNames.length === 1) {
1463
+ throw new UsageError(
1464
+ `Cannot delete the only remaining stack "${name}". Add another stack first or run \`clawops destroy\` to tear down resources.`
1465
+ );
1466
+ }
1467
+ if (name === config.defaults.stack && !args.force) {
1468
+ throw new UsageError(
1469
+ `"${name}" is the default stack. Use --force to delete it (clawops will switch the default to another stack).`
1470
+ );
1471
+ }
1472
+ warn(
1473
+ `This removes "${name}" from clawops config only. Cloud resources are NOT destroyed. Run \`clawops destroy --stack ` + name + "` first if you want to remove cloud resources."
1474
+ );
1475
+ if (!args.yes) {
1476
+ const confirmed = await confirm(`Delete stack "${name}" from config?`);
1477
+ if (!confirmed) {
1478
+ info("Aborted.");
1479
+ return;
1480
+ }
1481
+ }
1482
+ const updated = { ...config };
1483
+ const newStacks = { ...config.stacks };
1484
+ delete newStacks[name];
1485
+ updated.stacks = newStacks;
1486
+ if (name === config.defaults.stack) {
1487
+ updated.defaults = { ...config.defaults, stack: Object.keys(newStacks)[0] };
1488
+ }
1489
+ setConfig(updated);
1490
+ success(`Stack "${name}" removed from config.`);
1491
+ if (name === config.defaults.stack) {
1492
+ info(`Default stack switched to "${updated.defaults.stack}".`);
1493
+ }
1494
+ }
1495
+ });
1496
+ async function confirm(message) {
1497
+ const { createInterface: createInterface3 } = await import("readline/promises");
1498
+ const rl = createInterface3({ input: process17.stdin, output: process17.stdout });
1499
+ try {
1500
+ const answer = await rl.question(`${message} (y/N) `);
1501
+ return answer.trim().toLowerCase() === "y";
1502
+ } finally {
1503
+ rl.close();
1504
+ }
1505
+ }
1506
+
1507
+ // src/cli/commands/doctor.ts
1508
+ import { defineCommand as defineCommand16 } from "citty";
1509
+ import process18 from "process";
1510
+ import { accessSync, mkdirSync as mkdirSync2, constants } from "fs";
1511
+ import path2 from "path";
1512
+ var doctor_default = defineCommand16({
1513
+ meta: {
1514
+ name: "doctor",
1515
+ description: "Check system prerequisites, config, SSH keys, and cloud credentials"
1516
+ },
1517
+ async run() {
1518
+ const { getConfig: getConfig2, getConfigDir: getConfigDir2 } = await import("./store-ARJ2EO6L.js");
1519
+ process18.stdout.write("\nclawops doctor\n");
1520
+ process18.stdout.write("\nRuntime\n");
1521
+ const nodeVersion = process18.version;
1522
+ const nodeMajor = parseInt(nodeVersion.slice(1).split(".")[0] ?? "0", 10);
1523
+ const nodeOk = nodeMajor >= 22;
1524
+ if (nodeOk) {
1525
+ success(`Node.js ${nodeVersion}`);
1526
+ } else {
1527
+ failure(`Node.js ${nodeVersion} (requires >=22)`);
1528
+ }
1529
+ const configDir = getConfigDir2();
1530
+ const pulumiHome = path2.join(configDir, ".pulumi");
1531
+ try {
1532
+ mkdirSync2(pulumiHome, { recursive: true });
1533
+ success(`Pulumi home ${pulumiHome}`);
1534
+ } catch {
1535
+ failure(`Pulumi home ${pulumiHome} (not writable)`);
1536
+ }
1537
+ process18.stdout.write("\nConfig\n");
1538
+ const config = getConfig2();
1539
+ if (!config) {
1540
+ warn("No config file found \u2014 run `clawops init` to create one");
1541
+ } else {
1542
+ success(`Config file ${path2.join(configDir, "config.json")}`);
1543
+ }
1544
+ process18.stdout.write("\nSSH\n");
1545
+ if (!config) {
1546
+ info("SSH checks skipped \u2014 no config");
1547
+ } else {
1548
+ const keyPath = config.ssh.keyPath.replace(/^~/, process18.env["HOME"] ?? "~");
1549
+ try {
1550
+ accessSync(keyPath, constants.R_OK);
1551
+ success(`SSH key ${keyPath}`);
1552
+ } catch {
1553
+ failure(`SSH key ${keyPath} (not found or not readable)`);
1554
+ }
1555
+ const knownHostsPath = config.ssh.knownHostsPath.replace(/^~/, process18.env["HOME"] ?? "~");
1556
+ try {
1557
+ accessSync(knownHostsPath, constants.F_OK);
1558
+ success(`known_hosts ${knownHostsPath}`);
1559
+ } catch {
1560
+ warn(`known_hosts ${knownHostsPath} (does not exist \u2014 will be created on first connect)`);
1561
+ }
1562
+ }
1563
+ process18.stdout.write("\nCredentials\n");
1564
+ if (!config) {
1565
+ info("Credential checks skipped \u2014 no config");
1566
+ } else {
1567
+ const { getProvider } = await import("./providers-2OABPW2E.js");
1568
+ await import("./aws-A3323GNM.js");
1569
+ await import("./gcp-OHWCTFLL.js");
1570
+ await import("./azure-LNWBK2ZI.js");
1571
+ await import("./local-DXBEVZ5C.js");
1572
+ const checkedProviders = /* @__PURE__ */ new Set();
1573
+ for (const [stackName, stackCfg] of Object.entries(config.stacks)) {
1574
+ const providerName = stackCfg.provider;
1575
+ if (checkedProviders.has(providerName)) continue;
1576
+ checkedProviders.add(providerName);
1577
+ if (providerName === "local") {
1578
+ success(`local stack "${stackName}" (SSH-only, no cloud credentials required)`);
1579
+ continue;
1580
+ }
1581
+ try {
1582
+ const adapter = getProvider(providerName);
1583
+ const result = await adapter.validateConfig();
1584
+ if (result.ok) {
1585
+ success(`${providerName} stack "${stackName}"`);
1586
+ } else {
1587
+ for (const err of result.errors) {
1588
+ failure(`${providerName} stack "${stackName}" \u2014 ${err}`);
1589
+ }
1590
+ }
1591
+ } catch (err) {
1592
+ failure(`${providerName} stack "${stackName}" \u2014 ${err instanceof Error ? err.message : String(err)}`);
1593
+ }
1594
+ }
1595
+ if (checkedProviders.size === 0) {
1596
+ warn("No stacks configured \u2014 run `clawops init`");
1597
+ }
1598
+ }
1599
+ process18.stdout.write("\n");
1600
+ if (!nodeOk) process18.exit(1);
1601
+ }
1602
+ });
1603
+
1604
+ // src/cli/commands/mcp.ts
1605
+ import { defineCommand as defineCommand17 } from "citty";
1606
+ import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3 } from "fs";
1607
+ import path3 from "path";
1608
+ import os from "os";
1609
+ var mcp_default = defineCommand17({
1610
+ meta: {
1611
+ name: "mcp",
1612
+ description: "MCP server operations (serve | install)"
1613
+ },
1614
+ args: {
1615
+ http: { type: "string", description: "HTTP port for standalone mode" },
1616
+ bind: { type: "string", description: "Bind address for HTTP mode" },
1617
+ "read-only": { type: "boolean", description: "Only register read toolset" },
1618
+ "no-destructive": { type: "boolean", description: "Filter out destructive tools" },
1619
+ 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" }
1626
+ },
1627
+ async run({ args }) {
1628
+ const installFlags = ["claude", "cursor", "vscode", "windsurf", "zed"];
1629
+ const requestedClients = installFlags.filter((f) => Boolean(args[f]));
1630
+ if (requestedClients.length > 0) {
1631
+ for (const client of requestedClients) {
1632
+ installMcp(client);
1633
+ process.stderr.write(`Installed MCP config for: ${client}
1634
+ `);
1635
+ }
1636
+ return;
1637
+ }
1638
+ const { serveMcp } = await import("./server-TKDURITQ.js");
1639
+ await serveMcp({
1640
+ port: args.http ? Number(args.http) : void 0,
1641
+ bind: args.bind,
1642
+ readOnly: Boolean(args["read-only"]),
1643
+ noDestructive: Boolean(args["no-destructive"]),
1644
+ toolsets: args.toolsets ? args.toolsets.split(",").map((s) => s.trim()) : void 0,
1645
+ inspector: Boolean(args.inspector)
1646
+ });
1647
+ }
1648
+ });
1649
+ var CLAWOPS_ENTRY = {
1650
+ command: "clawops",
1651
+ args: ["mcp", "serve"],
1652
+ type: "stdio"
1653
+ };
1654
+ function getConfigPath(client) {
1655
+ const home = os.homedir();
1656
+ switch (client) {
1657
+ case "claude":
1658
+ return path3.join(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
1659
+ case "cursor":
1660
+ return path3.join(home, ".cursor", "mcp.json");
1661
+ case "vscode":
1662
+ return path3.join(home, "Library", "Application Support", "Code", "User", "mcp.json");
1663
+ case "windsurf":
1664
+ return path3.join(home, ".codeium", "windsurf", "mcp_config.json");
1665
+ case "zed":
1666
+ return path3.join(home, ".config", "zed", "settings.json");
1667
+ }
1668
+ }
1669
+ function installMcp(client) {
1670
+ const configPath = getConfigPath(client);
1671
+ const dir = path3.dirname(configPath);
1672
+ if (!existsSync2(dir)) {
1673
+ mkdirSync3(dir, { recursive: true });
1674
+ }
1675
+ let config = {};
1676
+ if (existsSync2(configPath)) {
1677
+ try {
1678
+ config = JSON.parse(readFileSync2(configPath, "utf-8"));
1679
+ } catch {
1680
+ }
1681
+ }
1682
+ if (client === "zed") {
1683
+ const contextServers = config["context_servers"] ?? {};
1684
+ contextServers["clawops"] = CLAWOPS_ENTRY;
1685
+ config["context_servers"] = contextServers;
1686
+ } else {
1687
+ const mcpServers = config["mcpServers"] ?? {};
1688
+ mcpServers["clawops"] = CLAWOPS_ENTRY;
1689
+ config["mcpServers"] = mcpServers;
1690
+ }
1691
+ writeFileSync3(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
1692
+ }
1693
+
1694
+ // src/cli/error-handler.ts
1695
+ import process19 from "process";
1696
+ function handleError(err) {
1697
+ if (err instanceof ClawopsError) {
1698
+ failure(err.message);
1699
+ process19.exit(err.exitCode);
1700
+ }
1701
+ if (err instanceof Error) {
1702
+ failure(`Unexpected error: ${err.message}`);
1703
+ if (process19.env["DEBUG"]) {
1704
+ process19.stderr.write(err.stack ?? "\n");
1705
+ }
1706
+ process19.exit(1);
1707
+ }
1708
+ failure(`Unknown error: ${String(err)}`);
1709
+ process19.exit(1);
1710
+ }
1711
+
1712
+ // src/cli/index.ts
1713
+ var main = defineCommand18({
1714
+ meta: {
1715
+ name: "clawops",
1716
+ description: "Deploy and manage self-hosted OpenClaw instances across clouds",
1717
+ version: "0.2.0"
1718
+ },
1719
+ args: {
1720
+ stack: { type: "string", description: "Target named stack (default from config)" },
1721
+ provider: { type: "string", description: "Override provider (aws|gcp|azure|local)" },
1722
+ json: { type: "boolean", description: "Emit JSON to stdout" },
1723
+ quiet: { type: "boolean", description: "Suppress non-error output" },
1724
+ profile: { type: "string", description: "Auth profile from ~/.clawops/config.json" },
1725
+ "dry-run": { type: "boolean", description: "Preview without applying (mutating commands)" },
1726
+ yes: { type: "boolean", description: "Skip interactive confirmations (CI mode)" }
1727
+ },
1728
+ subCommands: {
1729
+ init: init_default,
1730
+ up: up_default,
1731
+ down: down_default,
1732
+ status: status_default,
1733
+ plan: plan_default,
1734
+ apply: apply_default,
1735
+ destroy: destroy_default,
1736
+ ssh: ssh_default,
1737
+ tunnel: tunnel_default,
1738
+ logs: logs_default,
1739
+ config: config_default,
1740
+ agents: agents_default,
1741
+ gateway: gateway_default,
1742
+ backup: backup_default,
1743
+ stacks: stacks_default,
1744
+ doctor: doctor_default,
1745
+ mcp: mcp_default
1746
+ }
1747
+ });
1748
+ try {
1749
+ await runMain(main);
1750
+ } catch (err) {
1751
+ handleError(err);
1752
+ }