@clawops/cli 2.1.2 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/README.md +122 -2
  2. package/dist/agent-activity-25FAWLAI.js +16 -0
  3. package/dist/{apply-HJ5GAKHW.js → apply-F5SQ2ZOS.js} +12 -12
  4. package/dist/{automation-MKCJ46I6.js → automation-NBYQ6MFZ.js} +1 -1
  5. package/dist/backup-flows-NDP5JTR3.js +29 -0
  6. package/dist/{bootstrap-NN647RCY.js → bootstrap-32TZXVDF.js} +5 -5
  7. package/dist/bootstrap.sh.tmpl +4 -0
  8. package/dist/chunk-27335SIH.js +51 -0
  9. package/dist/{chunk-X56CQ6GB.js → chunk-4MHDW2NB.js} +1 -1
  10. package/dist/{chunk-E4NJSJ54.js → chunk-4PY6PN4G.js} +51 -7
  11. package/dist/{chunk-3Z5TDXCJ.js → chunk-55UMO37O.js} +1 -1
  12. package/dist/{chunk-IQQTB7ER.js → chunk-66R52R7K.js} +10 -14
  13. package/dist/chunk-755X5OGS.js +213 -0
  14. package/dist/{chunk-QDNFADJI.js → chunk-AVERNLUY.js} +6 -6
  15. package/dist/{chunk-R3DTA35B.js → chunk-AX65JMDZ.js} +20 -20
  16. package/dist/{chunk-T5XCMYAV.js → chunk-CQOYUWR7.js} +766 -210
  17. package/dist/chunk-E6BRQK67.js +289 -0
  18. package/dist/{chunk-64PONSA5.js → chunk-GFIR2IIJ.js} +3 -1
  19. package/dist/chunk-LV7UO5CG.js +82 -0
  20. package/dist/{chunk-GIEVAGEK.js → chunk-N5ZYUTZS.js} +16 -1
  21. package/dist/{chunk-GFUK6KDR.js → chunk-OUQVKIIA.js} +372 -34
  22. package/dist/{chunk-XFN3YDZE.js → chunk-PASAVVNS.js} +1 -1
  23. package/dist/chunk-PCSX6Y3U.js +297 -0
  24. package/dist/chunk-POYS3EVQ.js +14 -0
  25. package/dist/{chunk-ELU3XQEA.js → chunk-RDUKRIRC.js} +1 -1
  26. package/dist/chunk-SX75L2T3.js +46 -0
  27. package/dist/{chunk-2Z7J3ERQ.js → chunk-TYNMOYVM.js} +1 -1
  28. package/dist/{chunk-2I7KCMXZ.js → chunk-UAJJEDCJ.js} +2 -2
  29. package/dist/{chunk-NUDIHZEH.js → chunk-WPZIJONN.js} +14 -12
  30. package/dist/{chunk-BELTQGHT.js → chunk-YPVWF7IA.js} +4 -4
  31. package/dist/{chunk-3OLUJPCB.js → chunk-Z3XLWZWF.js} +1 -1
  32. package/dist/cli.js +383 -925
  33. package/dist/conn-M2V5HBPJ.js +9 -0
  34. package/dist/{context-7VI3R6EK.js → context-EHSLWKSH.js} +6 -6
  35. package/dist/{docker-3PDO4TS7.js → docker-IKM5GRMC.js} +2 -2
  36. package/dist/{flows-RI7FGHAM.js → flows-6B2I6TRW.js} +7 -7
  37. package/dist/{generate-Q2BGVMH7.js → generate-4YYNW3IU.js} +9 -8
  38. package/dist/{harden-3KKCR7BL.js → harden-UHAOVQMZ.js} +5 -5
  39. package/dist/init-HOPXDNWY.js +13 -0
  40. package/dist/{mcp-wire-FQOMM7MK.js → mcp-wire-BXF4DJFT.js} +26 -19
  41. package/dist/migrate-flow-24XA4GZL.js +25 -0
  42. package/dist/{package-JFVGCNKU.js → package-HYGHRX35.js} +3 -1
  43. package/dist/{pool-PIIKJ4L2.js → pool-NT4XBSUT.js} +2 -2
  44. package/dist/{privileged-JFA6LLTV.js → privileged-5Y6O4H7Z.js} +1 -1
  45. package/dist/{ready-KDNRBNSU.js → ready-IJEUBFVX.js} +2 -2
  46. package/dist/{remote-config-3HZLUWEA.js → remote-config-OZYBMBSK.js} +3 -3
  47. package/dist/{runtime-X3KW5TCP.js → runtime-VW53TU2W.js} +5 -1
  48. package/dist/server-E3YA32ND.js +53 -0
  49. package/dist/ssh-HOM7GF3M.js +13 -0
  50. package/dist/{stack-config-LUTS2XAD.js → stack-config-ICQ5YHUL.js} +2 -2
  51. package/dist/stack-delete-NTIGKWUF.js +13 -0
  52. package/dist/{state-AOAYLYY5.js → state-AKZP2S4T.js} +2 -2
  53. package/dist/{store-OVB3DAJR.js → store-YOLQYXMB.js} +1 -1
  54. package/dist/{tailscale-cutover-6YP2QKFM.js → tailscale-cutover-V42HO5UM.js} +3 -3
  55. package/dist/{validate-JXKN5WD6.js → validate-DZE6LFEB.js} +1 -1
  56. package/dist/{wait-WO72TRRQ.js → wait-DOUNNJNN.js} +2 -2
  57. package/package.json +3 -1
  58. package/spec/mcp-tools.yaml +282 -35
  59. package/dist/logs-5A7YYA53.js +0 -15
  60. package/dist/server-B7I6HBQH.js +0 -46
  61. package/dist/ssh-VAI5DU5M.js +0 -11
  62. package/dist/upgrade-ULD4KFYC.js +0 -88
  63. /package/dist/{chunk-NUL5F6YV.js → chunk-SOZ62ZXW.js} +0 -0
package/dist/cli.js CHANGED
@@ -5,38 +5,51 @@ import {
5
5
  writeAppConfigs
6
6
  } from "./chunk-OIGTOLB3.js";
7
7
  import {
8
+ AUDIT_CLEAN_MESSAGE,
9
+ AUDIT_FIX_HINT,
10
+ NO_SECRETS_MESSAGE,
11
+ SET_SECRET_HINT,
12
+ auditIssuesHeadline,
13
+ auditSecrets,
14
+ deleteConfirmQuestion,
15
+ deletePreparedSecret,
16
+ dockerRunCmd,
17
+ formatAuditIssue,
18
+ gatewayStatus,
19
+ listSecrets,
8
20
  monitor_default,
9
- runDiagnostics
10
- } from "./chunk-GFUK6KDR.js";
21
+ prepareSecretDelete,
22
+ resolveSecretPath,
23
+ resolveUpdateVersion,
24
+ runDiagnostics,
25
+ secretsDir,
26
+ updateGateway
27
+ } from "./chunk-OUQVKIIA.js";
11
28
  import {
12
29
  GATEWAY_LOGS_PROBE,
13
30
  chooseLogSource,
14
31
  containerLogsCommand,
15
32
  gatewayLogsCommand
16
- } from "./chunk-NUL5F6YV.js";
17
- import "./chunk-3OLUJPCB.js";
33
+ } from "./chunk-SOZ62ZXW.js";
34
+ import "./chunk-6ZFIFDBJ.js";
35
+ import "./chunk-Z3XLWZWF.js";
18
36
  import {
19
37
  execPrivileged,
20
- execPrivilegedWithInput,
21
38
  streamPrivileged
22
- } from "./chunk-NUDIHZEH.js";
39
+ } from "./chunk-WPZIJONN.js";
40
+ import "./chunk-YPVWF7IA.js";
23
41
  import {
24
42
  resolveSpecDir
25
43
  } from "./chunk-JW5A7E3X.js";
44
+ import "./chunk-LT4XMO5D.js";
45
+ import "./chunk-VUFMZEIM.js";
26
46
  import {
27
- interpretProbe,
28
- probeCommand
29
- } from "./chunk-LT4XMO5D.js";
30
- import "./chunk-BELTQGHT.js";
31
- import {
32
- CONFIG_FILENAME,
33
- CONTAINER_UID,
34
47
  PUBLISH_INSPECT_CMD,
35
48
  STATE_DIR_HOST_LINUX,
36
49
  configPathForOS,
37
50
  gatewayRunCommand,
38
51
  publishForRestart
39
- } from "./chunk-GIEVAGEK.js";
52
+ } from "./chunk-N5ZYUTZS.js";
40
53
  import {
41
54
  GATEWAY_PORT,
42
55
  IMAGE_INSPECT_CMD,
@@ -58,110 +71,28 @@ import {
58
71
  } from "./chunk-Q7NQY5HV.js";
59
72
  import "./chunk-N4WZLWJ4.js";
60
73
  import {
61
- getConfig,
74
+ SUPPORTED_PROVIDERS,
75
+ deriveStateBucket,
76
+ initStack,
77
+ resolveScopeAccount,
78
+ validateStateBucket
79
+ } from "./chunk-755X5OGS.js";
80
+ import {
62
81
  getConfigDir,
63
- requireConfig,
64
- setConfig
65
- } from "./chunk-64PONSA5.js";
82
+ requireConfig
83
+ } from "./chunk-GFIR2IIJ.js";
66
84
  import {
67
85
  ClawopsError,
68
86
  UsageError
69
87
  } from "./chunk-KGXPLI7W.js";
70
- import "./chunk-VUFMZEIM.js";
71
88
 
72
89
  // src/cli/index.ts
73
90
  import { defineCommand as defineCommand24, runMain } from "citty";
74
91
 
75
92
  // src/cli/commands/init.ts
76
93
  import { defineCommand } from "citty";
77
- import { spawnSync } from "child_process";
78
- import { writeFileSync, mkdirSync, existsSync } from "fs";
79
94
  import path from "path";
80
95
  import process2 from "process";
81
-
82
- // src/providers/state-bucket.ts
83
- function deriveStateBucket(provider, scope) {
84
- if (provider === "azure") {
85
- return { ok: true, name: "clawops-state" };
86
- }
87
- if (!scope.account) {
88
- return {
89
- ok: false,
90
- needs: provider === "aws" ? "an AWS account id \u2014 set AWS_PROFILE, or run `aws sso login --profile <name>`" : "a GCP project \u2014 run `gcloud config set project <id>`, or set GOOGLE_CLOUD_PROJECT"
91
- };
92
- }
93
- if (provider === "gcp") return { ok: true, name: `clawops-state-${scope.account}` };
94
- if (!scope.region) return { ok: false, needs: "a region" };
95
- return { ok: true, name: `clawops-state-${scope.account}-${scope.region}` };
96
- }
97
- function stateUrlFor(provider, bucket) {
98
- const verdict = validateStateBucket(provider, bucket);
99
- if (verdict !== true) throw new Error(`Not a usable ${provider} state backend name: ${verdict}`);
100
- const scheme = provider === "aws" ? "s3://" : provider === "gcp" ? "gs://" : "azblob://";
101
- return provider === "azure" ? `${scheme}${bucket}` : `${scheme}${bucket}/clawops`;
102
- }
103
- var IPV4 = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/;
104
- function validateStateBucket(provider, name) {
105
- const n = name.trim();
106
- if (n === "") return "Required";
107
- if (n !== name) return "No leading or trailing spaces";
108
- if (n.length < 3) return `Too short \u2014 ${labelFor(provider)} names are at least 3 characters`;
109
- const dotted = provider === "gcp" && n.includes(".");
110
- const max = dotted ? 222 : 63;
111
- if (n.length > max) return `Too long \u2014 ${labelFor(provider)} names are at most ${max} characters`;
112
- if (dotted && n.split(".").some((part) => part.length > 63)) {
113
- return "Each dot-separated part of a Cloud Storage name is at most 63 characters";
114
- }
115
- if (n !== n.toLowerCase()) return "Lowercase only";
116
- if (provider === "azure") {
117
- if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(n)) {
118
- return "Letters, digits and single hyphens only, starting and ending with a letter or digit";
119
- }
120
- return true;
121
- }
122
- if (!/^[a-z0-9][a-z0-9._-]*[a-z0-9]$/.test(n)) {
123
- return "Letters, digits, dots, hyphens and underscores only, starting and ending with a letter or digit";
124
- }
125
- if (n.includes("..")) return "No consecutive dots";
126
- if (IPV4.test(n)) return "Cannot be formatted as an IP address";
127
- if (provider === "aws") {
128
- if (n.startsWith("xn--")) return 'Cannot start with "xn--"';
129
- if (n.startsWith("sthree-")) return 'Cannot start with "sthree-"';
130
- if (n.endsWith("-s3alias")) return 'Cannot end with "-s3alias"';
131
- if (n.endsWith("--ol-s3")) return 'Cannot end with "--ol-s3"';
132
- if (n.includes("_")) return "Underscores are not allowed in S3 bucket names";
133
- return true;
134
- }
135
- if (n.startsWith("goog")) return 'Cannot start with "goog"';
136
- if (n.includes("google")) return 'Cannot contain "google"';
137
- return true;
138
- }
139
- function labelFor(provider) {
140
- if (provider === "aws") return "S3 bucket";
141
- if (provider === "gcp") return "Cloud Storage bucket";
142
- return "blob container";
143
- }
144
- async function resolveScopeAccount(provider, signal) {
145
- if (provider === "azure") return void 0;
146
- try {
147
- if (provider === "aws") {
148
- const { callerAccount } = await import("./preflight-TFD4N5ZU.js");
149
- return await callerAccount(signal);
150
- }
151
- const { resolveProjectId } = await import("./preflight-IE2YWBAC.js");
152
- return resolveProjectId();
153
- } catch {
154
- return void 0;
155
- }
156
- }
157
-
158
- // src/cli/commands/init.ts
159
- var SUPPORTED_PROVIDERS = ["gcp", "aws", "azure", "local"];
160
- var PROVIDER_DEFAULTS = {
161
- gcp: { region: "us-central1", credEnv: "GOOGLE_APPLICATION_CREDENTIALS", stateScheme: "gs://" },
162
- aws: { region: "us-east-1", credEnv: "AWS_PROFILE", stateScheme: "s3://" },
163
- azure: { region: "eastus", credEnv: "AZURE_CLIENT_ID", stateScheme: "azblob://" }
164
- };
165
96
  var init_default = defineCommand({
166
97
  meta: {
167
98
  name: "init",
@@ -183,8 +114,6 @@ var init_default = defineCommand({
183
114
  async run({ args }) {
184
115
  const nonInteractive = Boolean(args["non-interactive"]);
185
116
  const providerArg = typeof args.provider === "string" ? args.provider : null;
186
- const stackName = typeof args.stack === "string" ? args.stack : "default";
187
- const forceOverwrite = Boolean(args.force);
188
117
  if (nonInteractive && !providerArg) {
189
118
  throw new UsageError(
190
119
  "--non-interactive requires --provider. Example: clawops init --provider gcp --non-interactive"
@@ -196,107 +125,42 @@ var init_default = defineCommand({
196
125
  `Unsupported provider: ${provider}. Supported: ${SUPPORTED_PROVIDERS.join(", ")}`
197
126
  );
198
127
  }
199
- const existing = getConfig();
200
- if (existing?.stacks[stackName] && !forceOverwrite) {
201
- failure(
202
- `Stack "${stackName}" already exists in ${path.join(getConfigDir(), "config.json")} (${existing.stacks[stackName].stateUrl}). Use --force to overwrite its settings, or pass a different --stack name to add another one.`
203
- );
204
- process2.exit(1);
205
- }
206
- const configDir = getConfigDir();
207
- mkdirSync(configDir, { recursive: true });
208
- const keyPath = typeof args["key-path"] === "string" ? args["key-path"] : path.join(configDir, "id_ed25519");
209
- const knownHostsPath = path.join(configDir, "known_hosts");
210
- if (!existsSync(keyPath)) {
211
- if (typeof args["key-path"] === "string") {
212
- throw new UsageError(`SSH key not found at ${keyPath}`);
213
- }
214
- info("Generating SSH key pair...");
215
- const gen = spawnSync(
216
- "ssh-keygen",
217
- ["-t", "ed25519", "-f", keyPath, "-N", "", "-C", "clawops", "-q"],
218
- { stdio: ["ignore", "pipe", "pipe"] }
219
- );
220
- if (gen.error || gen.status !== 0) {
221
- const reason = gen.error?.message ?? gen.stderr?.toString().trim() ?? `exit ${gen.status}`;
222
- throw new UsageError(
223
- `Could not generate an SSH key: ${reason}
224
- Generate one yourself and point clawops at it:
225
- ssh-keygen -t ed25519 -f ${keyPath} -N '' -C clawops
226
- clawops init --key-path ${keyPath}`
227
- );
128
+ const result = await initStack({
129
+ provider,
130
+ stackName: typeof args.stack === "string" ? args.stack : void 0,
131
+ stateUrl: typeof args.state === "string" ? args.state : void 0,
132
+ region: typeof args.region === "string" ? args.region : void 0,
133
+ force: Boolean(args.force),
134
+ keyPath: typeof args["key-path"] === "string" ? args["key-path"] : void 0,
135
+ host: typeof args.host === "string" ? args.host : void 0,
136
+ sshUser: typeof args["ssh-user"] === "string" ? args["ssh-user"] : void 0,
137
+ sshPort: typeof args["ssh-port"] === "string" ? parseInt(args["ssh-port"], 10) : void 0
138
+ });
139
+ if (!result.ok) {
140
+ if (result.reason.includes("already exists")) {
141
+ failure(result.reason.replace("Pass force", "Use --force"));
142
+ process2.exit(1);
228
143
  }
229
- success(`SSH key pair written to ${keyPath} (and ${keyPath}.pub)`);
230
- } else {
231
- info(`Using existing SSH key at ${keyPath}`);
232
- }
233
- if (!existsSync(knownHostsPath)) {
234
- writeFileSync(knownHostsPath, "", "utf-8");
144
+ throw new UsageError(result.reason);
235
145
  }
236
- let config;
237
- if (provider === "local") {
238
- const host = typeof args.host === "string" ? args.host : "";
239
- if (!host) {
240
- throw new UsageError("--host is required for the local provider");
241
- }
242
- const sshUser = typeof args["ssh-user"] === "string" ? args["ssh-user"] : "root";
243
- const sshPort = typeof args["ssh-port"] === "string" ? parseInt(args["ssh-port"], 10) : 22;
244
- config = merge(existing, stackName, provider, keyPath, knownHostsPath, {
245
- provider,
246
- stateUrl: "file://~/.clawops/state",
247
- credentialsRef: { source: "file", envVars: [] },
248
- localOpts: { host, sshUser, sshPort, sshKeyPath: keyPath }
249
- });
250
- } else {
251
- const defaults = PROVIDER_DEFAULTS[provider];
252
- const region = typeof args.region === "string" ? args.region : defaults.region;
253
- let stateUrl;
254
- if (typeof args.state === "string") {
255
- stateUrl = args.state;
256
- } else {
257
- const account = await resolveScopeAccount(provider);
258
- const derived = deriveStateBucket(provider, { account, region });
259
- if (!derived.ok) {
260
- throw new UsageError(
261
- `Could not name a state backend for ${provider}: clawops needs ${derived.needs}.
262
- Authenticate and run this again, or name the backend yourself:
263
- clawops init --provider ${provider} --state ${defaults.stateScheme}your-bucket/clawops`
264
- );
265
- }
266
- stateUrl = stateUrlFor(provider, derived.name);
267
- }
268
- config = merge(existing, stackName, provider, keyPath, knownHostsPath, {
269
- provider,
270
- stateUrl,
271
- region,
272
- credentialsRef: { source: "env", envVars: [defaults.credEnv] }
273
- });
146
+ if (result.keyGenerated) success(`SSH key pair written to ${result.keyPath} (and ${result.keyPath}.pub)`);
147
+ else info(`Using existing SSH key at ${result.keyPath}`);
148
+ if (result.provider !== "local") {
274
149
  process2.stdout.write("\n");
275
150
  info(
276
- `State backend: ${stateUrl}
277
- clawops doctor --provider ${provider} checks it exists, and clawops setup offers to create it.`
151
+ `State backend: ${result.stateUrl}
152
+ clawops doctor --provider ${result.provider} checks it exists, and clawops setup offers to create it.`
278
153
  );
279
154
  process2.stdout.write("\n");
280
- success(`Provider: ${provider} Region: ${region} Stack: ${stackName}`);
281
- setConfig(config);
282
- success(`Config written to ${path.join(configDir, "config.json")}`);
155
+ success(`Provider: ${result.provider} Region: ${result.region} Stack: ${result.stackName}`);
156
+ success(`Config written to ${path.join(getConfigDir(), "config.json")}`);
283
157
  return;
284
158
  }
285
- setConfig(config);
286
- success(`Config written to ${path.join(configDir, "config.json")}`);
159
+ success(`Config written to ${result.configPath}`);
287
160
  process2.stdout.write("\n");
288
- success(`Provider: ${provider} Stack: ${stackName}`);
161
+ success(`Provider: ${result.provider} Stack: ${result.stackName}`);
289
162
  }
290
163
  });
291
- function merge(existing, stackName, provider, keyPath, knownHostsPath, stack) {
292
- return {
293
- ...existing ?? {},
294
- version: 1,
295
- defaults: { stack: stackName, provider },
296
- stacks: { ...existing?.stacks ?? {}, [stackName]: stack },
297
- ssh: { keyPath, knownHostsPath }
298
- };
299
- }
300
164
 
301
165
  // src/cli/commands/up.ts
302
166
  import { defineCommand as defineCommand2 } from "citty";
@@ -339,7 +203,7 @@ var up_default = defineCommand2({
339
203
  "publish-gateway": { type: "string", description: 'loopback (default) or all. "all" serves plaintext HTTP \u2014 put TLS in front of it' }
340
204
  },
341
205
  async run({ args }) {
342
- const { buildContext } = await import("./context-7VI3R6EK.js");
206
+ const { buildContext } = await import("./context-EHSLWKSH.js");
343
207
  const { guardOpenclawVersion, defaultOpenclawVersion } = await import("./version-guard-XVYW5WFJ.js");
344
208
  const ctx = buildContext(args);
345
209
  const requestedVersion = typeof args["openclaw-version"] === "string" ? args["openclaw-version"] : await defaultOpenclawVersion();
@@ -352,7 +216,7 @@ var up_default = defineCommand2({
352
216
  );
353
217
  }
354
218
  const { localOpts } = stackConfig;
355
- const { localBootstrap } = await import("./bootstrap-NN647RCY.js");
219
+ const { localBootstrap } = await import("./bootstrap-32TZXVDF.js");
356
220
  const gatewayPort = parseGatewayPort(args["gateway-port"]);
357
221
  const abortController2 = new AbortController();
358
222
  process3.on("SIGINT", () => abortController2.abort());
@@ -392,8 +256,8 @@ var up_default = defineCommand2({
392
256
  }
393
257
  return;
394
258
  }
395
- const { generatePlan } = await import("./generate-Q2BGVMH7.js");
396
- const { applyPlan } = await import("./apply-HJ5GAKHW.js");
259
+ const { generatePlan } = await import("./generate-4YYNW3IU.js");
260
+ const { applyPlan } = await import("./apply-F5SQ2ZOS.js");
397
261
  const { resolveNetworkFlags } = await import("./network-args-PYJRJBYO.js");
398
262
  const { detectEgressIp } = await import("./firewall-5VKZ4WBH.js");
399
263
  const isDryRun = Boolean(args["dry-run"]);
@@ -473,9 +337,9 @@ function strArg(value) {
473
337
  return typeof value === "string" ? value : void 0;
474
338
  }
475
339
  async function applyLocalConfigOverlay(opts) {
476
- const { connect } = await import("./ssh-VAI5DU5M.js");
340
+ const { connect } = await import("./ssh-HOM7GF3M.js");
477
341
  const { resolveSecrets } = await import("./secrets-SVZWNAPK.js");
478
- const { readRemoteConfig, atomicWriteConfig, restartGateway, deepMerge } = await import("./remote-config-3HZLUWEA.js");
342
+ const { readRemoteConfig, atomicWriteConfig, restartGateway, deepMerge } = await import("./remote-config-OZYBMBSK.js");
479
343
  let overlay;
480
344
  try {
481
345
  overlay = JSON.parse(readFileSync(opts.configPath, "utf-8"));
@@ -523,7 +387,7 @@ var down_default = defineCommand3({
523
387
  "dry-run": { type: "boolean", description: "Show what would be destroyed without destroying" }
524
388
  },
525
389
  async run({ args }) {
526
- const { buildContext } = await import("./context-7VI3R6EK.js");
390
+ const { buildContext } = await import("./context-EHSLWKSH.js");
527
391
  const ctx = buildContext(args);
528
392
  if (args["dry-run"]) {
529
393
  info(`Dry run \u2014 would destroy stack "${ctx.stackName}"`);
@@ -587,7 +451,7 @@ var status_default = defineCommand4({
587
451
  json: { type: "boolean", description: "Emit JSON" }
588
452
  },
589
453
  async run({ args }) {
590
- const { buildContext } = await import("./context-7VI3R6EK.js");
454
+ const { buildContext } = await import("./context-EHSLWKSH.js");
591
455
  const ctx = buildContext(args);
592
456
  if (ctx.adapter.name === "local") {
593
457
  const state = ctx.localState;
@@ -643,7 +507,7 @@ var status_default = defineCommand4({
643
507
  // src/cli/commands/plan.ts
644
508
  import { defineCommand as defineCommand5 } from "citty";
645
509
  import process7 from "process";
646
- import { writeFileSync as writeFileSync2 } from "fs";
510
+ import { writeFileSync } from "fs";
647
511
  import { isAbsolute } from "path";
648
512
  function disruptionWarnings(diff) {
649
513
  const out = [];
@@ -687,8 +551,8 @@ var plan_default = defineCommand5({
687
551
  "private-only": { type: "boolean", description: "Close public SSH and gateway access; reach the stack over its tailnet. Needs `clawops harden --tailscale` first, and checks the tailnet answers before planning" }
688
552
  },
689
553
  async run({ args }) {
690
- const { buildContext } = await import("./context-7VI3R6EK.js");
691
- const { generatePlan } = await import("./generate-Q2BGVMH7.js");
554
+ const { buildContext } = await import("./context-EHSLWKSH.js");
555
+ const { generatePlan } = await import("./generate-4YYNW3IU.js");
692
556
  const { resolveNetworkFlags } = await import("./network-args-PYJRJBYO.js");
693
557
  const { detectEgressIp } = await import("./firewall-5VKZ4WBH.js");
694
558
  const ctx = buildContext(args);
@@ -713,7 +577,7 @@ var plan_default = defineCommand5({
713
577
  let network = resolved;
714
578
  if (args["private-only"]) {
715
579
  const { privateOnlyNetwork, assertTailnetReachable } = await import("./private-only-DIA26I2E.js");
716
- const { probeSsh } = await import("./tailscale-cutover-6YP2QKFM.js");
580
+ const { probeSsh } = await import("./tailscale-cutover-V42HO5UM.js");
717
581
  network = privateOnlyNetwork(flags, resolved, ctx.config.stacks[ctx.stackName]?.tailscale, ctx.stackName);
718
582
  await assertTailnetReachable(ctx, (conn) => probeSsh(conn));
719
583
  }
@@ -741,7 +605,7 @@ var plan_default = defineCommand5({
741
605
  }
742
606
  const planJson = JSON.stringify(plan, null, 2);
743
607
  if (outPath) {
744
- writeFileSync2(outPath, planJson + "\n", "utf-8");
608
+ writeFileSync(outPath, planJson + "\n", "utf-8");
745
609
  success(`Plan written to ${outPath}`);
746
610
  } else {
747
611
  process7.stdout.write(planJson + "\n");
@@ -803,8 +667,8 @@ var apply_default = defineCommand6({
803
667
  "dry-run": { type: "boolean", description: "Validate plan and show diff without applying" }
804
668
  },
805
669
  async run({ args }) {
806
- const { validatePlan } = await import("./validate-JXKN5WD6.js");
807
- const { applyPlan } = await import("./apply-HJ5GAKHW.js");
670
+ const { validatePlan } = await import("./validate-DZE6LFEB.js");
671
+ const { applyPlan } = await import("./apply-F5SQ2ZOS.js");
808
672
  const planPath = args._?.[0];
809
673
  if (!planPath) {
810
674
  throw new UsageError("Usage: clawops apply <plan.json>");
@@ -930,7 +794,7 @@ var destroy_default = defineCommand7({
930
794
  "dry-run": { type: "boolean", description: "Show what would be destroyed without destroying" }
931
795
  },
932
796
  async run({ args }) {
933
- const { buildContext } = await import("./context-7VI3R6EK.js");
797
+ const { buildContext } = await import("./context-EHSLWKSH.js");
934
798
  const ctx = buildContext(args);
935
799
  if (ctx.adapter.name === "local") {
936
800
  throw new UsageError(
@@ -1023,8 +887,8 @@ var ssh_default = defineCommand8({
1023
887
  command: { type: "string", description: "Remote command to run (instead of interactive shell)" }
1024
888
  },
1025
889
  async run({ args }) {
1026
- const { buildContext } = await import("./context-7VI3R6EK.js");
1027
- const { acquireSession, drainPool } = await import("./pool-PIIKJ4L2.js");
890
+ const { buildContext } = await import("./context-EHSLWKSH.js");
891
+ const { acquireSession, drainPool } = await import("./pool-NT4XBSUT.js");
1028
892
  const ctx = buildContext(args);
1029
893
  let conn;
1030
894
  if (ctx.adapter.name === "local") {
@@ -1110,9 +974,9 @@ var tunnel_default = defineCommand9({
1110
974
  "no-open": { type: "boolean", description: "Do not open browser after tunnel is ready" }
1111
975
  },
1112
976
  async run({ args }) {
1113
- const { buildContext } = await import("./context-7VI3R6EK.js");
977
+ const { buildContext } = await import("./context-EHSLWKSH.js");
1114
978
  const { extractBaseOutputs } = await import("./outputs-DJHBY7EE.js");
1115
- const { acquireSession } = await import("./pool-PIIKJ4L2.js");
979
+ const { acquireSession } = await import("./pool-NT4XBSUT.js");
1116
980
  const ctx = buildContext(args);
1117
981
  const stack = await ctx.getStack();
1118
982
  const outputMap = await stack.outputs();
@@ -1179,25 +1043,10 @@ var logs_default = defineCommand10({
1179
1043
  json: { type: "boolean", description: "Emit the gateway's structured JSON log lines" }
1180
1044
  },
1181
1045
  async run({ args }) {
1182
- const { buildContext } = await import("./context-7VI3R6EK.js");
1183
- const { extractBaseOutputs } = await import("./outputs-DJHBY7EE.js");
1184
- const { acquireSession, drainPool } = await import("./pool-PIIKJ4L2.js");
1046
+ const { buildContext } = await import("./context-EHSLWKSH.js");
1047
+ const { acquireSession, drainPool } = await import("./pool-NT4XBSUT.js");
1185
1048
  const ctx = buildContext(args);
1186
- const stack = await ctx.getStack();
1187
- const outputMap = await stack.outputs();
1188
- const outputs = Object.fromEntries(
1189
- Object.entries(outputMap).map(([k, v]) => [k, v.value])
1190
- );
1191
- if (!outputs["publicIp"]) {
1192
- failure("Stack has no outputs. Run `clawops up` first.");
1193
- process12.exit(4);
1194
- }
1195
- const base = extractBaseOutputs(outputs);
1196
- const conn = ctx.adapter.getConnectionInfo({
1197
- ...base,
1198
- privateKeyPath: ctx.config.ssh.keyPath,
1199
- knownHostsPath: ctx.config.ssh.knownHostsPath
1200
- });
1049
+ const conn = await connFor(ctx);
1201
1050
  const tailLines = typeof args.tail === "string" ? parseInt(args.tail, 10) : 100;
1202
1051
  const follow = Boolean(args.follow);
1203
1052
  const since = typeof args.since === "string" ? args.since : void 0;
@@ -1239,12 +1088,33 @@ var logs_default = defineCommand10({
1239
1088
  }
1240
1089
  }
1241
1090
  });
1091
+ async function connFor(ctx) {
1092
+ if (ctx.adapter.name === "local") {
1093
+ const { resolveConn } = await import("./conn-M2V5HBPJ.js");
1094
+ return resolveConn(ctx);
1095
+ }
1096
+ const { extractBaseOutputs } = await import("./outputs-DJHBY7EE.js");
1097
+ const stack = await ctx.getStack();
1098
+ const outputMap = await stack.outputs();
1099
+ const outputs = Object.fromEntries(
1100
+ Object.entries(outputMap).map(([k, v]) => [k, v.value])
1101
+ );
1102
+ if (!outputs["publicIp"]) {
1103
+ failure("Stack has no outputs. Run `clawops up` first.");
1104
+ process12.exit(4);
1105
+ }
1106
+ return ctx.adapter.getConnectionInfo({
1107
+ ...extractBaseOutputs(outputs),
1108
+ privateKeyPath: ctx.config.ssh.keyPath,
1109
+ knownHostsPath: ctx.config.ssh.knownHostsPath
1110
+ });
1111
+ }
1242
1112
 
1243
1113
  // src/cli/commands/config.ts
1244
1114
  import { defineCommand as defineCommand11 } from "citty";
1245
1115
  import process13 from "process";
1246
1116
  var OPENCLAW_CONFIG = configPathForOS("Linux");
1247
- function dockerRunCmd(image) {
1117
+ function dockerRunCmd2(image) {
1248
1118
  return gatewayRunCommand({ image, stateDir: STATE_DIR_HOST_LINUX });
1249
1119
  }
1250
1120
  function getPath(obj, dotKey) {
@@ -1285,9 +1155,8 @@ var config_default = defineCommand11({
1285
1155
  "dry-run": { type: "boolean", description: "Show what would change without writing" }
1286
1156
  },
1287
1157
  async run({ args }) {
1288
- const { buildContext } = await import("./context-7VI3R6EK.js");
1289
- const { extractBaseOutputs } = await import("./outputs-DJHBY7EE.js");
1290
- const { acquireSession, drainPool } = await import("./pool-PIIKJ4L2.js");
1158
+ const { buildContext } = await import("./context-EHSLWKSH.js");
1159
+ const { acquireSession, drainPool } = await import("./pool-NT4XBSUT.js");
1291
1160
  const [action, key, value] = args._ ?? [];
1292
1161
  if (!action || !["get", "set", "unset", "validate"].includes(action)) {
1293
1162
  failure("Usage: clawops config <get [key] | set key value | unset key | validate>");
@@ -1302,17 +1171,8 @@ var config_default = defineCommand11({
1302
1171
  process13.exit(2);
1303
1172
  }
1304
1173
  const ctx = buildContext(args);
1305
- const stack = await ctx.getStack();
1306
- const outputMap = await stack.outputs();
1307
- const outputs = Object.fromEntries(
1308
- Object.entries(outputMap).map(([k, v]) => [k, v.value])
1309
- );
1310
- const base = extractBaseOutputs(outputs);
1311
- const conn = ctx.adapter.getConnectionInfo({
1312
- ...base,
1313
- privateKeyPath: ctx.config.ssh.keyPath,
1314
- knownHostsPath: ctx.config.ssh.knownHostsPath
1315
- });
1174
+ const { resolveConn } = await import("./conn-M2V5HBPJ.js");
1175
+ const conn = await resolveConn(ctx);
1316
1176
  const abortController = new AbortController();
1317
1177
  process13.on("SIGINT", () => abortController.abort());
1318
1178
  process13.on("SIGTERM", () => abortController.abort());
@@ -1375,7 +1235,7 @@ var config_default = defineCommand11({
1375
1235
  process13.stdout.write(JSON.stringify(cfg, null, 2) + "\n");
1376
1236
  return;
1377
1237
  }
1378
- const { atomicWriteConfig } = await import("./remote-config-3HZLUWEA.js");
1238
+ const { atomicWriteConfig } = await import("./remote-config-OZYBMBSK.js");
1379
1239
  try {
1380
1240
  await atomicWriteConfig(session, cfg, abortController.signal);
1381
1241
  } catch (err) {
@@ -1391,7 +1251,7 @@ var config_default = defineCommand11({
1391
1251
  abortController.signal
1392
1252
  );
1393
1253
  const image = imgResult.stdout.trim();
1394
- const restartResult = await execPrivileged(session, dockerRunCmd(image), abortController.signal);
1254
+ const restartResult = await execPrivileged(session, dockerRunCmd2(image), abortController.signal);
1395
1255
  if (restartResult.code !== 0) {
1396
1256
  failure(`Restart failed: ${restartResult.stderr}`);
1397
1257
  process13.exit(1);
@@ -1420,9 +1280,8 @@ var agents_default = defineCommand12({
1420
1280
  cursor: { type: "string", description: "Continue from a previous `logs` result cursor" }
1421
1281
  },
1422
1282
  async run({ args }) {
1423
- const { buildContext } = await import("./context-7VI3R6EK.js");
1424
- const { extractBaseOutputs } = await import("./outputs-DJHBY7EE.js");
1425
- const { acquireSession, drainPool } = await import("./pool-PIIKJ4L2.js");
1283
+ const { buildContext } = await import("./context-EHSLWKSH.js");
1284
+ const { acquireSession, drainPool } = await import("./pool-NT4XBSUT.js");
1426
1285
  const [action, name] = args._ ?? [];
1427
1286
  if (action === "restart") {
1428
1287
  failure(
@@ -1439,17 +1298,8 @@ var agents_default = defineCommand12({
1439
1298
  process14.exit(2);
1440
1299
  }
1441
1300
  const ctx = buildContext(args);
1442
- const stack = await ctx.getStack();
1443
- const outputMap = await stack.outputs();
1444
- const outputs = Object.fromEntries(
1445
- Object.entries(outputMap).map(([k, v]) => [k, v.value])
1446
- );
1447
- const base = extractBaseOutputs(outputs);
1448
- const conn = ctx.adapter.getConnectionInfo({
1449
- ...base,
1450
- privateKeyPath: ctx.config.ssh.keyPath,
1451
- knownHostsPath: ctx.config.ssh.knownHostsPath
1452
- });
1301
+ const { resolveConn } = await import("./conn-M2V5HBPJ.js");
1302
+ const conn = await resolveConn(ctx);
1453
1303
  const abortController = new AbortController();
1454
1304
  process14.on("SIGINT", () => abortController.abort());
1455
1305
  process14.on("SIGTERM", () => abortController.abort());
@@ -1495,22 +1345,23 @@ var agents_default = defineCommand12({
1495
1345
  );
1496
1346
  }
1497
1347
  } else {
1498
- const { agentAuditCommand } = await import("./logs-5A7YYA53.js");
1499
- const limit = typeof args.limit === "string" ? parseInt(args.limit, 10) : 50;
1500
- const result = await execPrivileged(
1348
+ const { readAgentActivityRaw, DEFAULT_AGENT_LOG_LIMIT } = await import("./agent-activity-25FAWLAI.js");
1349
+ const limit = typeof args.limit === "string" ? parseInt(args.limit, 10) : DEFAULT_AGENT_LOG_LIMIT;
1350
+ const result = await readAgentActivityRaw(
1501
1351
  session,
1502
- agentAuditCommand({ agentId: name, limit, cursor: typeof args.cursor === "string" ? args.cursor : void 0 }),
1352
+ { agentId: name, limit, ...typeof args.cursor === "string" ? { cursor: args.cursor } : {} },
1503
1353
  abortController.signal
1504
1354
  );
1505
- if (result.code !== 0) {
1506
- failure(`Cannot read activity for "${name}": ${result.stderr.trim() || result.stdout.trim() || `exit ${result.code}`}`);
1355
+ if (!result.ok) {
1356
+ failure(result.error);
1507
1357
  process14.exitCode = 1;
1508
1358
  return;
1509
1359
  }
1510
1360
  if (args.json) {
1511
- process14.stdout.write(result.stdout);
1361
+ process14.stdout.write(result.value);
1512
1362
  } else {
1513
- renderAgentRuns(name, result.stdout);
1363
+ const { parseAgentActivity } = await import("./agent-activity-25FAWLAI.js");
1364
+ renderAgentRuns(name, parseAgentActivity(name, result.value));
1514
1365
  }
1515
1366
  }
1516
1367
  } finally {
@@ -1519,16 +1370,14 @@ var agents_default = defineCommand12({
1519
1370
  }
1520
1371
  }
1521
1372
  });
1522
- function renderAgentRuns(agentId, stdout) {
1523
- let page;
1524
- try {
1525
- page = JSON.parse(stdout.trim() || "{}");
1526
- } catch {
1527
- failure(`Cannot read activity for "${agentId}": unexpected output: ${stdout.trim().slice(0, 200)}`);
1373
+ function renderAgentRuns(agentId, parsed) {
1374
+ if (!parsed.ok) {
1375
+ failure(parsed.error);
1528
1376
  process14.exitCode = 1;
1529
1377
  return;
1530
1378
  }
1531
- const records = page.records ?? [];
1379
+ const page = parsed.value;
1380
+ const records = page.records;
1532
1381
  if (records.length === 0) {
1533
1382
  info(`No recorded activity for agent "${agentId}".`);
1534
1383
  return;
@@ -1548,99 +1397,6 @@ function renderAgentRuns(agentId, stdout) {
1548
1397
  // src/cli/commands/migrate.ts
1549
1398
  import { defineCommand as defineCommand13 } from "citty";
1550
1399
  import process15 from "process";
1551
-
1552
- // src/openclaw/migrate.ts
1553
- async function migrate(steps) {
1554
- const sourceImage = await steps.inspectSource();
1555
- if (!sourceImage) {
1556
- return {
1557
- kind: "nothing-to-migrate",
1558
- reason: "No running OpenClaw container was found, so there is no 1.x state to extract. If this deployment was restarted on a clawops release before 2.0, its state was already lost \u2014 clawops mounted none, so every container replacement discarded it. Deploy fresh with `clawops up`."
1559
- };
1560
- }
1561
- const identityBefore = await steps.deviceId();
1562
- const backup = await steps.backup();
1563
- if (!backup.ok) {
1564
- return {
1565
- kind: "refused",
1566
- reason: `Refusing to migrate without a verified backup: ${backup.detail}
1567
- This replaces the running container, and the state it extracts is the only copy.`
1568
- };
1569
- }
1570
- const extracted = await steps.extract();
1571
- if (!extracted.ok) {
1572
- return {
1573
- kind: "failed",
1574
- reason: "Could not extract state from the running container; it has not been touched.",
1575
- backupDetail: backup.detail
1576
- };
1577
- }
1578
- await steps.chown();
1579
- await steps.removeSource();
1580
- await steps.writeConfig();
1581
- await steps.start();
1582
- let restarts = 0;
1583
- let gate = await steps.gate();
1584
- if (!gate.ok) {
1585
- await steps.start();
1586
- restarts = 1;
1587
- gate = await steps.gate();
1588
- }
1589
- if (!gate.ok) {
1590
- return {
1591
- kind: "failed",
1592
- reason: gate.reason ?? "the gateway did not start after migration",
1593
- backupDetail: backup.detail
1594
- };
1595
- }
1596
- const identityAfter = await steps.deviceId();
1597
- const identity = identityBefore === void 0 || identityAfter === void 0 ? "unknown" : identityBefore === identityAfter ? "preserved" : "changed";
1598
- return { kind: "migrated", entries: extracted.entries, identity, restarts };
1599
- }
1600
- function describeMigration(o) {
1601
- switch (o.kind) {
1602
- case "migrated": {
1603
- const lines = [
1604
- `Migrated to the 2.0 runtime contract. Carried over: ${o.entries.join(", ") || "(nothing)"}.`
1605
- ];
1606
- if (o.restarts > 0) {
1607
- lines.push(
1608
- "The gateway needed a second start to finish its state-schema migration, which is expected for a 1.x database."
1609
- );
1610
- }
1611
- if (o.identity === "preserved") {
1612
- lines.push("Device identity preserved \u2014 paired devices do not need re-pairing.");
1613
- } else if (o.identity === "changed") {
1614
- lines.push(
1615
- "Device identity CHANGED. Paired devices and nodes must be re-paired \u2014 check `openclaw devices list` on the host for what was paired."
1616
- );
1617
- } else {
1618
- lines.push(
1619
- "Device identity could not be compared, so pairings may or may not have carried over. Check `openclaw devices list` before relying on them."
1620
- );
1621
- }
1622
- lines.push("Your 1.x config was NOT applied \u2014 it never took effect on 1.x either. Review it");
1623
- lines.push("and set what you need with `clawops config set`.");
1624
- return lines.join("\n");
1625
- }
1626
- case "nothing-to-migrate":
1627
- return o.reason;
1628
- case "refused":
1629
- return o.reason;
1630
- case "failed":
1631
- return `Migration failed: ${o.reason}
1632
- A verified backup was taken first: ${o.backupDetail}
1633
- Restore it with \`clawops backup restore --file <archive>\` before retrying.`;
1634
- }
1635
- }
1636
-
1637
- // src/cli/commands/migrate.ts
1638
- var SYNTHESISED_CONFIG = JSON.stringify({
1639
- meta: { lastTouchedVersion: "2026.9.2" },
1640
- gateway: { mode: "local", port: GATEWAY_PORT, auth: { mode: "token" } },
1641
- models: {},
1642
- channels: {}
1643
- });
1644
1400
  var migrate_default = defineCommand13({
1645
1401
  meta: {
1646
1402
  name: "migrate",
@@ -1652,120 +1408,41 @@ var migrate_default = defineCommand13({
1652
1408
  yes: { type: "boolean", description: "Skip the confirmation prompt" }
1653
1409
  },
1654
1410
  async run({ args }) {
1655
- const { buildContext } = await import("./context-7VI3R6EK.js");
1656
- const { extractBaseOutputs } = await import("./outputs-DJHBY7EE.js");
1657
- const { acquireSession, drainPool } = await import("./pool-PIIKJ4L2.js");
1658
- const { guardOpenclawVersion, defaultOpenclawVersion } = await import("./version-guard-XVYW5WFJ.js");
1659
- const requested = typeof args["openclaw-version"] === "string" ? args["openclaw-version"] : void 0;
1660
- const target = requested ? await guardOpenclawVersion(requested) : await defaultOpenclawVersion();
1661
- const targetImage = `ghcr.io/openclaw/openclaw:${target}`;
1662
- const ctx = buildContext(args);
1663
- const stack = await ctx.getStack();
1664
- const outputs = Object.fromEntries(
1665
- Object.entries(await stack.outputs()).map(([k, v]) => [k, v.value])
1666
- );
1667
- const conn = ctx.adapter.getConnectionInfo({
1668
- ...extractBaseOutputs(outputs),
1669
- privateKeyPath: ctx.config.ssh.keyPath,
1670
- knownHostsPath: ctx.config.ssh.knownHostsPath
1411
+ const { prepareMigration, runMigration, migrationQuestion } = await import("./migrate-flow-24XA4GZL.js");
1412
+ const target = await prepareMigration({
1413
+ stack: typeof args.stack === "string" ? args.stack : void 0,
1414
+ openclawVersion: typeof args["openclaw-version"] === "string" ? args["openclaw-version"] : void 0
1671
1415
  });
1416
+ if (!args.yes) {
1417
+ const inquirer = (await import("inquirer")).default;
1418
+ const { confirmed } = await inquirer.prompt([{
1419
+ type: "confirm",
1420
+ name: "confirmed",
1421
+ message: migrationQuestion(target),
1422
+ default: false
1423
+ }]);
1424
+ if (!confirmed) {
1425
+ info("Aborted. Nothing was changed.");
1426
+ return;
1427
+ }
1428
+ }
1672
1429
  const ac = new AbortController();
1673
1430
  process15.on("SIGINT", () => ac.abort());
1674
- const { session, release } = await acquireSession({
1675
- host: conn.host,
1676
- port: conn.port,
1677
- user: conn.user,
1678
- privateKeyPath: conn.privateKeyPath,
1679
- knownHostsPath: conn.knownHostsPath,
1680
- signal: ac.signal
1681
- });
1682
- const spin = spinner("Inspecting the deployment...");
1683
- const run = (cmd) => execPrivileged(session, cmd, ac.signal);
1684
- const archive = "/tmp/clawops-premigration.tar.gz";
1685
- const configPath = `${STATE_DIR_HOST_LINUX}/${CONFIG_FILENAME}`;
1686
- const steps = {
1687
- inspectSource: async () => {
1688
- const r = await run(`docker inspect openclaw --format '{{.Config.Image}}'`);
1689
- const image = r.stdout.trim();
1690
- return r.code === 0 && image.includes(":") ? image : void 0;
1691
- },
1692
- backup: async () => {
1693
- spin.text = "Taking a verified backup...";
1694
- const r = await run(
1695
- `docker exec openclaw openclaw backup create --output ${archive} --verify --json`
1696
- );
1697
- return r.code === 0 ? { ok: true, detail: `${archive} on the host (verified)` } : { ok: false, detail: (r.stderr || r.stdout).slice(0, 200) };
1698
- },
1699
- extract: async () => {
1700
- spin.text = "Extracting state from the running container...";
1701
- const mk = await run(`mkdir -p ${STATE_DIR_HOST_LINUX}`);
1702
- if (mk.code !== 0) return { ok: false, entries: [] };
1703
- const cp = await run(`docker cp openclaw:/home/node/.openclaw/. ${STATE_DIR_HOST_LINUX}/`);
1704
- if (cp.code !== 0) return { ok: false, entries: [] };
1705
- const ls = await run(`ls ${STATE_DIR_HOST_LINUX}`);
1706
- return { ok: true, entries: ls.stdout.trim().split(/\s+/).filter(Boolean) };
1707
- },
1708
- chown: async () => {
1709
- await run(`chown -R ${CONTAINER_UID}:${CONTAINER_UID} ${STATE_DIR_HOST_LINUX}`);
1710
- },
1711
- removeSource: async () => {
1712
- spin.text = "Stopping the 1.x container...";
1713
- await run("docker stop openclaw 2>/dev/null || true");
1714
- await run("docker rm openclaw 2>/dev/null || true");
1715
- },
1716
- writeConfig: async () => {
1717
- const b64 = Buffer.from(SYNTHESISED_CONFIG, "utf-8").toString("base64");
1718
- await run(
1719
- `echo '${b64}' | base64 -d > ${configPath} && chown ${CONTAINER_UID}:${CONTAINER_UID} ${configPath}`
1720
- );
1721
- },
1722
- start: async () => {
1723
- spin.text = "Starting the 2.0 gateway...";
1724
- await run(gatewayRunCommand({ image: targetImage, stateDir: STATE_DIR_HOST_LINUX }));
1725
- },
1726
- gate: async () => {
1727
- spin.text = "Waiting for the gateway to start...";
1728
- let last;
1729
- for (let i = 0; i < 15; i++) {
1730
- if (ac.signal.aborted) return { ok: false, reason: "aborted" };
1731
- const r = await run(probeCommand("started", GATEWAY_PORT));
1732
- const v = interpretProbe("started", r.stdout);
1733
- if (v.ok) return { ok: true };
1734
- last = v.reason;
1735
- await new Promise((res) => setTimeout(res, 2e3));
1736
- }
1737
- return { ok: false, reason: last ?? "no response" };
1738
- },
1739
- deviceId: async () => {
1740
- const file = await run(
1741
- `docker exec openclaw sh -c 'cat ~/.openclaw/identity/device.json 2>/dev/null' || cat ${STATE_DIR_HOST_LINUX}/identity/device.json 2>/dev/null || true`
1742
- );
1743
- try {
1744
- const parsed = JSON.parse(file.stdout.trim());
1745
- return parsed.deviceId;
1746
- } catch {
1747
- return void 0;
1748
- }
1749
- }
1750
- };
1431
+ let spin;
1751
1432
  try {
1752
- const outcome = await migrate(steps);
1753
- spin.stop();
1754
- const report = describeMigration(outcome);
1755
- if (outcome.kind === "migrated") {
1756
- success(report.split("\n")[0]);
1757
- for (const line2 of report.split("\n").slice(1)) {
1758
- if (/CHANGED|could not be compared/.test(line2)) warn(line2);
1759
- else info(line2);
1433
+ const result = await runMigration(target, {
1434
+ signal: ac.signal,
1435
+ onStep: (text) => {
1436
+ if (spin) spin.text = text;
1437
+ else spin = spinner(text);
1760
1438
  }
1761
- info(`Pre-migration backup: ${archive}`);
1762
- } else {
1763
- failure(report);
1764
- process15.exit(outcome.kind === "nothing-to-migrate" ? 0 : 1);
1765
- }
1439
+ });
1440
+ spin?.stop();
1441
+ const render2 = { success, info, warn, failure };
1442
+ for (const line2 of result.lines) render2[line2.level](line2.text);
1443
+ if (result.outcome.kind !== "migrated") process15.exit(result.exitCode);
1766
1444
  } finally {
1767
- release();
1768
- drainPool();
1445
+ spin?.stop();
1769
1446
  }
1770
1447
  }
1771
1448
  });
@@ -1773,13 +1450,6 @@ var migrate_default = defineCommand13({
1773
1450
  // src/cli/commands/gateway.ts
1774
1451
  import { defineCommand as defineCommand14 } from "citty";
1775
1452
  import process16 from "process";
1776
- function dockerRunCmd2(version, publish = "loopback") {
1777
- return gatewayRunCommand({
1778
- image: `ghcr.io/openclaw/openclaw:${version}`,
1779
- stateDir: STATE_DIR_HOST_LINUX,
1780
- publish
1781
- });
1782
- }
1783
1453
  var gateway_default = defineCommand14({
1784
1454
  meta: {
1785
1455
  name: "gateway",
@@ -1791,26 +1461,16 @@ var gateway_default = defineCommand14({
1791
1461
  json: { type: "boolean", description: "Emit JSON (for status)" }
1792
1462
  },
1793
1463
  async run({ args }) {
1794
- const { buildContext } = await import("./context-7VI3R6EK.js");
1795
- const { extractBaseOutputs } = await import("./outputs-DJHBY7EE.js");
1796
- const { acquireSession, drainPool } = await import("./pool-PIIKJ4L2.js");
1464
+ const { buildContext } = await import("./context-EHSLWKSH.js");
1465
+ const { acquireSession, drainPool } = await import("./pool-NT4XBSUT.js");
1797
1466
  const [action, versionArg] = args._ ?? [];
1798
1467
  if (!action || !["status", "restart", "update"].includes(action)) {
1799
1468
  failure("Usage: clawops gateway <status | restart | update [version]>");
1800
1469
  process16.exit(2);
1801
1470
  }
1802
1471
  const ctx = buildContext(args);
1803
- const stack = await ctx.getStack();
1804
- const outputMap = await stack.outputs();
1805
- const outputs = Object.fromEntries(
1806
- Object.entries(outputMap).map(([k, v]) => [k, v.value])
1807
- );
1808
- const base = extractBaseOutputs(outputs);
1809
- const conn = ctx.adapter.getConnectionInfo({
1810
- ...base,
1811
- privateKeyPath: ctx.config.ssh.keyPath,
1812
- knownHostsPath: ctx.config.ssh.knownHostsPath
1813
- });
1472
+ const { resolveConn } = await import("./conn-M2V5HBPJ.js");
1473
+ const conn = await resolveConn(ctx);
1814
1474
  const abortController = new AbortController();
1815
1475
  process16.on("SIGINT", () => abortController.abort());
1816
1476
  process16.on("SIGTERM", () => abortController.abort());
@@ -1824,25 +1484,12 @@ var gateway_default = defineCommand14({
1824
1484
  });
1825
1485
  try {
1826
1486
  if (action === "status") {
1827
- const { inspectContainer } = await import("./docker-3PDO4TS7.js");
1828
- const inspected = await inspectContainer(
1829
- session,
1830
- "openclaw",
1831
- '{"status":"{{.State.Status}}","started":"{{.State.StartedAt}}","image":"{{.Config.Image}}"}',
1832
- abortController.signal
1833
- );
1834
- let status = { status: "unknown", started: "", image: "" };
1835
- if (inspected.kind === "ok") {
1836
- try {
1837
- status = JSON.parse(inspected.value);
1838
- } catch {
1839
- }
1840
- } else if (inspected.kind === "missing") {
1841
- status = { status: "not running", started: "", image: "" };
1842
- } else {
1843
- failure(`Could not ask docker about the gateway: ${inspected.detail}`);
1487
+ const inspected = await gatewayStatus(session, abortController.signal);
1488
+ if (!inspected.ok) {
1489
+ failure(inspected.error);
1844
1490
  process16.exit(1);
1845
1491
  }
1492
+ const status = inspected.value;
1846
1493
  if (args.json) {
1847
1494
  printJson(jsonOk(status));
1848
1495
  } else {
@@ -1870,7 +1517,7 @@ var gateway_default = defineCommand14({
1870
1517
  const spin = spinner("Restarting gateway...");
1871
1518
  const result = await execPrivileged(
1872
1519
  session,
1873
- dockerRunCmd2(version, publish),
1520
+ dockerRunCmd(version, publish),
1874
1521
  abortController.signal
1875
1522
  );
1876
1523
  spin.stop();
@@ -1880,121 +1527,23 @@ var gateway_default = defineCommand14({
1880
1527
  }
1881
1528
  success(`Gateway restarted (${version}).`);
1882
1529
  } else {
1883
- const { guardOpenclawVersion, defaultOpenclawVersion } = await import("./version-guard-XVYW5WFJ.js");
1884
- const requested = versionArg ?? args.channel;
1885
- const version = requested ? await guardOpenclawVersion(requested) : await defaultOpenclawVersion();
1530
+ const target = await resolveUpdateVersion(versionArg ?? args.channel);
1531
+ if (!target.ok) throw new UsageError(target.error);
1532
+ const version = target.value;
1886
1533
  const spin = spinner(`Updating gateway to ${version}...`);
1887
- const targetImage = `ghcr.io/openclaw/openclaw:${version}`;
1888
- const pullResult = await execPrivileged(
1889
- session,
1890
- `docker pull ${targetImage}`,
1891
- abortController.signal
1892
- );
1893
- if (pullResult.code !== 0) {
1894
- spin.stop();
1895
- failure(`Pull failed: ${pullResult.stderr}`);
1896
- process16.exit(1);
1897
- }
1898
- spin.text = "Checking state compatibility...";
1899
- const {
1900
- snapshotCommand,
1901
- snapshotPathFrom,
1902
- preflightCommand,
1903
- parsePreflight,
1904
- judgePreflight
1905
- } = await import("./upgrade-ULD4KFYC.js");
1906
- const cur = imageForRestart(
1907
- (await execPrivileged(session, IMAGE_INSPECT_CMD, abortController.signal)).stdout
1908
- );
1909
- const snapRepo = `${STATE_DIR_HOST_LINUX}/snapshots`;
1910
- const snapOut = await execPrivileged(
1911
- session,
1912
- snapshotCommand(cur.ok ? cur.value : targetImage, STATE_DIR_HOST_LINUX, snapRepo),
1913
- abortController.signal
1914
- );
1915
- const snapPath = snapshotPathFrom(snapOut.stdout);
1916
- if (!snapPath) {
1917
- spin.stop();
1918
- failure(
1919
- "Could not snapshot the state database before upgrading, so neither the compatibility check nor a rollback point is available.\n" + (snapOut.stderr || snapOut.stdout).slice(0, 300)
1920
- );
1921
- process16.exit(1);
1922
- }
1923
- const pre = await execPrivileged(
1924
- session,
1925
- preflightCommand(targetImage, STATE_DIR_HOST_LINUX, `${snapPath}/database.sqlite`),
1926
- abortController.signal
1927
- );
1928
- const report = parsePreflight(pre.stdout);
1929
- const verdict = report ? judgePreflight(report) : { ok: false, reason: `preflight produced no readable report: ${pre.stderr.slice(0, 200)}` };
1930
- if (!verdict.ok) {
1931
- spin.stop();
1932
- failure(`Refusing to upgrade to ${version}: ${verdict.reason}`);
1933
- info(`A snapshot of the current state was kept at ${snapPath}.`);
1934
- process16.exit(1);
1935
- }
1936
- if (verdict.ok && verdict.note) info(verdict.note);
1937
- spin.text = `Updating gateway to ${version}...`;
1938
- const pubU = await execPrivileged(session, PUBLISH_INSPECT_CMD, abortController.signal);
1939
- const runResult = await execPrivileged(
1940
- session,
1941
- dockerRunCmd2(version, publishForRestart(pubU.stdout)),
1942
- abortController.signal
1943
- );
1944
- if (runResult.code !== 0) {
1945
- spin.stop();
1946
- failure(`Start failed: ${runResult.stderr}`);
1947
- info(`State snapshot from before the upgrade: ${snapPath}`);
1948
- process16.exit(1);
1949
- }
1950
- spin.text = "Waiting for the gateway to start...";
1951
- const { probeCommand: probeCommand2, interpretProbe: interpretProbe2 } = await import("./health-5CCOXYGG.js");
1952
- const { repairCommand, describeOutcome } = await import("./upgrade-ULD4KFYC.js");
1953
- const publish = publishForRestart(pubU.stdout);
1954
- const gate = async () => {
1955
- let last;
1956
- for (let i = 0; i < 15; i++) {
1957
- if (abortController.signal.aborted) return { ok: false, reason: "aborted" };
1958
- const r = await execPrivileged(
1959
- session,
1960
- probeCommand2("started", GATEWAY_PORT),
1961
- abortController.signal
1962
- );
1963
- const v = interpretProbe2("started", r.stdout);
1964
- if (v.ok) return { ok: true };
1965
- last = v.reason;
1966
- await new Promise((res) => setTimeout(res, 2e3));
1967
- }
1968
- return { ok: false, reason: last ?? "no response" };
1969
- };
1970
- const { resolveUpgrade } = await import("./upgrade-ULD4KFYC.js");
1971
- const outcome = await resolveUpgrade(
1972
- {
1973
- gate,
1974
- repair: async () => {
1975
- spin.text = "Gateway did not start; attempting one-shot repair...";
1976
- await execPrivileged(
1977
- session,
1978
- repairCommand(targetImage, STATE_DIR_HOST_LINUX),
1979
- abortController.signal
1980
- );
1981
- },
1982
- run: async (v) => {
1983
- await execPrivileged(session, dockerRunCmd2(v, publish), abortController.signal);
1984
- }
1534
+ const result = await updateGateway(session, version, {
1535
+ signal: abortController.signal,
1536
+ onProgress: (text) => {
1537
+ spin.text = text;
1985
1538
  },
1986
- {
1987
- version,
1988
- previousVersion: cur.ok ? versionOf(cur.value) : void 0,
1989
- snapshotPath: snapPath
1990
- }
1991
- );
1539
+ onNote: (text) => info(text)
1540
+ });
1992
1541
  spin.stop();
1993
- const message = describeOutcome(outcome, version);
1994
- if (outcome.kind === "started" || outcome.kind === "repaired") {
1995
- success(message);
1542
+ if (result.ok) {
1543
+ success(result.message);
1996
1544
  } else {
1997
- failure(message);
1545
+ failure(result.message);
1546
+ if (result.hint) info(result.hint);
1998
1547
  process16.exit(1);
1999
1548
  }
2000
1549
  }
@@ -2007,24 +1556,23 @@ var gateway_default = defineCommand14({
2007
1556
 
2008
1557
  // src/cli/commands/backup.ts
2009
1558
  import { defineCommand as defineCommand15 } from "citty";
2010
- import { createWriteStream } from "fs";
2011
- import { pipeline } from "stream/promises";
2012
1559
  import process17 from "process";
2013
1560
  var backup_default = defineCommand15({
2014
1561
  meta: {
2015
1562
  name: "backup",
2016
- description: "Create an OpenClaw backup (restore is unavailable on this release line)"
1563
+ description: "Create an OpenClaw backup, or restore one into a staging directory"
2017
1564
  },
2018
1565
  args: {
2019
- action: { type: "positional", description: "Action: create (restore returns in clawops 2.x)", required: true },
1566
+ action: { type: "positional", description: "Action: create | restore", required: true },
2020
1567
  out: { type: "string", description: "[create] Local path to write the backup archive" },
2021
- file: { type: "string", description: "[restore] Local backup archive (restore is unavailable)" },
1568
+ file: { type: "string", description: "[restore] Local backup archive to verify and expand on the host" },
2022
1569
  stack: { type: "string", description: "Target stack name" },
2023
- yes: { type: "boolean", description: "[restore] Skip confirmation prompt" }
1570
+ yes: { type: "boolean", description: "[restore] Skip confirmation prompt" },
1571
+ activate: { type: "boolean", description: "[restore] Put the restored state into service: stop the gateway, swap it in, restart, and roll back if it does not come up" }
2024
1572
  },
2025
1573
  async run({ args }) {
2026
- const { buildContext } = await import("./context-7VI3R6EK.js");
2027
- const { acquireSession, drainPool } = await import("./pool-PIIKJ4L2.js");
1574
+ const { buildContext } = await import("./context-EHSLWKSH.js");
1575
+ const { acquireSession, drainPool } = await import("./pool-NT4XBSUT.js");
2028
1576
  const action = args.action;
2029
1577
  if (action !== "create" && action !== "restore") {
2030
1578
  throw new UsageError(`Unknown action: ${action}. Use "create" or "restore"`);
@@ -2074,86 +1622,91 @@ var backup_default = defineCommand15({
2074
1622
  signal: abortController.signal
2075
1623
  });
2076
1624
  try {
1625
+ const flows = await import("./backup-flows-NDP5JTR3.js");
2077
1626
  if (action === "create") {
2078
- const outPath = typeof args.out === "string" ? args.out : `openclaw-backup-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.tar.gz`;
1627
+ const outPath = typeof args.out === "string" ? args.out : flows.defaultBackupFilename();
2079
1628
  info(`Writing backup to ${outPath}...`);
2080
1629
  const spin = spinner("Creating backup on remote host...");
2081
- const remoteArchive = "/tmp/clawops-backup.tar.gz";
2082
- const createResult = await execPrivileged(
2083
- session,
2084
- `docker exec openclaw sh -lc 'rm -f ${remoteArchive} && openclaw backup create --output ${remoteArchive} --verify --json'`,
2085
- abortController.signal
2086
- );
2087
- if (createResult.code !== 0) {
2088
- spin.stop();
2089
- throw new Error(`Backup failed on the remote host: ${createResult.stderr.slice(0, 300)}`);
2090
- }
2091
- const backupStream = await streamPrivileged(
2092
- session,
2093
- `docker exec openclaw cat ${remoteArchive}`,
2094
- abortController.signal
2095
- );
1630
+ const created = await flows.createBackup(session, {
1631
+ outPath,
1632
+ signal: abortController.signal,
1633
+ onPhase: (phase) => {
1634
+ if (phase === "downloading") spin.stop();
1635
+ }
1636
+ });
2096
1637
  spin.stop();
2097
- const fileStream = createWriteStream(outPath, { mode: 384 });
2098
- await pipeline(backupStream, fileStream);
2099
- await execPrivileged(
2100
- session,
2101
- `docker exec openclaw rm -f ${remoteArchive}`,
2102
- abortController.signal
2103
- );
1638
+ if (!created.ok) throw new Error(created.reason);
2104
1639
  success(`Backup saved to ${outPath}`);
2105
- info("Saved with mode 0600. The archive contains the state database \u2014 OAuth tokens,");
2106
- info("secrets and device credentials, unencrypted. Treat it as a credential.");
1640
+ for (const line2 of flows.ARCHIVE_IS_A_CREDENTIAL) info(line2);
2107
1641
  } else {
2108
1642
  const file = typeof args.file === "string" ? args.file : void 0;
2109
1643
  if (!file) {
2110
1644
  throw new UsageError("Usage: clawops backup restore --file <archive.tar.gz>");
2111
1645
  }
2112
- const { createReadStream } = await import("fs");
2113
- const remoteArchive = "/tmp/clawops-restore.tar.gz";
2114
- const staging = `/tmp/clawops-restored-${Date.now()}`;
2115
- const spin = spinner("Uploading archive...");
2116
- const uploadResult = await execPrivilegedWithInput(
2117
- session,
2118
- `docker exec -i openclaw sh -c 'cat > ${remoteArchive}'`,
2119
- createReadStream(file),
2120
- abortController.signal
2121
- );
2122
- if (uploadResult.code !== 0) {
2123
- spin.stop();
2124
- throw new Error(`Could not upload the archive: ${uploadResult.stderr.slice(0, 300)}`);
1646
+ let spin;
1647
+ const staged = await flows.stageRestore(session, {
1648
+ file,
1649
+ signal: abortController.signal,
1650
+ onPhase: (phase) => {
1651
+ if (phase === "uploading") spin = spinner("Uploading archive...");
1652
+ else if (spin) spin.text = "Verifying and restoring to a staging directory...";
1653
+ }
1654
+ });
1655
+ spin?.stop();
1656
+ if (staged.unparsedReport !== void 0) info(staged.unparsedReport);
1657
+ if (!staged.ok) {
1658
+ if (staged.kind === "refused") throw new UsageError(staged.reason);
1659
+ if (staged.kind === "failed") throw new Error(staged.reason);
1660
+ failure(staged.reason);
1661
+ if (staged.stagingOnHost) info(flows.nothingChanged(staged.stagingOnHost));
1662
+ process17.exit(1);
2125
1663
  }
2126
- spin.text = "Verifying and restoring to a staging directory...";
2127
- const restore = await execPrivileged(
2128
- session,
2129
- `docker exec openclaw openclaw backup restore ${remoteArchive} --target ${staging} --json`,
2130
- abortController.signal
2131
- );
2132
- await execPrivileged(
2133
- session,
2134
- `docker exec openclaw rm -f ${remoteArchive}`,
2135
- abortController.signal
2136
- );
2137
- spin.stop();
2138
- if (restore.code !== 0) {
2139
- throw new Error(`Restore failed: ${(restore.stderr || restore.stdout).slice(0, 400)}`);
1664
+ const { stagingOnHost, stateDir } = staged;
1665
+ success(`Archive verified and expanded to ${stagingOnHost} on the host.`);
1666
+ if (staged.entryCount !== void 0) info(`${staged.entryCount} entries restored.`);
1667
+ for (const w of staged.warnings) warn(w);
1668
+ if (!args.activate) {
1669
+ info("");
1670
+ info("Nothing has been activated. Re-run with --activate to put this state into");
1671
+ info("service, or adopt it by hand:");
1672
+ info(" 1. clawops ssh --command 'sudo docker stop openclaw'");
1673
+ info(` 2. replace the contents of ${stateDir} with those of`);
1674
+ info(` ${staged.statePath}`);
1675
+ info(` (the archive holds ${staged.stateDirInArchive} under payload/posix, not a`);
1676
+ info(" drop-in state directory \u2014 moving the bundle itself stops the gateway)");
1677
+ info(" 3. clawops gateway restart");
1678
+ for (const line2 of flows.PLUGINS_NOT_CARRIED) info(line2);
1679
+ return;
2140
1680
  }
2141
- let report = {};
2142
- try {
2143
- report = JSON.parse(restore.stdout.trim());
2144
- } catch {
2145
- info(restore.stdout);
1681
+ if (!args.yes) {
1682
+ const { createInterface: createInterface3 } = await import("readline/promises");
1683
+ const rl = createInterface3({ input: process17.stdin, output: process17.stdout });
1684
+ const answer = await rl.question(
1685
+ `Stop the gateway and put this restored state into service? The state it replaces is kept alongside it, and clawops puts it back if the gateway does not come up. [y/N] `
1686
+ );
1687
+ rl.close();
1688
+ if (!answer.toLowerCase().startsWith("y")) {
1689
+ info(`Aborted. The restored state is at ${stagingOnHost}; nothing was changed.`);
1690
+ return;
1691
+ }
2146
1692
  }
2147
- success(`Archive verified and restored to ${staging} on the host.`);
2148
- if (report.entryCount !== void 0) info(`${report.entryCount} entries restored.`);
2149
- for (const w of report.warnings ?? []) warn(w);
2150
- info("");
2151
- info("Nothing has been activated. To adopt the restored state:");
2152
- info(" 1. clawops ssh --command 'sudo docker stop openclaw'");
2153
- info(` 2. replace the state directory contents with ${staging}`);
2154
- info(" 3. clawops gateway restart");
2155
- info("Provider plugins are not carried in the archive; re-run `clawops apply` to");
2156
- info("reinstall them, or the gateway starts without its model providers.");
1693
+ const activating = spinner("Activating the restored state...");
1694
+ const outcome = await flows.activateStaged(session, staged, {
1695
+ signal: abortController.signal,
1696
+ onPhase: (phase) => {
1697
+ if (phase === "restarting") activating.text = "Restarting the gateway...";
1698
+ else if (phase === "waiting") activating.text = "Waiting for the gateway to answer...";
1699
+ else if (phase === "done") activating.stop();
1700
+ }
1701
+ });
1702
+ activating.stop();
1703
+ if (!outcome.ok) {
1704
+ failure(outcome.reason);
1705
+ for (const line2 of flows.activationFailureDetail(outcome)) info(line2);
1706
+ process17.exit(1);
1707
+ }
1708
+ success(flows.ACTIVATED);
1709
+ for (const line2 of flows.activatedLines(outcome.preserved)) info(line2);
2157
1710
  }
2158
1711
  } finally {
2159
1712
  release();
@@ -2182,16 +1735,16 @@ var stacks_default = defineCommand16({
2182
1735
  process18.exit(2);
2183
1736
  }
2184
1737
  if (action === "list") {
2185
- const config2 = requireConfig();
2186
- const defaultStack = config2.defaults.stack;
2187
- const rows = Object.entries(config2.stacks).map(([n, s]) => [
1738
+ const config = requireConfig();
1739
+ const defaultStack = config.defaults.stack;
1740
+ const rows = Object.entries(config.stacks).map(([n, s]) => [
2188
1741
  n === defaultStack ? `${n} *` : n,
2189
1742
  s.provider,
2190
1743
  s.region ?? "\u2014",
2191
1744
  s.stateUrl
2192
1745
  ]);
2193
1746
  if (args.json) {
2194
- const data = Object.entries(config2.stacks).map(([n, s]) => ({
1747
+ const data = Object.entries(config.stacks).map(([n, s]) => ({
2195
1748
  name: n,
2196
1749
  provider: s.provider,
2197
1750
  region: s.region ?? null,
@@ -2216,49 +1769,17 @@ var stacks_default = defineCommand16({
2216
1769
  failure("Usage: clawops stacks delete <name>");
2217
1770
  process18.exit(2);
2218
1771
  }
2219
- const config = requireConfig();
2220
- if (!(name in config.stacks)) {
2221
- throw new UsageError(`Stack "${name}" not found in config.`);
2222
- }
2223
- const stackNames = Object.keys(config.stacks);
2224
- if (stackNames.length === 1) {
2225
- throw new UsageError(
2226
- `Cannot delete the only remaining stack "${name}". Add another stack first or run \`clawops destroy\` to tear down resources.`
2227
- );
2228
- }
2229
- if (name === config.defaults.stack && !args.force) {
2230
- throw new UsageError(
2231
- `"${name}" is the default stack. Use --force to delete it (clawops will switch the default to another stack).`
2232
- );
2233
- }
2234
- let isDeployed = false;
2235
- if (!args.force) {
2236
- try {
2237
- const { buildContext } = await import("./context-7VI3R6EK.js");
2238
- const ctx = buildContext({ stack: name });
2239
- if (ctx.adapter.name === "local") {
2240
- isDeployed = !!ctx.localState;
2241
- } else {
2242
- const stack = await ctx.getStack();
2243
- const outputMap = await stack.outputs();
2244
- const outputs = Object.fromEntries(
2245
- Object.entries(outputMap).map(([k, v]) => [k, v.value])
2246
- );
2247
- isDeployed = !!outputs["publicIp"];
2248
- }
2249
- } catch {
2250
- warn(`Could not verify deployment status for "${name}" \u2014 proceeding anyway.`);
1772
+ const { checkStackDelete, deleteStackFromConfig, forgetNotice } = await import("./stack-delete-NTIGKWUF.js");
1773
+ const check = await checkStackDelete(name, { force: !!args.force });
1774
+ if (!check.ok) {
1775
+ if (check.refusal === "deployed") {
1776
+ failure(check.reason);
1777
+ process18.exit(1);
2251
1778
  }
1779
+ throw new UsageError(check.reason);
2252
1780
  }
2253
- if (isDeployed) {
2254
- failure(
2255
- `Stack "${name}" is still deployed. Run \`clawops down --stack ${name}\` first to destroy cloud resources, or pass --force to remove from registry only.`
2256
- );
2257
- process18.exit(1);
2258
- }
2259
- warn(
2260
- `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."
2261
- );
1781
+ for (const w of check.warnings) warn(w);
1782
+ warn(forgetNotice(name));
2262
1783
  if (!args.yes) {
2263
1784
  const confirmed = await confirm(`Delete stack "${name}" from config?`);
2264
1785
  if (!confirmed) {
@@ -2266,18 +1787,9 @@ var stacks_default = defineCommand16({
2266
1787
  return;
2267
1788
  }
2268
1789
  }
2269
- const updated = { ...config };
2270
- const newStacks = { ...config.stacks };
2271
- delete newStacks[name];
2272
- updated.stacks = newStacks;
2273
- if (name === config.defaults.stack) {
2274
- updated.defaults = { ...config.defaults, stack: Object.keys(newStacks)[0] };
2275
- }
2276
- setConfig(updated);
2277
- success(`Stack "${name}" removed from config.`);
2278
- if (name === config.defaults.stack) {
2279
- info(`Default stack switched to "${updated.defaults.stack}".`);
2280
- }
1790
+ const deleted = deleteStackFromConfig(name);
1791
+ success(deleted.message);
1792
+ if (deleted.newDefaultMessage) info(deleted.newDefaultMessage);
2281
1793
  }
2282
1794
  });
2283
1795
  async function confirm(message) {
@@ -2375,7 +1887,7 @@ var serveCmd = defineCommand18({
2375
1887
  inspector: { type: "boolean", description: "Enable MCP inspector" }
2376
1888
  },
2377
1889
  async run({ args }) {
2378
- const { serveMcp } = await import("./server-B7I6HBQH.js");
1890
+ const { serveMcp } = await import("./server-E3YA32ND.js");
2379
1891
  await serveMcp({
2380
1892
  port: args.http ? Number(args.http) : void 0,
2381
1893
  bind: args.bind,
@@ -2453,11 +1965,11 @@ var wireCmd = defineCommand18({
2453
1965
  rewire: { type: "boolean", description: "Replace an existing clawops entry" }
2454
1966
  },
2455
1967
  async run({ args }) {
2456
- const { buildContext } = await import("./context-7VI3R6EK.js");
2457
- const { acquireSession, drainPool } = await import("./pool-PIIKJ4L2.js");
1968
+ const { buildContext } = await import("./context-EHSLWKSH.js");
1969
+ const { acquireSession, drainPool } = await import("./pool-NT4XBSUT.js");
2458
1970
  const { extractBaseOutputs } = await import("./outputs-DJHBY7EE.js");
2459
- const { wireGatewayMcp } = await import("./mcp-wire-FQOMM7MK.js");
2460
- const { MCP_HTTP_PORT } = await import("./server-B7I6HBQH.js");
1971
+ const { wireGatewayMcp } = await import("./mcp-wire-BXF4DJFT.js");
1972
+ const { MCP_HTTP_PORT } = await import("./server-E3YA32ND.js");
2461
1973
  const ac = new AbortController();
2462
1974
  process20.on("SIGINT", () => {
2463
1975
  ac.abort();
@@ -2557,8 +2069,8 @@ import { defineCommand as defineCommand19 } from "citty";
2557
2069
  import process21 from "process";
2558
2070
  import os from "os";
2559
2071
  import { randomBytes } from "crypto";
2560
- import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, existsSync as existsSync2, mkdirSync as mkdirSync2 } from "fs";
2561
- import { execSync, spawnSync as spawnSync2, spawn as spawn2 } from "child_process";
2072
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, existsSync, mkdirSync } from "fs";
2073
+ import { execSync, spawnSync, spawn as spawn2 } from "child_process";
2562
2074
  import path2 from "path";
2563
2075
  var setup_default = defineCommand19({
2564
2076
  meta: {
@@ -2617,7 +2129,7 @@ var setup_default = defineCommand19({
2617
2129
  } else {
2618
2130
  provider = "local";
2619
2131
  info("We'll connect to your server over SSH to install and configure OpenClaw.\n");
2620
- if (!existsSync2(localKeyPath)) {
2132
+ if (!existsSync(localKeyPath)) {
2621
2133
  info(`No SSH key found at ${localKeyPath}.`);
2622
2134
  const { genKey } = await inquirer.prompt([{
2623
2135
  type: "confirm",
@@ -2653,7 +2165,7 @@ var setup_default = defineCommand19({
2653
2165
  name: "keyPath",
2654
2166
  message: `SSH private key file: (the key file on your computer used to log in \u2014 e.g. ~/.ssh/id_ed25519)`,
2655
2167
  default: localKeyPath,
2656
- validate: (v) => existsSync2(v.trim()) || `File not found: ${v.trim()}`
2168
+ validate: (v) => existsSync(v.trim()) || `File not found: ${v.trim()}`
2657
2169
  }
2658
2170
  ]);
2659
2171
  localHost = localAnswers.host;
@@ -2875,10 +2387,10 @@ var setup_default = defineCommand19({
2875
2387
  }
2876
2388
  const gatewayToken = randomBytes(24).toString("hex");
2877
2389
  const secretsDir2 = path2.join(os.homedir(), ".clawops", "secrets");
2878
- mkdirSync2(secretsDir2, { recursive: true });
2879
- spawnSync2("chmod", ["700", secretsDir2], { stdio: "ignore" });
2390
+ mkdirSync(secretsDir2, { recursive: true });
2391
+ spawnSync("chmod", ["700", secretsDir2], { stdio: "ignore" });
2880
2392
  const tokenPath = path2.join(secretsDir2, `GATEWAY_TOKEN_${stackAnswers.stackName}`);
2881
- writeFileSync3(tokenPath, gatewayToken, { encoding: "utf-8", mode: 384 });
2393
+ writeFileSync2(tokenPath, gatewayToken, { encoding: "utf-8", mode: 384 });
2882
2394
  const openclawConfigOverlay = {
2883
2395
  models: builtModelConfig,
2884
2396
  gateway: { auth: { mode: "token", token: gatewayToken } }
@@ -2889,7 +2401,7 @@ var setup_default = defineCommand19({
2889
2401
  let outputPath;
2890
2402
  if (provider === "local") {
2891
2403
  outputPath = path2.join(outDir, `openclaw-${stackAnswers.stackName}.json`);
2892
- writeFileSync3(outputPath, JSON.stringify(openclawConfigOverlay, null, 2), "utf-8");
2404
+ writeFileSync2(outputPath, JSON.stringify(openclawConfigOverlay, null, 2), "utf-8");
2893
2405
  process21.stdout.write("\n");
2894
2406
  success(`Config file saved to ${outputPath}`);
2895
2407
  } else {
@@ -2935,7 +2447,7 @@ var setup_default = defineCommand19({
2935
2447
  }
2936
2448
  };
2937
2449
  outputPath = path2.join(outDir, `clawops-${stackAnswers.stackName}-plan.json`);
2938
- writeFileSync3(outputPath, JSON.stringify(plan, null, 2), "utf-8");
2450
+ writeFileSync2(outputPath, JSON.stringify(plan, null, 2), "utf-8");
2939
2451
  process21.stdout.write("\n");
2940
2452
  success(`Deployment plan saved to ${outputPath}`);
2941
2453
  }
@@ -3040,7 +2552,7 @@ var setup_default = defineCommand19({
3040
2552
  inquirer
3041
2553
  });
3042
2554
  } else {
3043
- const { getConfigDir: getConfigDir2 } = await import("./store-OVB3DAJR.js");
2555
+ const { getConfigDir: getConfigDir2 } = await import("./store-YOLQYXMB.js");
3044
2556
  const knownHostsPath = path2.join(getConfigDir2(), "known_hosts");
3045
2557
  process21.stdout.write("\n");
3046
2558
  info("To deploy later, run these two commands:");
@@ -3057,7 +2569,7 @@ var setup_default = defineCommand19({
3057
2569
  default: false
3058
2570
  }]);
3059
2571
  if (applyNow) {
3060
- const { applyPlan } = await import("./apply-HJ5GAKHW.js");
2572
+ const { applyPlan } = await import("./apply-F5SQ2ZOS.js");
3061
2573
  const plan = JSON.parse(readFileSync3(outputPath, "utf-8"));
3062
2574
  try {
3063
2575
  await applyPlan(plan, {
@@ -3085,13 +2597,13 @@ To deploy later: clawops apply ${outputPath}`);
3085
2597
  }
3086
2598
  });
3087
2599
  async function runLocalDeploy(opts) {
3088
- const { setConfig: setConfig2, getConfig: getConfig2, getConfigDir: getConfigDir2 } = await import("./store-OVB3DAJR.js");
3089
- const { localBootstrap } = await import("./bootstrap-NN647RCY.js");
3090
- const { connect } = await import("./ssh-VAI5DU5M.js");
3091
- const { readRemoteConfig, atomicWriteConfig, restartGateway, deepMerge } = await import("./remote-config-3HZLUWEA.js");
2600
+ const { setConfig, getConfig, getConfigDir: getConfigDir2 } = await import("./store-YOLQYXMB.js");
2601
+ const { localBootstrap } = await import("./bootstrap-32TZXVDF.js");
2602
+ const { connect } = await import("./ssh-HOM7GF3M.js");
2603
+ const { readRemoteConfig, atomicWriteConfig, restartGateway, deepMerge } = await import("./remote-config-OZYBMBSK.js");
3092
2604
  const { resolveSecrets } = await import("./secrets-SVZWNAPK.js");
3093
2605
  const knownHostsPath = path2.join(getConfigDir2(), "known_hosts");
3094
- const existing = getConfig2();
2606
+ const existing = getConfig();
3095
2607
  const stackEntry = {
3096
2608
  provider: "local",
3097
2609
  stateUrl: "file://~/.clawops/state",
@@ -3104,7 +2616,7 @@ async function runLocalDeploy(opts) {
3104
2616
  stacks: { [opts.stackName]: stackEntry },
3105
2617
  ssh: { keyPath: opts.keyPath, knownHostsPath }
3106
2618
  };
3107
- setConfig2(newConfig);
2619
+ setConfig(newConfig);
3108
2620
  success(`Deployment "${opts.stackName}" registered (~/.clawops/config.json)`);
3109
2621
  await checkDockerPreflight({ host: opts.host, port: opts.port, user: opts.user, keyPath: opts.keyPath, knownHostsPath, signal: opts.signal, inquirer: opts.inquirer });
3110
2622
  const STAGES = [
@@ -3169,7 +2681,7 @@ async function runLocalDeploy(opts) {
3169
2681
  spin2.fail("Configuration failed");
3170
2682
  throw err;
3171
2683
  }
3172
- const { drainPool } = await import("./pool-PIIKJ4L2.js");
2684
+ const { drainPool } = await import("./pool-NT4XBSUT.js");
3173
2685
  drainPool();
3174
2686
  const tokenPath = path2.join(os.homedir(), ".clawops", "secrets", `GATEWAY_TOKEN_${opts.stackName}`);
3175
2687
  const dashboardUrl = `${state.gatewayUrl}?token=${opts.gatewayToken}`;
@@ -3229,7 +2741,7 @@ ${cfg.loginHint}`);
3229
2741
  }]);
3230
2742
  if (runNow) {
3231
2743
  process21.stdout.write("\n");
3232
- const result = spawnSync2(cfg.loginCmd[0], cfg.loginCmd.slice(1), { stdio: "inherit" });
2744
+ const result = spawnSync(cfg.loginCmd[0], cfg.loginCmd.slice(1), { stdio: "inherit" });
3233
2745
  if (result.error) {
3234
2746
  failure(`Could not launch ${cliName} \u2014 is it installed? (${result.error.message})`);
3235
2747
  info(`Install guide: ${cliInstallUrl(provider)}`);
@@ -3273,12 +2785,12 @@ async function promptSecret(name, label, envDefault, inquirer) {
3273
2785
  message: `Paste your ${label}: (input is hidden)`,
3274
2786
  validate: (v) => v.trim() !== "" || "Value cannot be empty"
3275
2787
  }]);
3276
- mkdirSync2(secretsDir2, { recursive: true });
3277
- spawnSync2("chmod", ["700", secretsDir2], { stdio: "ignore" });
3278
- const secretPath2 = path2.join(secretsDir2, name);
3279
- writeFileSync3(secretPath2, secretValue.trim(), { encoding: "utf-8", mode: 384 });
3280
- success(`Secret saved to ${secretPath2} (chmod 600)`);
3281
- return { name, source: "file", ref: secretPath2 };
2788
+ mkdirSync(secretsDir2, { recursive: true });
2789
+ spawnSync("chmod", ["700", secretsDir2], { stdio: "ignore" });
2790
+ const secretPath = path2.join(secretsDir2, name);
2791
+ writeFileSync2(secretPath, secretValue.trim(), { encoding: "utf-8", mode: 384 });
2792
+ success(`Secret saved to ${secretPath} (chmod 600)`);
2793
+ return { name, source: "file", ref: secretPath };
3282
2794
  }
3283
2795
  if (secretSource === "env") {
3284
2796
  const { envVar } = await inquirer.prompt([{
@@ -3293,7 +2805,7 @@ async function promptSecret(name, label, envDefault, inquirer) {
3293
2805
  type: "input",
3294
2806
  name: "filePath",
3295
2807
  message: `File path: (full path to the file containing your ${label})`,
3296
- validate: (v) => existsSync2(v.trim()) || `File not found: ${v.trim()}`
2808
+ validate: (v) => existsSync(v.trim()) || `File not found: ${v.trim()}`
3297
2809
  }]);
3298
2810
  return { name, source: "file", ref: filePath };
3299
2811
  }
@@ -3307,7 +2819,7 @@ var DOCKER_CHECK_CMD = [
3307
2819
  "echo NOT_RUNNING"
3308
2820
  ].join("; ");
3309
2821
  async function checkDockerPreflight(opts) {
3310
- const { acquireSession } = await import("./pool-PIIKJ4L2.js");
2822
+ const { acquireSession } = await import("./pool-NT4XBSUT.js");
3311
2823
  const { ProviderError } = await import("./errors-OK47MQFD.js");
3312
2824
  const spin = spinner("Checking Docker on target host...");
3313
2825
  const timeoutSignal = AbortSignal.timeout(DOCKER_PREFLIGHT_TIMEOUT_MS);
@@ -3354,7 +2866,7 @@ async function checkDockerPreflight(opts) {
3354
2866
  var DOCKER_POLL_MS = 3e3;
3355
2867
  var DOCKER_TIMEOUT_MS = 9e4;
3356
2868
  async function startRemoteDocker(opts, spin) {
3357
- const { acquireSession } = await import("./pool-PIIKJ4L2.js");
2869
+ const { acquireSession } = await import("./pool-NT4XBSUT.js");
3358
2870
  const { ProviderError } = await import("./errors-OK47MQFD.js");
3359
2871
  spin.text = "Starting Docker on the remote server...";
3360
2872
  const { session, release } = await acquireSession({
@@ -3451,7 +2963,7 @@ async function startDockerAndWait(inquirer) {
3451
2963
  await pollUntilDockerReady("Waiting for Docker daemon...");
3452
2964
  } else if (choice === "systemctl") {
3453
2965
  const spin = spinner("Running: sudo systemctl start docker...");
3454
- const res = spawnSync2("sudo", ["systemctl", "start", "docker"], { stdio: "pipe" });
2966
+ const res = spawnSync("sudo", ["systemctl", "start", "docker"], { stdio: "pipe" });
3455
2967
  if (res.status !== 0) {
3456
2968
  spin.fail("systemctl start docker failed.");
3457
2969
  throw new Error("systemctl start docker failed");
@@ -3459,18 +2971,18 @@ async function startDockerAndWait(inquirer) {
3459
2971
  spin.succeed("Docker service started.");
3460
2972
  await pollUntilDockerReady("Waiting for Docker daemon to be reachable...");
3461
2973
  } else if (choice === "desktop") {
3462
- const backendRunning = spawnSync2("pgrep", ["-f", "com.docker.backend"], { stdio: "ignore" }).status === 0;
2974
+ const backendRunning = spawnSync("pgrep", ["-f", "com.docker.backend"], { stdio: "ignore" }).status === 0;
3463
2975
  if (backendRunning) {
3464
2976
  const killSpin = spinner("Docker Desktop is stuck \u2014 force-restarting...");
3465
2977
  for (const pat of ["com.docker.backend", "com.docker.virtualization", "com.docker.build", "Docker Desktop"]) {
3466
- spawnSync2("pkill", ["-9", "-f", pat], { stdio: "ignore" });
2978
+ spawnSync("pkill", ["-9", "-f", pat], { stdio: "ignore" });
3467
2979
  }
3468
2980
  await new Promise((r) => setTimeout(r, 2e3));
3469
2981
  killSpin.succeed("Old Docker processes cleared.");
3470
2982
  }
3471
2983
  const openSpin = spinner("Launching Docker Desktop...");
3472
- let opened = spawnSync2("open", ["-a", "Docker"], { stdio: "ignore" });
3473
- if (opened.status !== 0) opened = spawnSync2("open", ["/Applications/Docker.app"], { stdio: "ignore" });
2984
+ let opened = spawnSync("open", ["-a", "Docker"], { stdio: "ignore" });
2985
+ if (opened.status !== 0) opened = spawnSync("open", ["/Applications/Docker.app"], { stdio: "ignore" });
3474
2986
  if (opened.status !== 0) {
3475
2987
  openSpin.fail("Could not open Docker Desktop \u2014 is it installed in /Applications?");
3476
2988
  throw new Error("Failed to open Docker Desktop");
@@ -3479,7 +2991,7 @@ async function startDockerAndWait(inquirer) {
3479
2991
  await pollUntilDockerReady("Waiting for Docker engine to start...");
3480
2992
  } else if (choice === "colima") {
3481
2993
  const spin = spinner("Running: colima start...");
3482
- const res = spawnSync2("colima", ["start"], { stdio: "pipe" });
2994
+ const res = spawnSync("colima", ["start"], { stdio: "pipe" });
3483
2995
  if (res.status !== 0) {
3484
2996
  spin.fail("colima start failed \u2014 is Colima installed? (brew install colima docker)");
3485
2997
  throw new Error("colima start failed");
@@ -3491,12 +3003,12 @@ async function startDockerAndWait(inquirer) {
3491
3003
  var LOCALHOST_ALIASES = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1"]);
3492
3004
  async function ensureAuthorizedKey(keyPath, host, inquirer) {
3493
3005
  const pubKeyPath = `${keyPath}.pub`;
3494
- if (!existsSync2(pubKeyPath)) return;
3006
+ if (!existsSync(pubKeyPath)) return;
3495
3007
  const pubKey = readFileSync3(pubKeyPath, "utf-8").trim();
3496
3008
  const isLocal = LOCALHOST_ALIASES.has(host.toLowerCase());
3497
3009
  const authorizedKeysPath = path2.join(os.homedir(), ".ssh", "authorized_keys");
3498
3010
  if (isLocal) {
3499
- const existing = existsSync2(authorizedKeysPath) ? readFileSync3(authorizedKeysPath, "utf-8") : "";
3011
+ const existing = existsSync(authorizedKeysPath) ? readFileSync3(authorizedKeysPath, "utf-8") : "";
3500
3012
  if (existing.includes(pubKey)) {
3501
3013
  success("SSH public key is already in authorized_keys.");
3502
3014
  return;
@@ -3509,10 +3021,10 @@ async function ensureAuthorizedKey(keyPath, host, inquirer) {
3509
3021
  default: true
3510
3022
  }]);
3511
3023
  if (addKey) {
3512
- mkdirSync2(path2.join(os.homedir(), ".ssh"), { recursive: true });
3024
+ mkdirSync(path2.join(os.homedir(), ".ssh"), { recursive: true });
3513
3025
  const entry = existing.endsWith("\n") || existing === "" ? pubKey + "\n" : "\n" + pubKey + "\n";
3514
- writeFileSync3(authorizedKeysPath, existing + entry, { encoding: "utf-8", flag: "a" });
3515
- spawnSync2("chmod", ["600", authorizedKeysPath], { stdio: "ignore" });
3026
+ writeFileSync2(authorizedKeysPath, existing + entry, { encoding: "utf-8", flag: "a" });
3027
+ spawnSync("chmod", ["600", authorizedKeysPath], { stdio: "ignore" });
3516
3028
  success("Public key added to ~/.ssh/authorized_keys.");
3517
3029
  } else {
3518
3030
  info("SSH will likely fail. Add this line to ~/.ssh/authorized_keys manually:");
@@ -3527,8 +3039,8 @@ async function ensureAuthorizedKey(keyPath, host, inquirer) {
3527
3039
  }
3528
3040
  async function generateSshKey() {
3529
3041
  const keyPath = path2.join(os.homedir(), ".ssh", "id_ed25519");
3530
- mkdirSync2(path2.join(os.homedir(), ".ssh"), { recursive: true });
3531
- const result = spawnSync2("ssh-keygen", ["-t", "ed25519", "-f", keyPath, "-N", "", "-C", "clawops"], {
3042
+ mkdirSync(path2.join(os.homedir(), ".ssh"), { recursive: true });
3043
+ const result = spawnSync("ssh-keygen", ["-t", "ed25519", "-f", keyPath, "-N", "", "-C", "clawops"], {
3532
3044
  stdio: ["ignore", "pipe", "pipe"]
3533
3045
  });
3534
3046
  if (result.status !== 0) {
@@ -3543,7 +3055,7 @@ function detectSshKey() {
3543
3055
  const candidates = ["id_ed25519", "id_ecdsa", "id_rsa", "id_dsa"];
3544
3056
  for (const name of candidates) {
3545
3057
  const p = path2.join(os.homedir(), ".ssh", name);
3546
- if (existsSync2(p)) return p;
3058
+ if (existsSync(p)) return p;
3547
3059
  }
3548
3060
  return path2.join(os.homedir(), ".ssh", "id_ed25519");
3549
3061
  }
@@ -3559,9 +3071,9 @@ async function maybeWireGatewayMcp(opts) {
3559
3071
  process21.stdout.write("\n");
3560
3072
  const { wireGateway } = await inquirerPromptWire(opts.inquirer);
3561
3073
  if (!wireGateway) return;
3562
- const { connect } = await import("./ssh-VAI5DU5M.js");
3563
- const { getConfigDir: getConfigDir2 } = await import("./store-OVB3DAJR.js");
3564
- const { wireGatewayMcp } = await import("./mcp-wire-FQOMM7MK.js");
3074
+ const { connect } = await import("./ssh-HOM7GF3M.js");
3075
+ const { getConfigDir: getConfigDir2 } = await import("./store-YOLQYXMB.js");
3076
+ const { wireGatewayMcp } = await import("./mcp-wire-BXF4DJFT.js");
3565
3077
  const knownHostsPath = path2.join(getConfigDir2(), "known_hosts");
3566
3078
  const spin = spinner("Wiring gateway MCP client...");
3567
3079
  const session = await connect({
@@ -3680,9 +3192,9 @@ function instanceChoices(provider) {
3680
3192
  return table[provider] ?? [];
3681
3193
  }
3682
3194
  async function maybeHarden(opts) {
3683
- const { MODULE_CATALOG, resolveModules, runHardening, formatHardenSummary } = await import("./harden-3KKCR7BL.js");
3684
- const { getConfig: getConfig2 } = await import("./store-OVB3DAJR.js");
3685
- const config = getConfig2();
3195
+ const { MODULE_CATALOG, resolveModules, runHardening, formatHardenSummary } = await import("./harden-UHAOVQMZ.js");
3196
+ const { getConfig } = await import("./store-YOLQYXMB.js");
3197
+ const config = getConfig();
3686
3198
  if (!config) return;
3687
3199
  const availableModules = resolveModules(MODULE_CATALOG, void 0, opts.provider);
3688
3200
  if (availableModules.length === 0) return;
@@ -3704,7 +3216,7 @@ async function maybeHarden(opts) {
3704
3216
  const modules = availableModules.filter((m) => selectedIds.includes(m.id));
3705
3217
  process21.stdout.write("\nApplying hardening modules...\n\n");
3706
3218
  try {
3707
- const { buildContext } = await import("./context-7VI3R6EK.js");
3219
+ const { buildContext } = await import("./context-EHSLWKSH.js");
3708
3220
  const { extractBaseOutputs } = await import("./outputs-DJHBY7EE.js");
3709
3221
  const ctx = buildContext({ stack: opts.stackName });
3710
3222
  const stackObj = await ctx.getStack();
@@ -3827,21 +3339,16 @@ async function runAccountPreflight(opts) {
3827
3339
 
3828
3340
  // src/cli/commands/secret.ts
3829
3341
  import { defineCommand as defineCommand20 } from "citty";
3830
- import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync4, writeFileSync as writeFileSync4, unlinkSync, mkdirSync as mkdirSync3, statSync } from "fs";
3831
- import { spawnSync as spawnSync3 } from "child_process";
3832
- import path3 from "path";
3833
- import os2 from "os";
3342
+ import { writeFileSync as writeFileSync3, mkdirSync as mkdirSync2 } from "fs";
3343
+ import { spawnSync as spawnSync2 } from "child_process";
3834
3344
  import process22 from "process";
3835
- var SECRETS_DIR = path3.join(os2.homedir(), ".clawops", "secrets");
3836
- function secretsDir() {
3837
- return SECRETS_DIR;
3838
- }
3839
- function secretPath(name) {
3840
- return path3.join(secretsDir(), name);
3841
- }
3842
- function listSecretNames() {
3843
- if (!existsSync3(secretsDir())) return [];
3844
- return readdirSync(secretsDir()).filter((f) => !f.startsWith("."));
3345
+ function secretPathOrExit(name) {
3346
+ const resolved = resolveSecretPath(name);
3347
+ if (!resolved.ok) {
3348
+ failure(resolved.reason);
3349
+ process22.exit(2);
3350
+ }
3351
+ return resolved.path;
3845
3352
  }
3846
3353
  var listCmd = defineCommand20({
3847
3354
  meta: { name: "list", description: "List all stored secrets and their status" },
@@ -3849,24 +3356,12 @@ var listCmd = defineCommand20({
3849
3356
  json: { type: "boolean", description: "Emit JSON" }
3850
3357
  },
3851
3358
  run({ args }) {
3852
- const names = listSecretNames();
3853
- if (names.length === 0) {
3854
- info("No secrets stored in ~/.clawops/secrets/");
3855
- info("Run `clawops secret set <name>` to add one.");
3359
+ const rows = listSecrets();
3360
+ if (rows.length === 0) {
3361
+ info(NO_SECRETS_MESSAGE);
3362
+ info(SET_SECRET_HINT);
3856
3363
  return;
3857
3364
  }
3858
- const rows = names.map((name) => {
3859
- const p = secretPath(name);
3860
- try {
3861
- const stat = statSync(p);
3862
- const content = readFileSync4(p, "utf-8").trim();
3863
- const status = content.length > 0 ? "ok" : "empty";
3864
- const modified = stat.mtime.toISOString().slice(0, 10);
3865
- return { name, status, source: "file", path: p, modified, resolvable: content.length > 0 };
3866
- } catch {
3867
- return { name, status: "unreadable", source: "file", path: p, modified: "\u2014", resolvable: false };
3868
- }
3869
- });
3870
3365
  if (args.json) {
3871
3366
  process22.stdout.write(JSON.stringify(rows, null, 2) + "\n");
3872
3367
  return;
@@ -3891,6 +3386,7 @@ var setCmd = defineCommand20({
3891
3386
  failure("Usage: clawops secret set <name>");
3892
3387
  process22.exit(2);
3893
3388
  }
3389
+ const target = secretPathOrExit(name);
3894
3390
  const inquirer = (await import("inquirer")).default;
3895
3391
  let value;
3896
3392
  if (args.value) {
@@ -3904,10 +3400,10 @@ var setCmd = defineCommand20({
3904
3400
  }]);
3905
3401
  value = secretValue;
3906
3402
  }
3907
- mkdirSync3(secretsDir(), { recursive: true });
3908
- spawnSync3("chmod", ["700", secretsDir()], { stdio: "ignore" });
3909
- writeFileSync4(secretPath(name), value.trim(), { encoding: "utf-8", mode: 384 });
3910
- success(`Secret "${name}" saved to ${secretPath(name)} (chmod 600)`);
3403
+ mkdirSync2(secretsDir(), { recursive: true });
3404
+ spawnSync2("chmod", ["700", secretsDir()], { stdio: "ignore" });
3405
+ writeFileSync3(target, value.trim(), { encoding: "utf-8", mode: 384 });
3406
+ success(`Secret "${name}" saved to ${target} (chmod 600)`);
3911
3407
  info("Run `clawops secret rotate <name>` to propagate it to a running stack.");
3912
3408
  }
3913
3409
  });
@@ -3922,23 +3418,18 @@ var deleteCmd = defineCommand20({
3922
3418
  failure("Usage: clawops secret delete <name>");
3923
3419
  process22.exit(2);
3924
3420
  }
3925
- const p = secretPath(name);
3926
- if (!existsSync3(p)) {
3927
- failure(`Secret "${name}" not found at ${p}`);
3928
- process22.exit(1);
3929
- }
3930
- const { listOverlays } = await import("./overlay-store-ADZPD7EU.js");
3931
- const referencingStacks = listOverlays().filter((o) => o.secrets.some((s) => s.name === name)).map((o) => o.stackName);
3932
- if (referencingStacks.length > 0) {
3933
- warn(`Secret "${name}" is referenced by stack(s): ${referencingStacks.join(", ")}`);
3934
- warn("Deleting it will leave those stacks with an unresolvable $secret: ref.");
3421
+ const prepared = prepareSecretDelete(name);
3422
+ if (!prepared.ok) {
3423
+ failure(prepared.reason);
3424
+ process22.exit(prepared.kind === "invalid-name" ? 2 : 1);
3935
3425
  }
3426
+ for (const line2 of prepared.warnings) warn(line2);
3936
3427
  if (!args.yes) {
3937
3428
  const inquirer = (await import("inquirer")).default;
3938
3429
  const { confirmed } = await inquirer.prompt([{
3939
3430
  type: "confirm",
3940
3431
  name: "confirmed",
3941
- message: `Delete secret "${name}"?`,
3432
+ message: deleteConfirmQuestion(name),
3942
3433
  default: false
3943
3434
  }]);
3944
3435
  if (!confirmed) {
@@ -3946,8 +3437,7 @@ var deleteCmd = defineCommand20({
3946
3437
  return;
3947
3438
  }
3948
3439
  }
3949
- unlinkSync(p);
3950
- success(`Secret "${name}" deleted.`);
3440
+ success(deletePreparedSecret(prepared));
3951
3441
  }
3952
3442
  });
3953
3443
  var rotateCmd = defineCommand20({
@@ -3962,6 +3452,7 @@ var rotateCmd = defineCommand20({
3962
3452
  failure("Usage: clawops secret rotate <name> [--stack <name>]");
3963
3453
  process22.exit(2);
3964
3454
  }
3455
+ const target = secretPathOrExit(name);
3965
3456
  const inquirer = (await import("inquirer")).default;
3966
3457
  let value;
3967
3458
  if (args.value) {
@@ -3975,12 +3466,12 @@ var rotateCmd = defineCommand20({
3975
3466
  }]);
3976
3467
  value = secretValue;
3977
3468
  }
3978
- mkdirSync3(secretsDir(), { recursive: true });
3979
- spawnSync3("chmod", ["700", secretsDir()], { stdio: "ignore" });
3980
- writeFileSync4(secretPath(name), value.trim(), { encoding: "utf-8", mode: 384 });
3469
+ mkdirSync2(secretsDir(), { recursive: true });
3470
+ spawnSync2("chmod", ["700", secretsDir()], { stdio: "ignore" });
3471
+ writeFileSync3(target, value.trim(), { encoding: "utf-8", mode: 384 });
3981
3472
  success(`Secret "${name}" updated.`);
3982
- const { getConfig: getConfig2 } = await import("./store-OVB3DAJR.js");
3983
- const cfg = getConfig2();
3473
+ const { getConfig } = await import("./store-YOLQYXMB.js");
3474
+ const cfg = getConfig();
3984
3475
  const targetStack = args.stack ?? cfg?.defaults?.stack;
3985
3476
  if (!targetStack) {
3986
3477
  warn("No stack specified and no default stack in config \u2014 skipping re-apply.");
@@ -3996,11 +3487,11 @@ var rotateCmd = defineCommand20({
3996
3487
  }
3997
3488
  info(`Re-applying config overlay to stack "${targetStack}"\u2026`);
3998
3489
  try {
3999
- const { buildContext } = await import("./context-7VI3R6EK.js");
4000
- const { readRemoteConfig, atomicWriteConfig, restartGateway, deepMerge } = await import("./remote-config-3HZLUWEA.js");
3490
+ const { buildContext } = await import("./context-EHSLWKSH.js");
3491
+ const { readRemoteConfig, atomicWriteConfig, restartGateway, deepMerge } = await import("./remote-config-OZYBMBSK.js");
4001
3492
  const { resolveSecrets } = await import("./secrets-SVZWNAPK.js");
4002
- const { acquireSession, drainPool } = await import("./pool-PIIKJ4L2.js");
4003
- const { localStateToConnectionInfo } = await import("./state-AOAYLYY5.js");
3493
+ const { acquireSession, drainPool } = await import("./pool-NT4XBSUT.js");
3494
+ const { localStateToConnectionInfo } = await import("./state-AKZP2S4T.js");
4004
3495
  const ctx = buildContext({ stack: targetStack });
4005
3496
  let conn;
4006
3497
  if (ctx.adapter.name === "local") {
@@ -4040,53 +3531,20 @@ var auditCmd = defineCommand20({
4040
3531
  json: { type: "boolean", description: "Emit JSON" }
4041
3532
  },
4042
3533
  async run({ args }) {
4043
- const { listOverlays } = await import("./overlay-store-ADZPD7EU.js");
4044
- const overlays = listOverlays();
4045
- const issues = [];
4046
- for (const name of listSecretNames()) {
4047
- const p = secretPath(name);
4048
- try {
4049
- const content = readFileSync4(p, "utf-8").trim();
4050
- if (content.length === 0) {
4051
- issues.push({ kind: "empty-secret", secret: name, detail: `File exists but is empty: ${p}` });
4052
- }
4053
- } catch {
4054
- issues.push({ kind: "unreadable-secret", secret: name, detail: `Cannot read: ${p}` });
4055
- }
4056
- }
4057
- for (const overlay of overlays) {
4058
- for (const s of overlay.secrets) {
4059
- if (s.source === "file") {
4060
- const ref = s.ref ?? secretPath(s.name);
4061
- if (!existsSync3(ref)) {
4062
- issues.push({ kind: "missing-file", stack: overlay.stackName, secret: s.name, detail: `File not found: ${ref}` });
4063
- }
4064
- } else if (s.source === "env") {
4065
- const envVar = s.ref ?? s.name;
4066
- if (!process22.env[envVar]) {
4067
- issues.push({ kind: "missing-env", stack: overlay.stackName, secret: s.name, detail: `Env var not set: ${envVar}` });
4068
- }
4069
- } else {
4070
- issues.push({ kind: "cloud-sm-unresolved", stack: overlay.stackName, secret: s.name, detail: `Source "${s.source}" is not auto-resolved \u2014 set manually` });
4071
- }
4072
- }
4073
- }
3534
+ const { issues } = auditSecrets();
4074
3535
  if (args.json) {
4075
3536
  process22.stdout.write(JSON.stringify({ issues, ok: issues.length === 0 }, null, 2) + "\n");
4076
3537
  return;
4077
3538
  }
4078
3539
  if (issues.length === 0) {
4079
- success("All secrets are resolvable. No issues found.");
3540
+ success(AUDIT_CLEAN_MESSAGE);
4080
3541
  return;
4081
3542
  }
4082
- failure(`${issues.length} issue${issues.length === 1 ? "" : "s"} found:
3543
+ failure(`${auditIssuesHeadline(issues.length)}
4083
3544
  `);
4084
- for (const issue of issues) {
4085
- const prefix = issue.stack ? `[${issue.stack}] ` : "";
4086
- warn(`${prefix}${issue.secret}: ${issue.detail}`);
4087
- }
3545
+ for (const issue of issues) warn(formatAuditIssue(issue));
4088
3546
  process22.stdout.write("\n");
4089
- info("Run `clawops secret set <name>` to update a missing or empty secret.");
3547
+ info(AUDIT_FIX_HINT);
4090
3548
  }
4091
3549
  });
4092
3550
  var secret_default = defineCommand20({
@@ -4183,16 +3641,16 @@ import { accessSync, constants } from "fs";
4183
3641
  import { execSync as execSync2 } from "child_process";
4184
3642
  var ISSUES_URL = `${REPO_URL}/issues/new`;
4185
3643
  var BODY_CAP = 2e3;
4186
- var VERSION = true ? "2.1.2" : "dev";
3644
+ var VERSION = true ? "2.2.0" : "dev";
4187
3645
  async function collectContext() {
4188
3646
  const nodeVersion = process23.version;
4189
- const os3 = `${process23.platform} ${process23.arch}`;
3647
+ const os2 = `${process23.platform} ${process23.arch}`;
4190
3648
  let provider = "unknown";
4191
3649
  let stackCount = 0;
4192
3650
  let sshKey = "unknown";
4193
3651
  try {
4194
- const { getConfig: getConfig2 } = await import("./store-OVB3DAJR.js");
4195
- const config = getConfig2();
3652
+ const { getConfig } = await import("./store-YOLQYXMB.js");
3653
+ const config = getConfig();
4196
3654
  if (config) {
4197
3655
  const stacks = Object.values(config.stacks);
4198
3656
  stackCount = stacks.length;
@@ -4213,7 +3671,7 @@ async function collectContext() {
4213
3671
  return [
4214
3672
  `**clawops version:** ${VERSION}`,
4215
3673
  `**Node:** ${nodeVersion}`,
4216
- `**OS:** ${os3}`,
3674
+ `**OS:** ${os2}`,
4217
3675
  `**Provider:** ${provider}`,
4218
3676
  `**Stacks:** ${stackCount}`,
4219
3677
  `**SSH key:** ${sshKey}`
@@ -4302,9 +3760,9 @@ var harden_default = defineCommand23({
4302
3760
  "tailscale-revert": { type: "boolean", description: "Take the host off its tailnet and go back to its public address. A private-only stack must be reopened with a plan first" }
4303
3761
  },
4304
3762
  async run({ args }) {
4305
- const { MODULE_CATALOG, resolveModules, runHardening, formatHardenSummary } = await import("./harden-3KKCR7BL.js");
4306
- const { buildContext } = await import("./context-7VI3R6EK.js");
4307
- const { getConfig: getConfig2 } = await import("./store-OVB3DAJR.js");
3763
+ const { MODULE_CATALOG, resolveModules, runHardening, formatHardenSummary } = await import("./harden-UHAOVQMZ.js");
3764
+ const { buildContext } = await import("./context-EHSLWKSH.js");
3765
+ const { getConfig } = await import("./store-YOLQYXMB.js");
4308
3766
  if (args.list) {
4309
3767
  process24.stdout.write("\nAvailable hardening modules:\n\n");
4310
3768
  for (const mod of MODULE_CATALOG) {
@@ -4318,7 +3776,7 @@ var harden_default = defineCommand23({
4318
3776
  process24.stdout.write("\n");
4319
3777
  return;
4320
3778
  }
4321
- const config = getConfig2();
3779
+ const config = getConfig();
4322
3780
  if (!config) {
4323
3781
  failure("No clawops config found. Run `clawops init` first.");
4324
3782
  process24.exit(1);
@@ -4326,7 +3784,7 @@ var harden_default = defineCommand23({
4326
3784
  const ctx = buildContext({ stack: args.stack });
4327
3785
  const provider = ctx.adapter.name;
4328
3786
  if (args["tailscale-revert"]) {
4329
- const { revertTailnet } = await import("./flows-RI7FGHAM.js");
3787
+ const { revertTailnet } = await import("./flows-6B2I6TRW.js");
4330
3788
  const override = config.stacks[ctx.stackName]?.tailscale;
4331
3789
  if (override && args.yes !== true) {
4332
3790
  const { createInterface: createInterface3 } = await import("readline/promises");
@@ -4349,7 +3807,7 @@ var harden_default = defineCommand23({
4349
3807
  else success(outcome.message);
4350
3808
  return;
4351
3809
  }
4352
- const { makeTailscaleModule } = await import("./harden-3KKCR7BL.js");
3810
+ const { makeTailscaleModule } = await import("./harden-UHAOVQMZ.js");
4353
3811
  const forStack = makeTailscaleModule(ctx.stackName);
4354
3812
  const resolved = resolveModules(MODULE_CATALOG, args.options, provider).map(
4355
3813
  (m) => m.id === "tailscale" ? forStack : m
@@ -4433,7 +3891,7 @@ var harden_default = defineCommand23({
4433
3891
  success("Hardening complete.");
4434
3892
  }
4435
3893
  if (args.tailscale && !args["dry-run"]) {
4436
- const { cutOverToTailnet } = await import("./flows-RI7FGHAM.js");
3894
+ const { cutOverToTailnet } = await import("./flows-6B2I6TRW.js");
4437
3895
  process24.stdout.write("\nVerifying the tailnet address before using it...\n");
4438
3896
  const outcome = await cutOverToTailnet(ctx, conn, config);
4439
3897
  if (!outcome.ok) {
@@ -4470,7 +3928,7 @@ function resolveArgv(argv2, stdinIsTty) {
4470
3928
  }
4471
3929
 
4472
3930
  // src/cli/index.ts
4473
- var CLI_VERSION = true ? "2.1.2" : "dev";
3931
+ var CLI_VERSION = true ? "2.2.0" : "dev";
4474
3932
  var main = defineCommand24({
4475
3933
  meta: {
4476
3934
  name: "clawops",