@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.
- package/README.md +122 -2
- package/dist/agent-activity-25FAWLAI.js +16 -0
- package/dist/{apply-HJ5GAKHW.js → apply-F5SQ2ZOS.js} +12 -12
- package/dist/{automation-MKCJ46I6.js → automation-NBYQ6MFZ.js} +1 -1
- package/dist/backup-flows-NDP5JTR3.js +29 -0
- package/dist/{bootstrap-NN647RCY.js → bootstrap-32TZXVDF.js} +5 -5
- package/dist/bootstrap.sh.tmpl +4 -0
- package/dist/chunk-27335SIH.js +51 -0
- package/dist/{chunk-X56CQ6GB.js → chunk-4MHDW2NB.js} +1 -1
- package/dist/{chunk-E4NJSJ54.js → chunk-4PY6PN4G.js} +51 -7
- package/dist/{chunk-3Z5TDXCJ.js → chunk-55UMO37O.js} +1 -1
- package/dist/{chunk-IQQTB7ER.js → chunk-66R52R7K.js} +10 -14
- package/dist/chunk-755X5OGS.js +213 -0
- package/dist/{chunk-QDNFADJI.js → chunk-AVERNLUY.js} +6 -6
- package/dist/{chunk-R3DTA35B.js → chunk-AX65JMDZ.js} +20 -20
- package/dist/{chunk-T5XCMYAV.js → chunk-CQOYUWR7.js} +766 -210
- package/dist/chunk-E6BRQK67.js +289 -0
- package/dist/{chunk-64PONSA5.js → chunk-GFIR2IIJ.js} +3 -1
- package/dist/chunk-LV7UO5CG.js +82 -0
- package/dist/{chunk-GIEVAGEK.js → chunk-N5ZYUTZS.js} +16 -1
- package/dist/{chunk-GFUK6KDR.js → chunk-OUQVKIIA.js} +372 -34
- package/dist/{chunk-XFN3YDZE.js → chunk-PASAVVNS.js} +1 -1
- package/dist/chunk-PCSX6Y3U.js +297 -0
- package/dist/chunk-POYS3EVQ.js +14 -0
- package/dist/{chunk-ELU3XQEA.js → chunk-RDUKRIRC.js} +1 -1
- package/dist/chunk-SX75L2T3.js +46 -0
- package/dist/{chunk-2Z7J3ERQ.js → chunk-TYNMOYVM.js} +1 -1
- package/dist/{chunk-2I7KCMXZ.js → chunk-UAJJEDCJ.js} +2 -2
- package/dist/{chunk-NUDIHZEH.js → chunk-WPZIJONN.js} +14 -12
- package/dist/{chunk-BELTQGHT.js → chunk-YPVWF7IA.js} +4 -4
- package/dist/{chunk-3OLUJPCB.js → chunk-Z3XLWZWF.js} +1 -1
- package/dist/cli.js +383 -925
- package/dist/conn-M2V5HBPJ.js +9 -0
- package/dist/{context-7VI3R6EK.js → context-EHSLWKSH.js} +6 -6
- package/dist/{docker-3PDO4TS7.js → docker-IKM5GRMC.js} +2 -2
- package/dist/{flows-RI7FGHAM.js → flows-6B2I6TRW.js} +7 -7
- package/dist/{generate-Q2BGVMH7.js → generate-4YYNW3IU.js} +9 -8
- package/dist/{harden-3KKCR7BL.js → harden-UHAOVQMZ.js} +5 -5
- package/dist/init-HOPXDNWY.js +13 -0
- package/dist/{mcp-wire-FQOMM7MK.js → mcp-wire-BXF4DJFT.js} +26 -19
- package/dist/migrate-flow-24XA4GZL.js +25 -0
- package/dist/{package-JFVGCNKU.js → package-HYGHRX35.js} +3 -1
- package/dist/{pool-PIIKJ4L2.js → pool-NT4XBSUT.js} +2 -2
- package/dist/{privileged-JFA6LLTV.js → privileged-5Y6O4H7Z.js} +1 -1
- package/dist/{ready-KDNRBNSU.js → ready-IJEUBFVX.js} +2 -2
- package/dist/{remote-config-3HZLUWEA.js → remote-config-OZYBMBSK.js} +3 -3
- package/dist/{runtime-X3KW5TCP.js → runtime-VW53TU2W.js} +5 -1
- package/dist/server-E3YA32ND.js +53 -0
- package/dist/ssh-HOM7GF3M.js +13 -0
- package/dist/{stack-config-LUTS2XAD.js → stack-config-ICQ5YHUL.js} +2 -2
- package/dist/stack-delete-NTIGKWUF.js +13 -0
- package/dist/{state-AOAYLYY5.js → state-AKZP2S4T.js} +2 -2
- package/dist/{store-OVB3DAJR.js → store-YOLQYXMB.js} +1 -1
- package/dist/{tailscale-cutover-6YP2QKFM.js → tailscale-cutover-V42HO5UM.js} +3 -3
- package/dist/{validate-JXKN5WD6.js → validate-DZE6LFEB.js} +1 -1
- package/dist/{wait-WO72TRRQ.js → wait-DOUNNJNN.js} +2 -2
- package/package.json +3 -1
- package/spec/mcp-tools.yaml +282 -35
- package/dist/logs-5A7YYA53.js +0 -15
- package/dist/server-B7I6HBQH.js +0 -46
- package/dist/ssh-VAI5DU5M.js +0 -11
- package/dist/upgrade-ULD4KFYC.js +0 -88
- /package/dist/{chunk-NUL5F6YV.js → chunk-SOZ62ZXW.js} +0 -0
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
compareVersions
|
|
4
|
+
} from "./chunk-GAZPPSKU.js";
|
|
5
|
+
import {
|
|
6
|
+
interpretProbe,
|
|
7
|
+
probeCommand
|
|
8
|
+
} from "./chunk-LT4XMO5D.js";
|
|
9
|
+
import {
|
|
10
|
+
CONFIG_FILENAME,
|
|
11
|
+
CONTAINER_UID,
|
|
12
|
+
GATEWAY_STOP_COMMAND,
|
|
13
|
+
STATE_DIR_HOST_LINUX,
|
|
14
|
+
gatewayRunCommand
|
|
15
|
+
} from "./chunk-N5ZYUTZS.js";
|
|
16
|
+
import {
|
|
17
|
+
GATEWAY_PORT
|
|
18
|
+
} from "./chunk-VOEJYRIK.js";
|
|
19
|
+
|
|
20
|
+
// src/openclaw/migrate.ts
|
|
21
|
+
function imageVersion(image) {
|
|
22
|
+
const last = image.slice(image.lastIndexOf("/") + 1);
|
|
23
|
+
const colon = last.indexOf(":");
|
|
24
|
+
if (colon < 0) return void 0;
|
|
25
|
+
const m = /^(\d{4}\.\d+\.\d+(?:-\d+)?)(?:-[a-z]+)?$/.exec(last.slice(colon + 1));
|
|
26
|
+
return m?.[1];
|
|
27
|
+
}
|
|
28
|
+
async function migrate(steps, opts = {}) {
|
|
29
|
+
const sourceImage = await steps.inspectSource();
|
|
30
|
+
if (sourceImage && opts.targetLineMin) {
|
|
31
|
+
const source = imageVersion(sourceImage);
|
|
32
|
+
if (source !== void 0 && compareVersions(source, opts.targetLineMin) >= 0) {
|
|
33
|
+
return {
|
|
34
|
+
kind: "already-current",
|
|
35
|
+
reason: `This deployment already runs ${sourceImage}, which is OpenClaw ${opts.targetLineMin} or later \u2014 there is no 1.x state to migrate, and migrating would replace its config. To change versions within 2.x, use \`clawops gateway update <version>\`.`
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (!sourceImage) {
|
|
40
|
+
return {
|
|
41
|
+
kind: "nothing-to-migrate",
|
|
42
|
+
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`."
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
const identityBefore = await steps.deviceId();
|
|
46
|
+
const pulled = await steps.pullTarget();
|
|
47
|
+
if (!pulled.ok) {
|
|
48
|
+
return {
|
|
49
|
+
kind: "refused",
|
|
50
|
+
reason: `Could not pull the 2.x image, so nothing was stopped: ${pulled.detail}`
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
const backup = await steps.backup();
|
|
54
|
+
if (!backup.ok) {
|
|
55
|
+
return {
|
|
56
|
+
kind: "refused",
|
|
57
|
+
reason: `Refusing to migrate without a verified backup: ${backup.detail}
|
|
58
|
+
This replaces the running container, and the state it extracts is the only copy.`
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
const extracted = await steps.extract();
|
|
62
|
+
if (!extracted.ok) {
|
|
63
|
+
return {
|
|
64
|
+
kind: "failed",
|
|
65
|
+
reason: "Could not extract state from the running container; it has not been touched.",
|
|
66
|
+
backupDetail: backup.detail
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
await steps.chown();
|
|
70
|
+
await steps.removeSource();
|
|
71
|
+
await steps.writeConfig();
|
|
72
|
+
await steps.start();
|
|
73
|
+
let restarts = 0;
|
|
74
|
+
let gate = await steps.gate();
|
|
75
|
+
if (!gate.ok) {
|
|
76
|
+
await steps.start();
|
|
77
|
+
restarts = 1;
|
|
78
|
+
gate = await steps.gate();
|
|
79
|
+
}
|
|
80
|
+
if (!gate.ok) {
|
|
81
|
+
return {
|
|
82
|
+
kind: "failed",
|
|
83
|
+
reason: gate.reason ?? "the gateway did not start after migration",
|
|
84
|
+
backupDetail: backup.detail
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
const identityAfter = await steps.deviceId();
|
|
88
|
+
const identity = identityBefore === void 0 || identityAfter === void 0 ? "unknown" : identityBefore === identityAfter ? "preserved" : "changed";
|
|
89
|
+
return { kind: "migrated", entries: extracted.entries, identity, restarts };
|
|
90
|
+
}
|
|
91
|
+
function describeMigration(o) {
|
|
92
|
+
switch (o.kind) {
|
|
93
|
+
case "migrated": {
|
|
94
|
+
const lines = [
|
|
95
|
+
`Migrated to the 2.0 runtime contract. Carried over: ${o.entries.join(", ") || "(nothing)"}.`
|
|
96
|
+
];
|
|
97
|
+
if (o.restarts > 0) {
|
|
98
|
+
lines.push(
|
|
99
|
+
"The gateway needed a second start to finish its state-schema migration, which is expected for a 1.x database."
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
if (o.identity === "preserved") {
|
|
103
|
+
lines.push("Device identity preserved \u2014 paired devices do not need re-pairing.");
|
|
104
|
+
} else if (o.identity === "changed") {
|
|
105
|
+
lines.push(
|
|
106
|
+
"Device identity CHANGED. Paired devices and nodes must be re-paired \u2014 check `openclaw devices list` on the host for what was paired."
|
|
107
|
+
);
|
|
108
|
+
} else {
|
|
109
|
+
lines.push(
|
|
110
|
+
"Device identity could not be compared, so pairings may or may not have carried over. Check `openclaw devices list` before relying on them."
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
lines.push("Your 1.x config was NOT applied \u2014 it never took effect on 1.x either. Review it");
|
|
114
|
+
lines.push("and set what you need with `clawops config set`.");
|
|
115
|
+
return lines.join("\n");
|
|
116
|
+
}
|
|
117
|
+
case "nothing-to-migrate":
|
|
118
|
+
return o.reason;
|
|
119
|
+
case "already-current":
|
|
120
|
+
return o.reason;
|
|
121
|
+
case "refused":
|
|
122
|
+
return o.reason;
|
|
123
|
+
case "failed":
|
|
124
|
+
return `Migration failed: ${o.reason}
|
|
125
|
+
A verified backup was taken first: ${o.backupDetail}
|
|
126
|
+
Restore it with \`clawops backup restore --file <archive>\` before retrying.`;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// src/openclaw/migrate-flow.ts
|
|
131
|
+
var PREMIGRATION_ARCHIVE = "/tmp/clawops-premigration.tar.gz";
|
|
132
|
+
var SYNTHESISED_CONFIG = JSON.stringify({
|
|
133
|
+
meta: { lastTouchedVersion: "2026.9.2" },
|
|
134
|
+
gateway: { mode: "local", port: GATEWAY_PORT, auth: { mode: "token" } },
|
|
135
|
+
models: {},
|
|
136
|
+
channels: {}
|
|
137
|
+
});
|
|
138
|
+
async function prepareMigration(args) {
|
|
139
|
+
const { buildContext } = await import("./context-EHSLWKSH.js");
|
|
140
|
+
const { guardOpenclawVersion, defaultOpenclawVersion } = await import("./version-guard-XVYW5WFJ.js");
|
|
141
|
+
const version = args.openclawVersion ? await guardOpenclawVersion(args.openclawVersion) : await defaultOpenclawVersion();
|
|
142
|
+
const ctx = buildContext({ stack: args.stack });
|
|
143
|
+
return { ctx, stackName: ctx.stackName, version, image: `ghcr.io/openclaw/openclaw:${version}` };
|
|
144
|
+
}
|
|
145
|
+
function migrationSteps(run, opts) {
|
|
146
|
+
const step = opts.onStep ?? (() => {
|
|
147
|
+
});
|
|
148
|
+
const sleep = opts.sleep ?? ((ms) => new Promise((res) => setTimeout(res, ms)));
|
|
149
|
+
const archive = PREMIGRATION_ARCHIVE;
|
|
150
|
+
const configPath = `${STATE_DIR_HOST_LINUX}/${CONFIG_FILENAME}`;
|
|
151
|
+
return {
|
|
152
|
+
inspectSource: async () => {
|
|
153
|
+
const r = await run(`docker inspect openclaw --format '{{.Config.Image}}'`);
|
|
154
|
+
const image = r.stdout.trim();
|
|
155
|
+
return r.code === 0 && image.includes(":") ? image : void 0;
|
|
156
|
+
},
|
|
157
|
+
backup: async () => {
|
|
158
|
+
step("Taking a verified backup...");
|
|
159
|
+
const r = await run(
|
|
160
|
+
`docker exec openclaw openclaw backup create --output ${archive} --verify --json`
|
|
161
|
+
);
|
|
162
|
+
return r.code === 0 ? { ok: true, detail: `${archive} on the host (verified)` } : { ok: false, detail: (r.stderr || r.stdout).slice(0, 200) };
|
|
163
|
+
},
|
|
164
|
+
extract: async () => {
|
|
165
|
+
step("Extracting state from the running container...");
|
|
166
|
+
const mk = await run(`mkdir -p ${STATE_DIR_HOST_LINUX}`);
|
|
167
|
+
if (mk.code !== 0) return { ok: false, entries: [] };
|
|
168
|
+
const cp = await run(`docker cp openclaw:/home/node/.openclaw/. ${STATE_DIR_HOST_LINUX}/`);
|
|
169
|
+
if (cp.code !== 0) return { ok: false, entries: [] };
|
|
170
|
+
const ls = await run(`ls ${STATE_DIR_HOST_LINUX}`);
|
|
171
|
+
return { ok: true, entries: ls.stdout.trim().split(/\s+/).filter(Boolean) };
|
|
172
|
+
},
|
|
173
|
+
chown: async () => {
|
|
174
|
+
await run(`chown -R ${CONTAINER_UID}:${CONTAINER_UID} ${STATE_DIR_HOST_LINUX}`);
|
|
175
|
+
},
|
|
176
|
+
pullTarget: async () => {
|
|
177
|
+
step(`Pulling ${opts.targetImage}...`);
|
|
178
|
+
const r = await run(`docker pull ${opts.targetImage}`);
|
|
179
|
+
return { ok: r.code === 0, detail: (r.stderr || r.stdout).trim().slice(0, 500) };
|
|
180
|
+
},
|
|
181
|
+
removeSource: async () => {
|
|
182
|
+
step("Stopping the 1.x container...");
|
|
183
|
+
await run(GATEWAY_STOP_COMMAND);
|
|
184
|
+
await run("docker rm openclaw 2>/dev/null || true");
|
|
185
|
+
},
|
|
186
|
+
writeConfig: async () => {
|
|
187
|
+
const b64 = Buffer.from(SYNTHESISED_CONFIG, "utf-8").toString("base64");
|
|
188
|
+
await run(
|
|
189
|
+
`echo '${b64}' | base64 -d > ${configPath} && chown ${CONTAINER_UID}:${CONTAINER_UID} ${configPath}`
|
|
190
|
+
);
|
|
191
|
+
},
|
|
192
|
+
start: async () => {
|
|
193
|
+
step("Starting the 2.0 gateway...");
|
|
194
|
+
await run(gatewayRunCommand({ image: opts.targetImage, stateDir: STATE_DIR_HOST_LINUX }));
|
|
195
|
+
},
|
|
196
|
+
gate: async () => {
|
|
197
|
+
step("Waiting for the gateway to start...");
|
|
198
|
+
let last;
|
|
199
|
+
let answeredFor = 0;
|
|
200
|
+
for (let i = 0; i < 60 && answeredFor < 15; i++) {
|
|
201
|
+
if (opts.signal?.aborted) return { ok: false, reason: "aborted" };
|
|
202
|
+
const r = await run(probeCommand("started", GATEWAY_PORT));
|
|
203
|
+
const v = interpretProbe("started", r.stdout);
|
|
204
|
+
if (v.ok) return { ok: true };
|
|
205
|
+
last = v.reason;
|
|
206
|
+
if (r.stdout.trim() !== "") answeredFor++;
|
|
207
|
+
await sleep(2e3);
|
|
208
|
+
}
|
|
209
|
+
const said = await run(
|
|
210
|
+
`echo "container: $(docker ps -a --filter name=^openclaw$ --format '{{.Status}}' | head -1)"; systemctl is-enabled openclaw >/dev/null 2>&1 && echo "unit: $(systemctl is-active openclaw)"; docker logs --tail 15 openclaw 2>&1 | tail -15`
|
|
211
|
+
);
|
|
212
|
+
const detail = said.stdout.trim();
|
|
213
|
+
return { ok: false, reason: (last ?? "no response") + (detail ? `
|
|
214
|
+
${detail}` : "") };
|
|
215
|
+
},
|
|
216
|
+
deviceId: async () => {
|
|
217
|
+
const file = await run(
|
|
218
|
+
`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`
|
|
219
|
+
);
|
|
220
|
+
try {
|
|
221
|
+
const parsed = JSON.parse(file.stdout.trim());
|
|
222
|
+
return parsed.deviceId;
|
|
223
|
+
} catch {
|
|
224
|
+
return void 0;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
function migrationResult(outcome) {
|
|
230
|
+
const report = describeMigration(outcome);
|
|
231
|
+
if (outcome.kind === "migrated") {
|
|
232
|
+
const [first, ...rest] = report.split("\n");
|
|
233
|
+
return {
|
|
234
|
+
outcome,
|
|
235
|
+
exitCode: 0,
|
|
236
|
+
lines: [
|
|
237
|
+
{ level: "success", text: first },
|
|
238
|
+
...rest.map((text) => ({
|
|
239
|
+
level: /CHANGED|could not be compared/.test(text) ? "warn" : "info",
|
|
240
|
+
text
|
|
241
|
+
})),
|
|
242
|
+
{ level: "info", text: `Pre-migration backup: ${PREMIGRATION_ARCHIVE}` }
|
|
243
|
+
]
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
const benign = outcome.kind === "nothing-to-migrate" || outcome.kind === "already-current";
|
|
247
|
+
return { outcome, exitCode: benign ? 0 : 1, lines: [{ level: "failure", text: report }] };
|
|
248
|
+
}
|
|
249
|
+
async function runMigration(target, opts = {}) {
|
|
250
|
+
const { resolveConn } = await import("./conn-M2V5HBPJ.js");
|
|
251
|
+
const { connect } = await import("./ssh-HOM7GF3M.js");
|
|
252
|
+
const { execPrivileged } = await import("./privileged-5Y6O4H7Z.js");
|
|
253
|
+
const { loadVersionSpec } = await import("./versions-W5LRQCAJ.js");
|
|
254
|
+
const yaml = await import("./js-yaml-PTEEG4FO.js");
|
|
255
|
+
const conn = await resolveConn(target.ctx);
|
|
256
|
+
const session = await connect({
|
|
257
|
+
host: conn.host,
|
|
258
|
+
port: conn.port,
|
|
259
|
+
user: conn.user,
|
|
260
|
+
privateKeyPath: conn.privateKeyPath,
|
|
261
|
+
knownHostsPath: conn.knownHostsPath,
|
|
262
|
+
...opts.signal ? { signal: opts.signal } : {}
|
|
263
|
+
});
|
|
264
|
+
try {
|
|
265
|
+
opts.onStep?.("Inspecting the deployment...");
|
|
266
|
+
const steps = migrationSteps((cmd) => execPrivileged(session, cmd, opts.signal), {
|
|
267
|
+
targetImage: target.image,
|
|
268
|
+
...opts.signal ? { signal: opts.signal } : {},
|
|
269
|
+
...opts.onStep ? { onStep: opts.onStep } : {}
|
|
270
|
+
});
|
|
271
|
+
const outcome = await migrate(steps, { targetLineMin: loadVersionSpec(yaml).support.min });
|
|
272
|
+
return migrationResult(outcome);
|
|
273
|
+
} finally {
|
|
274
|
+
session.close();
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
function migrationQuestion(target) {
|
|
278
|
+
return `Migrate stack "${target.stackName}" from OpenClaw 1.x to ${target.version}? This takes a verified backup, then stops and replaces the running 1.x gateway container and writes a new minimal config. The 1.x config is not carried over.`;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export {
|
|
282
|
+
PREMIGRATION_ARCHIVE,
|
|
283
|
+
SYNTHESISED_CONFIG,
|
|
284
|
+
prepareMigration,
|
|
285
|
+
migrationSteps,
|
|
286
|
+
migrationResult,
|
|
287
|
+
runMigration,
|
|
288
|
+
migrationQuestion
|
|
289
|
+
};
|
|
@@ -76,7 +76,9 @@ function getConfig() {
|
|
|
76
76
|
function requireConfig() {
|
|
77
77
|
const cfg = getConfig();
|
|
78
78
|
if (!cfg) {
|
|
79
|
-
throw new UsageError(
|
|
79
|
+
throw new UsageError(
|
|
80
|
+
`No clawops config at ${getConfigPath()}. Create one with \`clawops init\` in a terminal, or by calling the clawops_init tool. Either way it registers a stack \u2014 a provider and a state backend \u2014 and generates an SSH key; nothing is provisioned and nothing is charged.`
|
|
81
|
+
);
|
|
80
82
|
}
|
|
81
83
|
return cfg;
|
|
82
84
|
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
requireConfig,
|
|
4
|
+
setConfig
|
|
5
|
+
} from "./chunk-GFIR2IIJ.js";
|
|
6
|
+
|
|
7
|
+
// src/config/stack-delete.ts
|
|
8
|
+
function forgetNotice(name) {
|
|
9
|
+
return `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.";
|
|
10
|
+
}
|
|
11
|
+
async function checkStackDelete(name, opts) {
|
|
12
|
+
const config = requireConfig();
|
|
13
|
+
if (!(name in config.stacks)) {
|
|
14
|
+
return { ok: false, refusal: "not-found", reason: `Stack "${name}" not found in config.` };
|
|
15
|
+
}
|
|
16
|
+
if (Object.keys(config.stacks).length === 1) {
|
|
17
|
+
return {
|
|
18
|
+
ok: false,
|
|
19
|
+
refusal: "only-stack",
|
|
20
|
+
reason: `Cannot delete the only remaining stack "${name}". Add another stack first or run \`clawops destroy\` to tear down resources.`
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
const isDefault = name === config.defaults.stack;
|
|
24
|
+
if (isDefault && !opts.force) {
|
|
25
|
+
return {
|
|
26
|
+
ok: false,
|
|
27
|
+
refusal: "default",
|
|
28
|
+
reason: `"${name}" is the default stack. Use --force to delete it (clawops will switch the default to another stack).`
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
const warnings = [];
|
|
32
|
+
if (!opts.force) {
|
|
33
|
+
let isDeployed = false;
|
|
34
|
+
try {
|
|
35
|
+
isDeployed = await isStackDeployed(name);
|
|
36
|
+
} catch {
|
|
37
|
+
warnings.push(`Could not verify deployment status for "${name}" \u2014 proceeding anyway.`);
|
|
38
|
+
}
|
|
39
|
+
if (isDeployed) {
|
|
40
|
+
return {
|
|
41
|
+
ok: false,
|
|
42
|
+
refusal: "deployed",
|
|
43
|
+
reason: `Stack "${name}" is still deployed. Run \`clawops down --stack ${name}\` first to destroy cloud resources, or pass --force to remove from registry only.`
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return { ok: true, name, isDefault, warnings };
|
|
48
|
+
}
|
|
49
|
+
async function isStackDeployed(name) {
|
|
50
|
+
const { buildContext } = await import("./context-EHSLWKSH.js");
|
|
51
|
+
const ctx = buildContext({ stack: name });
|
|
52
|
+
if (ctx.adapter.name === "local") return !!ctx.localState;
|
|
53
|
+
const stack = await ctx.getStack();
|
|
54
|
+
const outputMap = await stack.outputs();
|
|
55
|
+
const outputs = Object.fromEntries(
|
|
56
|
+
Object.entries(outputMap).map(([k, v]) => [k, v.value])
|
|
57
|
+
);
|
|
58
|
+
return !!outputs["publicIp"];
|
|
59
|
+
}
|
|
60
|
+
function deleteStackFromConfig(name) {
|
|
61
|
+
const config = requireConfig();
|
|
62
|
+
const newStacks = { ...config.stacks };
|
|
63
|
+
delete newStacks[name];
|
|
64
|
+
const updated = { ...config, stacks: newStacks };
|
|
65
|
+
const wasDefault = name === config.defaults.stack;
|
|
66
|
+
if (wasDefault) {
|
|
67
|
+
updated.defaults = { ...config.defaults, stack: Object.keys(newStacks)[0] };
|
|
68
|
+
}
|
|
69
|
+
setConfig(updated);
|
|
70
|
+
const result = { name, message: `Stack "${name}" removed from config.` };
|
|
71
|
+
if (wasDefault) {
|
|
72
|
+
result.newDefault = updated.defaults.stack;
|
|
73
|
+
result.newDefaultMessage = `Default stack switched to "${updated.defaults.stack}".`;
|
|
74
|
+
}
|
|
75
|
+
return result;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export {
|
|
79
|
+
forgetNotice,
|
|
80
|
+
checkStackDelete,
|
|
81
|
+
deleteStackFromConfig
|
|
82
|
+
};
|
|
@@ -67,14 +67,27 @@ function gatewayRunArgs(spec) {
|
|
|
67
67
|
`node openclaw.mjs gateway run --port ${port}`
|
|
68
68
|
].filter(Boolean).join(" ");
|
|
69
69
|
}
|
|
70
|
+
var SYSTEMD_DROPIN = "/etc/systemd/system/openclaw.service.d/clawops-run.conf";
|
|
71
|
+
var UNDER_SYSTEMD = "systemctl is-enabled openclaw >/dev/null 2>&1";
|
|
70
72
|
function gatewayRunCommand(spec) {
|
|
71
73
|
const { pathPrefix = "" } = spec;
|
|
72
|
-
|
|
74
|
+
const detached = [
|
|
73
75
|
"docker stop openclaw 2>/dev/null || true",
|
|
74
76
|
"docker rm openclaw 2>/dev/null || true",
|
|
75
77
|
gatewayRunArgs(spec)
|
|
76
78
|
].join(" && ");
|
|
79
|
+
if (pathPrefix) return pathPrefix + detached;
|
|
80
|
+
const execStart = gatewayRunArgs({ ...spec, supervisor: "systemd" });
|
|
81
|
+
const viaSystemd = [
|
|
82
|
+
`mkdir -p ${SYSTEMD_DROPIN.replace(/\/[^/]+$/, "")}`,
|
|
83
|
+
`printf '%s
|
|
84
|
+
' '[Service]' 'ExecStart=' "ExecStart=/usr/bin/${execStart}" > ${SYSTEMD_DROPIN}`,
|
|
85
|
+
"systemctl daemon-reload",
|
|
86
|
+
"systemctl restart openclaw"
|
|
87
|
+
].join(" && ");
|
|
88
|
+
return `if ${UNDER_SYSTEMD}; then ${viaSystemd}; else ${detached}; fi`;
|
|
77
89
|
}
|
|
90
|
+
var GATEWAY_STOP_COMMAND = `if ${UNDER_SYSTEMD}; then systemctl stop openclaw; fi; docker stop openclaw 2>/dev/null || true`;
|
|
78
91
|
var PUBLISH_INSPECT_CMD = `docker inspect openclaw --format '{{json .HostConfig.PortBindings}}'`;
|
|
79
92
|
function publishedGatewayPort(inspectStdout) {
|
|
80
93
|
try {
|
|
@@ -119,7 +132,9 @@ export {
|
|
|
119
132
|
SUPERVISOR_ENV,
|
|
120
133
|
ADD_HOST_FLAG,
|
|
121
134
|
gatewayRunArgs,
|
|
135
|
+
SYSTEMD_DROPIN,
|
|
122
136
|
gatewayRunCommand,
|
|
137
|
+
GATEWAY_STOP_COMMAND,
|
|
123
138
|
PUBLISH_INSPECT_CMD,
|
|
124
139
|
publishedGatewayPort,
|
|
125
140
|
publishForRestart
|