clawup 1.0.1 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin.js +3553 -214
- package/dist/bin.js.map +7 -1
- package/package.json +6 -4
- package/scripts/bundle.mjs +35 -0
package/dist/bin.js
CHANGED
|
@@ -1,221 +1,3560 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
"use strict";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
var
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __esm = (fn, res) => function __init() {
|
|
10
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
11
|
+
};
|
|
12
|
+
var __export = (target, all) => {
|
|
13
|
+
for (var name in all)
|
|
14
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
15
|
+
};
|
|
16
|
+
var __copyProps = (to, from, except, desc) => {
|
|
17
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
18
|
+
for (let key of __getOwnPropNames(from))
|
|
19
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
20
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
21
|
+
}
|
|
22
|
+
return to;
|
|
23
|
+
};
|
|
24
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
25
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
26
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
27
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
28
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
29
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
30
|
+
mod
|
|
31
|
+
));
|
|
32
|
+
|
|
33
|
+
// lib/process.ts
|
|
34
|
+
function trackChild(child) {
|
|
35
|
+
activeChildren.add(child);
|
|
36
|
+
const remove = () => activeChildren.delete(child);
|
|
37
|
+
child.on("close", remove);
|
|
38
|
+
child.on("error", remove);
|
|
39
|
+
}
|
|
40
|
+
function setupGracefulShutdown() {
|
|
41
|
+
const cleanup = (signal, exitCode) => {
|
|
42
|
+
for (const child of activeChildren) {
|
|
43
|
+
child.kill(signal);
|
|
44
|
+
}
|
|
45
|
+
process.exit(exitCode);
|
|
46
|
+
};
|
|
47
|
+
process.on("SIGINT", () => cleanup("SIGINT", 130));
|
|
48
|
+
process.on("SIGTERM", () => cleanup("SIGTERM", 143));
|
|
49
|
+
}
|
|
50
|
+
var activeChildren;
|
|
51
|
+
var init_process = __esm({
|
|
52
|
+
"lib/process.ts"() {
|
|
53
|
+
"use strict";
|
|
54
|
+
activeChildren = /* @__PURE__ */ new Set();
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// lib/vendor.ts
|
|
59
|
+
function getVendorDir() {
|
|
60
|
+
return path.join(__dirname, "..", "..", "vendor");
|
|
61
|
+
}
|
|
62
|
+
function getVendorBinDir(tool) {
|
|
63
|
+
return path.join(getVendorDir(), tool);
|
|
64
|
+
}
|
|
65
|
+
function resolveVendoredBinary(command) {
|
|
66
|
+
const entry = VENDOR_COMMANDS[command];
|
|
67
|
+
if (!entry) return null;
|
|
68
|
+
const binPath = path.join(getVendorBinDir(entry.dir), entry.bin);
|
|
69
|
+
if (fs.existsSync(binPath)) {
|
|
70
|
+
return binPath;
|
|
71
|
+
}
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
function resolveCommand(command) {
|
|
75
|
+
const vendored = resolveVendoredBinary(command);
|
|
76
|
+
if (vendored) return vendored;
|
|
77
|
+
return command;
|
|
78
|
+
}
|
|
79
|
+
function isVendored(command) {
|
|
80
|
+
return resolveVendoredBinary(command) !== null;
|
|
81
|
+
}
|
|
82
|
+
function commandExistsWithVendor(command) {
|
|
83
|
+
if (isVendored(command)) return true;
|
|
84
|
+
const bin = process.platform === "win32" ? "where" : "which";
|
|
85
|
+
const result = (0, import_child_process.spawnSync)(bin, [command], { shell: false, stdio: "ignore" });
|
|
86
|
+
return result.status === 0;
|
|
87
|
+
}
|
|
88
|
+
var fs, path, import_child_process, VENDOR_COMMANDS;
|
|
89
|
+
var init_vendor = __esm({
|
|
90
|
+
"lib/vendor.ts"() {
|
|
91
|
+
"use strict";
|
|
92
|
+
fs = __toESM(require("fs"));
|
|
93
|
+
path = __toESM(require("path"));
|
|
94
|
+
import_child_process = require("child_process");
|
|
95
|
+
VENDOR_COMMANDS = {
|
|
96
|
+
pulumi: { dir: "pulumi", bin: process.platform === "win32" ? "pulumi.exe" : "pulumi" },
|
|
97
|
+
aws: { dir: "aws-cli", bin: process.platform === "win32" ? "aws.exe" : "aws" }
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// lib/exec.ts
|
|
103
|
+
function capture(command, args = [], cwd) {
|
|
104
|
+
const resolved = resolveCommand(command);
|
|
105
|
+
try {
|
|
106
|
+
const result = (0, import_child_process2.execSync)([resolved, ...args].join(" "), {
|
|
107
|
+
cwd,
|
|
108
|
+
encoding: "utf-8",
|
|
109
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
110
|
+
});
|
|
111
|
+
return { stdout: result.trim(), stderr: "", exitCode: 0 };
|
|
112
|
+
} catch (err) {
|
|
113
|
+
const e = err;
|
|
114
|
+
return {
|
|
115
|
+
stdout: (e.stdout ?? "").toString().trim(),
|
|
116
|
+
stderr: (e.stderr ?? "").toString().trim(),
|
|
117
|
+
exitCode: e.status ?? 1
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function stream(command, args = [], cwd) {
|
|
122
|
+
const resolved = resolveCommand(command);
|
|
123
|
+
return new Promise((resolve2) => {
|
|
124
|
+
const opts = {
|
|
125
|
+
cwd,
|
|
126
|
+
stdio: "inherit",
|
|
127
|
+
shell: true
|
|
128
|
+
};
|
|
129
|
+
const child = (0, import_child_process2.spawn)(resolved, args, opts);
|
|
130
|
+
trackChild(child);
|
|
131
|
+
child.on("close", (code) => resolve2(code ?? 1));
|
|
132
|
+
child.on("error", (err) => {
|
|
133
|
+
console.warn(`[exec] Child process error: ${err.message}`);
|
|
134
|
+
resolve2(1);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
function commandExists(command) {
|
|
139
|
+
return commandExistsWithVendor(command);
|
|
140
|
+
}
|
|
141
|
+
var import_child_process2;
|
|
142
|
+
var init_exec = __esm({
|
|
143
|
+
"lib/exec.ts"() {
|
|
144
|
+
"use strict";
|
|
145
|
+
import_child_process2 = require("child_process");
|
|
146
|
+
init_process();
|
|
147
|
+
init_vendor();
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// lib/workspace.ts
|
|
152
|
+
function cliVersion() {
|
|
153
|
+
const pkgPath = path2.join(__dirname, "..", "..", "package.json");
|
|
154
|
+
try {
|
|
155
|
+
return JSON.parse(fs2.readFileSync(pkgPath, "utf-8")).version;
|
|
156
|
+
} catch {
|
|
157
|
+
return "unknown";
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
function getBundledInfraDir() {
|
|
161
|
+
return path2.join(__dirname, "..", "..", "infra");
|
|
162
|
+
}
|
|
163
|
+
function isDevMode() {
|
|
164
|
+
const repoRoot = path2.resolve(__dirname, "..", "..", "..");
|
|
165
|
+
return fs2.existsSync(path2.join(repoRoot, "Pulumi.yaml")) && fs2.existsSync(path2.join(repoRoot, "node_modules", "@pulumi"));
|
|
166
|
+
}
|
|
167
|
+
function getWorkspaceDir() {
|
|
168
|
+
if (isDevMode()) return void 0;
|
|
169
|
+
return WORKSPACE_DIR;
|
|
170
|
+
}
|
|
171
|
+
function ensureWorkspace() {
|
|
172
|
+
if (isDevMode()) return { ok: true };
|
|
173
|
+
const bundled = getBundledInfraDir();
|
|
174
|
+
if (!fs2.existsSync(bundled)) {
|
|
175
|
+
return {
|
|
176
|
+
ok: false,
|
|
177
|
+
error: `Bundled infrastructure not found at ${bundled}. The CLI package may be corrupt \u2014 try reinstalling.`
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
const version = cliVersion();
|
|
181
|
+
const versionFile = path2.join(WORKSPACE_DIR, VERSION_FILE);
|
|
182
|
+
const currentVersion = fs2.existsSync(versionFile) ? fs2.readFileSync(versionFile, "utf-8").trim() : null;
|
|
183
|
+
if (currentVersion === version) {
|
|
184
|
+
return { ok: true };
|
|
185
|
+
}
|
|
186
|
+
fs2.mkdirSync(WORKSPACE_DIR, { recursive: true });
|
|
187
|
+
const preservePatterns = ["Pulumi.*.yaml", "clawup.yaml"];
|
|
188
|
+
const preserved = /* @__PURE__ */ new Map();
|
|
189
|
+
for (const pattern of preservePatterns) {
|
|
190
|
+
const files = fs2.readdirSync(WORKSPACE_DIR).filter((f) => {
|
|
191
|
+
if (pattern === "clawup.yaml") return f === "clawup.yaml";
|
|
192
|
+
return f.startsWith("Pulumi.") && f.endsWith(".yaml") && f !== "Pulumi.yaml";
|
|
193
|
+
});
|
|
194
|
+
for (const f of files) {
|
|
195
|
+
preserved.set(f, fs2.readFileSync(path2.join(WORKSPACE_DIR, f)));
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
copyDirSync(bundled, WORKSPACE_DIR);
|
|
199
|
+
for (const [name, content] of preserved) {
|
|
200
|
+
fs2.writeFileSync(path2.join(WORKSPACE_DIR, name), content);
|
|
201
|
+
}
|
|
202
|
+
try {
|
|
203
|
+
(0, import_child_process3.execSync)("npm install --production", {
|
|
204
|
+
cwd: WORKSPACE_DIR,
|
|
205
|
+
stdio: "pipe",
|
|
206
|
+
timeout: 3e5
|
|
207
|
+
// 5 minutes
|
|
208
|
+
});
|
|
209
|
+
} catch (err) {
|
|
210
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
211
|
+
return {
|
|
212
|
+
ok: false,
|
|
213
|
+
error: `Failed to install Pulumi SDK dependencies in workspace:
|
|
214
|
+
${msg}`
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
fs2.writeFileSync(versionFile, version, "utf-8");
|
|
218
|
+
return { ok: true };
|
|
219
|
+
}
|
|
220
|
+
function copyDirSync(src, dest) {
|
|
221
|
+
fs2.mkdirSync(dest, { recursive: true });
|
|
222
|
+
for (const entry of fs2.readdirSync(src, { withFileTypes: true })) {
|
|
223
|
+
const srcPath = path2.join(src, entry.name);
|
|
224
|
+
const destPath = path2.join(dest, entry.name);
|
|
225
|
+
if (entry.isDirectory()) {
|
|
226
|
+
copyDirSync(srcPath, destPath);
|
|
227
|
+
} else {
|
|
228
|
+
fs2.copyFileSync(srcPath, destPath);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
var fs2, path2, os, import_child_process3, WORKSPACE_DIR, VERSION_FILE;
|
|
233
|
+
var init_workspace = __esm({
|
|
234
|
+
"lib/workspace.ts"() {
|
|
235
|
+
"use strict";
|
|
236
|
+
fs2 = __toESM(require("fs"));
|
|
237
|
+
path2 = __toESM(require("path"));
|
|
238
|
+
os = __toESM(require("os"));
|
|
239
|
+
import_child_process3 = require("child_process");
|
|
240
|
+
WORKSPACE_DIR = path2.join(os.homedir(), ".clawup", "workspace");
|
|
241
|
+
VERSION_FILE = ".cli-version";
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
// lib/tailscale.ts
|
|
246
|
+
function isTailscaleInstalled() {
|
|
247
|
+
const bin = process.platform === "win32" ? "where" : "which";
|
|
248
|
+
const result = (0, import_child_process5.spawnSync)(bin, ["tailscale"], { shell: false, stdio: "ignore" });
|
|
249
|
+
if (result.status === 0) return true;
|
|
250
|
+
if (process.platform === "darwin") {
|
|
251
|
+
try {
|
|
252
|
+
const fs8 = require("fs");
|
|
253
|
+
return fs8.existsSync("/Applications/Tailscale.app/Contents/MacOS/Tailscale");
|
|
254
|
+
} catch {
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
function isTailscaleRunning() {
|
|
261
|
+
try {
|
|
262
|
+
const output = (0, import_child_process4.execSync)("tailscale status --json", {
|
|
263
|
+
encoding: "utf-8",
|
|
264
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
265
|
+
timeout: 5e3
|
|
266
|
+
});
|
|
267
|
+
const status = JSON.parse(output);
|
|
268
|
+
return status.BackendState === "Running";
|
|
269
|
+
} catch {
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
function requireTailscale() {
|
|
274
|
+
if (!isTailscaleInstalled()) {
|
|
275
|
+
const installHint = process.platform === "darwin" ? "Install from the Mac App Store or https://tailscale.com/download" : "Install from https://tailscale.com/download";
|
|
276
|
+
console.error(
|
|
277
|
+
`
|
|
278
|
+
${import_picocolors.default.red(import_picocolors.default.bold("Error:"))} Tailscale is not installed.
|
|
279
|
+
|
|
280
|
+
This command connects to agents over Tailscale.
|
|
281
|
+
${installHint}
|
|
282
|
+
`
|
|
283
|
+
);
|
|
284
|
+
process.exit(1);
|
|
285
|
+
}
|
|
286
|
+
if (!isTailscaleRunning()) {
|
|
287
|
+
console.error(
|
|
288
|
+
`
|
|
289
|
+
${import_picocolors.default.red(import_picocolors.default.bold("Error:"))} Tailscale is not connected.
|
|
290
|
+
|
|
291
|
+
This command connects to agents over Tailscale.
|
|
292
|
+
Tailscale is not running. Open the Tailscale app or run: ${import_picocolors.default.cyan("tailscale up")}
|
|
293
|
+
`
|
|
294
|
+
);
|
|
295
|
+
process.exit(1);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
function sleepSync(ms) {
|
|
299
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
300
|
+
}
|
|
301
|
+
function listTailscaleDevices(apiKey, tailnet) {
|
|
302
|
+
let lastError;
|
|
303
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
304
|
+
try {
|
|
305
|
+
const result = (0, import_child_process4.execSync)(
|
|
306
|
+
`curl -sf -H "Authorization: Bearer ${apiKey}" "https://api.tailscale.com/api/v2/tailnet/${tailnet}/devices?fields=default"`,
|
|
307
|
+
{ encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], timeout: 15e3 }
|
|
308
|
+
);
|
|
309
|
+
const data = JSON.parse(result);
|
|
310
|
+
if (!data.devices || !Array.isArray(data.devices)) return null;
|
|
311
|
+
return data.devices.map((d) => ({
|
|
312
|
+
id: d.id,
|
|
313
|
+
name: d.name ?? "",
|
|
314
|
+
hostname: d.hostname ?? ""
|
|
315
|
+
}));
|
|
316
|
+
} catch (err) {
|
|
317
|
+
lastError = err;
|
|
318
|
+
if (attempt < MAX_RETRIES) {
|
|
319
|
+
sleepSync(BASE_DELAY_MS * Math.pow(2, attempt));
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
console.warn(
|
|
324
|
+
`[tailscale] Failed to list devices after ${MAX_RETRIES + 1} attempts: ${lastError instanceof Error ? lastError.message : String(lastError)}`
|
|
325
|
+
);
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
function cleanupTailscaleDevices(apiKey, tailnet, stackName, agents) {
|
|
329
|
+
const cleaned = [];
|
|
330
|
+
const failed = [];
|
|
331
|
+
const devices = listTailscaleDevices(apiKey, tailnet);
|
|
332
|
+
if (!devices) return { cleaned, failed };
|
|
333
|
+
for (const agent of agents) {
|
|
334
|
+
const tsHost = `${stackName}-${agent.name}`;
|
|
335
|
+
const matching = devices.filter(
|
|
336
|
+
(d) => d.hostname === tsHost || d.name.startsWith(`${tsHost}.`)
|
|
337
|
+
);
|
|
338
|
+
for (const device of matching) {
|
|
339
|
+
const deleted = deleteTailscaleDevice(apiKey, device.id);
|
|
340
|
+
if (deleted) {
|
|
341
|
+
cleaned.push(`${agent.name} (${device.hostname})`);
|
|
342
|
+
} else {
|
|
343
|
+
failed.push(agent.name);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
return { cleaned, failed };
|
|
348
|
+
}
|
|
349
|
+
function deleteTailscaleDevice(apiKey, deviceId) {
|
|
350
|
+
let lastError;
|
|
351
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
352
|
+
try {
|
|
353
|
+
(0, import_child_process4.execSync)(
|
|
354
|
+
`curl -sf -X DELETE -H "Authorization: Bearer ${apiKey}" "https://api.tailscale.com/api/v2/device/${deviceId}"`,
|
|
355
|
+
{ encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], timeout: 15e3 }
|
|
356
|
+
);
|
|
357
|
+
return true;
|
|
358
|
+
} catch (err) {
|
|
359
|
+
lastError = err;
|
|
360
|
+
if (attempt < MAX_RETRIES) {
|
|
361
|
+
sleepSync(BASE_DELAY_MS * Math.pow(2, attempt));
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
console.warn(
|
|
366
|
+
`[tailscale] Failed to delete device ${deviceId} after ${MAX_RETRIES + 1} attempts: ${lastError instanceof Error ? lastError.message : String(lastError)}`
|
|
367
|
+
);
|
|
368
|
+
return false;
|
|
369
|
+
}
|
|
370
|
+
function ensureMagicDns(apiKey) {
|
|
371
|
+
try {
|
|
372
|
+
const dnsResp = tsApiGet(apiKey, "/tailnet/-/dns/preferences");
|
|
373
|
+
const dns = JSON.parse(dnsResp);
|
|
374
|
+
if (dns.magicDNS) return false;
|
|
375
|
+
const nsResp = tsApiGet(apiKey, "/tailnet/-/dns/nameservers");
|
|
376
|
+
const ns = JSON.parse(nsResp);
|
|
377
|
+
if (!ns.dns || ns.dns.length === 0) {
|
|
378
|
+
tsApiPost(apiKey, "/tailnet/-/dns/nameservers", { dns: ["1.1.1.1", "8.8.8.8"] });
|
|
379
|
+
}
|
|
380
|
+
tsApiPost(apiKey, "/tailnet/-/dns/preferences", { magicDNS: true });
|
|
381
|
+
return true;
|
|
382
|
+
} catch (err) {
|
|
383
|
+
console.warn(
|
|
384
|
+
`[tailscale] Could not check/enable MagicDNS: ${err instanceof Error ? err.message : String(err)}`
|
|
385
|
+
);
|
|
386
|
+
return false;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
function ensureTailscaleFunnel(apiKey) {
|
|
390
|
+
const result = { magicDns: false, funnelAcl: false };
|
|
391
|
+
result.magicDns = ensureMagicDns(apiKey);
|
|
392
|
+
try {
|
|
393
|
+
const aclResp = tsApiGet(apiKey, "/tailnet/-/acl");
|
|
394
|
+
const acl = JSON.parse(aclResp);
|
|
395
|
+
const nodeAttrs = acl.nodeAttrs ?? [];
|
|
396
|
+
const hasFunnel = nodeAttrs.some(
|
|
397
|
+
(a) => Array.isArray(a.attr) && a.attr.includes("funnel")
|
|
398
|
+
);
|
|
399
|
+
if (!hasFunnel) {
|
|
400
|
+
nodeAttrs.push({ target: ["autogroup:member"], attr: ["funnel"] });
|
|
401
|
+
acl.nodeAttrs = nodeAttrs;
|
|
402
|
+
tsApiPost(apiKey, "/tailnet/-/acl", acl);
|
|
403
|
+
result.funnelAcl = true;
|
|
404
|
+
}
|
|
405
|
+
} catch (err) {
|
|
406
|
+
console.warn(
|
|
407
|
+
`[tailscale] Could not check/enable Funnel ACL: ${err instanceof Error ? err.message : String(err)}`
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
return result;
|
|
411
|
+
}
|
|
412
|
+
function tsApiGet(apiKey, path8) {
|
|
413
|
+
return (0, import_child_process4.execSync)(
|
|
414
|
+
`curl -sf -H "Authorization: Bearer ${apiKey}" -H "Accept: application/json" "https://api.tailscale.com/api/v2${path8}"`,
|
|
415
|
+
{ encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], timeout: 15e3 }
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
function tsApiPost(apiKey, path8, body) {
|
|
419
|
+
const fs8 = require("fs");
|
|
420
|
+
const os6 = require("os");
|
|
421
|
+
const nodePath = require("path");
|
|
422
|
+
const tmpFile = nodePath.join(os6.tmpdir(), `ts-api-${Date.now()}.json`);
|
|
423
|
+
try {
|
|
424
|
+
fs8.writeFileSync(tmpFile, JSON.stringify(body));
|
|
425
|
+
return (0, import_child_process4.execSync)(
|
|
426
|
+
`curl -sf -X POST -H "Authorization: Bearer ${apiKey}" -H "Content-Type: application/json" -d @${tmpFile} "https://api.tailscale.com/api/v2${path8}"`,
|
|
427
|
+
{ encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], timeout: 15e3 }
|
|
428
|
+
);
|
|
429
|
+
} finally {
|
|
430
|
+
try {
|
|
431
|
+
fs8.unlinkSync(tmpFile);
|
|
432
|
+
} catch {
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
var import_child_process4, import_child_process5, import_picocolors, MAX_RETRIES, BASE_DELAY_MS;
|
|
437
|
+
var init_tailscale = __esm({
|
|
438
|
+
"lib/tailscale.ts"() {
|
|
439
|
+
"use strict";
|
|
440
|
+
import_child_process4 = require("child_process");
|
|
441
|
+
import_child_process5 = require("child_process");
|
|
442
|
+
import_picocolors = __toESM(require("picocolors"));
|
|
443
|
+
MAX_RETRIES = 3;
|
|
444
|
+
BASE_DELAY_MS = 1e3;
|
|
445
|
+
}
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
// lib/pulumi.ts
|
|
449
|
+
function selectOrCreateStack(stackName, cwd) {
|
|
450
|
+
const select3 = capture("pulumi", ["stack", "select", stackName], cwd);
|
|
451
|
+
if (select3.exitCode === 0) return { ok: true };
|
|
452
|
+
const init = capture("pulumi", ["stack", "init", stackName], cwd);
|
|
453
|
+
if (init.exitCode === 0) return { ok: true };
|
|
454
|
+
return { ok: false, error: init.stderr || select3.stderr };
|
|
455
|
+
}
|
|
456
|
+
function setConfig(key, value, secret = false, cwd) {
|
|
457
|
+
const args = ["config", "set", key, value];
|
|
458
|
+
if (secret) args.push("--secret");
|
|
459
|
+
const result = capture("pulumi", args, cwd);
|
|
460
|
+
return result.exitCode === 0;
|
|
461
|
+
}
|
|
462
|
+
function getConfig(key, cwd) {
|
|
463
|
+
const result = capture("pulumi", ["config", "get", key], cwd);
|
|
464
|
+
return result.exitCode === 0 ? result.stdout.trim() : null;
|
|
465
|
+
}
|
|
466
|
+
var init_pulumi = __esm({
|
|
467
|
+
"lib/pulumi.ts"() {
|
|
468
|
+
"use strict";
|
|
469
|
+
init_exec();
|
|
470
|
+
}
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
// lib/config.ts
|
|
474
|
+
function configsDir() {
|
|
475
|
+
return path3.join(os2.homedir(), import_core.CONFIG_DIR);
|
|
476
|
+
}
|
|
477
|
+
function ensureConfigsDir() {
|
|
478
|
+
const dir = configsDir();
|
|
479
|
+
if (!fs3.existsSync(dir)) {
|
|
480
|
+
fs3.mkdirSync(dir, { recursive: true });
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
function configPath(name) {
|
|
484
|
+
return path3.join(configsDir(), `${name}.yaml`);
|
|
485
|
+
}
|
|
486
|
+
function syncManifestToProject(name, projectDir) {
|
|
487
|
+
const src = configPath(name);
|
|
488
|
+
const dest = path3.join(projectDir ?? process.cwd(), import_core.MANIFEST_FILE);
|
|
489
|
+
fs3.copyFileSync(src, dest);
|
|
490
|
+
}
|
|
491
|
+
function manifestExists(name) {
|
|
492
|
+
return fs3.existsSync(configPath(name));
|
|
493
|
+
}
|
|
494
|
+
function loadManifest(name) {
|
|
495
|
+
const filePath = configPath(name);
|
|
496
|
+
if (!fs3.existsSync(filePath)) return null;
|
|
497
|
+
try {
|
|
498
|
+
const raw = fs3.readFileSync(filePath, "utf-8");
|
|
499
|
+
return import_yaml.default.parse(raw);
|
|
500
|
+
} catch (err) {
|
|
501
|
+
console.warn(`[config] Failed to load manifest '${name}' at ${filePath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
502
|
+
return null;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
function saveManifest(name, manifest) {
|
|
506
|
+
ensureConfigsDir();
|
|
507
|
+
const filePath = configPath(name);
|
|
508
|
+
fs3.writeFileSync(filePath, import_yaml.default.stringify(manifest), "utf-8");
|
|
509
|
+
}
|
|
510
|
+
function listManifests() {
|
|
511
|
+
const dir = configsDir();
|
|
512
|
+
if (!fs3.existsSync(dir)) return [];
|
|
513
|
+
return fs3.readdirSync(dir).filter((f) => f.endsWith(".yaml")).map((f) => f.replace(/\.yaml$/, "")).sort();
|
|
514
|
+
}
|
|
515
|
+
function resolveConfigName(name) {
|
|
516
|
+
if (name) {
|
|
517
|
+
if (!manifestExists(name)) {
|
|
518
|
+
const available = listManifests();
|
|
519
|
+
if (available.length === 0) {
|
|
520
|
+
throw new Error(`Config '${name}' not found. No configs exist. Run 'clawup init' to create one.`);
|
|
521
|
+
}
|
|
522
|
+
throw new Error(
|
|
523
|
+
`Config '${name}' not found. Available configs:
|
|
524
|
+
${available.join("\n ")}`
|
|
525
|
+
);
|
|
526
|
+
}
|
|
527
|
+
return name;
|
|
528
|
+
}
|
|
529
|
+
const configs = listManifests();
|
|
530
|
+
if (configs.length === 0) {
|
|
531
|
+
throw new Error("No configs found. Run 'clawup init' to create one.");
|
|
532
|
+
}
|
|
533
|
+
if (configs.length === 1) {
|
|
534
|
+
return configs[0];
|
|
535
|
+
}
|
|
536
|
+
throw new Error(
|
|
537
|
+
`Multiple configs found. Specify one with --config:
|
|
538
|
+
${configs.join("\n ")}`
|
|
539
|
+
);
|
|
540
|
+
}
|
|
541
|
+
function pluginsDir(stackName) {
|
|
542
|
+
return path3.join(configsDir(), stackName, import_core.PLUGINS_DIR);
|
|
543
|
+
}
|
|
544
|
+
function loadPluginConfig(stackName, pluginName) {
|
|
545
|
+
const filePath = path3.join(pluginsDir(stackName), `${pluginName}.yaml`);
|
|
546
|
+
if (!fs3.existsSync(filePath)) return null;
|
|
547
|
+
try {
|
|
548
|
+
const raw = fs3.readFileSync(filePath, "utf-8");
|
|
549
|
+
return import_yaml.default.parse(raw);
|
|
550
|
+
} catch (err) {
|
|
551
|
+
console.warn(`[config] Failed to load plugin config '${pluginName}' for stack '${stackName}': ${err instanceof Error ? err.message : String(err)}`);
|
|
552
|
+
return null;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
var fs3, path3, os2, import_yaml, import_core;
|
|
556
|
+
var init_config = __esm({
|
|
557
|
+
"lib/config.ts"() {
|
|
558
|
+
"use strict";
|
|
559
|
+
fs3 = __toESM(require("fs"));
|
|
560
|
+
path3 = __toESM(require("path"));
|
|
561
|
+
os2 = __toESM(require("os"));
|
|
562
|
+
import_yaml = __toESM(require("yaml"));
|
|
563
|
+
import_core = require("@clawup/core");
|
|
564
|
+
}
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
// lib/ui.ts
|
|
568
|
+
function showBanner() {
|
|
569
|
+
console.log();
|
|
570
|
+
p2.intro(import_picocolors2.default.bgCyan(import_picocolors2.default.black(" Agent Army ")));
|
|
571
|
+
}
|
|
572
|
+
function handleCancel(value) {
|
|
573
|
+
if (p2.isCancel(value)) {
|
|
574
|
+
p2.cancel("Operation cancelled.");
|
|
575
|
+
process.exit(0);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
function exitWithError(message) {
|
|
579
|
+
p2.log.error(message);
|
|
580
|
+
process.exit(1);
|
|
581
|
+
}
|
|
582
|
+
function formatCost(monthlyCost) {
|
|
583
|
+
return `~$${monthlyCost}/mo`;
|
|
584
|
+
}
|
|
585
|
+
function formatAgentList(agents) {
|
|
586
|
+
return agents.map((a) => ` ${import_picocolors2.default.bold(a.displayName)} (${a.role})`).join("\n");
|
|
587
|
+
}
|
|
588
|
+
var p2, import_picocolors2;
|
|
589
|
+
var init_ui = __esm({
|
|
590
|
+
"lib/ui.ts"() {
|
|
591
|
+
"use strict";
|
|
592
|
+
p2 = __toESM(require("@clack/prompts"));
|
|
593
|
+
import_picocolors2 = __toESM(require("picocolors"));
|
|
594
|
+
}
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
// lib/tool-helpers.ts
|
|
598
|
+
function getConfig2(exec, key, cwd) {
|
|
599
|
+
const result = exec.capture("pulumi", ["config", "get", key], cwd);
|
|
600
|
+
return result.exitCode === 0 ? result.stdout.trim() : null;
|
|
601
|
+
}
|
|
602
|
+
function getStackOutputs(exec, showSecrets = false, cwd) {
|
|
603
|
+
const args = ["stack", "output", "--json"];
|
|
604
|
+
if (showSecrets) args.push("--show-secrets");
|
|
605
|
+
const result = exec.capture("pulumi", args, cwd);
|
|
606
|
+
if (result.exitCode !== 0) return null;
|
|
607
|
+
try {
|
|
608
|
+
return JSON.parse(result.stdout);
|
|
609
|
+
} catch {
|
|
610
|
+
return null;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
var init_tool_helpers = __esm({
|
|
614
|
+
"lib/tool-helpers.ts"() {
|
|
615
|
+
"use strict";
|
|
616
|
+
}
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
// tools/deploy.ts
|
|
620
|
+
var import_core2, import_picocolors3, deployTool;
|
|
621
|
+
var init_deploy = __esm({
|
|
622
|
+
"tools/deploy.ts"() {
|
|
623
|
+
"use strict";
|
|
624
|
+
init_config();
|
|
625
|
+
import_core2 = require("@clawup/core");
|
|
626
|
+
init_workspace();
|
|
627
|
+
init_tailscale();
|
|
628
|
+
init_tool_helpers();
|
|
629
|
+
init_ui();
|
|
630
|
+
import_picocolors3 = __toESM(require("picocolors"));
|
|
631
|
+
deployTool = async (runtime, options) => {
|
|
632
|
+
const { ui, exec } = runtime;
|
|
633
|
+
ui.intro("Agent Army");
|
|
634
|
+
const wsResult = ensureWorkspace();
|
|
635
|
+
if (!wsResult.ok) {
|
|
636
|
+
ui.log.error(wsResult.error ?? "Failed to set up workspace.");
|
|
637
|
+
process.exit(1);
|
|
638
|
+
}
|
|
639
|
+
const cwd = getWorkspaceDir();
|
|
640
|
+
let configName;
|
|
641
|
+
try {
|
|
642
|
+
configName = resolveConfigName(options.config);
|
|
643
|
+
} catch (err) {
|
|
644
|
+
ui.log.error(err.message);
|
|
645
|
+
process.exit(1);
|
|
646
|
+
}
|
|
647
|
+
const manifest = loadManifest(configName);
|
|
648
|
+
if (!manifest) {
|
|
649
|
+
ui.log.error(`Config '${configName}' could not be loaded.`);
|
|
650
|
+
process.exit(1);
|
|
651
|
+
}
|
|
652
|
+
const selectResult = exec.capture("pulumi", ["stack", "select", manifest.stackName], cwd);
|
|
653
|
+
if (selectResult.exitCode !== 0) {
|
|
654
|
+
const initResult = exec.capture("pulumi", ["stack", "init", manifest.stackName], cwd);
|
|
655
|
+
if (initResult.exitCode !== 0) {
|
|
656
|
+
ui.log.error(initResult.stderr || selectResult.stderr);
|
|
657
|
+
ui.log.error(`Could not select Pulumi stack "${manifest.stackName}".`);
|
|
658
|
+
process.exit(1);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
const totalCost = manifest.agents.reduce((sum, a) => {
|
|
662
|
+
const costs = manifest.provider === "hetzner" ? import_core2.HETZNER_COST_ESTIMATES : import_core2.COST_ESTIMATES;
|
|
663
|
+
return sum + (costs[a.instanceType ?? manifest.instanceType] ?? 30);
|
|
664
|
+
}, 0);
|
|
665
|
+
ui.note(
|
|
666
|
+
[
|
|
667
|
+
`Stack: ${manifest.stackName}`,
|
|
668
|
+
`Region: ${manifest.region}`,
|
|
669
|
+
``,
|
|
670
|
+
`Agents (${manifest.agents.length}):`,
|
|
671
|
+
formatAgentList(manifest.agents),
|
|
672
|
+
``,
|
|
673
|
+
`Estimated cost: ${formatCost(totalCost)}`
|
|
674
|
+
].join("\n"),
|
|
675
|
+
"Deployment Summary"
|
|
676
|
+
);
|
|
677
|
+
if (!options.yes) {
|
|
678
|
+
const confirmed = await ui.confirm({
|
|
679
|
+
message: "Proceed with deployment?"
|
|
680
|
+
});
|
|
681
|
+
if (!confirmed) {
|
|
682
|
+
ui.cancel("Deployment cancelled.");
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
syncManifestToProject(configName, cwd);
|
|
686
|
+
exec.capture("pulumi", ["config", "set", "instanceType", manifest.instanceType], cwd);
|
|
687
|
+
const tailnetDnsName = getConfig2(exec, "tailnetDnsName", cwd);
|
|
688
|
+
const tailscaleApiKey = getConfig2(exec, "tailscaleApiKey", cwd);
|
|
689
|
+
if (tailnetDnsName && tailscaleApiKey) {
|
|
690
|
+
const spinner4 = ui.spinner("Cleaning up stale Tailscale devices...");
|
|
691
|
+
const { cleaned, failed } = cleanupTailscaleDevices(
|
|
692
|
+
tailscaleApiKey,
|
|
693
|
+
tailnetDnsName,
|
|
694
|
+
manifest.stackName,
|
|
695
|
+
manifest.agents
|
|
696
|
+
);
|
|
697
|
+
if (cleaned.length > 0) {
|
|
698
|
+
spinner4.stop(`Removed ${cleaned.length} stale Tailscale device(s)`);
|
|
699
|
+
} else {
|
|
700
|
+
spinner4.stop("No stale Tailscale devices found");
|
|
701
|
+
}
|
|
702
|
+
if (failed.length > 0) {
|
|
703
|
+
ui.log.warn(
|
|
704
|
+
`Could not remove some devices: ${failed.join(", ")}. Check https://login.tailscale.com/admin/machines`
|
|
705
|
+
);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
if (tailscaleApiKey) {
|
|
709
|
+
const spinner4 = ui.spinner("Ensuring Tailscale MagicDNS is enabled...");
|
|
710
|
+
const magicDnsChanged = ensureMagicDns(tailscaleApiKey);
|
|
711
|
+
spinner4.stop(magicDnsChanged ? "MagicDNS enabled" : "MagicDNS OK");
|
|
712
|
+
}
|
|
713
|
+
if (tailscaleApiKey) {
|
|
714
|
+
const spinner4 = ui.spinner("Ensuring Tailscale Funnel prerequisites...");
|
|
715
|
+
const funnel = ensureTailscaleFunnel(tailscaleApiKey);
|
|
716
|
+
const changes = [];
|
|
717
|
+
if (funnel.funnelAcl) changes.push("Funnel ACL enabled");
|
|
718
|
+
spinner4.stop(changes.length > 0 ? changes.join(", ") : "Funnel prerequisites OK");
|
|
719
|
+
}
|
|
720
|
+
ui.log.step("Running pulumi up...");
|
|
721
|
+
console.log();
|
|
722
|
+
const exitCode = await exec.stream("pulumi", ["up", "--yes"], { cwd });
|
|
723
|
+
console.log();
|
|
724
|
+
if (exitCode !== 0) {
|
|
725
|
+
ui.log.error("Deployment failed. Check the output above for details.");
|
|
726
|
+
process.exit(1);
|
|
727
|
+
}
|
|
728
|
+
ui.log.success("Deployment complete!");
|
|
729
|
+
const outputsResult = exec.capture("pulumi", [
|
|
730
|
+
"stack",
|
|
731
|
+
"output",
|
|
732
|
+
"--json",
|
|
733
|
+
"--show-secrets"
|
|
734
|
+
], cwd);
|
|
735
|
+
if (outputsResult.exitCode === 0) {
|
|
736
|
+
try {
|
|
737
|
+
const outputs = JSON.parse(outputsResult.stdout);
|
|
738
|
+
const lines = [];
|
|
739
|
+
for (const agent of manifest.agents) {
|
|
740
|
+
const urlKey = `${agent.role}TailscaleUrl`;
|
|
741
|
+
const ipKey = `${agent.role}PublicIp`;
|
|
742
|
+
const idKey = `${agent.role}InstanceId`;
|
|
743
|
+
lines.push(`${agent.displayName} (${agent.role}):`);
|
|
744
|
+
if (outputs[urlKey]) lines.push(` Tailscale URL: ${outputs[urlKey]}`);
|
|
745
|
+
if (outputs[ipKey]) lines.push(` Public IP: ${outputs[ipKey]}`);
|
|
746
|
+
if (outputs[idKey]) lines.push(` Instance ID: ${outputs[idKey]}`);
|
|
747
|
+
lines.push("");
|
|
748
|
+
}
|
|
749
|
+
ui.note(lines.join("\n"), "Agent Details");
|
|
750
|
+
const webhookLines = [];
|
|
751
|
+
for (const agent of manifest.agents) {
|
|
752
|
+
const webhookUrl = outputs[`${agent.role}WebhookUrl`];
|
|
753
|
+
if (webhookUrl) {
|
|
754
|
+
webhookLines.push(` ${agent.displayName} (${agent.role}): ${webhookUrl}`);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
if (webhookLines.length > 0) {
|
|
758
|
+
ui.note(webhookLines.join("\n"), "Webhook URLs");
|
|
759
|
+
}
|
|
760
|
+
} catch (err) {
|
|
761
|
+
ui.log.warn(`Could not parse stack outputs: ${err instanceof Error ? err.message : String(err)}`);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
if (!isTailscaleInstalled()) {
|
|
765
|
+
const hint = process.platform === "darwin" ? "Install from the Mac App Store or https://tailscale.com/download" : "Install from https://tailscale.com/download";
|
|
766
|
+
ui.log.warn(
|
|
767
|
+
`Tailscale is required to connect to agents.
|
|
768
|
+
${hint}
|
|
769
|
+
Then run: ${import_picocolors3.default.cyan("tailscale up")}`
|
|
770
|
+
);
|
|
771
|
+
} else if (!isTailscaleRunning()) {
|
|
772
|
+
ui.log.warn(
|
|
773
|
+
`Tailscale is not running. Start it before validating agents.
|
|
774
|
+
Open the Tailscale app or run: ${import_picocolors3.default.cyan("tailscale up")}`
|
|
775
|
+
);
|
|
776
|
+
}
|
|
777
|
+
ui.log.info(
|
|
778
|
+
`Run ${import_picocolors3.default.cyan("clawup webhooks setup")} to configure Linear webhooks for your agents.`
|
|
779
|
+
);
|
|
780
|
+
ui.outro("Agents will be ready in 3-5 minutes. Run `clawup validate` to check.");
|
|
781
|
+
};
|
|
782
|
+
}
|
|
783
|
+
});
|
|
784
|
+
|
|
785
|
+
// tools/status.ts
|
|
786
|
+
function getClaudeCodeVersion(exec, host, timeout = 5) {
|
|
787
|
+
const result = exec.capture("ssh", [
|
|
788
|
+
"-o",
|
|
789
|
+
`ConnectTimeout=${timeout}`,
|
|
790
|
+
...SSH_OPTS,
|
|
791
|
+
`${import_core3.SSH_USER}@${host}`,
|
|
792
|
+
`"/home/${import_core3.SSH_USER}/.local/bin/claude --version 2>/dev/null || echo ''"`
|
|
793
|
+
]);
|
|
794
|
+
if (result.exitCode === 0 && result.stdout?.trim()) {
|
|
795
|
+
return result.stdout.trim();
|
|
796
|
+
}
|
|
797
|
+
return "\u2014";
|
|
798
|
+
}
|
|
799
|
+
function getGhVersion(exec, host, timeout = 5) {
|
|
800
|
+
const result = exec.capture("ssh", [
|
|
801
|
+
"-o",
|
|
802
|
+
`ConnectTimeout=${timeout}`,
|
|
803
|
+
...SSH_OPTS,
|
|
804
|
+
`${import_core3.SSH_USER}@${host}`,
|
|
805
|
+
`"gh --version 2>/dev/null | head -n1 | awk '{print \\$3}' || echo ''"`
|
|
806
|
+
]);
|
|
807
|
+
if (result.exitCode === 0 && result.stdout?.trim()) {
|
|
808
|
+
return result.stdout.trim();
|
|
809
|
+
}
|
|
810
|
+
return "\u2014";
|
|
811
|
+
}
|
|
812
|
+
function getGhAuthStatus(exec, host, timeout = 5) {
|
|
813
|
+
const result = exec.capture("ssh", [
|
|
814
|
+
"-o",
|
|
815
|
+
`ConnectTimeout=${timeout}`,
|
|
816
|
+
...SSH_OPTS,
|
|
817
|
+
`${import_core3.SSH_USER}@${host}`,
|
|
818
|
+
`"gh auth status 2>&1 >/dev/null && echo 'OK' || echo 'no'"`
|
|
819
|
+
]);
|
|
820
|
+
if (result.exitCode === 0 && result.stdout?.trim() === "OK") {
|
|
821
|
+
return "\u2713";
|
|
822
|
+
}
|
|
823
|
+
return "\u2014";
|
|
824
|
+
}
|
|
825
|
+
var import_core3, SSH_OPTS, statusTool;
|
|
826
|
+
var init_status = __esm({
|
|
827
|
+
"tools/status.ts"() {
|
|
828
|
+
"use strict";
|
|
829
|
+
init_config();
|
|
830
|
+
import_core3 = require("@clawup/core");
|
|
831
|
+
init_workspace();
|
|
832
|
+
init_tailscale();
|
|
833
|
+
init_tool_helpers();
|
|
834
|
+
SSH_OPTS = [
|
|
835
|
+
"-o",
|
|
836
|
+
"StrictHostKeyChecking=no",
|
|
837
|
+
"-o",
|
|
838
|
+
"UserKnownHostsFile=/dev/null",
|
|
839
|
+
"-o",
|
|
840
|
+
"BatchMode=yes"
|
|
841
|
+
];
|
|
842
|
+
statusTool = async (runtime, options) => {
|
|
843
|
+
const { ui, exec } = runtime;
|
|
844
|
+
if (!options.json) {
|
|
845
|
+
ui.intro("Agent Army");
|
|
846
|
+
}
|
|
847
|
+
const wsResult = ensureWorkspace();
|
|
848
|
+
if (!wsResult.ok) {
|
|
849
|
+
ui.log.error(wsResult.error ?? "Failed to set up workspace.");
|
|
850
|
+
process.exit(1);
|
|
851
|
+
}
|
|
852
|
+
const cwd = getWorkspaceDir();
|
|
853
|
+
let configName;
|
|
854
|
+
try {
|
|
855
|
+
configName = resolveConfigName(options.config);
|
|
856
|
+
} catch (err) {
|
|
857
|
+
ui.log.error(err.message);
|
|
858
|
+
process.exit(1);
|
|
859
|
+
}
|
|
860
|
+
const manifest = loadManifest(configName);
|
|
861
|
+
if (!manifest) {
|
|
862
|
+
ui.log.error(`Config '${configName}' could not be loaded.`);
|
|
863
|
+
process.exit(1);
|
|
864
|
+
}
|
|
865
|
+
const selectResult = exec.capture("pulumi", ["stack", "select", manifest.stackName], cwd);
|
|
866
|
+
if (selectResult.exitCode !== 0) {
|
|
867
|
+
const initResult = exec.capture("pulumi", ["stack", "init", manifest.stackName], cwd);
|
|
868
|
+
if (initResult.exitCode !== 0) {
|
|
869
|
+
if (!options.json) {
|
|
870
|
+
ui.log.error(initResult.stderr || selectResult.stderr);
|
|
871
|
+
ui.log.error(`Could not select Pulumi stack "${manifest.stackName}".`);
|
|
872
|
+
}
|
|
873
|
+
process.exit(1);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
const outputs = getStackOutputs(exec, true, cwd);
|
|
877
|
+
if (!outputs) {
|
|
878
|
+
ui.log.error("Could not fetch stack outputs. Has the stack been deployed?");
|
|
879
|
+
process.exit(1);
|
|
880
|
+
}
|
|
881
|
+
const tailnetDnsName = getConfig2(exec, "tailnetDnsName", cwd);
|
|
882
|
+
const tailscaleUp = isTailscaleRunning();
|
|
883
|
+
if (!tailscaleUp && !options.json) {
|
|
884
|
+
ui.log.warn(
|
|
885
|
+
'Tailscale is not running \u2014 CLI version columns will show "\u2014".\n Open the Tailscale app or run: tailscale up'
|
|
886
|
+
);
|
|
887
|
+
}
|
|
888
|
+
const statusData = manifest.agents.map((agent) => {
|
|
889
|
+
let claudeCodeVersion = "\u2014";
|
|
890
|
+
let ghVersion = "\u2014";
|
|
891
|
+
let ghAuth = "\u2014";
|
|
892
|
+
if (tailnetDnsName) {
|
|
893
|
+
const tsHost = (0, import_core3.tailscaleHostname)(manifest.stackName, agent.name);
|
|
894
|
+
const host = `${tsHost}.${tailnetDnsName}`;
|
|
895
|
+
claudeCodeVersion = getClaudeCodeVersion(exec, host);
|
|
896
|
+
ghVersion = getGhVersion(exec, host);
|
|
897
|
+
if (ghVersion !== "\u2014") {
|
|
898
|
+
ghAuth = getGhAuthStatus(exec, host);
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
return {
|
|
902
|
+
name: agent.displayName,
|
|
903
|
+
role: agent.role,
|
|
904
|
+
instanceId: outputs[`${agent.role}InstanceId`] ?? "\u2014",
|
|
905
|
+
publicIp: outputs[`${agent.role}PublicIp`] ?? "\u2014",
|
|
906
|
+
tailscaleUrl: outputs[`${agent.role}TailscaleUrl`] ?? "\u2014",
|
|
907
|
+
claudeCodeVersion,
|
|
908
|
+
ghVersion,
|
|
909
|
+
ghAuth
|
|
31
910
|
};
|
|
32
|
-
|
|
911
|
+
});
|
|
912
|
+
if (options.json) {
|
|
913
|
+
console.log(JSON.stringify(statusData, null, 2));
|
|
914
|
+
return;
|
|
915
|
+
}
|
|
916
|
+
ui.log.step(`Stack: ${manifest.stackName} | Region: ${manifest.region}`);
|
|
917
|
+
console.log();
|
|
918
|
+
const nameW = 12;
|
|
919
|
+
const roleW = 10;
|
|
920
|
+
const idW = 22;
|
|
921
|
+
const ipW = 16;
|
|
922
|
+
const claudeW = 16;
|
|
923
|
+
const ghW = 8;
|
|
924
|
+
const authW = 6;
|
|
925
|
+
const header = [
|
|
926
|
+
"Agent".padEnd(nameW),
|
|
927
|
+
"Role".padEnd(roleW),
|
|
928
|
+
"Instance ID".padEnd(idW),
|
|
929
|
+
"Public IP".padEnd(ipW),
|
|
930
|
+
"Claude Code".padEnd(claudeW),
|
|
931
|
+
"gh".padEnd(ghW),
|
|
932
|
+
"Auth".padEnd(authW)
|
|
933
|
+
].join(" ");
|
|
934
|
+
const separator = [
|
|
935
|
+
"\u2500".repeat(nameW),
|
|
936
|
+
"\u2500".repeat(roleW),
|
|
937
|
+
"\u2500".repeat(idW),
|
|
938
|
+
"\u2500".repeat(ipW),
|
|
939
|
+
"\u2500".repeat(claudeW),
|
|
940
|
+
"\u2500".repeat(ghW),
|
|
941
|
+
"\u2500".repeat(authW)
|
|
942
|
+
].join(" ");
|
|
943
|
+
console.log(` ${header}`);
|
|
944
|
+
console.log(` ${separator}`);
|
|
945
|
+
for (const s of statusData) {
|
|
946
|
+
const row = [
|
|
947
|
+
s.name.padEnd(nameW),
|
|
948
|
+
s.role.padEnd(roleW),
|
|
949
|
+
s.instanceId.padEnd(idW),
|
|
950
|
+
s.publicIp.padEnd(ipW),
|
|
951
|
+
s.claudeCodeVersion.padEnd(claudeW),
|
|
952
|
+
s.ghVersion.padEnd(ghW),
|
|
953
|
+
s.ghAuth.padEnd(authW)
|
|
954
|
+
].join(" ");
|
|
955
|
+
console.log(` ${row}`);
|
|
956
|
+
}
|
|
957
|
+
console.log();
|
|
958
|
+
const urlLines = statusData.filter((s) => s.tailscaleUrl !== "\u2014").map((s) => ` ${s.name}: ${s.tailscaleUrl}`);
|
|
959
|
+
if (urlLines.length > 0) {
|
|
960
|
+
ui.note(urlLines.join("\n"), "Tailscale URLs");
|
|
961
|
+
}
|
|
33
962
|
};
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
963
|
+
}
|
|
964
|
+
});
|
|
965
|
+
|
|
966
|
+
// tools/validate.ts
|
|
967
|
+
function runSshCheck(exec, host, command, timeout) {
|
|
968
|
+
const result = exec.capture("ssh", [
|
|
969
|
+
"-o",
|
|
970
|
+
`ConnectTimeout=${timeout}`,
|
|
971
|
+
...SSH_OPTS2,
|
|
972
|
+
`${import_core4.SSH_USER}@${host}`,
|
|
973
|
+
`"${command.replace(/"/g, '\\"')}"`
|
|
974
|
+
]);
|
|
975
|
+
return { ok: result.exitCode === 0, output: result.stdout || result.stderr };
|
|
976
|
+
}
|
|
977
|
+
var import_core4, import_picocolors4, SSH_OPTS2, validateTool;
|
|
978
|
+
var init_validate = __esm({
|
|
979
|
+
"tools/validate.ts"() {
|
|
980
|
+
"use strict";
|
|
981
|
+
init_config();
|
|
982
|
+
import_core4 = require("@clawup/core");
|
|
983
|
+
init_workspace();
|
|
984
|
+
init_tailscale();
|
|
985
|
+
init_tool_helpers();
|
|
986
|
+
import_picocolors4 = __toESM(require("picocolors"));
|
|
987
|
+
SSH_OPTS2 = [
|
|
988
|
+
"-o",
|
|
989
|
+
"StrictHostKeyChecking=no",
|
|
990
|
+
"-o",
|
|
991
|
+
"UserKnownHostsFile=/dev/null",
|
|
992
|
+
"-o",
|
|
993
|
+
"BatchMode=yes"
|
|
994
|
+
];
|
|
995
|
+
validateTool = async (runtime, options) => {
|
|
996
|
+
const { ui, exec } = runtime;
|
|
997
|
+
ui.intro("Agent Army");
|
|
998
|
+
requireTailscale();
|
|
999
|
+
const wsResult = ensureWorkspace();
|
|
1000
|
+
if (!wsResult.ok) {
|
|
1001
|
+
ui.log.error(wsResult.error ?? "Failed to set up workspace.");
|
|
1002
|
+
process.exit(1);
|
|
1003
|
+
}
|
|
1004
|
+
const cwd = getWorkspaceDir();
|
|
1005
|
+
let configName;
|
|
1006
|
+
try {
|
|
1007
|
+
configName = resolveConfigName(options.config);
|
|
1008
|
+
} catch (err) {
|
|
1009
|
+
ui.log.error(err.message);
|
|
1010
|
+
process.exit(1);
|
|
1011
|
+
}
|
|
1012
|
+
const manifest = loadManifest(configName);
|
|
1013
|
+
if (!manifest) {
|
|
1014
|
+
ui.log.error(`Config '${configName}' could not be loaded.`);
|
|
1015
|
+
process.exit(1);
|
|
1016
|
+
}
|
|
1017
|
+
const selectResult = exec.capture("pulumi", ["stack", "select", manifest.stackName], cwd);
|
|
1018
|
+
if (selectResult.exitCode !== 0) {
|
|
1019
|
+
const initResult = exec.capture("pulumi", ["stack", "init", manifest.stackName], cwd);
|
|
1020
|
+
if (initResult.exitCode !== 0) {
|
|
1021
|
+
ui.log.error(`Could not select Pulumi stack "${manifest.stackName}".`);
|
|
1022
|
+
process.exit(1);
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
const tailnetDnsName = getConfig2(exec, "tailnetDnsName", cwd);
|
|
1026
|
+
if (!tailnetDnsName) {
|
|
1027
|
+
ui.log.error("Could not determine tailnet DNS name from Pulumi config.");
|
|
1028
|
+
process.exit(1);
|
|
1029
|
+
}
|
|
1030
|
+
const timeout = parseInt(options.timeout ?? "30", 10);
|
|
1031
|
+
const results = [];
|
|
1032
|
+
ui.log.step(`Validating ${manifest.agents.length} agents (timeout: ${timeout}s)...`);
|
|
1033
|
+
console.log();
|
|
1034
|
+
for (const agent of manifest.agents) {
|
|
1035
|
+
const tsHost = (0, import_core4.tailscaleHostname)(manifest.stackName, agent.name);
|
|
1036
|
+
const host = `${tsHost}.${tailnetDnsName}`;
|
|
1037
|
+
const checks = [];
|
|
1038
|
+
ui.log.info(`${import_picocolors4.default.bold(agent.displayName)} (${agent.role}) \u2014 ${host}`);
|
|
1039
|
+
const ssh = runSshCheck(exec, host, "echo 'SSH OK'", timeout);
|
|
1040
|
+
checks.push({
|
|
1041
|
+
name: "SSH connectivity",
|
|
1042
|
+
passed: ssh.ok,
|
|
1043
|
+
detail: ssh.ok ? "connected" : "connection failed"
|
|
1044
|
+
});
|
|
1045
|
+
if (ssh.ok) {
|
|
1046
|
+
const gateway = runSshCheck(exec, host, "systemctl --user is-active openclaw-gateway", timeout);
|
|
1047
|
+
checks.push({
|
|
1048
|
+
name: "OpenClaw gateway",
|
|
1049
|
+
passed: gateway.ok,
|
|
1050
|
+
detail: gateway.ok ? "running" : gateway.output || "not running"
|
|
1051
|
+
});
|
|
1052
|
+
const workspace = runSshCheck(
|
|
1053
|
+
exec,
|
|
1054
|
+
host,
|
|
1055
|
+
`test -f /home/${import_core4.SSH_USER}/.openclaw/workspace/SOUL.md && test -f /home/${import_core4.SSH_USER}/.openclaw/workspace/HEARTBEAT.md && echo 'OK'`,
|
|
1056
|
+
timeout
|
|
1057
|
+
);
|
|
1058
|
+
checks.push({
|
|
1059
|
+
name: "Workspace files",
|
|
1060
|
+
passed: workspace.ok,
|
|
1061
|
+
detail: workspace.ok ? "SOUL.md + HEARTBEAT.md present" : "missing files"
|
|
1062
|
+
});
|
|
1063
|
+
const claudeCode = runSshCheck(
|
|
1064
|
+
exec,
|
|
1065
|
+
host,
|
|
1066
|
+
`/home/${import_core4.SSH_USER}/.local/bin/claude --version 2>/dev/null || echo 'not installed'`,
|
|
1067
|
+
timeout
|
|
1068
|
+
);
|
|
1069
|
+
const claudeVersion = claudeCode.output.trim();
|
|
1070
|
+
const claudeInstalled = claudeCode.ok && !claudeVersion.includes("not installed");
|
|
1071
|
+
checks.push({
|
|
1072
|
+
name: "Claude Code CLI",
|
|
1073
|
+
passed: claudeInstalled,
|
|
1074
|
+
detail: claudeInstalled ? claudeVersion : "not installed"
|
|
1075
|
+
});
|
|
1076
|
+
if (claudeInstalled) {
|
|
1077
|
+
const credCheck = runSshCheck(
|
|
1078
|
+
exec,
|
|
1079
|
+
host,
|
|
1080
|
+
`grep -E '"(ANTHROPIC_API_KEY|CLAUDE_CODE_OAUTH_TOKEN)"' /home/${import_core4.SSH_USER}/.openclaw/openclaw.json | head -1`,
|
|
1081
|
+
timeout
|
|
1082
|
+
);
|
|
1083
|
+
const hasApiKey = credCheck.output.includes("ANTHROPIC_API_KEY");
|
|
1084
|
+
const hasOAuthToken = credCheck.output.includes("CLAUDE_CODE_OAUTH_TOKEN");
|
|
1085
|
+
const credIsConfigured = credCheck.ok && (hasApiKey || hasOAuthToken);
|
|
1086
|
+
const credType = hasOAuthToken ? "OAuth token" : "API key";
|
|
1087
|
+
if (credIsConfigured) {
|
|
1088
|
+
const envVar = hasOAuthToken ? "CLAUDE_CODE_OAUTH_TOKEN" : "ANTHROPIC_API_KEY";
|
|
1089
|
+
const testScript = `
|
|
1090
|
+
export ${envVar}=$(jq -r '.env.${envVar}' /home/${import_core4.SSH_USER}/.openclaw/openclaw.json)
|
|
1091
|
+
timeout 15 /home/${import_core4.SSH_USER}/.local/bin/claude -p 'hi' 2>&1 | head -5
|
|
1092
|
+
`.trim();
|
|
1093
|
+
const authTest = runSshCheck(exec, host, testScript, timeout + 15);
|
|
1094
|
+
const authWorks = authTest.ok && !authTest.output.includes("Invalid API key") && !authTest.output.includes("not authenticated");
|
|
1095
|
+
checks.push({
|
|
1096
|
+
name: "Claude Code auth",
|
|
1097
|
+
passed: authWorks,
|
|
1098
|
+
detail: authWorks ? `${credType} verified` : `${credType} test failed: ${authTest.output.substring(0, 50)}`
|
|
1099
|
+
});
|
|
1100
|
+
} else {
|
|
1101
|
+
checks.push({
|
|
1102
|
+
name: "Claude Code auth",
|
|
1103
|
+
passed: false,
|
|
1104
|
+
detail: "No credentials configured"
|
|
1105
|
+
});
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
const ghVersion = runSshCheck(
|
|
1109
|
+
exec,
|
|
1110
|
+
host,
|
|
1111
|
+
`gh --version 2>/dev/null | head -n1 || echo 'not installed'`,
|
|
1112
|
+
timeout
|
|
1113
|
+
);
|
|
1114
|
+
const ghVersionStr = ghVersion.output.trim();
|
|
1115
|
+
const ghInstalled = ghVersion.ok && !ghVersionStr.includes("not installed");
|
|
1116
|
+
checks.push({
|
|
1117
|
+
name: "GitHub CLI",
|
|
1118
|
+
passed: ghInstalled,
|
|
1119
|
+
detail: ghInstalled ? ghVersionStr : "not installed"
|
|
1120
|
+
});
|
|
1121
|
+
if (ghInstalled) {
|
|
1122
|
+
const ghAuth = runSshCheck(
|
|
1123
|
+
exec,
|
|
1124
|
+
host,
|
|
1125
|
+
`gh auth status 2>&1 | head -n2 || echo 'not authenticated'`,
|
|
1126
|
+
timeout
|
|
1127
|
+
);
|
|
1128
|
+
const ghAuthStr = ghAuth.output.trim();
|
|
1129
|
+
const ghAuthenticated = ghAuth.ok && !ghAuthStr.includes("not authenticated") && !ghAuthStr.includes("not logged");
|
|
1130
|
+
checks.push({
|
|
1131
|
+
name: "GitHub CLI auth",
|
|
1132
|
+
passed: ghAuthenticated,
|
|
1133
|
+
detail: ghAuthenticated ? "authenticated" : "not authenticated (optional)"
|
|
1134
|
+
});
|
|
1135
|
+
}
|
|
1136
|
+
} else {
|
|
1137
|
+
checks.push({ name: "OpenClaw gateway", passed: false, detail: "skipped (no SSH)" });
|
|
1138
|
+
checks.push({ name: "Workspace files", passed: false, detail: "skipped (no SSH)" });
|
|
1139
|
+
checks.push({ name: "Claude Code CLI", passed: false, detail: "skipped (no SSH)" });
|
|
1140
|
+
checks.push({ name: "Claude Code auth", passed: false, detail: "skipped (no SSH)" });
|
|
1141
|
+
checks.push({ name: "GitHub CLI", passed: false, detail: "skipped (no SSH)" });
|
|
1142
|
+
checks.push({ name: "GitHub CLI auth", passed: false, detail: "skipped (no SSH)" });
|
|
1143
|
+
}
|
|
1144
|
+
const allPassed = checks.every((c) => c.passed);
|
|
1145
|
+
for (const check of checks) {
|
|
1146
|
+
const icon = check.passed ? import_picocolors4.default.green("PASS") : import_picocolors4.default.red("FAIL");
|
|
1147
|
+
console.log(` ${icon} ${check.name}: ${check.detail ?? ""}`);
|
|
1148
|
+
}
|
|
1149
|
+
console.log();
|
|
1150
|
+
results.push({ agent: agent.displayName, checks, passed: allPassed });
|
|
1151
|
+
}
|
|
1152
|
+
const passed = results.filter((r) => r.passed).length;
|
|
1153
|
+
const failed = results.filter((r) => !r.passed).length;
|
|
1154
|
+
const summaryLines = [
|
|
1155
|
+
`Total: ${results.length}`,
|
|
1156
|
+
`Passed: ${passed}`,
|
|
1157
|
+
`Failed: ${failed}`
|
|
1158
|
+
];
|
|
1159
|
+
ui.note(summaryLines.join("\n"), "Validation Summary");
|
|
1160
|
+
if (failed > 0) {
|
|
1161
|
+
ui.log.warn("Some agents failed validation.");
|
|
1162
|
+
console.log(" 1. Wait 3-5 minutes for cloud-init to complete");
|
|
1163
|
+
const agentHints = results.filter((r) => !r.passed).map((r) => {
|
|
1164
|
+
const agent = manifest.agents.find((a) => a.displayName === r.agent);
|
|
1165
|
+
return agent ? agent.role : r.agent;
|
|
1166
|
+
});
|
|
1167
|
+
console.log(` 2. Check logs: clawup ssh ${agentHints[0] ?? "<role>"} -- journalctl -u openclaw`);
|
|
1168
|
+
console.log(" 3. Verify Tailscale: tailscale status");
|
|
1169
|
+
process.exit(1);
|
|
1170
|
+
} else {
|
|
1171
|
+
ui.log.success("All agents are healthy!");
|
|
1172
|
+
}
|
|
1173
|
+
};
|
|
1174
|
+
}
|
|
1175
|
+
});
|
|
1176
|
+
|
|
1177
|
+
// tools/destroy.ts
|
|
1178
|
+
var destroyTool;
|
|
1179
|
+
var init_destroy = __esm({
|
|
1180
|
+
"tools/destroy.ts"() {
|
|
1181
|
+
"use strict";
|
|
1182
|
+
init_config();
|
|
1183
|
+
init_tailscale();
|
|
1184
|
+
init_workspace();
|
|
1185
|
+
init_tool_helpers();
|
|
1186
|
+
init_ui();
|
|
1187
|
+
destroyTool = async (runtime, options) => {
|
|
1188
|
+
const { ui, exec } = runtime;
|
|
1189
|
+
ui.intro("Agent Army");
|
|
1190
|
+
const wsResult = ensureWorkspace();
|
|
1191
|
+
if (!wsResult.ok) {
|
|
1192
|
+
ui.log.error(wsResult.error ?? "Failed to set up workspace.");
|
|
1193
|
+
process.exit(1);
|
|
1194
|
+
}
|
|
1195
|
+
const cwd = getWorkspaceDir();
|
|
1196
|
+
let configName;
|
|
1197
|
+
try {
|
|
1198
|
+
configName = resolveConfigName(options.config);
|
|
1199
|
+
} catch (err) {
|
|
1200
|
+
ui.log.error(err.message);
|
|
1201
|
+
process.exit(1);
|
|
1202
|
+
}
|
|
1203
|
+
const manifest = loadManifest(configName);
|
|
1204
|
+
if (!manifest) {
|
|
1205
|
+
ui.log.error(`Config '${configName}' could not be loaded.`);
|
|
1206
|
+
process.exit(1);
|
|
1207
|
+
}
|
|
1208
|
+
const selectResult = exec.capture("pulumi", ["stack", "select", manifest.stackName], cwd);
|
|
1209
|
+
if (selectResult.exitCode !== 0) {
|
|
1210
|
+
const initResult = exec.capture("pulumi", ["stack", "init", manifest.stackName], cwd);
|
|
1211
|
+
if (initResult.exitCode !== 0) {
|
|
1212
|
+
ui.log.error(`Could not select Pulumi stack "${manifest.stackName}".`);
|
|
1213
|
+
process.exit(1);
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
const manifestProvider = manifest.provider ?? "aws";
|
|
1217
|
+
const resourceLabel = manifestProvider === "hetzner" ? "Hetzner servers" : "EC2 instances";
|
|
1218
|
+
const infraLabel = manifestProvider === "hetzner" ? "Firewall rules" : "VPC, subnet, and security group";
|
|
1219
|
+
ui.note(
|
|
1220
|
+
[
|
|
1221
|
+
`Stack: ${manifest.stackName}`,
|
|
1222
|
+
`Region: ${manifest.region}`,
|
|
1223
|
+
``,
|
|
1224
|
+
`Agents (${manifest.agents.length}):`,
|
|
1225
|
+
formatAgentList(manifest.agents),
|
|
1226
|
+
``,
|
|
1227
|
+
`This will PERMANENTLY DESTROY:`,
|
|
1228
|
+
` - ${manifest.agents.length} ${resourceLabel}`,
|
|
1229
|
+
` - All workspace data on those instances`,
|
|
1230
|
+
` - ${infraLabel}`,
|
|
1231
|
+
` - Tailscale device registrations`
|
|
1232
|
+
].join("\n"),
|
|
1233
|
+
"Destruction Plan"
|
|
1234
|
+
);
|
|
1235
|
+
if (!options.yes) {
|
|
1236
|
+
const typedName = await ui.text({
|
|
1237
|
+
message: `Type the stack name to confirm: "${manifest.stackName}"`,
|
|
1238
|
+
validate: (val) => {
|
|
1239
|
+
if (val !== manifest.stackName) return `Must type "${manifest.stackName}" to confirm`;
|
|
1240
|
+
return void 0;
|
|
1241
|
+
}
|
|
1242
|
+
});
|
|
1243
|
+
const confirmed = await ui.confirm({
|
|
1244
|
+
message: "Are you ABSOLUTELY sure?",
|
|
1245
|
+
initialValue: false
|
|
1246
|
+
});
|
|
1247
|
+
if (!confirmed) {
|
|
1248
|
+
ui.cancel("Destruction cancelled.");
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
syncManifestToProject(configName, cwd);
|
|
1252
|
+
ui.log.step("Running pulumi destroy...");
|
|
1253
|
+
console.log();
|
|
1254
|
+
const exitCode = await exec.stream("pulumi", ["destroy", "--yes"], { cwd });
|
|
1255
|
+
console.log();
|
|
1256
|
+
if (exitCode !== 0) {
|
|
1257
|
+
ui.log.error("Destruction failed. Check the output above for details.");
|
|
1258
|
+
process.exit(1);
|
|
1259
|
+
}
|
|
1260
|
+
const tailnetDnsName = getConfig2(exec, "tailnetDnsName", cwd);
|
|
1261
|
+
const tailscaleApiKey = getConfig2(exec, "tailscaleApiKey", cwd);
|
|
1262
|
+
if (tailnetDnsName && tailscaleApiKey) {
|
|
1263
|
+
const spinner4 = ui.spinner("Removing agents from Tailscale...");
|
|
1264
|
+
const { cleaned, failed } = cleanupTailscaleDevices(
|
|
1265
|
+
tailscaleApiKey,
|
|
1266
|
+
tailnetDnsName,
|
|
1267
|
+
manifest.stackName,
|
|
1268
|
+
manifest.agents
|
|
1269
|
+
);
|
|
1270
|
+
if (failed.length === 0) {
|
|
1271
|
+
spinner4.stop(cleaned.length > 0 ? "Tailscale devices cleaned up" : "No Tailscale devices found");
|
|
1272
|
+
} else {
|
|
1273
|
+
spinner4.stop("Some Tailscale devices could not be removed");
|
|
1274
|
+
ui.log.warn(
|
|
1275
|
+
`Could not remove: ${failed.join(", ")}. Remove manually from https://login.tailscale.com/admin/machines`
|
|
1276
|
+
);
|
|
1277
|
+
}
|
|
1278
|
+
} else if (tailnetDnsName && !tailscaleApiKey) {
|
|
1279
|
+
ui.log.warn("No Tailscale API key configured - devices must be removed manually.");
|
|
1280
|
+
console.log(" Remove devices at: https://login.tailscale.com/admin/machines");
|
|
1281
|
+
console.log(" Tip: Set a Tailscale API key (`clawup init`) for automatic cleanup.");
|
|
1282
|
+
}
|
|
1283
|
+
ui.log.success(`Stack "${manifest.stackName}" has been destroyed.`);
|
|
1284
|
+
ui.outro("Done!");
|
|
1285
|
+
};
|
|
1286
|
+
}
|
|
1287
|
+
});
|
|
1288
|
+
|
|
1289
|
+
// tools/redeploy.ts
|
|
1290
|
+
var import_picocolors5, redeployTool;
|
|
1291
|
+
var init_redeploy = __esm({
|
|
1292
|
+
"tools/redeploy.ts"() {
|
|
1293
|
+
"use strict";
|
|
1294
|
+
init_config();
|
|
1295
|
+
init_workspace();
|
|
1296
|
+
init_tailscale();
|
|
1297
|
+
init_tool_helpers();
|
|
1298
|
+
init_ui();
|
|
1299
|
+
import_picocolors5 = __toESM(require("picocolors"));
|
|
1300
|
+
redeployTool = async (runtime, options) => {
|
|
1301
|
+
const { ui, exec } = runtime;
|
|
1302
|
+
ui.intro("Agent Army");
|
|
1303
|
+
const wsResult = ensureWorkspace();
|
|
1304
|
+
if (!wsResult.ok) {
|
|
1305
|
+
ui.log.error(wsResult.error ?? "Failed to set up workspace.");
|
|
1306
|
+
process.exit(1);
|
|
1307
|
+
}
|
|
1308
|
+
const cwd = getWorkspaceDir();
|
|
1309
|
+
let configName;
|
|
1310
|
+
try {
|
|
1311
|
+
configName = resolveConfigName(options.config);
|
|
1312
|
+
} catch (err) {
|
|
1313
|
+
ui.log.error(err.message);
|
|
1314
|
+
process.exit(1);
|
|
1315
|
+
}
|
|
1316
|
+
const manifest = loadManifest(configName);
|
|
1317
|
+
if (!manifest) {
|
|
1318
|
+
ui.log.error(`Config '${configName}' could not be loaded.`);
|
|
1319
|
+
process.exit(1);
|
|
1320
|
+
}
|
|
1321
|
+
const selectResult = exec.capture("pulumi", ["stack", "select", manifest.stackName], cwd);
|
|
1322
|
+
const stackExists = selectResult.exitCode === 0;
|
|
1323
|
+
if (!stackExists) {
|
|
1324
|
+
ui.log.warn(`Stack "${manifest.stackName}" does not exist. Falling back to fresh deploy.`);
|
|
1325
|
+
const initResult = exec.capture("pulumi", ["stack", "init", manifest.stackName], cwd);
|
|
1326
|
+
if (initResult.exitCode !== 0) {
|
|
1327
|
+
ui.log.error(initResult.stderr || `Could not create Pulumi stack "${manifest.stackName}".`);
|
|
1328
|
+
process.exit(1);
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
ui.note(
|
|
1332
|
+
[
|
|
1333
|
+
`Stack: ${manifest.stackName}`,
|
|
1334
|
+
`Region: ${manifest.region}`,
|
|
1335
|
+
`Mode: ${stackExists ? "In-place update (--refresh)" : "Fresh deploy (new stack)"}`,
|
|
1336
|
+
``,
|
|
1337
|
+
`Agents (${manifest.agents.length}):`,
|
|
1338
|
+
formatAgentList(manifest.agents),
|
|
1339
|
+
``,
|
|
1340
|
+
stackExists ? `This will update resources in-place where possible.
|
|
1341
|
+
Tailscale devices will be preserved.` : `This will create all resources from scratch.`
|
|
1342
|
+
].join("\n"),
|
|
1343
|
+
"Redeploy Summary"
|
|
1344
|
+
);
|
|
1345
|
+
if (!options.yes) {
|
|
1346
|
+
const confirmed = await ui.confirm({
|
|
1347
|
+
message: "Proceed with redeploy?"
|
|
1348
|
+
});
|
|
1349
|
+
if (!confirmed) {
|
|
1350
|
+
ui.cancel("Redeploy cancelled.");
|
|
1351
|
+
return;
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
syncManifestToProject(configName, cwd);
|
|
1355
|
+
const configSetResult = exec.capture("pulumi", ["config", "set", "instanceType", manifest.instanceType], cwd);
|
|
1356
|
+
if (configSetResult.exitCode !== 0) {
|
|
1357
|
+
ui.log.error(`Failed to set Pulumi config: ${configSetResult.stderr || "unknown error"}`);
|
|
1358
|
+
process.exit(1);
|
|
1359
|
+
}
|
|
1360
|
+
const tailnetDnsName = getConfig2(exec, "tailnetDnsName", cwd);
|
|
1361
|
+
const tailscaleApiKey = getConfig2(exec, "tailscaleApiKey", cwd);
|
|
1362
|
+
if (tailnetDnsName && tailscaleApiKey) {
|
|
1363
|
+
const spinner4 = ui.spinner("Cleaning up stale Tailscale devices...");
|
|
1364
|
+
const { cleaned, failed } = cleanupTailscaleDevices(
|
|
1365
|
+
tailscaleApiKey,
|
|
1366
|
+
tailnetDnsName,
|
|
1367
|
+
manifest.stackName,
|
|
1368
|
+
manifest.agents
|
|
1369
|
+
);
|
|
1370
|
+
if (cleaned.length > 0) {
|
|
1371
|
+
spinner4.stop(`Removed ${cleaned.length} stale Tailscale device(s)`);
|
|
1372
|
+
} else {
|
|
1373
|
+
spinner4.stop("No stale Tailscale devices found");
|
|
1374
|
+
}
|
|
1375
|
+
if (failed.length > 0) {
|
|
1376
|
+
ui.log.warn(
|
|
1377
|
+
`Could not remove some devices: ${failed.join(", ")}. Check https://login.tailscale.com/admin/machines`
|
|
1378
|
+
);
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
if (tailscaleApiKey) {
|
|
1382
|
+
const spinner4 = ui.spinner("Ensuring Tailscale MagicDNS is enabled...");
|
|
1383
|
+
const magicDnsChanged = ensureMagicDns(tailscaleApiKey);
|
|
1384
|
+
spinner4.stop(magicDnsChanged ? "MagicDNS enabled" : "MagicDNS OK");
|
|
1385
|
+
}
|
|
1386
|
+
const hasLinearAgents = manifest.agents.some(
|
|
1387
|
+
(a) => !!getConfig2(exec, `${a.role}LinearApiKey`, cwd)
|
|
1388
|
+
);
|
|
1389
|
+
if (hasLinearAgents && tailscaleApiKey) {
|
|
1390
|
+
const spinner4 = ui.spinner("Ensuring Tailscale Funnel prerequisites...");
|
|
1391
|
+
const funnel = ensureTailscaleFunnel(tailscaleApiKey);
|
|
1392
|
+
const changes = [];
|
|
1393
|
+
if (funnel.funnelAcl) changes.push("Funnel ACL enabled");
|
|
1394
|
+
spinner4.stop(changes.length > 0 ? changes.join(", ") : "Funnel prerequisites OK");
|
|
1395
|
+
}
|
|
1396
|
+
const pulumiArgs = stackExists ? ["up", "--refresh", "--yes"] : ["up", "--yes"];
|
|
1397
|
+
ui.log.step(stackExists ? "Running pulumi up --refresh..." : "Running pulumi up...");
|
|
1398
|
+
console.log();
|
|
1399
|
+
const exitCode = await exec.stream("pulumi", pulumiArgs, { cwd });
|
|
1400
|
+
console.log();
|
|
1401
|
+
if (exitCode !== 0) {
|
|
1402
|
+
ui.log.error("Redeploy failed. Check the output above for details.");
|
|
1403
|
+
if (stackExists) {
|
|
1404
|
+
ui.log.warn(
|
|
1405
|
+
"If the in-place update cannot recover, try: clawup destroy && clawup deploy"
|
|
1406
|
+
);
|
|
1407
|
+
}
|
|
1408
|
+
process.exit(1);
|
|
1409
|
+
}
|
|
1410
|
+
ui.log.success("Redeploy complete!");
|
|
1411
|
+
const outputsResult = exec.capture("pulumi", [
|
|
1412
|
+
"stack",
|
|
1413
|
+
"output",
|
|
1414
|
+
"--json",
|
|
1415
|
+
"--show-secrets"
|
|
1416
|
+
], cwd);
|
|
1417
|
+
if (outputsResult.exitCode === 0) {
|
|
1418
|
+
try {
|
|
1419
|
+
const outputs = JSON.parse(outputsResult.stdout);
|
|
1420
|
+
const lines = [];
|
|
1421
|
+
for (const agent of manifest.agents) {
|
|
1422
|
+
const urlKey = `${agent.role}TailscaleUrl`;
|
|
1423
|
+
const ipKey = `${agent.role}PublicIp`;
|
|
1424
|
+
const idKey = `${agent.role}InstanceId`;
|
|
1425
|
+
lines.push(`${agent.displayName} (${agent.role}):`);
|
|
1426
|
+
if (outputs[urlKey]) lines.push(` Tailscale URL: ${outputs[urlKey]}`);
|
|
1427
|
+
if (outputs[ipKey]) lines.push(` Public IP: ${outputs[ipKey]}`);
|
|
1428
|
+
if (outputs[idKey]) lines.push(` Instance ID: ${outputs[idKey]}`);
|
|
1429
|
+
lines.push("");
|
|
1430
|
+
}
|
|
1431
|
+
ui.note(lines.join("\n"), "Agent Details");
|
|
1432
|
+
} catch (err) {
|
|
1433
|
+
ui.log.warn(`Could not parse stack outputs: ${err instanceof Error ? err.message : String(err)}`);
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
if (!isTailscaleInstalled()) {
|
|
1437
|
+
const hint = process.platform === "darwin" ? "Install from the Mac App Store or https://tailscale.com/download" : "Install from https://tailscale.com/download";
|
|
1438
|
+
ui.log.warn(
|
|
1439
|
+
`Tailscale is required to connect to agents.
|
|
1440
|
+
${hint}
|
|
1441
|
+
Then run: ${import_picocolors5.default.cyan("tailscale up")}`
|
|
1442
|
+
);
|
|
1443
|
+
} else if (!isTailscaleRunning()) {
|
|
1444
|
+
ui.log.warn(
|
|
1445
|
+
`Tailscale is not running. Start it before validating agents.
|
|
1446
|
+
Open the Tailscale app or run: ${import_picocolors5.default.cyan("tailscale up")}`
|
|
1447
|
+
);
|
|
1448
|
+
}
|
|
1449
|
+
ui.outro("Agents updated. Run `clawup validate` to verify.");
|
|
1450
|
+
};
|
|
1451
|
+
}
|
|
1452
|
+
});
|
|
1453
|
+
|
|
1454
|
+
// tools/push.ts
|
|
1455
|
+
function sshExec(exec, host, command) {
|
|
1456
|
+
const result = exec.capture("ssh", [
|
|
1457
|
+
"-o",
|
|
1458
|
+
"ConnectTimeout=15",
|
|
1459
|
+
...SSH_OPTS3,
|
|
1460
|
+
`${import_core5.SSH_USER}@${host}`,
|
|
1461
|
+
`"${command.replace(/"/g, '\\"')}"`
|
|
1462
|
+
]);
|
|
1463
|
+
return { ok: result.exitCode === 0, output: result.stdout || result.stderr };
|
|
1464
|
+
}
|
|
1465
|
+
function rsyncDir(exec, localDir, host, remoteDir) {
|
|
1466
|
+
const src = localDir.endsWith("/") ? localDir : `${localDir}/`;
|
|
1467
|
+
const result = exec.capture("rsync", [
|
|
1468
|
+
"-avz",
|
|
1469
|
+
"--delete",
|
|
1470
|
+
"-e",
|
|
1471
|
+
`ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o BatchMode=yes`,
|
|
1472
|
+
src,
|
|
1473
|
+
`${import_core5.SSH_USER}@${host}:${remoteDir}/`
|
|
1474
|
+
]);
|
|
1475
|
+
return { ok: result.exitCode === 0, output: result.stdout || result.stderr };
|
|
1476
|
+
}
|
|
1477
|
+
function scpFile(exec, localFile, host, remotePath) {
|
|
1478
|
+
const result = exec.capture("scp", [
|
|
1479
|
+
...SSH_OPTS3,
|
|
1480
|
+
localFile,
|
|
1481
|
+
`${import_core5.SSH_USER}@${host}:${remotePath}`
|
|
1482
|
+
]);
|
|
1483
|
+
return { ok: result.exitCode === 0, output: result.stdout || result.stderr };
|
|
1484
|
+
}
|
|
1485
|
+
function findAgent(agents, query) {
|
|
1486
|
+
const q = query.toLowerCase();
|
|
1487
|
+
const resolvedRole = import_core5.AGENT_ALIASES[q] ?? q;
|
|
1488
|
+
return agents.find(
|
|
1489
|
+
(a) => a.role === resolvedRole || a.name === q || a.name === `agent-${q}` || a.displayName.toLowerCase() === q
|
|
1490
|
+
);
|
|
1491
|
+
}
|
|
1492
|
+
var fs4, path4, import_core5, import_identity, import_core6, import_core7, os3, import_picocolors6, SSH_OPTS3, REMOTE_WORKSPACE, REMOTE_SKILLS, pushTool;
|
|
1493
|
+
var init_push = __esm({
|
|
1494
|
+
"tools/push.ts"() {
|
|
1495
|
+
"use strict";
|
|
1496
|
+
fs4 = __toESM(require("fs"));
|
|
1497
|
+
path4 = __toESM(require("path"));
|
|
1498
|
+
init_config();
|
|
1499
|
+
import_core5 = require("@clawup/core");
|
|
1500
|
+
init_workspace();
|
|
1501
|
+
init_pulumi();
|
|
1502
|
+
import_identity = require("@clawup/core/identity");
|
|
1503
|
+
import_core6 = require("@clawup/core");
|
|
1504
|
+
import_core7 = require("@clawup/core");
|
|
1505
|
+
os3 = __toESM(require("os"));
|
|
1506
|
+
import_picocolors6 = __toESM(require("picocolors"));
|
|
1507
|
+
SSH_OPTS3 = [
|
|
1508
|
+
"-o",
|
|
1509
|
+
"StrictHostKeyChecking=no",
|
|
1510
|
+
"-o",
|
|
1511
|
+
"UserKnownHostsFile=/dev/null",
|
|
1512
|
+
"-o",
|
|
1513
|
+
"BatchMode=yes"
|
|
1514
|
+
];
|
|
1515
|
+
REMOTE_WORKSPACE = `/home/${import_core5.SSH_USER}/.openclaw/workspace`;
|
|
1516
|
+
REMOTE_SKILLS = `${REMOTE_WORKSPACE}/skills`;
|
|
1517
|
+
pushTool = async (runtime, options) => {
|
|
1518
|
+
const { ui, exec } = runtime;
|
|
1519
|
+
ui.intro("Clawup");
|
|
1520
|
+
const wsResult = ensureWorkspace();
|
|
1521
|
+
if (!wsResult.ok) {
|
|
1522
|
+
throw new Error(wsResult.error ?? "Failed to set up workspace.");
|
|
1523
|
+
}
|
|
1524
|
+
const cwd = getWorkspaceDir();
|
|
1525
|
+
let configName;
|
|
1526
|
+
try {
|
|
1527
|
+
configName = resolveConfigName(options.config);
|
|
1528
|
+
} catch (err) {
|
|
1529
|
+
throw new Error(err.message);
|
|
1530
|
+
}
|
|
1531
|
+
const manifest = loadManifest(configName);
|
|
1532
|
+
if (!manifest) {
|
|
1533
|
+
throw new Error(`Config '${configName}' could not be loaded.`);
|
|
1534
|
+
}
|
|
1535
|
+
const stackResult = selectOrCreateStack(manifest.stackName, cwd);
|
|
1536
|
+
if (!stackResult.ok) {
|
|
1537
|
+
throw new Error(`Could not select Pulumi stack "${manifest.stackName}".`);
|
|
1538
|
+
}
|
|
1539
|
+
const tailnetDnsName = getConfig("tailnetDnsName", cwd);
|
|
1540
|
+
if (!tailnetDnsName) {
|
|
1541
|
+
throw new Error("Could not determine tailnet DNS name from Pulumi config.");
|
|
1542
|
+
}
|
|
1543
|
+
const noFlags = !options.skills && !options.workspace && !options.memoryReset && !options.openclaw && !options.pushConfig;
|
|
1544
|
+
const doSkills = options.skills || noFlags;
|
|
1545
|
+
const doWorkspace = options.workspace || noFlags;
|
|
1546
|
+
let targetAgents = manifest.agents;
|
|
1547
|
+
if (options.agent) {
|
|
1548
|
+
const matched = findAgent(manifest.agents, options.agent);
|
|
1549
|
+
if (!matched) {
|
|
1550
|
+
const validNames = manifest.agents.map((a) => `${a.role}, ${a.displayName.toLowerCase()}, ${a.name}`).join("\n ");
|
|
1551
|
+
ui.log.error(
|
|
1552
|
+
`Unknown agent: "${options.agent}"
|
|
1553
|
+
Valid identifiers:
|
|
1554
|
+
${validNames}`
|
|
1555
|
+
);
|
|
1556
|
+
return;
|
|
1557
|
+
}
|
|
1558
|
+
targetAgents = [matched];
|
|
1559
|
+
}
|
|
1560
|
+
const actions = [];
|
|
1561
|
+
if (doSkills) actions.push("skills");
|
|
1562
|
+
if (doWorkspace) actions.push("workspace");
|
|
1563
|
+
if (options.memoryReset) actions.push("memory-reset");
|
|
1564
|
+
if (options.openclaw) actions.push("openclaw upgrade");
|
|
1565
|
+
if (options.pushConfig) actions.push("config");
|
|
1566
|
+
ui.log.step(
|
|
1567
|
+
`Pushing [${actions.join(", ")}] to ${targetAgents.length} agent(s)...`
|
|
1568
|
+
);
|
|
1569
|
+
console.log();
|
|
1570
|
+
let allOk = true;
|
|
1571
|
+
for (const agent of targetAgents) {
|
|
1572
|
+
const tsHost = (0, import_core5.tailscaleHostname)(manifest.stackName, agent.name);
|
|
1573
|
+
const host = `${tsHost}.${tailnetDnsName}`;
|
|
1574
|
+
ui.log.info(`${import_picocolors6.default.bold(agent.displayName)} (${agent.role}) \u2014 ${host}`);
|
|
1575
|
+
let needsRestart = false;
|
|
1576
|
+
if (doWorkspace || doSkills) {
|
|
1577
|
+
try {
|
|
1578
|
+
const identityCacheDir = path4.join(os3.homedir(), ".clawup", "identity-cache");
|
|
1579
|
+
const identity = (0, import_identity.fetchIdentitySync)(agent.identity, identityCacheDir);
|
|
1580
|
+
sshExec(exec, host, `mkdir -p ${REMOTE_WORKSPACE}`);
|
|
1581
|
+
const tmpDir = fs4.mkdtempSync(path4.join(os3.tmpdir(), "push-identity-"));
|
|
1582
|
+
for (const [relPath, content] of Object.entries(identity.files)) {
|
|
1583
|
+
const localFile = path4.join(tmpDir, relPath);
|
|
1584
|
+
fs4.mkdirSync(path4.dirname(localFile), { recursive: true });
|
|
1585
|
+
fs4.writeFileSync(localFile, content);
|
|
1586
|
+
}
|
|
1587
|
+
if (doWorkspace) {
|
|
1588
|
+
const topFiles = Object.keys(identity.files).filter((f) => !f.startsWith("skills/"));
|
|
1589
|
+
let wsOk = true;
|
|
1590
|
+
for (const relPath of topFiles) {
|
|
1591
|
+
const localFile = path4.join(tmpDir, relPath);
|
|
1592
|
+
const result = scpFile(exec, localFile, host, `${REMOTE_WORKSPACE}/${relPath}`);
|
|
1593
|
+
if (!result.ok) {
|
|
1594
|
+
console.log(` ${import_picocolors6.default.red("FAIL")} workspace file ${relPath}: ${result.output.substring(0, 100)}`);
|
|
1595
|
+
wsOk = false;
|
|
1596
|
+
allOk = false;
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
if (wsOk) {
|
|
1600
|
+
console.log(` ${import_picocolors6.default.green("OK")} workspace files synced (${topFiles.length} files)`);
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
if (doSkills) {
|
|
1604
|
+
const skillsLocalDir = path4.join(tmpDir, "skills");
|
|
1605
|
+
if (fs4.existsSync(skillsLocalDir)) {
|
|
1606
|
+
sshExec(exec, host, `mkdir -p ${REMOTE_SKILLS}`);
|
|
1607
|
+
const result = rsyncDir(exec, skillsLocalDir, host, REMOTE_SKILLS);
|
|
1608
|
+
if (result.ok) {
|
|
1609
|
+
console.log(` ${import_picocolors6.default.green("OK")} skills synced`);
|
|
1610
|
+
} else {
|
|
1611
|
+
console.log(` ${import_picocolors6.default.red("FAIL")} skills sync failed: ${result.output.substring(0, 100)}`);
|
|
1612
|
+
allOk = false;
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
const { public: publicSkills } = (0, import_core6.classifySkills)(identity.manifest.skills);
|
|
1616
|
+
for (const skill of publicSkills) {
|
|
1617
|
+
const cmd = `export NVM_DIR="$HOME/.nvm" && [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" && clawhub install ${skill.slug}`;
|
|
1618
|
+
const result = sshExec(exec, host, cmd);
|
|
1619
|
+
if (result.ok) {
|
|
1620
|
+
console.log(` ${import_picocolors6.default.green("OK")} clawhub skill installed: ${skill.slug}`);
|
|
1621
|
+
} else {
|
|
1622
|
+
console.log(` ${import_picocolors6.default.red("FAIL")} clawhub skill ${skill.slug}: ${result.output.substring(0, 100)}`);
|
|
1623
|
+
allOk = false;
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
if (identity.manifest.deps?.length) {
|
|
1627
|
+
const resolved = (0, import_core7.resolveDeps)(identity.manifest.deps);
|
|
1628
|
+
for (const dep of resolved) {
|
|
1629
|
+
if (!dep.entry.postInstallScript) continue;
|
|
1630
|
+
const cmd = dep.entry.postInstallScript;
|
|
1631
|
+
const result = sshExec(exec, host, cmd);
|
|
1632
|
+
if (result.ok) {
|
|
1633
|
+
console.log(` ${import_picocolors6.default.green("OK")} dep post-install: ${dep.name}`);
|
|
1634
|
+
} else {
|
|
1635
|
+
console.log(` ${import_picocolors6.default.red("FAIL")} dep post-install ${dep.name}: ${result.output.substring(0, 100)}`);
|
|
1636
|
+
allOk = false;
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
fs4.rmSync(tmpDir, { recursive: true, force: true });
|
|
1642
|
+
} catch (err) {
|
|
1643
|
+
console.log(` ${import_picocolors6.default.red("FAIL")} identity fetch failed: ${err.message}`);
|
|
1644
|
+
allOk = false;
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
if (options.memoryReset) {
|
|
1648
|
+
const cmd = `rm -rf ${REMOTE_WORKSPACE}/memory ${REMOTE_WORKSPACE}/MEMORY.md`;
|
|
1649
|
+
const result = sshExec(exec, host, cmd);
|
|
1650
|
+
if (result.ok) {
|
|
1651
|
+
console.log(` ${import_picocolors6.default.green("OK")} memory reset`);
|
|
1652
|
+
} else {
|
|
1653
|
+
console.log(` ${import_picocolors6.default.red("FAIL")} memory reset failed: ${result.output.substring(0, 100)}`);
|
|
1654
|
+
allOk = false;
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
if (options.openclaw) {
|
|
1658
|
+
const cmd = `npm install -g openclaw@latest 2>&1`;
|
|
1659
|
+
const result = sshExec(exec, host, cmd);
|
|
1660
|
+
if (result.ok) {
|
|
1661
|
+
console.log(` ${import_picocolors6.default.green("OK")} openclaw upgraded`);
|
|
1662
|
+
needsRestart = true;
|
|
1663
|
+
} else {
|
|
1664
|
+
console.log(` ${import_picocolors6.default.red("FAIL")} openclaw upgrade failed: ${result.output.substring(0, 100)}`);
|
|
1665
|
+
allOk = false;
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
if (options.pushConfig) {
|
|
1669
|
+
const localConfig = path4.join(
|
|
1670
|
+
`/home/${import_core5.SSH_USER}/.openclaw`,
|
|
1671
|
+
"openclaw.json"
|
|
1672
|
+
);
|
|
1673
|
+
const operatorConfig = path4.join(
|
|
1674
|
+
process.env.HOME ?? "",
|
|
1675
|
+
".openclaw",
|
|
1676
|
+
"openclaw.json"
|
|
1677
|
+
);
|
|
1678
|
+
if (fs4.existsSync(operatorConfig)) {
|
|
1679
|
+
const result = scpFile(
|
|
1680
|
+
exec,
|
|
1681
|
+
operatorConfig,
|
|
1682
|
+
host,
|
|
1683
|
+
`/home/${import_core5.SSH_USER}/.openclaw/openclaw.json`
|
|
1684
|
+
);
|
|
1685
|
+
if (result.ok) {
|
|
1686
|
+
console.log(` ${import_picocolors6.default.green("OK")} openclaw.json pushed`);
|
|
1687
|
+
needsRestart = true;
|
|
1688
|
+
} else {
|
|
1689
|
+
console.log(` ${import_picocolors6.default.red("FAIL")} config push failed: ${result.output.substring(0, 100)}`);
|
|
1690
|
+
allOk = false;
|
|
1691
|
+
}
|
|
1692
|
+
} else {
|
|
1693
|
+
console.log(` ${import_picocolors6.default.red("FAIL")} local openclaw.json not found at ${operatorConfig}`);
|
|
1694
|
+
allOk = false;
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
if (needsRestart) {
|
|
1698
|
+
const result = sshExec(exec, host, "systemctl --user restart openclaw-gateway");
|
|
1699
|
+
if (result.ok) {
|
|
1700
|
+
console.log(` ${import_picocolors6.default.green("OK")} gateway restarted`);
|
|
1701
|
+
} else {
|
|
1702
|
+
console.log(` ${import_picocolors6.default.red("FAIL")} gateway restart failed: ${result.output.substring(0, 100)}`);
|
|
1703
|
+
allOk = false;
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
console.log();
|
|
1707
|
+
}
|
|
1708
|
+
if (allOk) {
|
|
1709
|
+
ui.log.success(`Push completed successfully for ${targetAgents.length} agent(s).`);
|
|
1710
|
+
} else {
|
|
1711
|
+
ui.log.warn("Push completed with some failures. Check output above.");
|
|
1712
|
+
throw new Error("Push completed with failures.");
|
|
1713
|
+
}
|
|
1714
|
+
};
|
|
1715
|
+
}
|
|
1716
|
+
});
|
|
1717
|
+
|
|
1718
|
+
// tools/webhooks.ts
|
|
1719
|
+
function sshExec2(exec, host, command) {
|
|
1720
|
+
const result = exec.capture("ssh", [
|
|
1721
|
+
"-o",
|
|
1722
|
+
"ConnectTimeout=15",
|
|
1723
|
+
...SSH_OPTS4,
|
|
1724
|
+
`${import_core8.SSH_USER}@${host}`,
|
|
1725
|
+
`"${command.replace(/"/g, '\\"')}"`
|
|
1726
|
+
]);
|
|
1727
|
+
return { ok: result.exitCode === 0, output: result.stdout || result.stderr };
|
|
1728
|
+
}
|
|
1729
|
+
var import_core8, import_picocolors7, SSH_OPTS4, webhooksSetupTool;
|
|
1730
|
+
var init_webhooks = __esm({
|
|
1731
|
+
"tools/webhooks.ts"() {
|
|
1732
|
+
"use strict";
|
|
1733
|
+
init_config();
|
|
1734
|
+
import_core8 = require("@clawup/core");
|
|
1735
|
+
init_workspace();
|
|
1736
|
+
init_tool_helpers();
|
|
1737
|
+
import_picocolors7 = __toESM(require("picocolors"));
|
|
1738
|
+
SSH_OPTS4 = [
|
|
1739
|
+
"-o",
|
|
1740
|
+
"StrictHostKeyChecking=no",
|
|
1741
|
+
"-o",
|
|
1742
|
+
"UserKnownHostsFile=/dev/null",
|
|
1743
|
+
"-o",
|
|
1744
|
+
"BatchMode=yes"
|
|
1745
|
+
];
|
|
1746
|
+
webhooksSetupTool = async (runtime, options) => {
|
|
1747
|
+
const { ui, exec } = runtime;
|
|
1748
|
+
ui.intro("Agent Army \u2014 Webhook Setup");
|
|
1749
|
+
const wsResult = ensureWorkspace();
|
|
1750
|
+
if (!wsResult.ok) {
|
|
1751
|
+
ui.log.error(wsResult.error ?? "Failed to set up workspace.");
|
|
1752
|
+
process.exit(1);
|
|
1753
|
+
}
|
|
1754
|
+
const cwd = getWorkspaceDir();
|
|
1755
|
+
let configName;
|
|
1756
|
+
try {
|
|
1757
|
+
configName = resolveConfigName(options.config);
|
|
1758
|
+
} catch (err) {
|
|
1759
|
+
ui.log.error(err.message);
|
|
1760
|
+
process.exit(1);
|
|
1761
|
+
}
|
|
1762
|
+
const manifest = loadManifest(configName);
|
|
1763
|
+
if (!manifest) {
|
|
1764
|
+
ui.log.error(`Config '${configName}' could not be loaded.`);
|
|
1765
|
+
process.exit(1);
|
|
1766
|
+
}
|
|
1767
|
+
const selectResult = exec.capture("pulumi", ["stack", "select", manifest.stackName], cwd);
|
|
1768
|
+
if (selectResult.exitCode !== 0) {
|
|
1769
|
+
ui.log.error(`Could not select Pulumi stack "${manifest.stackName}". Run ${import_picocolors7.default.cyan("clawup deploy")} first.`);
|
|
1770
|
+
process.exit(1);
|
|
1771
|
+
}
|
|
1772
|
+
const outputs = getStackOutputs(exec, true, cwd);
|
|
1773
|
+
if (!outputs) {
|
|
1774
|
+
ui.log.error(`Could not fetch stack outputs. Run ${import_picocolors7.default.cyan("clawup deploy")} first.`);
|
|
1775
|
+
process.exit(1);
|
|
1776
|
+
}
|
|
1777
|
+
const tailnetDnsName = getConfig2(exec, "tailnetDnsName", cwd);
|
|
1778
|
+
if (!tailnetDnsName) {
|
|
1779
|
+
ui.log.error("Could not read tailnetDnsName from Pulumi config.");
|
|
1780
|
+
process.exit(1);
|
|
1781
|
+
}
|
|
1782
|
+
const agentsWithUrls = manifest.agents.filter(
|
|
1783
|
+
(agent) => outputs[`${agent.role}WebhookUrl`]
|
|
1784
|
+
);
|
|
1785
|
+
if (agentsWithUrls.length === 0) {
|
|
1786
|
+
ui.log.error(
|
|
1787
|
+
`No webhook URLs found in stack outputs.
|
|
1788
|
+
Make sure agents are deployed and exposing webhook endpoints.
|
|
1789
|
+
Run ${import_picocolors7.default.cyan("clawup deploy")} if you haven't already.`
|
|
1790
|
+
);
|
|
1791
|
+
process.exit(1);
|
|
1792
|
+
}
|
|
1793
|
+
ui.note(
|
|
1794
|
+
[
|
|
1795
|
+
"This will walk you through creating Linear webhooks for each agent.",
|
|
1796
|
+
"You'll need access to your Linear workspace settings."
|
|
1797
|
+
].join("\n"),
|
|
1798
|
+
"Linear Webhook Setup"
|
|
1799
|
+
);
|
|
1800
|
+
const secrets = [];
|
|
1801
|
+
for (const agent of manifest.agents) {
|
|
1802
|
+
const webhookUrl = outputs[`${agent.role}WebhookUrl`];
|
|
1803
|
+
if (!webhookUrl) {
|
|
1804
|
+
ui.log.warn(`No webhook URL found for ${agent.displayName} (${agent.role}) \u2014 skipping.`);
|
|
1805
|
+
continue;
|
|
1806
|
+
}
|
|
1807
|
+
ui.note(
|
|
1808
|
+
[
|
|
1809
|
+
`Webhook URL: ${import_picocolors7.default.cyan(String(webhookUrl))}`,
|
|
1810
|
+
"",
|
|
1811
|
+
"Steps:",
|
|
1812
|
+
'1. Go to Linear Settings \u2192 API \u2192 Webhooks \u2192 "New webhook"',
|
|
1813
|
+
"2. Paste the URL above",
|
|
1814
|
+
"3. Select events to receive (e.g., Issues, Comments)",
|
|
1815
|
+
'4. Create the webhook and copy the "Signing secret"'
|
|
1816
|
+
].join("\n"),
|
|
1817
|
+
`${agent.displayName} (${agent.role})`
|
|
1818
|
+
);
|
|
1819
|
+
const secret = await ui.text({
|
|
1820
|
+
message: `Signing secret for ${agent.displayName}`,
|
|
1821
|
+
placeholder: "Paste the signing secret from Linear",
|
|
1822
|
+
validate: (val) => {
|
|
1823
|
+
if (!val) return "Signing secret is required";
|
|
1824
|
+
}
|
|
1825
|
+
});
|
|
1826
|
+
secrets.push({
|
|
1827
|
+
role: agent.role,
|
|
1828
|
+
name: agent.name,
|
|
1829
|
+
agentName: agent.displayName,
|
|
1830
|
+
secret
|
|
1831
|
+
});
|
|
1832
|
+
}
|
|
1833
|
+
if (secrets.length === 0) {
|
|
1834
|
+
ui.log.warn("No webhook secrets collected.");
|
|
1835
|
+
ui.outro("Nothing to do.");
|
|
1836
|
+
return;
|
|
1837
|
+
}
|
|
1838
|
+
const configSpinner = ui.spinner("Saving webhook secrets to Pulumi config...");
|
|
1839
|
+
for (const { role, secret } of secrets) {
|
|
1840
|
+
exec.capture(
|
|
1841
|
+
"pulumi",
|
|
1842
|
+
["config", "set", `${role}LinearWebhookSecret`, secret, "--secret"],
|
|
1843
|
+
cwd
|
|
1844
|
+
);
|
|
1845
|
+
}
|
|
1846
|
+
configSpinner.stop(`Saved ${secrets.length} webhook secret(s) to Pulumi config`);
|
|
1847
|
+
const applySpinner = ui.spinner("Applying webhook secrets to running agents...");
|
|
1848
|
+
let applied = 0;
|
|
1849
|
+
let failed = 0;
|
|
1850
|
+
for (const { role, name, agentName, secret } of secrets) {
|
|
1851
|
+
const tsHost = (0, import_core8.tailscaleHostname)(manifest.stackName, name);
|
|
1852
|
+
const host = `${tsHost}.${tailnetDnsName}`;
|
|
1853
|
+
const escapedSecret = secret.replace(/'/g, "'\\''");
|
|
1854
|
+
const jqCmd = `jq '.plugins.entries.linear.config.webhookSecret = \\"${escapedSecret.replace(/\\/g, "\\\\").replace(/"/g, '\\\\\\"')}\\"' /home/${import_core8.SSH_USER}/.openclaw/openclaw.json > /tmp/openclaw-patched.json && mv /tmp/openclaw-patched.json /home/${import_core8.SSH_USER}/.openclaw/openclaw.json`;
|
|
1855
|
+
const patchResult = sshExec2(exec, host, jqCmd);
|
|
1856
|
+
if (!patchResult.ok) {
|
|
1857
|
+
applySpinner.stop(`Failed to patch config on ${agentName}`);
|
|
1858
|
+
ui.log.warn(` ${agentName}: ${patchResult.output}`);
|
|
1859
|
+
failed++;
|
|
1860
|
+
continue;
|
|
1861
|
+
}
|
|
1862
|
+
const restartResult = sshExec2(exec, host, "systemctl --user restart openclaw-gateway");
|
|
1863
|
+
if (!restartResult.ok) {
|
|
1864
|
+
applySpinner.stop(`Failed to restart gateway on ${agentName}`);
|
|
1865
|
+
ui.log.warn(` ${agentName}: ${restartResult.output}`);
|
|
1866
|
+
failed++;
|
|
1867
|
+
continue;
|
|
1868
|
+
}
|
|
1869
|
+
applied++;
|
|
1870
|
+
}
|
|
1871
|
+
if (applied > 0 && failed === 0) {
|
|
1872
|
+
applySpinner.stop(`Applied to ${applied} agent(s)`);
|
|
1873
|
+
} else if (applied > 0) {
|
|
1874
|
+
applySpinner.stop(`Applied to ${applied} agent(s), ${failed} failed`);
|
|
1875
|
+
} else {
|
|
1876
|
+
applySpinner.stop(`Failed to apply to any agents`);
|
|
1877
|
+
}
|
|
1878
|
+
if (failed > 0) {
|
|
1879
|
+
ui.log.warn(
|
|
1880
|
+
`Some agents could not be reached. They will pick up the secrets on next deploy.`
|
|
1881
|
+
);
|
|
1882
|
+
}
|
|
1883
|
+
ui.outro("Webhook setup complete!");
|
|
1884
|
+
};
|
|
1885
|
+
}
|
|
1886
|
+
});
|
|
1887
|
+
|
|
1888
|
+
// adapters/cli-adapter.ts
|
|
1889
|
+
function createCLIAdapter() {
|
|
1890
|
+
return {
|
|
1891
|
+
ui: new CLIUIAdapter(),
|
|
1892
|
+
exec: new CLIExecAdapter(),
|
|
1893
|
+
platform: "cli"
|
|
1894
|
+
};
|
|
1895
|
+
}
|
|
1896
|
+
var p3, import_picocolors8, import_child_process6, CLIExecAdapter, CLIUIAdapter, cliAdapter;
|
|
1897
|
+
var init_cli_adapter = __esm({
|
|
1898
|
+
"adapters/cli-adapter.ts"() {
|
|
1899
|
+
"use strict";
|
|
1900
|
+
p3 = __toESM(require("@clack/prompts"));
|
|
1901
|
+
import_picocolors8 = __toESM(require("picocolors"));
|
|
1902
|
+
import_child_process6 = require("child_process");
|
|
1903
|
+
init_process();
|
|
1904
|
+
init_vendor();
|
|
1905
|
+
CLIExecAdapter = class {
|
|
1906
|
+
capture(command, args = [], cwd) {
|
|
1907
|
+
const resolved = resolveCommand(command);
|
|
1908
|
+
try {
|
|
1909
|
+
const result = (0, import_child_process6.execSync)([resolved, ...args].join(" "), {
|
|
1910
|
+
cwd,
|
|
1911
|
+
encoding: "utf-8",
|
|
1912
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1913
|
+
});
|
|
1914
|
+
return { stdout: result.trim(), stderr: "", exitCode: 0 };
|
|
1915
|
+
} catch (err) {
|
|
1916
|
+
const e = err;
|
|
1917
|
+
return {
|
|
1918
|
+
stdout: (e.stdout ?? "").toString().trim(),
|
|
1919
|
+
stderr: (e.stderr ?? "").toString().trim(),
|
|
1920
|
+
exitCode: e.status ?? 1
|
|
1921
|
+
};
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1924
|
+
stream(command, args = [], options) {
|
|
1925
|
+
const resolved = resolveCommand(command);
|
|
1926
|
+
return new Promise((resolve2) => {
|
|
1927
|
+
const opts = {
|
|
1928
|
+
cwd: options?.cwd,
|
|
1929
|
+
stdio: options?.capture ? "pipe" : "inherit",
|
|
1930
|
+
shell: true
|
|
1931
|
+
};
|
|
1932
|
+
const child = (0, import_child_process6.spawn)(resolved, args, opts);
|
|
1933
|
+
trackChild(child);
|
|
1934
|
+
child.on("close", (code) => resolve2(code ?? 1));
|
|
1935
|
+
child.on("error", (err) => {
|
|
1936
|
+
console.warn(`[exec] Child process error: ${err.message}`);
|
|
1937
|
+
resolve2(1);
|
|
1938
|
+
});
|
|
1939
|
+
});
|
|
1940
|
+
}
|
|
1941
|
+
commandExists(command) {
|
|
1942
|
+
return commandExistsWithVendor(command);
|
|
1943
|
+
}
|
|
1944
|
+
};
|
|
1945
|
+
CLIUIAdapter = class {
|
|
1946
|
+
intro(message) {
|
|
1947
|
+
console.log();
|
|
1948
|
+
p3.intro(import_picocolors8.default.bgCyan(import_picocolors8.default.black(` ${message} `)));
|
|
1949
|
+
}
|
|
1950
|
+
note(content, title) {
|
|
1951
|
+
p3.note(content, title);
|
|
1952
|
+
}
|
|
1953
|
+
outro(message) {
|
|
1954
|
+
p3.outro(message);
|
|
1955
|
+
}
|
|
1956
|
+
cancel(message) {
|
|
1957
|
+
p3.cancel(message);
|
|
1958
|
+
process.exit(0);
|
|
1959
|
+
}
|
|
1960
|
+
log = {
|
|
1961
|
+
info(message) {
|
|
1962
|
+
p3.log.info(message);
|
|
1963
|
+
},
|
|
1964
|
+
step(message) {
|
|
1965
|
+
p3.log.step(message);
|
|
1966
|
+
},
|
|
1967
|
+
success(message) {
|
|
1968
|
+
p3.log.success(message);
|
|
1969
|
+
},
|
|
1970
|
+
warn(message) {
|
|
1971
|
+
p3.log.warn(message);
|
|
1972
|
+
},
|
|
1973
|
+
error(message) {
|
|
1974
|
+
p3.log.error(message);
|
|
1975
|
+
}
|
|
1976
|
+
};
|
|
1977
|
+
async text(options) {
|
|
1978
|
+
const result = await p3.text({
|
|
1979
|
+
message: options.message,
|
|
1980
|
+
placeholder: options.placeholder,
|
|
1981
|
+
defaultValue: options.defaultValue,
|
|
1982
|
+
validate: options.validate
|
|
1983
|
+
});
|
|
1984
|
+
if (p3.isCancel(result)) {
|
|
1985
|
+
this.cancel("Operation cancelled.");
|
|
1986
|
+
}
|
|
1987
|
+
return result;
|
|
1988
|
+
}
|
|
1989
|
+
async confirm(options) {
|
|
1990
|
+
const result = await p3.confirm({
|
|
1991
|
+
message: options.message,
|
|
1992
|
+
initialValue: options.initialValue
|
|
1993
|
+
});
|
|
1994
|
+
if (p3.isCancel(result)) {
|
|
1995
|
+
this.cancel("Operation cancelled.");
|
|
1996
|
+
}
|
|
39
1997
|
return result;
|
|
1998
|
+
}
|
|
1999
|
+
async select(options) {
|
|
2000
|
+
const result = await p3.select({
|
|
2001
|
+
message: options.message,
|
|
2002
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2003
|
+
options: options.options,
|
|
2004
|
+
initialValue: options.initialValue
|
|
2005
|
+
});
|
|
2006
|
+
if (p3.isCancel(result)) {
|
|
2007
|
+
this.cancel("Operation cancelled.");
|
|
2008
|
+
}
|
|
2009
|
+
return result;
|
|
2010
|
+
}
|
|
2011
|
+
async multiSelect(options) {
|
|
2012
|
+
const result = await p3.multiselect({
|
|
2013
|
+
message: options.message,
|
|
2014
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2015
|
+
options: options.options,
|
|
2016
|
+
initialValues: options.initialValues,
|
|
2017
|
+
required: options.required
|
|
2018
|
+
});
|
|
2019
|
+
if (p3.isCancel(result)) {
|
|
2020
|
+
this.cancel("Operation cancelled.");
|
|
2021
|
+
}
|
|
2022
|
+
return result;
|
|
2023
|
+
}
|
|
2024
|
+
spinner(message) {
|
|
2025
|
+
const s = p3.spinner();
|
|
2026
|
+
s.start(message);
|
|
2027
|
+
return {
|
|
2028
|
+
stop(msg, code) {
|
|
2029
|
+
s.stop(msg, code);
|
|
2030
|
+
},
|
|
2031
|
+
message(msg) {
|
|
2032
|
+
s.message(msg);
|
|
2033
|
+
}
|
|
2034
|
+
};
|
|
2035
|
+
}
|
|
40
2036
|
};
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
.
|
|
118
|
-
|
|
119
|
-
.
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
.
|
|
150
|
-
|
|
151
|
-
.
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
.
|
|
160
|
-
|
|
161
|
-
.
|
|
162
|
-
.
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
}
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
.
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
(
|
|
2037
|
+
cliAdapter = createCLIAdapter();
|
|
2038
|
+
}
|
|
2039
|
+
});
|
|
2040
|
+
|
|
2041
|
+
// adapters/api-adapter.ts
|
|
2042
|
+
var init_api_adapter = __esm({
|
|
2043
|
+
"adapters/api-adapter.ts"() {
|
|
2044
|
+
"use strict";
|
|
2045
|
+
init_vendor();
|
|
2046
|
+
}
|
|
2047
|
+
});
|
|
2048
|
+
|
|
2049
|
+
// adapters/index.ts
|
|
2050
|
+
var init_adapters = __esm({
|
|
2051
|
+
"adapters/index.ts"() {
|
|
2052
|
+
"use strict";
|
|
2053
|
+
init_cli_adapter();
|
|
2054
|
+
init_api_adapter();
|
|
2055
|
+
}
|
|
2056
|
+
});
|
|
2057
|
+
|
|
2058
|
+
// tools/index.ts
|
|
2059
|
+
var init_tools = __esm({
|
|
2060
|
+
"tools/index.ts"() {
|
|
2061
|
+
"use strict";
|
|
2062
|
+
init_deploy();
|
|
2063
|
+
init_status();
|
|
2064
|
+
init_validate();
|
|
2065
|
+
init_destroy();
|
|
2066
|
+
init_redeploy();
|
|
2067
|
+
init_push();
|
|
2068
|
+
init_webhooks();
|
|
2069
|
+
init_adapters();
|
|
2070
|
+
}
|
|
2071
|
+
});
|
|
2072
|
+
|
|
2073
|
+
// commands/deploy.ts
|
|
2074
|
+
var deploy_exports = {};
|
|
2075
|
+
__export(deploy_exports, {
|
|
2076
|
+
deployCommand: () => deployCommand
|
|
2077
|
+
});
|
|
2078
|
+
async function deployCommand(opts) {
|
|
2079
|
+
await deployTool(createCLIAdapter(), opts);
|
|
2080
|
+
}
|
|
2081
|
+
var init_deploy2 = __esm({
|
|
2082
|
+
"commands/deploy.ts"() {
|
|
2083
|
+
"use strict";
|
|
2084
|
+
init_tools();
|
|
2085
|
+
}
|
|
2086
|
+
});
|
|
2087
|
+
|
|
2088
|
+
// bin.ts
|
|
2089
|
+
var fs7 = __toESM(require("fs"));
|
|
2090
|
+
var path7 = __toESM(require("path"));
|
|
2091
|
+
var import_commander = require("commander");
|
|
2092
|
+
init_process();
|
|
2093
|
+
|
|
2094
|
+
// commands/init.ts
|
|
2095
|
+
var import_child_process7 = require("child_process");
|
|
2096
|
+
var p4 = __toESM(require("@clack/prompts"));
|
|
2097
|
+
var import_core9 = require("@clawup/core");
|
|
2098
|
+
var import_identity2 = require("@clawup/core/identity");
|
|
2099
|
+
var os4 = __toESM(require("os"));
|
|
2100
|
+
var path5 = __toESM(require("path"));
|
|
2101
|
+
|
|
2102
|
+
// lib/prerequisites.ts
|
|
2103
|
+
var p = __toESM(require("@clack/prompts"));
|
|
2104
|
+
init_exec();
|
|
2105
|
+
init_vendor();
|
|
2106
|
+
init_workspace();
|
|
2107
|
+
init_tailscale();
|
|
2108
|
+
async function checkPrerequisites() {
|
|
2109
|
+
const results = [];
|
|
2110
|
+
if (commandExists("pulumi")) {
|
|
2111
|
+
const ver = capture("pulumi", ["version"]);
|
|
2112
|
+
const source = isVendored("pulumi") ? "vendored" : "system";
|
|
2113
|
+
results.push({ name: "Pulumi", ok: true, message: `found (${ver.stdout}, ${source})` });
|
|
2114
|
+
} else {
|
|
2115
|
+
results.push({
|
|
2116
|
+
name: "Pulumi",
|
|
2117
|
+
ok: false,
|
|
2118
|
+
message: "not found",
|
|
2119
|
+
hint: "Install from https://www.pulumi.com/docs/iac/download-install/ or run `npm install` to vendor automatically"
|
|
2120
|
+
});
|
|
2121
|
+
}
|
|
2122
|
+
if (commandExists("node")) {
|
|
2123
|
+
const ver = capture("node", ["-v"]);
|
|
2124
|
+
const major = parseInt(ver.stdout.replace("v", "").split(".")[0], 10);
|
|
2125
|
+
if (major >= 18) {
|
|
2126
|
+
results.push({ name: "Node.js", ok: true, message: `${ver.stdout} (>= 18 required)` });
|
|
2127
|
+
} else {
|
|
2128
|
+
results.push({
|
|
2129
|
+
name: "Node.js",
|
|
2130
|
+
ok: false,
|
|
2131
|
+
message: `${ver.stdout} is too old`,
|
|
2132
|
+
hint: "Requires Node.js 18+. Install from https://nodejs.org/"
|
|
2133
|
+
});
|
|
2134
|
+
}
|
|
2135
|
+
} else {
|
|
2136
|
+
results.push({
|
|
2137
|
+
name: "Node.js",
|
|
2138
|
+
ok: false,
|
|
2139
|
+
message: "not found",
|
|
2140
|
+
hint: "Install from https://nodejs.org/"
|
|
2141
|
+
});
|
|
2142
|
+
}
|
|
2143
|
+
if (commandExists("aws")) {
|
|
2144
|
+
const source = isVendored("aws") ? "vendored" : "system";
|
|
2145
|
+
results.push({ name: "AWS CLI", ok: true, message: `found (${source})` });
|
|
2146
|
+
} else {
|
|
2147
|
+
results.push({
|
|
2148
|
+
name: "AWS CLI",
|
|
2149
|
+
ok: false,
|
|
2150
|
+
message: "not found",
|
|
2151
|
+
hint: "Install from https://aws.amazon.com/cli/ or run `npm install` to vendor automatically"
|
|
2152
|
+
});
|
|
2153
|
+
}
|
|
2154
|
+
if (isTailscaleInstalled()) {
|
|
2155
|
+
results.push({ name: "Tailscale", ok: true, message: "found" });
|
|
2156
|
+
} else {
|
|
2157
|
+
const hint = process.platform === "darwin" ? "Install from the Mac App Store or https://tailscale.com/download" : "Install from https://tailscale.com/download";
|
|
2158
|
+
results.push({
|
|
2159
|
+
name: "Tailscale",
|
|
2160
|
+
ok: false,
|
|
2161
|
+
message: "not found",
|
|
2162
|
+
hint
|
|
2163
|
+
});
|
|
2164
|
+
}
|
|
2165
|
+
if (isDevMode()) {
|
|
2166
|
+
if (commandExists("pnpm")) {
|
|
2167
|
+
results.push({ name: "pnpm", ok: true, message: "found" });
|
|
2168
|
+
} else {
|
|
2169
|
+
results.push({
|
|
2170
|
+
name: "pnpm",
|
|
2171
|
+
ok: false,
|
|
2172
|
+
message: "not found",
|
|
2173
|
+
hint: "Install with: npm install -g pnpm"
|
|
2174
|
+
});
|
|
2175
|
+
}
|
|
2176
|
+
}
|
|
2177
|
+
const allOk = results.every((r) => r.ok);
|
|
2178
|
+
for (const r of results) {
|
|
2179
|
+
if (r.ok) {
|
|
2180
|
+
p.log.success(`${r.name}: ${r.message}`);
|
|
2181
|
+
} else {
|
|
2182
|
+
p.log.error(`${r.name}: ${r.message}`);
|
|
2183
|
+
if (r.hint) {
|
|
2184
|
+
p.log.message(` ${r.hint}`);
|
|
2185
|
+
}
|
|
2186
|
+
}
|
|
2187
|
+
}
|
|
2188
|
+
return allOk;
|
|
2189
|
+
}
|
|
2190
|
+
|
|
2191
|
+
// commands/init.ts
|
|
2192
|
+
init_pulumi();
|
|
2193
|
+
init_config();
|
|
2194
|
+
init_workspace();
|
|
2195
|
+
init_ui();
|
|
2196
|
+
async function initCommand(opts = {}) {
|
|
2197
|
+
showBanner();
|
|
2198
|
+
p4.log.step("Checking prerequisites...");
|
|
2199
|
+
const prereqsOk = await checkPrerequisites();
|
|
2200
|
+
if (!prereqsOk) {
|
|
2201
|
+
exitWithError("Prerequisites not met. Please install the missing tools and try again.");
|
|
2202
|
+
}
|
|
2203
|
+
p4.log.success("All prerequisites satisfied!");
|
|
2204
|
+
const stackName = await p4.text({
|
|
2205
|
+
message: "Pulumi stack name",
|
|
2206
|
+
placeholder: "dev",
|
|
2207
|
+
defaultValue: "dev"
|
|
2208
|
+
});
|
|
2209
|
+
handleCancel(stackName);
|
|
2210
|
+
const provider = await p4.select({
|
|
2211
|
+
message: "Cloud provider",
|
|
2212
|
+
options: import_core9.PROVIDERS.map((prov) => ({ value: prov.value, label: prov.label, hint: prov.hint })),
|
|
2213
|
+
initialValue: "aws"
|
|
2214
|
+
});
|
|
2215
|
+
handleCancel(provider);
|
|
2216
|
+
let region;
|
|
2217
|
+
let instanceType;
|
|
2218
|
+
if (provider === "aws") {
|
|
2219
|
+
const awsRegion = await p4.select({
|
|
2220
|
+
message: "AWS region",
|
|
2221
|
+
options: import_core9.AWS_REGIONS,
|
|
2222
|
+
initialValue: "us-east-1"
|
|
2223
|
+
});
|
|
2224
|
+
handleCancel(awsRegion);
|
|
2225
|
+
region = awsRegion;
|
|
2226
|
+
const awsInstanceType = await p4.select({
|
|
2227
|
+
message: "Default instance type",
|
|
2228
|
+
options: import_core9.INSTANCE_TYPES,
|
|
2229
|
+
initialValue: "t3.medium"
|
|
2230
|
+
});
|
|
2231
|
+
handleCancel(awsInstanceType);
|
|
2232
|
+
instanceType = awsInstanceType;
|
|
2233
|
+
} else {
|
|
2234
|
+
const hetznerLocation = await p4.select({
|
|
2235
|
+
message: "Hetzner location",
|
|
2236
|
+
options: import_core9.HETZNER_LOCATIONS,
|
|
2237
|
+
initialValue: "fsn1"
|
|
2238
|
+
});
|
|
2239
|
+
handleCancel(hetznerLocation);
|
|
2240
|
+
region = hetznerLocation;
|
|
2241
|
+
const serverTypeOptions = (0, import_core9.hetznerServerTypes)(region);
|
|
2242
|
+
const hetznerServerType = await p4.select({
|
|
2243
|
+
message: "Default server type",
|
|
2244
|
+
options: serverTypeOptions,
|
|
2245
|
+
initialValue: serverTypeOptions[0].value
|
|
2246
|
+
});
|
|
2247
|
+
handleCancel(hetznerServerType);
|
|
2248
|
+
instanceType = hetznerServerType;
|
|
2249
|
+
}
|
|
2250
|
+
const ownerName = await p4.text({
|
|
2251
|
+
message: "Owner name (for workspace templates)",
|
|
2252
|
+
placeholder: "Boss",
|
|
2253
|
+
defaultValue: "Boss"
|
|
2254
|
+
});
|
|
2255
|
+
handleCancel(ownerName);
|
|
2256
|
+
const timezone = await p4.text({
|
|
2257
|
+
message: "Your timezone",
|
|
2258
|
+
placeholder: "PST (America/Los_Angeles)",
|
|
2259
|
+
defaultValue: "PST (America/Los_Angeles)"
|
|
2260
|
+
});
|
|
2261
|
+
handleCancel(timezone);
|
|
2262
|
+
const workingHours = await p4.text({
|
|
2263
|
+
message: "Your working hours",
|
|
2264
|
+
placeholder: "9am-6pm",
|
|
2265
|
+
defaultValue: "9am-6pm"
|
|
2266
|
+
});
|
|
2267
|
+
handleCancel(workingHours);
|
|
2268
|
+
const userNotes = await p4.text({
|
|
2269
|
+
message: "Any notes for your agents about you? (optional)",
|
|
2270
|
+
placeholder: "e.g., Prefers concise updates, hates unnecessary meetings",
|
|
2271
|
+
defaultValue: ""
|
|
2272
|
+
});
|
|
2273
|
+
handleCancel(userNotes);
|
|
2274
|
+
const basicConfig = {
|
|
2275
|
+
stackName,
|
|
2276
|
+
provider,
|
|
2277
|
+
region,
|
|
2278
|
+
instanceType,
|
|
2279
|
+
ownerName,
|
|
2280
|
+
timezone,
|
|
2281
|
+
workingHours,
|
|
2282
|
+
userNotes: userNotes || "No additional notes provided yet."
|
|
2283
|
+
};
|
|
2284
|
+
p4.log.step("Configure Anthropic API key");
|
|
2285
|
+
p4.note(
|
|
2286
|
+
import_core9.KEY_INSTRUCTIONS.anthropicApiKey.steps.join("\n"),
|
|
2287
|
+
import_core9.KEY_INSTRUCTIONS.anthropicApiKey.title
|
|
2288
|
+
);
|
|
2289
|
+
const anthropicApiKey = await p4.text({
|
|
2290
|
+
message: "Anthropic API key",
|
|
2291
|
+
placeholder: `${import_core9.MODEL_PROVIDERS.anthropic.keyPrefix}...`,
|
|
2292
|
+
validate: (val) => {
|
|
2293
|
+
if (!val) return "API key is required";
|
|
2294
|
+
if (!val.startsWith(import_core9.MODEL_PROVIDERS.anthropic.keyPrefix)) {
|
|
2295
|
+
return `Must start with ${import_core9.MODEL_PROVIDERS.anthropic.keyPrefix}`;
|
|
2296
|
+
}
|
|
2297
|
+
}
|
|
2298
|
+
});
|
|
2299
|
+
handleCancel(anthropicApiKey);
|
|
2300
|
+
p4.log.step("Configure infrastructure secrets");
|
|
2301
|
+
p4.note(
|
|
2302
|
+
import_core9.KEY_INSTRUCTIONS.tailscaleAuthKey.steps.join("\n"),
|
|
2303
|
+
import_core9.KEY_INSTRUCTIONS.tailscaleAuthKey.title
|
|
2304
|
+
);
|
|
2305
|
+
const tailscaleAuthKey = await p4.password({
|
|
2306
|
+
message: "Tailscale auth key",
|
|
2307
|
+
validate: (val) => {
|
|
2308
|
+
if (!val.startsWith("tskey-auth-")) return "Must start with tskey-auth-";
|
|
2309
|
+
}
|
|
2310
|
+
});
|
|
2311
|
+
handleCancel(tailscaleAuthKey);
|
|
2312
|
+
p4.note(
|
|
2313
|
+
import_core9.KEY_INSTRUCTIONS.tailnetDnsName.steps.join("\n"),
|
|
2314
|
+
import_core9.KEY_INSTRUCTIONS.tailnetDnsName.title
|
|
2315
|
+
);
|
|
2316
|
+
const tailnetDnsName = await p4.text({
|
|
2317
|
+
message: "Tailnet DNS name",
|
|
2318
|
+
placeholder: "my-tailnet.ts.net",
|
|
2319
|
+
validate: (val) => {
|
|
2320
|
+
if (!val.endsWith(".ts.net")) return "Must end with .ts.net";
|
|
2321
|
+
}
|
|
2322
|
+
});
|
|
2323
|
+
handleCancel(tailnetDnsName);
|
|
2324
|
+
p4.note(
|
|
2325
|
+
import_core9.KEY_INSTRUCTIONS.tailscaleApiKey.steps.join("\n"),
|
|
2326
|
+
import_core9.KEY_INSTRUCTIONS.tailscaleApiKey.title
|
|
2327
|
+
);
|
|
2328
|
+
const tailscaleApiKey = await p4.text({
|
|
2329
|
+
message: "Tailscale API key (press Enter to skip)",
|
|
2330
|
+
placeholder: "tskey-api-... (optional)",
|
|
2331
|
+
defaultValue: ""
|
|
2332
|
+
});
|
|
2333
|
+
handleCancel(tailscaleApiKey);
|
|
2334
|
+
let hcloudToken;
|
|
2335
|
+
if (provider === "hetzner") {
|
|
2336
|
+
p4.note(
|
|
2337
|
+
import_core9.KEY_INSTRUCTIONS.hcloudToken.steps.join("\n"),
|
|
2338
|
+
import_core9.KEY_INSTRUCTIONS.hcloudToken.title
|
|
2339
|
+
);
|
|
2340
|
+
const token = await p4.password({
|
|
2341
|
+
message: "Hetzner Cloud API token",
|
|
2342
|
+
validate: (val) => {
|
|
2343
|
+
if (!val) return "API token is required for Hetzner deployments";
|
|
2344
|
+
}
|
|
2345
|
+
});
|
|
2346
|
+
handleCancel(token);
|
|
2347
|
+
hcloudToken = token;
|
|
2348
|
+
}
|
|
2349
|
+
p4.log.step("Configure agents");
|
|
2350
|
+
const agentMode = await p4.select({
|
|
2351
|
+
message: "How would you like to configure agents?",
|
|
2352
|
+
options: [
|
|
2353
|
+
{ value: "built-in", label: "Built-in agents", hint: "PM (Juno), Eng (Titus), QA (Scout)" },
|
|
2354
|
+
{ value: "identity", label: "From identity source", hint: "Load from a Git URL or local path" },
|
|
2355
|
+
{ value: "mix", label: "Mix of both", hint: "Pick built-in + add from identity source" }
|
|
2356
|
+
]
|
|
2357
|
+
});
|
|
2358
|
+
handleCancel(agentMode);
|
|
2359
|
+
const fetchedIdentities = [];
|
|
2360
|
+
const identityCacheDir = path5.join(os4.homedir(), ".clawup", "identity-cache");
|
|
2361
|
+
if (agentMode === "built-in" || agentMode === "mix") {
|
|
2362
|
+
const selectedBuiltIns = await p4.multiselect({
|
|
2363
|
+
message: "Select agents",
|
|
2364
|
+
options: Object.entries(import_core9.BUILT_IN_IDENTITIES).map(([key, entry]) => ({
|
|
2365
|
+
value: key,
|
|
2366
|
+
label: entry.label,
|
|
2367
|
+
hint: entry.hint
|
|
2368
|
+
})),
|
|
2369
|
+
required: agentMode === "built-in"
|
|
2370
|
+
});
|
|
2371
|
+
handleCancel(selectedBuiltIns);
|
|
2372
|
+
for (const key of selectedBuiltIns) {
|
|
2373
|
+
const entry = import_core9.BUILT_IN_IDENTITIES[key];
|
|
2374
|
+
const identity = await (0, import_identity2.fetchIdentity)(entry.path, identityCacheDir);
|
|
2375
|
+
const agent = {
|
|
2376
|
+
name: `agent-${identity.manifest.name}`,
|
|
2377
|
+
displayName: identity.manifest.displayName,
|
|
2378
|
+
role: identity.manifest.role,
|
|
2379
|
+
identity: entry.path,
|
|
2380
|
+
volumeSize: identity.manifest.volumeSize
|
|
2381
|
+
};
|
|
2382
|
+
fetchedIdentities.push({ agent, manifest: identity.manifest });
|
|
2383
|
+
}
|
|
2384
|
+
}
|
|
2385
|
+
if (agentMode === "identity" || agentMode === "mix") {
|
|
2386
|
+
let addMore = true;
|
|
2387
|
+
while (addMore) {
|
|
2388
|
+
const identityUrl = await p4.text({
|
|
2389
|
+
message: "Identity source (Git URL or local path)",
|
|
2390
|
+
placeholder: "https://github.com/org/identities#agent-name",
|
|
2391
|
+
validate: (val) => {
|
|
2392
|
+
if (!val.trim()) return "Identity source is required";
|
|
2393
|
+
}
|
|
2394
|
+
});
|
|
2395
|
+
handleCancel(identityUrl);
|
|
2396
|
+
const spinner4 = p4.spinner();
|
|
2397
|
+
spinner4.start("Validating identity...");
|
|
2398
|
+
try {
|
|
2399
|
+
const identity = await (0, import_identity2.fetchIdentity)(identityUrl, identityCacheDir);
|
|
2400
|
+
spinner4.stop(
|
|
2401
|
+
`Found: ${identity.manifest.displayName} (${identity.manifest.role}) \u2014 ${identity.manifest.description}`
|
|
2402
|
+
);
|
|
2403
|
+
const volumeOverride = await p4.text({
|
|
2404
|
+
message: `Volume size in GB (default: ${identity.manifest.volumeSize})`,
|
|
2405
|
+
placeholder: String(identity.manifest.volumeSize),
|
|
2406
|
+
defaultValue: String(identity.manifest.volumeSize),
|
|
2407
|
+
validate: (val) => {
|
|
2408
|
+
const n = parseInt(val, 10);
|
|
2409
|
+
if (isNaN(n) || n < 8 || n > 500) return "Must be between 8 and 500";
|
|
2410
|
+
}
|
|
2411
|
+
});
|
|
2412
|
+
handleCancel(volumeOverride);
|
|
2413
|
+
const agent = {
|
|
2414
|
+
name: `agent-${identity.manifest.name}`,
|
|
2415
|
+
displayName: identity.manifest.displayName,
|
|
2416
|
+
role: identity.manifest.role,
|
|
2417
|
+
identity: identityUrl,
|
|
2418
|
+
volumeSize: parseInt(volumeOverride, 10)
|
|
2419
|
+
};
|
|
2420
|
+
fetchedIdentities.push({ agent, manifest: identity.manifest });
|
|
2421
|
+
} catch (err) {
|
|
2422
|
+
spinner4.stop(`Failed to validate identity: ${err.message}`);
|
|
2423
|
+
p4.log.error("Please check the URL and try again.");
|
|
2424
|
+
continue;
|
|
2425
|
+
}
|
|
2426
|
+
const more = await p4.confirm({
|
|
2427
|
+
message: "Add another identity-based agent?",
|
|
2428
|
+
initialValue: false
|
|
2429
|
+
});
|
|
2430
|
+
handleCancel(more);
|
|
2431
|
+
addMore = more;
|
|
2432
|
+
}
|
|
2433
|
+
}
|
|
2434
|
+
if (fetchedIdentities.length === 0) {
|
|
2435
|
+
exitWithError("No agents configured. At least one agent is required.");
|
|
2436
|
+
}
|
|
2437
|
+
const agents = fetchedIdentities.map((fi) => fi.agent);
|
|
2438
|
+
const autoVars = {
|
|
2439
|
+
OWNER_NAME: basicConfig.ownerName,
|
|
2440
|
+
TIMEZONE: basicConfig.timezone,
|
|
2441
|
+
WORKING_HOURS: basicConfig.workingHours,
|
|
2442
|
+
USER_NOTES: basicConfig.userNotes
|
|
2443
|
+
};
|
|
2444
|
+
const allTemplateVarNames = /* @__PURE__ */ new Set();
|
|
2445
|
+
for (const fi of fetchedIdentities) {
|
|
2446
|
+
for (const v of fi.manifest.templateVars ?? []) {
|
|
2447
|
+
allTemplateVarNames.add(v);
|
|
2448
|
+
}
|
|
2449
|
+
}
|
|
2450
|
+
const templateVars = {};
|
|
2451
|
+
for (const varName of allTemplateVarNames) {
|
|
2452
|
+
if (autoVars[varName]) {
|
|
2453
|
+
templateVars[varName] = autoVars[varName];
|
|
2454
|
+
}
|
|
2455
|
+
}
|
|
2456
|
+
const remainingVars = [...allTemplateVarNames].filter((v) => !templateVars[v]);
|
|
2457
|
+
if (remainingVars.length > 0) {
|
|
2458
|
+
p4.log.step("Configure template variables");
|
|
2459
|
+
p4.log.info(`Your agents use the following template variables: ${remainingVars.join(", ")}`);
|
|
2460
|
+
for (const varName of remainingVars) {
|
|
2461
|
+
const value = await p4.text({
|
|
2462
|
+
message: `Value for ${varName}`,
|
|
2463
|
+
placeholder: varName === "LINEAR_TEAM" ? "e.g., ENG" : varName === "GITHUB_REPO" ? "https://github.com/org/repo" : "",
|
|
2464
|
+
validate: (val) => {
|
|
2465
|
+
if (!val.trim()) return `${varName} is required`;
|
|
2466
|
+
}
|
|
2467
|
+
});
|
|
2468
|
+
handleCancel(value);
|
|
2469
|
+
templateVars[varName] = value;
|
|
2470
|
+
}
|
|
2471
|
+
}
|
|
2472
|
+
p4.log.step("Configure integrations");
|
|
2473
|
+
const agentPlugins = /* @__PURE__ */ new Map();
|
|
2474
|
+
const agentDeps = /* @__PURE__ */ new Map();
|
|
2475
|
+
const allPluginNames = /* @__PURE__ */ new Set();
|
|
2476
|
+
const allDepNames = /* @__PURE__ */ new Set();
|
|
2477
|
+
for (const fi of fetchedIdentities) {
|
|
2478
|
+
const plugins = new Set(fi.manifest.plugins ?? []);
|
|
2479
|
+
const deps = new Set(fi.manifest.deps ?? []);
|
|
2480
|
+
agentPlugins.set(fi.agent.name, plugins);
|
|
2481
|
+
agentDeps.set(fi.agent.name, deps);
|
|
2482
|
+
for (const pl of plugins) allPluginNames.add(pl);
|
|
2483
|
+
for (const d of deps) allDepNames.add(d);
|
|
2484
|
+
}
|
|
2485
|
+
const identityPluginDefaults = {};
|
|
2486
|
+
for (const fi of fetchedIdentities) {
|
|
2487
|
+
if (fi.manifest.pluginDefaults) {
|
|
2488
|
+
identityPluginDefaults[fi.agent.name] = fi.manifest.pluginDefaults;
|
|
2489
|
+
}
|
|
2490
|
+
}
|
|
2491
|
+
const integrationCredentials = {};
|
|
2492
|
+
for (const agent of agents) {
|
|
2493
|
+
integrationCredentials[agent.role] = {};
|
|
2494
|
+
}
|
|
2495
|
+
const slackCredentials = {};
|
|
2496
|
+
if (allPluginNames.has("slack")) {
|
|
2497
|
+
p4.note(
|
|
2498
|
+
import_core9.KEY_INSTRUCTIONS.slackCredentials.steps.join("\n"),
|
|
2499
|
+
import_core9.KEY_INSTRUCTIONS.slackCredentials.title
|
|
2500
|
+
);
|
|
2501
|
+
for (const fi of fetchedIdentities) {
|
|
2502
|
+
if (!agentPlugins.get(fi.agent.name)?.has("slack")) continue;
|
|
2503
|
+
const slackManifest = (0, import_core9.slackAppManifest)(fi.agent.displayName);
|
|
2504
|
+
try {
|
|
2505
|
+
(0, import_child_process7.execSync)(
|
|
2506
|
+
process.platform === "darwin" ? "pbcopy" : "xclip -selection clipboard",
|
|
2507
|
+
{ input: slackManifest }
|
|
2508
|
+
);
|
|
2509
|
+
p4.log.success(`Slack manifest for ${fi.agent.displayName} copied to clipboard \u2014 paste it into Slack`);
|
|
2510
|
+
} catch {
|
|
2511
|
+
p4.log.warn(`Could not copy to clipboard. Manifest for ${fi.agent.displayName}:`);
|
|
2512
|
+
console.log(slackManifest);
|
|
2513
|
+
}
|
|
2514
|
+
const botToken = await p4.password({
|
|
2515
|
+
message: `Slack Bot Token for ${fi.agent.displayName} (${fi.agent.role})`,
|
|
2516
|
+
validate: (val) => {
|
|
2517
|
+
if (!val.startsWith("xoxb-")) return "Must start with xoxb-";
|
|
2518
|
+
}
|
|
2519
|
+
});
|
|
2520
|
+
handleCancel(botToken);
|
|
2521
|
+
const appToken = await p4.password({
|
|
2522
|
+
message: `Slack App Token for ${fi.agent.displayName} (${fi.agent.role})`,
|
|
2523
|
+
validate: (val) => {
|
|
2524
|
+
if (!val.startsWith("xapp-")) return "Must start with xapp-";
|
|
2525
|
+
}
|
|
2526
|
+
});
|
|
2527
|
+
handleCancel(appToken);
|
|
2528
|
+
slackCredentials[fi.agent.role] = {
|
|
2529
|
+
botToken,
|
|
2530
|
+
appToken
|
|
2531
|
+
};
|
|
2532
|
+
}
|
|
2533
|
+
}
|
|
2534
|
+
if (allPluginNames.has("openclaw-linear")) {
|
|
2535
|
+
p4.note(
|
|
2536
|
+
import_core9.KEY_INSTRUCTIONS.linearApiKey.steps.join("\n"),
|
|
2537
|
+
import_core9.KEY_INSTRUCTIONS.linearApiKey.title
|
|
2538
|
+
);
|
|
2539
|
+
const linearAgents = fetchedIdentities.filter(
|
|
2540
|
+
(fi) => agentPlugins.get(fi.agent.name)?.has("openclaw-linear")
|
|
2541
|
+
);
|
|
2542
|
+
for (const fi of linearAgents) {
|
|
2543
|
+
const linearKey = await p4.password({
|
|
2544
|
+
message: `Linear API key for ${fi.agent.displayName} (${fi.agent.role})`,
|
|
2545
|
+
validate: (val) => {
|
|
2546
|
+
if (!val.startsWith("lin_api_")) return "Must start with lin_api_";
|
|
2547
|
+
}
|
|
2548
|
+
});
|
|
2549
|
+
handleCancel(linearKey);
|
|
2550
|
+
integrationCredentials[fi.agent.role].linearApiKey = linearKey;
|
|
2551
|
+
const s2 = p4.spinner();
|
|
2552
|
+
s2.start(`Fetching Linear user ID for ${fi.agent.displayName}...`);
|
|
2553
|
+
try {
|
|
2554
|
+
const res = await fetch("https://api.linear.app/graphql", {
|
|
2555
|
+
method: "POST",
|
|
2556
|
+
headers: {
|
|
2557
|
+
"Content-Type": "application/json",
|
|
2558
|
+
Authorization: linearKey
|
|
2559
|
+
},
|
|
2560
|
+
body: JSON.stringify({ query: "{ viewer { id } }" })
|
|
2561
|
+
});
|
|
2562
|
+
const data = await res.json();
|
|
2563
|
+
const uuid = data?.data?.viewer?.id;
|
|
2564
|
+
if (!uuid) throw new Error("No user ID in response");
|
|
2565
|
+
integrationCredentials[fi.agent.role].linearUserUuid = uuid;
|
|
2566
|
+
s2.stop(`${fi.agent.displayName}: ${uuid}`);
|
|
2567
|
+
} catch (err) {
|
|
2568
|
+
s2.stop(`Could not fetch Linear user ID for ${fi.agent.displayName}`);
|
|
2569
|
+
p4.log.warn(`${err instanceof Error ? err.message : String(err)}`);
|
|
2570
|
+
const linearUserUuid = await p4.text({
|
|
2571
|
+
message: `Enter Linear user UUID manually for ${fi.agent.displayName}`,
|
|
2572
|
+
placeholder: "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
|
|
2573
|
+
validate: (val) => {
|
|
2574
|
+
if (!val) return "Linear user UUID is required";
|
|
2575
|
+
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(val)) {
|
|
2576
|
+
return "Must be a valid UUID format";
|
|
2577
|
+
}
|
|
2578
|
+
}
|
|
2579
|
+
});
|
|
2580
|
+
handleCancel(linearUserUuid);
|
|
2581
|
+
integrationCredentials[fi.agent.role].linearUserUuid = linearUserUuid;
|
|
2582
|
+
}
|
|
2583
|
+
}
|
|
2584
|
+
p4.note(
|
|
2585
|
+
[
|
|
2586
|
+
"Create a webhook in Linear for each agent:",
|
|
2587
|
+
'1. Go to Settings \u2192 API \u2192 Webhooks \u2192 "New webhook"',
|
|
2588
|
+
"2. Paste the webhook URL shown below for each agent",
|
|
2589
|
+
"3. Select events to receive (e.g., Issues, Comments)",
|
|
2590
|
+
'4. Copy the "Signing secret" shown after creating the webhook'
|
|
2591
|
+
].join("\n"),
|
|
2592
|
+
"Linear Webhook Setup"
|
|
2593
|
+
);
|
|
2594
|
+
for (const fi of linearAgents) {
|
|
2595
|
+
const webhookUrl = `https://${(0, import_core9.tailscaleHostname)(basicConfig.stackName, fi.agent.name)}.${tailnetDnsName}/hooks/linear`;
|
|
2596
|
+
p4.log.info(`${fi.agent.displayName} (${fi.agent.role}): ${webhookUrl}`);
|
|
2597
|
+
const webhookSecretInput = await p4.password({
|
|
2598
|
+
message: `Signing secret for ${fi.agent.displayName} (${fi.agent.role})`,
|
|
2599
|
+
validate: (val) => {
|
|
2600
|
+
if (!val) return "Webhook signing secret is required";
|
|
2601
|
+
}
|
|
2602
|
+
});
|
|
2603
|
+
handleCancel(webhookSecretInput);
|
|
2604
|
+
integrationCredentials[fi.agent.role].linearWebhookSecret = webhookSecretInput;
|
|
2605
|
+
}
|
|
2606
|
+
}
|
|
2607
|
+
if (allDepNames.has("gh")) {
|
|
2608
|
+
p4.note(
|
|
2609
|
+
import_core9.KEY_INSTRUCTIONS.githubToken.steps.join("\n"),
|
|
2610
|
+
import_core9.KEY_INSTRUCTIONS.githubToken.title
|
|
2611
|
+
);
|
|
2612
|
+
for (const fi of fetchedIdentities) {
|
|
2613
|
+
if (!agentDeps.get(fi.agent.name)?.has("gh")) continue;
|
|
2614
|
+
const githubKey = await p4.password({
|
|
2615
|
+
message: `GitHub token for ${fi.agent.displayName} (${fi.agent.role})`,
|
|
2616
|
+
validate: (val) => {
|
|
2617
|
+
if (!val.startsWith("ghp_") && !val.startsWith("github_pat_")) {
|
|
2618
|
+
return "Must start with ghp_ or github_pat_";
|
|
2619
|
+
}
|
|
2620
|
+
}
|
|
2621
|
+
});
|
|
2622
|
+
handleCancel(githubKey);
|
|
2623
|
+
integrationCredentials[fi.agent.role].githubToken = githubKey;
|
|
2624
|
+
}
|
|
2625
|
+
}
|
|
2626
|
+
let braveApiKey;
|
|
2627
|
+
if (allDepNames.has("brave-search")) {
|
|
2628
|
+
p4.note(
|
|
2629
|
+
import_core9.KEY_INSTRUCTIONS.braveApiKey.steps.join("\n"),
|
|
2630
|
+
import_core9.KEY_INSTRUCTIONS.braveApiKey.title
|
|
2631
|
+
);
|
|
2632
|
+
const braveKey = await p4.password({
|
|
2633
|
+
message: "Brave Search API key",
|
|
2634
|
+
validate: (val) => {
|
|
2635
|
+
if (!val) return "API key is required";
|
|
2636
|
+
}
|
|
2637
|
+
});
|
|
2638
|
+
handleCancel(braveKey);
|
|
2639
|
+
braveApiKey = braveKey;
|
|
2640
|
+
}
|
|
2641
|
+
const costEstimates = basicConfig.provider === "aws" ? import_core9.COST_ESTIMATES : import_core9.HETZNER_COST_ESTIMATES;
|
|
2642
|
+
const costPerAgent = costEstimates[basicConfig.instanceType] ?? 30;
|
|
2643
|
+
const totalCost = agents.reduce((sum, a) => {
|
|
2644
|
+
const agentCost = costEstimates[a.instanceType ?? basicConfig.instanceType] ?? costPerAgent;
|
|
2645
|
+
return sum + agentCost;
|
|
2646
|
+
}, 0);
|
|
2647
|
+
const integrationNames = [];
|
|
2648
|
+
if (allPluginNames.has("openclaw-linear")) integrationNames.push("Linear");
|
|
2649
|
+
if (allPluginNames.has("slack")) integrationNames.push("Slack");
|
|
2650
|
+
if (allDepNames.has("gh")) integrationNames.push("GitHub CLI");
|
|
2651
|
+
if (allDepNames.has("brave-search")) integrationNames.push("Brave Search");
|
|
2652
|
+
const providerLabel = basicConfig.provider === "aws" ? "AWS" : "Hetzner";
|
|
2653
|
+
const regionLabel = basicConfig.provider === "aws" ? "Region" : "Location";
|
|
2654
|
+
const customVarEntries = Object.entries(templateVars).filter(
|
|
2655
|
+
([k]) => !autoVars[k]
|
|
2656
|
+
);
|
|
2657
|
+
const summaryLines = [
|
|
2658
|
+
`Stack: ${basicConfig.stackName}`,
|
|
2659
|
+
`Provider: ${providerLabel}`,
|
|
2660
|
+
`${regionLabel.padEnd(14, " ")} ${basicConfig.region}`,
|
|
2661
|
+
`Instance type: ${basicConfig.instanceType}`,
|
|
2662
|
+
`Owner: ${basicConfig.ownerName}`,
|
|
2663
|
+
`Timezone: ${basicConfig.timezone}`,
|
|
2664
|
+
`Working hours: ${basicConfig.workingHours}`
|
|
2665
|
+
];
|
|
2666
|
+
if (customVarEntries.length > 0) {
|
|
2667
|
+
for (const [k, v] of customVarEntries) {
|
|
2668
|
+
summaryLines.push(`${k.padEnd(14, " ")} ${v}`);
|
|
2669
|
+
}
|
|
2670
|
+
}
|
|
2671
|
+
if (integrationNames.length > 0) {
|
|
2672
|
+
summaryLines.push(`Integrations: ${integrationNames.join(", ")}`);
|
|
2673
|
+
}
|
|
2674
|
+
summaryLines.push(
|
|
2675
|
+
``,
|
|
2676
|
+
`Agents (${agents.length}):`,
|
|
2677
|
+
formatAgentList(agents),
|
|
2678
|
+
``,
|
|
2679
|
+
`Estimated cost: ${formatCost(totalCost)}`
|
|
2680
|
+
);
|
|
2681
|
+
p4.note(summaryLines.join("\n"), "Deployment Summary");
|
|
2682
|
+
const confirmed = await p4.confirm({
|
|
2683
|
+
message: "Proceed with setup?"
|
|
2684
|
+
});
|
|
2685
|
+
handleCancel(confirmed);
|
|
2686
|
+
if (!confirmed) {
|
|
2687
|
+
p4.cancel("Setup cancelled.");
|
|
2688
|
+
process.exit(0);
|
|
2689
|
+
}
|
|
2690
|
+
const s = p4.spinner();
|
|
2691
|
+
s.start("Setting up workspace...");
|
|
2692
|
+
const wsResult = ensureWorkspace();
|
|
2693
|
+
if (!wsResult.ok) {
|
|
2694
|
+
s.stop("Failed to set up workspace");
|
|
2695
|
+
exitWithError(wsResult.error ?? "Failed to set up workspace.");
|
|
2696
|
+
}
|
|
2697
|
+
s.stop("Workspace ready");
|
|
2698
|
+
const cwd = getWorkspaceDir();
|
|
2699
|
+
s.start("Selecting Pulumi stack...");
|
|
2700
|
+
const stackResult = selectOrCreateStack(basicConfig.stackName, cwd);
|
|
2701
|
+
if (!stackResult.ok) {
|
|
2702
|
+
s.stop("Failed to select/create stack");
|
|
2703
|
+
if (stackResult.error) p4.log.error(stackResult.error);
|
|
2704
|
+
exitWithError(`Could not select or create Pulumi stack "${basicConfig.stackName}".`);
|
|
2705
|
+
}
|
|
2706
|
+
s.stop("Pulumi stack ready");
|
|
2707
|
+
s.start("Setting Pulumi configuration...");
|
|
2708
|
+
setConfig("provider", basicConfig.provider, false, cwd);
|
|
2709
|
+
if (basicConfig.provider === "aws") {
|
|
2710
|
+
setConfig("aws:region", basicConfig.region, false, cwd);
|
|
2711
|
+
} else {
|
|
2712
|
+
setConfig("hetzner:location", basicConfig.region, false, cwd);
|
|
2713
|
+
if (hcloudToken) {
|
|
2714
|
+
setConfig("hcloud:token", hcloudToken, true, cwd);
|
|
2715
|
+
}
|
|
2716
|
+
}
|
|
2717
|
+
setConfig("anthropicApiKey", anthropicApiKey, true, cwd);
|
|
2718
|
+
setConfig("tailscaleAuthKey", tailscaleAuthKey, true, cwd);
|
|
2719
|
+
setConfig("tailnetDnsName", tailnetDnsName, false, cwd);
|
|
2720
|
+
if (tailscaleApiKey) {
|
|
2721
|
+
setConfig("tailscaleApiKey", tailscaleApiKey, true, cwd);
|
|
2722
|
+
}
|
|
2723
|
+
setConfig("instanceType", basicConfig.instanceType, false, cwd);
|
|
2724
|
+
setConfig("ownerName", basicConfig.ownerName, false, cwd);
|
|
2725
|
+
setConfig("timezone", basicConfig.timezone, false, cwd);
|
|
2726
|
+
setConfig("workingHours", basicConfig.workingHours, false, cwd);
|
|
2727
|
+
setConfig("userNotes", basicConfig.userNotes, false, cwd);
|
|
2728
|
+
for (const [role, creds] of Object.entries(integrationCredentials)) {
|
|
2729
|
+
if (creds.linearApiKey) setConfig(`${role}LinearApiKey`, creds.linearApiKey, true, cwd);
|
|
2730
|
+
if (creds.linearWebhookSecret) setConfig(`${role}LinearWebhookSecret`, creds.linearWebhookSecret, true, cwd);
|
|
2731
|
+
if (creds.linearUserUuid) setConfig(`${role}LinearUserUuid`, creds.linearUserUuid, false, cwd);
|
|
2732
|
+
if (creds.githubToken) setConfig(`${role}GithubToken`, creds.githubToken, true, cwd);
|
|
2733
|
+
}
|
|
2734
|
+
for (const [role, creds] of Object.entries(slackCredentials)) {
|
|
2735
|
+
setConfig(`${role}SlackBotToken`, creds.botToken, true, cwd);
|
|
2736
|
+
setConfig(`${role}SlackAppToken`, creds.appToken, true, cwd);
|
|
2737
|
+
}
|
|
2738
|
+
if (braveApiKey) setConfig("braveApiKey", braveApiKey, true, cwd);
|
|
2739
|
+
s.stop("Configuration saved");
|
|
2740
|
+
const configName = basicConfig.stackName;
|
|
2741
|
+
s.start(`Writing config to ~/.clawup/configs/${configName}.yaml...`);
|
|
2742
|
+
const manifestTemplateVars = {};
|
|
2743
|
+
for (const [k, v] of Object.entries(templateVars)) {
|
|
2744
|
+
if (!autoVars[k]) {
|
|
2745
|
+
manifestTemplateVars[k] = v;
|
|
2746
|
+
}
|
|
2747
|
+
}
|
|
2748
|
+
const manifest = {
|
|
2749
|
+
stackName: configName,
|
|
2750
|
+
provider: basicConfig.provider,
|
|
2751
|
+
region: basicConfig.region,
|
|
2752
|
+
instanceType: basicConfig.instanceType,
|
|
2753
|
+
ownerName: basicConfig.ownerName,
|
|
2754
|
+
timezone: basicConfig.timezone,
|
|
2755
|
+
workingHours: basicConfig.workingHours,
|
|
2756
|
+
userNotes: basicConfig.userNotes,
|
|
2757
|
+
templateVars: Object.keys(manifestTemplateVars).length > 0 ? manifestTemplateVars : void 0,
|
|
2758
|
+
agents
|
|
2759
|
+
};
|
|
2760
|
+
for (const fi of fetchedIdentities) {
|
|
2761
|
+
const rolePlugins = agentPlugins.get(fi.agent.name);
|
|
2762
|
+
if (!rolePlugins || rolePlugins.size === 0) continue;
|
|
2763
|
+
const inlinePlugins = {};
|
|
2764
|
+
const defaults = identityPluginDefaults[fi.agent.name] ?? {};
|
|
2765
|
+
for (const pluginName of rolePlugins) {
|
|
2766
|
+
const pluginDefaults = defaults[pluginName] ?? {};
|
|
2767
|
+
const agentConfig = {
|
|
2768
|
+
...pluginDefaults,
|
|
2769
|
+
agentId: fi.agent.name
|
|
2770
|
+
};
|
|
2771
|
+
if (pluginName === "openclaw-linear") {
|
|
2772
|
+
const creds = integrationCredentials[fi.agent.role];
|
|
2773
|
+
if (creds?.linearUserUuid) {
|
|
2774
|
+
agentConfig.linearUserUuid = creds.linearUserUuid;
|
|
2775
|
+
}
|
|
2776
|
+
}
|
|
2777
|
+
inlinePlugins[pluginName] = agentConfig;
|
|
2778
|
+
}
|
|
2779
|
+
if (Object.keys(inlinePlugins).length > 0) {
|
|
2780
|
+
fi.agent.plugins = inlinePlugins;
|
|
2781
|
+
}
|
|
2782
|
+
}
|
|
2783
|
+
saveManifest(configName, manifest);
|
|
2784
|
+
s.stop("Config saved");
|
|
2785
|
+
if (opts.deploy) {
|
|
2786
|
+
p4.log.success("Config saved! Starting deployment...\n");
|
|
2787
|
+
const { deployCommand: deployCommand2 } = await Promise.resolve().then(() => (init_deploy2(), deploy_exports));
|
|
2788
|
+
await deployCommand2({ config: configName, yes: opts.yes });
|
|
2789
|
+
} else {
|
|
2790
|
+
p4.outro("Setup complete! Run `clawup deploy` to deploy your agents.");
|
|
2791
|
+
}
|
|
2792
|
+
}
|
|
2793
|
+
|
|
2794
|
+
// bin.ts
|
|
2795
|
+
init_deploy2();
|
|
2796
|
+
|
|
2797
|
+
// commands/status.ts
|
|
2798
|
+
init_tools();
|
|
2799
|
+
async function statusCommand(opts) {
|
|
2800
|
+
await statusTool(createCLIAdapter(), opts);
|
|
2801
|
+
}
|
|
2802
|
+
|
|
2803
|
+
// commands/ssh.ts
|
|
2804
|
+
var p5 = __toESM(require("@clack/prompts"));
|
|
2805
|
+
init_config();
|
|
2806
|
+
init_pulumi();
|
|
2807
|
+
var import_core10 = require("@clawup/core");
|
|
2808
|
+
init_workspace();
|
|
2809
|
+
init_ui();
|
|
2810
|
+
init_tailscale();
|
|
2811
|
+
var import_child_process8 = require("child_process");
|
|
2812
|
+
async function sshCommand(agentNameOrAlias, commandArgs, opts) {
|
|
2813
|
+
requireTailscale();
|
|
2814
|
+
const wsResult = ensureWorkspace();
|
|
2815
|
+
if (!wsResult.ok) {
|
|
2816
|
+
exitWithError(wsResult.error ?? "Failed to set up workspace.");
|
|
2817
|
+
}
|
|
2818
|
+
const cwd = getWorkspaceDir();
|
|
2819
|
+
let configName;
|
|
2820
|
+
try {
|
|
2821
|
+
configName = resolveConfigName(opts.config);
|
|
2822
|
+
} catch (err) {
|
|
2823
|
+
exitWithError(err.message);
|
|
2824
|
+
}
|
|
2825
|
+
const manifest = loadManifest(configName);
|
|
2826
|
+
if (!manifest) {
|
|
2827
|
+
exitWithError(`Config '${configName}' could not be loaded.`);
|
|
2828
|
+
}
|
|
2829
|
+
const stackResult = selectOrCreateStack(manifest.stackName, cwd);
|
|
2830
|
+
if (!stackResult.ok) {
|
|
2831
|
+
exitWithError(`Could not select Pulumi stack "${manifest.stackName}".`);
|
|
2832
|
+
}
|
|
2833
|
+
const query = agentNameOrAlias.toLowerCase();
|
|
2834
|
+
const resolvedRole = import_core10.AGENT_ALIASES[query] ?? query;
|
|
2835
|
+
const agent = manifest.agents.find(
|
|
2836
|
+
(a) => a.role === resolvedRole || a.name === query || a.name === `agent-${query}` || a.displayName.toLowerCase() === query
|
|
2837
|
+
);
|
|
2838
|
+
if (!agent) {
|
|
2839
|
+
const validNames = manifest.agents.map((a) => `${a.role}, ${a.displayName.toLowerCase()}, ${a.name}`).join("\n ");
|
|
2840
|
+
exitWithError(
|
|
2841
|
+
`Unknown agent: "${agentNameOrAlias}"
|
|
2842
|
+
Valid identifiers (any of these work):
|
|
2843
|
+
${validNames}`
|
|
2844
|
+
);
|
|
2845
|
+
}
|
|
2846
|
+
const tailnetDnsName = getConfig("tailnetDnsName", cwd);
|
|
2847
|
+
if (!tailnetDnsName) {
|
|
2848
|
+
exitWithError("Could not determine tailnet DNS name from Pulumi config.");
|
|
2849
|
+
}
|
|
2850
|
+
const tsHost = (0, import_core10.tailscaleHostname)(manifest.stackName, agent.name);
|
|
2851
|
+
const sshHost = `${tsHost}.${tailnetDnsName}`;
|
|
2852
|
+
const user = opts.user ?? import_core10.SSH_USER;
|
|
2853
|
+
p5.log.info(`Connecting to ${agent.displayName} (${sshHost})...`);
|
|
2854
|
+
const sshArgs = ["-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null", `${user}@${sshHost}`];
|
|
2855
|
+
if (commandArgs.length > 0) {
|
|
2856
|
+
sshArgs.push(commandArgs.join(" "));
|
|
2857
|
+
}
|
|
2858
|
+
const child = (0, import_child_process8.spawn)("ssh", sshArgs, { stdio: "inherit" });
|
|
2859
|
+
child.on("close", (code) => {
|
|
2860
|
+
process.exit(code ?? 0);
|
|
2861
|
+
});
|
|
2862
|
+
}
|
|
2863
|
+
|
|
2864
|
+
// commands/validate.ts
|
|
2865
|
+
init_tools();
|
|
2866
|
+
async function validateCommand(opts) {
|
|
2867
|
+
await validateTool(createCLIAdapter(), opts);
|
|
2868
|
+
}
|
|
2869
|
+
|
|
2870
|
+
// commands/push.ts
|
|
2871
|
+
init_tools();
|
|
2872
|
+
init_ui();
|
|
2873
|
+
async function pushCommand(opts) {
|
|
2874
|
+
try {
|
|
2875
|
+
await pushTool(createCLIAdapter(), opts);
|
|
2876
|
+
} catch (err) {
|
|
2877
|
+
exitWithError(err instanceof Error ? err.message : String(err));
|
|
2878
|
+
}
|
|
2879
|
+
}
|
|
2880
|
+
|
|
2881
|
+
// commands/destroy.ts
|
|
2882
|
+
init_tools();
|
|
2883
|
+
async function destroyCommand(opts) {
|
|
2884
|
+
await destroyTool(createCLIAdapter(), opts);
|
|
2885
|
+
}
|
|
2886
|
+
|
|
2887
|
+
// commands/list.ts
|
|
2888
|
+
init_config();
|
|
2889
|
+
async function listCommand(opts) {
|
|
2890
|
+
const configs = listManifests();
|
|
2891
|
+
if (configs.length === 0) {
|
|
2892
|
+
console.log("No configs found. Run 'clawup init' to create one.");
|
|
2893
|
+
return;
|
|
2894
|
+
}
|
|
2895
|
+
const data = configs.map((name) => {
|
|
2896
|
+
const manifest = loadManifest(name);
|
|
2897
|
+
return {
|
|
2898
|
+
name,
|
|
2899
|
+
agents: manifest?.agents.length ?? 0,
|
|
2900
|
+
region: manifest?.region ?? "-",
|
|
2901
|
+
stack: manifest?.stackName ?? "-",
|
|
2902
|
+
path: configPath(name)
|
|
2903
|
+
};
|
|
2904
|
+
});
|
|
2905
|
+
if (opts.json) {
|
|
2906
|
+
console.log(JSON.stringify(data, null, 2));
|
|
2907
|
+
return;
|
|
2908
|
+
}
|
|
2909
|
+
const nameWidth = Math.max(6, ...data.map((d) => d.name.length));
|
|
2910
|
+
const agentsWidth = 6;
|
|
2911
|
+
const regionWidth = Math.max(6, ...data.map((d) => d.region.length));
|
|
2912
|
+
const stackWidth = Math.max(5, ...data.map((d) => d.stack.length));
|
|
2913
|
+
console.log(
|
|
2914
|
+
`${"NAME".padEnd(nameWidth)} ${"AGENTS".padEnd(agentsWidth)} ${"REGION".padEnd(regionWidth)} ${"STACK".padEnd(stackWidth)}`
|
|
2915
|
+
);
|
|
2916
|
+
console.log(
|
|
2917
|
+
`${"-".repeat(nameWidth)} ${"-".repeat(agentsWidth)} ${"-".repeat(regionWidth)} ${"-".repeat(stackWidth)}`
|
|
2918
|
+
);
|
|
2919
|
+
for (const row of data) {
|
|
2920
|
+
console.log(
|
|
2921
|
+
`${row.name.padEnd(nameWidth)} ${String(row.agents).padEnd(agentsWidth)} ${row.region.padEnd(regionWidth)} ${row.stack.padEnd(stackWidth)}`
|
|
2922
|
+
);
|
|
2923
|
+
}
|
|
2924
|
+
console.log(`
|
|
2925
|
+
${data.length} config(s) found`);
|
|
2926
|
+
}
|
|
2927
|
+
|
|
2928
|
+
// commands/update.ts
|
|
2929
|
+
var p6 = __toESM(require("@clack/prompts"));
|
|
2930
|
+
var import_picocolors9 = __toESM(require("picocolors"));
|
|
2931
|
+
init_exec();
|
|
2932
|
+
async function updateCommand(_opts) {
|
|
2933
|
+
p6.intro(import_picocolors9.default.bgCyan(import_picocolors9.default.black(" Agent Army \u2014 Update ")));
|
|
2934
|
+
const s = p6.spinner();
|
|
2935
|
+
s.start("Checking npm for latest version\u2026");
|
|
2936
|
+
const { default: https2 } = await import("https");
|
|
2937
|
+
const latest = await new Promise((resolve2) => {
|
|
2938
|
+
const req = https2.get(
|
|
2939
|
+
"https://registry.npmjs.org/clawup/latest",
|
|
2940
|
+
{ timeout: 5e3 },
|
|
2941
|
+
(res) => {
|
|
2942
|
+
let data = "";
|
|
2943
|
+
res.on("data", (chunk) => data += chunk);
|
|
2944
|
+
res.on("end", () => {
|
|
2945
|
+
try {
|
|
2946
|
+
resolve2(JSON.parse(data).version ?? null);
|
|
2947
|
+
} catch {
|
|
2948
|
+
resolve2(null);
|
|
2949
|
+
}
|
|
2950
|
+
});
|
|
2951
|
+
}
|
|
2952
|
+
);
|
|
2953
|
+
req.on("timeout", () => {
|
|
2954
|
+
req.destroy();
|
|
2955
|
+
resolve2(null);
|
|
2956
|
+
});
|
|
2957
|
+
req.on("error", () => resolve2(null));
|
|
2958
|
+
});
|
|
2959
|
+
if (!latest) {
|
|
2960
|
+
s.stop("Failed to reach npm registry");
|
|
2961
|
+
p6.log.error("Could not check for updates. Check your internet connection.");
|
|
2962
|
+
process.exit(1);
|
|
2963
|
+
}
|
|
2964
|
+
const fs8 = await import("fs");
|
|
2965
|
+
const path8 = await import("path");
|
|
2966
|
+
const pkgJson2 = JSON.parse(
|
|
2967
|
+
fs8.readFileSync(path8.join(__dirname, "..", "..", "package.json"), "utf-8")
|
|
2968
|
+
);
|
|
2969
|
+
const current = pkgJson2.version;
|
|
2970
|
+
s.stop(`Current: ${import_picocolors9.default.dim(current)} Latest: ${import_picocolors9.default.green(latest)}`);
|
|
2971
|
+
const cParts = current.split(".").map(Number);
|
|
2972
|
+
const lParts = latest.split(".").map(Number);
|
|
2973
|
+
let isOutdated = false;
|
|
2974
|
+
for (let i = 0; i < 3; i++) {
|
|
2975
|
+
if ((lParts[i] ?? 0) > (cParts[i] ?? 0)) {
|
|
2976
|
+
isOutdated = true;
|
|
2977
|
+
break;
|
|
2978
|
+
}
|
|
2979
|
+
if ((lParts[i] ?? 0) < (cParts[i] ?? 0)) break;
|
|
2980
|
+
}
|
|
2981
|
+
if (!isOutdated) {
|
|
2982
|
+
p6.log.success("You're already on the latest version!");
|
|
2983
|
+
p6.outro("Done");
|
|
2984
|
+
return;
|
|
2985
|
+
}
|
|
2986
|
+
p6.log.step(`Updating ${import_picocolors9.default.dim(current)} \u2192 ${import_picocolors9.default.green(latest)}`);
|
|
2987
|
+
console.log();
|
|
2988
|
+
const MAX_RETRIES2 = 3;
|
|
2989
|
+
const RETRY_DELAY_MS = 1e4;
|
|
2990
|
+
let exitCode = 1;
|
|
2991
|
+
for (let attempt = 1; attempt <= MAX_RETRIES2; attempt++) {
|
|
2992
|
+
exitCode = await stream("npm", ["install", "-g", `clawup@${latest}`]);
|
|
2993
|
+
if (exitCode === 0) break;
|
|
2994
|
+
if (attempt < MAX_RETRIES2) {
|
|
2995
|
+
console.log();
|
|
2996
|
+
p6.log.warn(
|
|
2997
|
+
`Install failed (attempt ${attempt}/${MAX_RETRIES2}). Retrying in ${RETRY_DELAY_MS / 1e3}s \u2014 the new version may still be propagating\u2026`
|
|
2998
|
+
);
|
|
2999
|
+
await new Promise((r) => setTimeout(r, RETRY_DELAY_MS));
|
|
3000
|
+
console.log();
|
|
3001
|
+
}
|
|
3002
|
+
}
|
|
3003
|
+
console.log();
|
|
3004
|
+
if (exitCode === 0) {
|
|
3005
|
+
p6.log.success(`Updated to ${import_picocolors9.default.green(latest)}`);
|
|
3006
|
+
} else {
|
|
3007
|
+
p6.log.error("Update failed. You may need to run with sudo:");
|
|
3008
|
+
p6.log.message(` sudo npm install -g clawup@${latest}`);
|
|
3009
|
+
process.exit(1);
|
|
3010
|
+
}
|
|
3011
|
+
p6.outro("Done");
|
|
3012
|
+
}
|
|
3013
|
+
|
|
3014
|
+
// commands/config.ts
|
|
3015
|
+
var process2 = __toESM(require("process"));
|
|
3016
|
+
var import_yaml2 = __toESM(require("yaml"));
|
|
3017
|
+
var fs5 = __toESM(require("fs"));
|
|
3018
|
+
init_config();
|
|
3019
|
+
var import_core11 = require("@clawup/core");
|
|
3020
|
+
var import_picocolors10 = __toESM(require("picocolors"));
|
|
3021
|
+
var SETTABLE_TOP_KEYS = [
|
|
3022
|
+
"region",
|
|
3023
|
+
"instanceType",
|
|
3024
|
+
"ownerName",
|
|
3025
|
+
"timezone",
|
|
3026
|
+
"workingHours",
|
|
3027
|
+
"userNotes"
|
|
3028
|
+
];
|
|
3029
|
+
var SETTABLE_AGENT_KEYS = [
|
|
3030
|
+
"instanceType",
|
|
3031
|
+
"volumeSize",
|
|
3032
|
+
"displayName"
|
|
3033
|
+
];
|
|
3034
|
+
function isSettableTopKey(key) {
|
|
3035
|
+
return SETTABLE_TOP_KEYS.includes(key);
|
|
3036
|
+
}
|
|
3037
|
+
function isSettableAgentKey(key) {
|
|
3038
|
+
return SETTABLE_AGENT_KEYS.includes(key);
|
|
3039
|
+
}
|
|
3040
|
+
function allRegionValues(provider) {
|
|
3041
|
+
if (provider === "hetzner") return import_core11.HETZNER_LOCATIONS.map((r) => r.value);
|
|
3042
|
+
return import_core11.AWS_REGIONS.map((r) => r.value);
|
|
3043
|
+
}
|
|
3044
|
+
function allInstanceTypeValues(provider, region) {
|
|
3045
|
+
if (provider === "hetzner") {
|
|
3046
|
+
const types = region ? (0, import_core11.hetznerServerTypes)(region) : [...import_core11.HETZNER_SERVER_TYPES_EU, ...import_core11.HETZNER_SERVER_TYPES_US];
|
|
3047
|
+
return types.map((t) => t.value);
|
|
3048
|
+
}
|
|
3049
|
+
return import_core11.INSTANCE_TYPES.map((t) => t.value);
|
|
3050
|
+
}
|
|
3051
|
+
function validateTopValue(manifest, key, value) {
|
|
3052
|
+
const provider = manifest.provider ?? "aws";
|
|
3053
|
+
if (key === "region") {
|
|
3054
|
+
const valid = allRegionValues(provider);
|
|
3055
|
+
if (!valid.includes(value)) {
|
|
3056
|
+
return `Invalid region '${value}' for provider '${provider}'. Valid options:
|
|
3057
|
+
${valid.join("\n ")}`;
|
|
3058
|
+
}
|
|
3059
|
+
if (provider === "hetzner" && manifest.instanceType) {
|
|
3060
|
+
const validTypes = allInstanceTypeValues(provider, value);
|
|
3061
|
+
if (!validTypes.includes(manifest.instanceType)) {
|
|
3062
|
+
return `Region '${value}' is not compatible with current instanceType '${manifest.instanceType}'.
|
|
3063
|
+
Update instanceType first, or set both:
|
|
3064
|
+
clawup config set instanceType <type>
|
|
3065
|
+
clawup config set region ${value}`;
|
|
3066
|
+
}
|
|
3067
|
+
}
|
|
3068
|
+
}
|
|
3069
|
+
if (key === "instanceType") {
|
|
3070
|
+
const valid = allInstanceTypeValues(provider, manifest.region);
|
|
3071
|
+
if (!valid.includes(value)) {
|
|
3072
|
+
return `Invalid instanceType '${value}' for provider '${provider}'. Valid options:
|
|
3073
|
+
${valid.join("\n ")}`;
|
|
3074
|
+
}
|
|
3075
|
+
}
|
|
3076
|
+
return null;
|
|
3077
|
+
}
|
|
3078
|
+
function validateAgentValue(manifest, key, value) {
|
|
3079
|
+
if (key === "instanceType") {
|
|
3080
|
+
const provider = manifest.provider ?? "aws";
|
|
3081
|
+
const valid = allInstanceTypeValues(provider, manifest.region);
|
|
3082
|
+
if (!valid.includes(value)) {
|
|
3083
|
+
return `Invalid instanceType '${value}' for provider '${provider}'. Valid options:
|
|
3084
|
+
${valid.join("\n ")}`;
|
|
3085
|
+
}
|
|
3086
|
+
}
|
|
3087
|
+
if (key === "volumeSize") {
|
|
3088
|
+
const num = Number(value);
|
|
3089
|
+
if (isNaN(num) || num < 8 || num > 1e3) {
|
|
3090
|
+
return `volumeSize must be a number between 8 and 1000 (GB). Got: '${value}'`;
|
|
3091
|
+
}
|
|
3092
|
+
}
|
|
3093
|
+
return null;
|
|
3094
|
+
}
|
|
3095
|
+
async function configShowCommand(opts) {
|
|
3096
|
+
let configName;
|
|
3097
|
+
try {
|
|
3098
|
+
configName = resolveConfigName(opts.config);
|
|
3099
|
+
} catch (err) {
|
|
3100
|
+
console.error(import_picocolors10.default.red(err.message));
|
|
3101
|
+
process2.exit(1);
|
|
3102
|
+
}
|
|
3103
|
+
const manifest = loadManifest(configName);
|
|
3104
|
+
if (!manifest) {
|
|
3105
|
+
console.error(import_picocolors10.default.red(`Config '${configName}' could not be loaded.`));
|
|
3106
|
+
process2.exit(1);
|
|
3107
|
+
}
|
|
3108
|
+
if (opts.json) {
|
|
3109
|
+
console.log(import_yaml2.default.stringify(manifest).trimEnd());
|
|
3110
|
+
return;
|
|
3111
|
+
}
|
|
3112
|
+
console.log();
|
|
3113
|
+
console.log(import_picocolors10.default.bold(`Config: ${configName}`));
|
|
3114
|
+
console.log();
|
|
3115
|
+
console.log(` Stack: ${manifest.stackName}`);
|
|
3116
|
+
console.log(` Provider: ${manifest.provider ?? "aws"}`);
|
|
3117
|
+
console.log(` Region: ${manifest.region}`);
|
|
3118
|
+
console.log(` Instance Type: ${manifest.instanceType}`);
|
|
3119
|
+
console.log(` Owner: ${manifest.ownerName}`);
|
|
3120
|
+
if (manifest.timezone) console.log(` Timezone: ${manifest.timezone}`);
|
|
3121
|
+
if (manifest.workingHours) console.log(` Working Hours: ${manifest.workingHours}`);
|
|
3122
|
+
if (manifest.userNotes) console.log(` User Notes: ${manifest.userNotes}`);
|
|
3123
|
+
if (manifest.templateVars && Object.keys(manifest.templateVars).length > 0) {
|
|
3124
|
+
console.log();
|
|
3125
|
+
console.log(import_picocolors10.default.bold("Template Variables:"));
|
|
3126
|
+
for (const [key, value] of Object.entries(manifest.templateVars)) {
|
|
3127
|
+
console.log(` ${key}: ${value}`);
|
|
3128
|
+
}
|
|
3129
|
+
}
|
|
3130
|
+
console.log();
|
|
3131
|
+
console.log(import_picocolors10.default.bold(`Agents (${manifest.agents.length}):`));
|
|
3132
|
+
for (const agent of manifest.agents) {
|
|
3133
|
+
const override = agent.instanceType ? ` [${agent.instanceType}]` : "";
|
|
3134
|
+
console.log(` ${import_picocolors10.default.bold(agent.displayName)} (${agent.role}) vol:${agent.volumeSize}GB${override}`);
|
|
3135
|
+
console.log(` identity: ${agent.identity}`);
|
|
3136
|
+
}
|
|
3137
|
+
console.log();
|
|
3138
|
+
}
|
|
3139
|
+
async function configSetCommand(key, value, opts) {
|
|
3140
|
+
let configName;
|
|
3141
|
+
try {
|
|
3142
|
+
configName = resolveConfigName(opts.config);
|
|
3143
|
+
} catch (err) {
|
|
3144
|
+
console.error(import_picocolors10.default.red(err.message));
|
|
3145
|
+
process2.exit(1);
|
|
3146
|
+
}
|
|
3147
|
+
const manifest = loadManifest(configName);
|
|
3148
|
+
if (!manifest) {
|
|
3149
|
+
console.error(import_picocolors10.default.red(`Config '${configName}' could not be loaded.`));
|
|
3150
|
+
process2.exit(1);
|
|
3151
|
+
}
|
|
3152
|
+
if (opts.agent) {
|
|
3153
|
+
if (!isSettableAgentKey(key)) {
|
|
3154
|
+
console.error(import_picocolors10.default.red(`Invalid per-agent key '${key}'. Valid keys:
|
|
3155
|
+
${SETTABLE_AGENT_KEYS.join("\n ")}`));
|
|
3156
|
+
process2.exit(1);
|
|
3157
|
+
}
|
|
3158
|
+
const agent = manifest.agents.find(
|
|
3159
|
+
(a) => a.name === opts.agent || a.displayName.toLowerCase() === opts.agent.toLowerCase() || a.role === opts.agent
|
|
3160
|
+
);
|
|
3161
|
+
if (!agent) {
|
|
3162
|
+
const names = manifest.agents.map((a) => `${a.displayName} (${a.role})`).join(", ");
|
|
3163
|
+
console.error(import_picocolors10.default.red(`Agent '${opts.agent}' not found. Available: ${names}`));
|
|
3164
|
+
process2.exit(1);
|
|
3165
|
+
}
|
|
3166
|
+
const err = validateAgentValue(manifest, key, value);
|
|
3167
|
+
if (err) {
|
|
3168
|
+
console.error(import_picocolors10.default.red(err));
|
|
3169
|
+
process2.exit(1);
|
|
3170
|
+
}
|
|
3171
|
+
const agentRec = agent;
|
|
3172
|
+
const oldValue = agentRec[key];
|
|
3173
|
+
if (key === "volumeSize") {
|
|
3174
|
+
agent.volumeSize = Number(value);
|
|
3175
|
+
} else {
|
|
3176
|
+
agentRec[key] = value;
|
|
3177
|
+
}
|
|
3178
|
+
saveManifest(configName, manifest);
|
|
3179
|
+
console.log(
|
|
3180
|
+
import_picocolors10.default.green(`\u2713 ${agent.displayName}.${key}: ${String(oldValue ?? "(unset)")} \u2192 ${value}`)
|
|
3181
|
+
);
|
|
3182
|
+
} else if (key.startsWith("templateVars.")) {
|
|
3183
|
+
const varName = key.slice("templateVars.".length);
|
|
3184
|
+
if (!varName) {
|
|
3185
|
+
console.error(import_picocolors10.default.red("Template variable name cannot be empty. Use: templateVars.KEY"));
|
|
3186
|
+
process2.exit(1);
|
|
3187
|
+
}
|
|
3188
|
+
if (!manifest.templateVars) manifest.templateVars = {};
|
|
3189
|
+
const oldValue = manifest.templateVars[varName];
|
|
3190
|
+
manifest.templateVars[varName] = value;
|
|
3191
|
+
saveManifest(configName, manifest);
|
|
3192
|
+
console.log(import_picocolors10.default.green(`\u2713 templateVars.${varName}: ${String(oldValue ?? "(unset)")} \u2192 ${value}`));
|
|
3193
|
+
} else {
|
|
3194
|
+
if (!isSettableTopKey(key)) {
|
|
3195
|
+
console.error(import_picocolors10.default.red(`Invalid key '${key}'. Valid keys:
|
|
3196
|
+
${SETTABLE_TOP_KEYS.join("\n ")}
|
|
3197
|
+
templateVars.<KEY>`));
|
|
3198
|
+
process2.exit(1);
|
|
3199
|
+
}
|
|
3200
|
+
const err = validateTopValue(manifest, key, value);
|
|
3201
|
+
if (err) {
|
|
3202
|
+
console.error(import_picocolors10.default.red(err));
|
|
3203
|
+
process2.exit(1);
|
|
3204
|
+
}
|
|
3205
|
+
const manifestRec = manifest;
|
|
3206
|
+
const oldValue = manifestRec[key];
|
|
3207
|
+
manifestRec[key] = value;
|
|
3208
|
+
saveManifest(configName, manifest);
|
|
3209
|
+
console.log(import_picocolors10.default.green(`\u2713 ${key}: ${String(oldValue ?? "(unset)")} \u2192 ${value}`));
|
|
3210
|
+
}
|
|
3211
|
+
console.log(import_picocolors10.default.dim("\nRun 'clawup redeploy' or 'clawup destroy && clawup deploy' to apply changes."));
|
|
3212
|
+
}
|
|
3213
|
+
async function configMigrateCommand(opts) {
|
|
3214
|
+
let configName;
|
|
3215
|
+
try {
|
|
3216
|
+
configName = resolveConfigName(opts.config);
|
|
3217
|
+
} catch (err) {
|
|
3218
|
+
console.error(import_picocolors10.default.red(err.message));
|
|
3219
|
+
process2.exit(1);
|
|
3220
|
+
}
|
|
3221
|
+
const manifest = loadManifest(configName);
|
|
3222
|
+
if (!manifest) {
|
|
3223
|
+
console.error(import_picocolors10.default.red(`Config '${configName}' could not be loaded.`));
|
|
3224
|
+
process2.exit(1);
|
|
3225
|
+
}
|
|
3226
|
+
const pDir = pluginsDir(configName);
|
|
3227
|
+
if (!fs5.existsSync(pDir)) {
|
|
3228
|
+
console.log(import_picocolors10.default.green("No plugin config files found \u2014 nothing to migrate."));
|
|
3229
|
+
return;
|
|
3230
|
+
}
|
|
3231
|
+
const pluginFiles = fs5.readdirSync(pDir).filter((f) => f.endsWith(".yaml"));
|
|
3232
|
+
if (pluginFiles.length === 0) {
|
|
3233
|
+
console.log(import_picocolors10.default.green("No plugin config files found \u2014 nothing to migrate."));
|
|
3234
|
+
return;
|
|
3235
|
+
}
|
|
3236
|
+
let migratedCount = 0;
|
|
3237
|
+
for (const file of pluginFiles) {
|
|
3238
|
+
const pluginName = file.replace(/\.yaml$/, "");
|
|
3239
|
+
const pluginConfig = loadPluginConfig(configName, pluginName);
|
|
3240
|
+
if (!pluginConfig) continue;
|
|
3241
|
+
for (const agent of manifest.agents) {
|
|
3242
|
+
const roleConfig = pluginConfig.agents?.[agent.role];
|
|
3243
|
+
if (!roleConfig) continue;
|
|
3244
|
+
if (!agent.plugins) agent.plugins = {};
|
|
3245
|
+
if (!agent.plugins[pluginName]) {
|
|
3246
|
+
agent.plugins[pluginName] = roleConfig;
|
|
3247
|
+
migratedCount++;
|
|
3248
|
+
}
|
|
3249
|
+
}
|
|
3250
|
+
const filePath = `${pDir}/${file}`;
|
|
3251
|
+
fs5.unlinkSync(filePath);
|
|
3252
|
+
console.log(import_picocolors10.default.dim(` Removed ${filePath}`));
|
|
3253
|
+
}
|
|
3254
|
+
try {
|
|
3255
|
+
const remaining = fs5.readdirSync(pDir);
|
|
3256
|
+
if (remaining.length === 0) {
|
|
3257
|
+
fs5.rmdirSync(pDir);
|
|
3258
|
+
const stackDir = `${pDir}/..`;
|
|
3259
|
+
const stackRemaining = fs5.readdirSync(stackDir);
|
|
3260
|
+
if (stackRemaining.length === 0) {
|
|
3261
|
+
fs5.rmdirSync(stackDir);
|
|
3262
|
+
}
|
|
3263
|
+
}
|
|
3264
|
+
} catch {
|
|
3265
|
+
}
|
|
3266
|
+
saveManifest(configName, manifest);
|
|
3267
|
+
console.log(import_picocolors10.default.green(`
|
|
3268
|
+
\u2713 Migrated ${migratedCount} plugin config(s) into ${configName}.yaml`));
|
|
3269
|
+
console.log(import_picocolors10.default.dim("Plugin config is now inline in the manifest under each agent's 'plugins' field."));
|
|
3270
|
+
}
|
|
3271
|
+
|
|
3272
|
+
// commands/secrets.ts
|
|
3273
|
+
var process3 = __toESM(require("process"));
|
|
3274
|
+
init_config();
|
|
3275
|
+
init_pulumi();
|
|
3276
|
+
init_workspace();
|
|
3277
|
+
var import_picocolors11 = __toESM(require("picocolors"));
|
|
3278
|
+
var KNOWN_SECRETS = {
|
|
3279
|
+
anthropicApiKey: { label: "Anthropic API Key", perAgent: false, isSecret: true },
|
|
3280
|
+
tailscaleAuthKey: { label: "Tailscale Auth Key", perAgent: false, isSecret: true },
|
|
3281
|
+
tailscaleApiKey: { label: "Tailscale API Key", perAgent: false, isSecret: true },
|
|
3282
|
+
tailnetDnsName: { label: "Tailnet DNS Name", perAgent: false, isSecret: false },
|
|
3283
|
+
braveApiKey: { label: "Brave Search API Key", perAgent: false, isSecret: true },
|
|
3284
|
+
slackBotToken: { label: "Slack Bot Token", perAgent: true, isSecret: true },
|
|
3285
|
+
slackAppToken: { label: "Slack App Token", perAgent: true, isSecret: true },
|
|
3286
|
+
linearApiKey: { label: "Linear API Key", perAgent: true, isSecret: true },
|
|
3287
|
+
linearWebhookSecret: { label: "Linear Webhook Secret", perAgent: true, isSecret: true },
|
|
3288
|
+
linearUserUuid: { label: "Linear User UUID", perAgent: true, isSecret: false },
|
|
3289
|
+
githubToken: { label: "GitHub Token", perAgent: true, isSecret: true }
|
|
3290
|
+
};
|
|
3291
|
+
function resolveStackAndCwd(configName) {
|
|
3292
|
+
const manifest = loadManifest(configName);
|
|
3293
|
+
if (!manifest) {
|
|
3294
|
+
console.error(import_picocolors11.default.red(`Config '${configName}' could not be loaded.`));
|
|
3295
|
+
process3.exit(1);
|
|
3296
|
+
}
|
|
3297
|
+
const wsResult = ensureWorkspace();
|
|
3298
|
+
if (!wsResult.ok) {
|
|
3299
|
+
console.error(import_picocolors11.default.red(wsResult.error ?? "Failed to set up workspace."));
|
|
3300
|
+
process3.exit(1);
|
|
3301
|
+
}
|
|
3302
|
+
const cwd = getWorkspaceDir();
|
|
3303
|
+
const stackResult = selectOrCreateStack(manifest.stackName, cwd);
|
|
3304
|
+
if (!stackResult.ok) {
|
|
3305
|
+
console.error(import_picocolors11.default.red(`Could not select Pulumi stack "${manifest.stackName}": ${stackResult.error}`));
|
|
3306
|
+
process3.exit(1);
|
|
3307
|
+
}
|
|
3308
|
+
return { stackName: manifest.stackName, cwd };
|
|
3309
|
+
}
|
|
3310
|
+
async function secretsSetCommand(key, value, opts) {
|
|
3311
|
+
let configName;
|
|
3312
|
+
try {
|
|
3313
|
+
configName = resolveConfigName(opts.config);
|
|
3314
|
+
} catch (err) {
|
|
3315
|
+
console.error(import_picocolors11.default.red(err.message));
|
|
3316
|
+
process3.exit(1);
|
|
3317
|
+
}
|
|
3318
|
+
const meta = KNOWN_SECRETS[key];
|
|
3319
|
+
if (!meta) {
|
|
3320
|
+
const validKeys = Object.entries(KNOWN_SECRETS).map(([k, m]) => ` ${k}${m.perAgent ? " (per-agent: use --agent)" : ""}`).join("\n");
|
|
3321
|
+
console.error(import_picocolors11.default.red(`Unknown secret '${key}'. Valid keys:
|
|
3322
|
+
${validKeys}`));
|
|
3323
|
+
process3.exit(1);
|
|
3324
|
+
}
|
|
3325
|
+
let pulumiKey = key;
|
|
3326
|
+
if (meta.perAgent) {
|
|
3327
|
+
if (!opts.agent) {
|
|
3328
|
+
console.error(import_picocolors11.default.red(`'${key}' is a per-agent secret. Use --agent <role> to specify which agent.`));
|
|
3329
|
+
process3.exit(1);
|
|
3330
|
+
}
|
|
3331
|
+
pulumiKey = `${opts.agent}${key.charAt(0).toUpperCase()}${key.slice(1)}`;
|
|
3332
|
+
}
|
|
3333
|
+
const { cwd } = resolveStackAndCwd(configName);
|
|
3334
|
+
const ok = setConfig(pulumiKey, value, meta.isSecret, cwd);
|
|
3335
|
+
if (!ok) {
|
|
3336
|
+
console.error(import_picocolors11.default.red(`Failed to set '${pulumiKey}' in Pulumi config.`));
|
|
3337
|
+
process3.exit(1);
|
|
3338
|
+
}
|
|
3339
|
+
const maskedValue = meta.isSecret ? value.slice(0, 4) + "..." + value.slice(-4) : value;
|
|
3340
|
+
console.log(import_picocolors11.default.green(`\u2713 ${pulumiKey}: set to ${maskedValue}`));
|
|
3341
|
+
console.log(import_picocolors11.default.dim("\nRun 'clawup deploy' or 'clawup redeploy' to apply changes."));
|
|
3342
|
+
}
|
|
3343
|
+
async function secretsListCommand(opts) {
|
|
3344
|
+
let configName;
|
|
3345
|
+
try {
|
|
3346
|
+
configName = resolveConfigName(opts.config);
|
|
3347
|
+
} catch (err) {
|
|
3348
|
+
console.error(import_picocolors11.default.red(err.message));
|
|
3349
|
+
process3.exit(1);
|
|
3350
|
+
}
|
|
3351
|
+
const manifest = loadManifest(configName);
|
|
3352
|
+
if (!manifest) {
|
|
3353
|
+
console.error(import_picocolors11.default.red(`Config '${configName}' could not be loaded.`));
|
|
3354
|
+
process3.exit(1);
|
|
3355
|
+
}
|
|
3356
|
+
const { cwd } = resolveStackAndCwd(configName);
|
|
3357
|
+
const roles = manifest.agents.map((a) => a.role);
|
|
3358
|
+
console.log();
|
|
3359
|
+
console.log(import_picocolors11.default.bold(`Secrets for ${configName}:`));
|
|
3360
|
+
console.log();
|
|
3361
|
+
for (const [key, meta] of Object.entries(KNOWN_SECRETS)) {
|
|
3362
|
+
if (meta.perAgent) continue;
|
|
3363
|
+
const val = getConfig(key, cwd);
|
|
3364
|
+
const status = val ? import_picocolors11.default.green("\u2713 set") : import_picocolors11.default.dim("\u2717 not set");
|
|
3365
|
+
console.log(` ${meta.label.padEnd(24)} ${status}`);
|
|
3366
|
+
}
|
|
3367
|
+
const perAgentKeys = Object.entries(KNOWN_SECRETS).filter(([, m]) => m.perAgent);
|
|
3368
|
+
if (perAgentKeys.length > 0) {
|
|
3369
|
+
for (const role of roles) {
|
|
3370
|
+
console.log();
|
|
3371
|
+
console.log(import_picocolors11.default.bold(` ${role}:`));
|
|
3372
|
+
for (const [key, meta] of perAgentKeys) {
|
|
3373
|
+
const pulumiKey = `${role}${key.charAt(0).toUpperCase()}${key.slice(1)}`;
|
|
3374
|
+
const val = getConfig(pulumiKey, cwd);
|
|
3375
|
+
const status = val ? import_picocolors11.default.green("\u2713 set") : import_picocolors11.default.dim("\u2717 not set");
|
|
3376
|
+
console.log(` ${meta.label.padEnd(24)} ${status}`);
|
|
3377
|
+
}
|
|
3378
|
+
}
|
|
3379
|
+
}
|
|
3380
|
+
console.log();
|
|
3381
|
+
}
|
|
3382
|
+
|
|
3383
|
+
// commands/redeploy.ts
|
|
3384
|
+
init_tools();
|
|
3385
|
+
async function redeployCommand(opts) {
|
|
3386
|
+
await redeployTool(createCLIAdapter(), opts);
|
|
3387
|
+
}
|
|
3388
|
+
|
|
3389
|
+
// commands/webhooks.ts
|
|
3390
|
+
init_tools();
|
|
3391
|
+
async function webhooksSetupCommand(opts) {
|
|
3392
|
+
await webhooksSetupTool(createCLIAdapter(), opts);
|
|
3393
|
+
}
|
|
3394
|
+
|
|
3395
|
+
// lib/update-check.ts
|
|
3396
|
+
var https = __toESM(require("https"));
|
|
3397
|
+
var fs6 = __toESM(require("fs"));
|
|
3398
|
+
var path6 = __toESM(require("path"));
|
|
3399
|
+
var os5 = __toESM(require("os"));
|
|
3400
|
+
var import_picocolors12 = __toESM(require("picocolors"));
|
|
3401
|
+
var PACKAGE_NAME = "clawup";
|
|
3402
|
+
var CACHE_FILE = ".update-check.json";
|
|
3403
|
+
var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
3404
|
+
var FETCH_TIMEOUT_MS = 3e3;
|
|
3405
|
+
function getCacheDir() {
|
|
3406
|
+
return path6.join(os5.homedir(), ".clawup");
|
|
3407
|
+
}
|
|
3408
|
+
function getCachePath() {
|
|
3409
|
+
return path6.join(getCacheDir(), CACHE_FILE);
|
|
3410
|
+
}
|
|
3411
|
+
function loadCache() {
|
|
3412
|
+
try {
|
|
3413
|
+
const raw = fs6.readFileSync(getCachePath(), "utf-8");
|
|
3414
|
+
return JSON.parse(raw);
|
|
3415
|
+
} catch {
|
|
3416
|
+
return null;
|
|
3417
|
+
}
|
|
3418
|
+
}
|
|
3419
|
+
function saveCache(cache) {
|
|
3420
|
+
const dir = getCacheDir();
|
|
3421
|
+
if (!fs6.existsSync(dir)) {
|
|
3422
|
+
fs6.mkdirSync(dir, { recursive: true });
|
|
3423
|
+
}
|
|
3424
|
+
fs6.writeFileSync(getCachePath(), JSON.stringify(cache), "utf-8");
|
|
3425
|
+
}
|
|
3426
|
+
function compareSemver(a, b) {
|
|
3427
|
+
const pa = a.split(".").map(Number);
|
|
3428
|
+
const pb = b.split(".").map(Number);
|
|
3429
|
+
for (let i = 0; i < 3; i++) {
|
|
3430
|
+
const na = pa[i] ?? 0;
|
|
3431
|
+
const nb = pb[i] ?? 0;
|
|
3432
|
+
if (na > nb) return 1;
|
|
3433
|
+
if (na < nb) return -1;
|
|
3434
|
+
}
|
|
3435
|
+
return 0;
|
|
3436
|
+
}
|
|
3437
|
+
function fetchLatestVersion() {
|
|
3438
|
+
return new Promise((resolve2) => {
|
|
3439
|
+
const req = https.get(
|
|
3440
|
+
`https://registry.npmjs.org/${PACKAGE_NAME}/latest`,
|
|
3441
|
+
{ timeout: FETCH_TIMEOUT_MS },
|
|
3442
|
+
(res) => {
|
|
3443
|
+
let data = "";
|
|
3444
|
+
res.on("data", (chunk) => data += chunk);
|
|
3445
|
+
res.on("end", () => {
|
|
3446
|
+
try {
|
|
3447
|
+
const json = JSON.parse(data);
|
|
3448
|
+
resolve2(json.version ?? null);
|
|
3449
|
+
} catch {
|
|
3450
|
+
resolve2(null);
|
|
3451
|
+
}
|
|
3452
|
+
});
|
|
3453
|
+
}
|
|
3454
|
+
);
|
|
3455
|
+
req.on("timeout", () => {
|
|
3456
|
+
req.destroy();
|
|
3457
|
+
resolve2(null);
|
|
3458
|
+
});
|
|
3459
|
+
req.on("error", () => resolve2(null));
|
|
3460
|
+
});
|
|
3461
|
+
}
|
|
3462
|
+
async function checkForUpdates(currentVersion) {
|
|
3463
|
+
try {
|
|
3464
|
+
const cache = loadCache();
|
|
3465
|
+
const now = Date.now();
|
|
3466
|
+
if (cache && now - cache.lastChecked < CHECK_INTERVAL_MS) {
|
|
3467
|
+
if (compareSemver(cache.latestVersion, currentVersion) > 0) {
|
|
3468
|
+
printUpdateNotice(currentVersion, cache.latestVersion);
|
|
3469
|
+
}
|
|
3470
|
+
return;
|
|
3471
|
+
}
|
|
3472
|
+
const latest = await fetchLatestVersion();
|
|
3473
|
+
if (!latest) {
|
|
3474
|
+
saveCache({ lastChecked: now, latestVersion: currentVersion });
|
|
3475
|
+
return;
|
|
3476
|
+
}
|
|
3477
|
+
saveCache({ lastChecked: now, latestVersion: latest });
|
|
3478
|
+
if (compareSemver(latest, currentVersion) > 0) {
|
|
3479
|
+
printUpdateNotice(currentVersion, latest);
|
|
3480
|
+
}
|
|
3481
|
+
} catch {
|
|
3482
|
+
}
|
|
3483
|
+
}
|
|
3484
|
+
function printUpdateNotice(current, latest) {
|
|
3485
|
+
console.log();
|
|
3486
|
+
console.log(
|
|
3487
|
+
` Update available: ${import_picocolors12.default.dim(current)} ${import_picocolors12.default.dim("\u2192")} ${import_picocolors12.default.green(latest)}`
|
|
3488
|
+
);
|
|
3489
|
+
console.log(` Run ${import_picocolors12.default.cyan("clawup update")} to install`);
|
|
3490
|
+
console.log();
|
|
3491
|
+
}
|
|
3492
|
+
|
|
3493
|
+
// bin.ts
|
|
3494
|
+
setupGracefulShutdown();
|
|
3495
|
+
var pkgJson = JSON.parse(fs7.readFileSync(path7.join(__dirname, "..", "package.json"), "utf-8"));
|
|
3496
|
+
var program = new import_commander.Command();
|
|
3497
|
+
program.name("clawup").description("Deploy and manage a fleet of OpenClaw AI agents on AWS").version(pkgJson.version);
|
|
3498
|
+
program.command("init").description("Interactive setup wizard \u2014 configure stack, secrets, and agents").option("--deploy", "Deploy immediately after init").option("-y, --yes", "Skip confirmation prompt (for deploy)").action(async (opts) => {
|
|
3499
|
+
await initCommand(opts);
|
|
3500
|
+
});
|
|
3501
|
+
program.command("deploy").description("Deploy agents with pulumi up").option("-y, --yes", "Skip confirmation prompt").option("-c, --config <name>", "Config name (auto-detected if only one)").action(async (opts) => {
|
|
3502
|
+
await deployCommand(opts);
|
|
3503
|
+
});
|
|
3504
|
+
program.command("status").description("Show agent statuses from stack outputs").option("--json", "Output as JSON").option("-c, --config <name>", "Config name (auto-detected if only one)").action(async (opts) => {
|
|
3505
|
+
await statusCommand(opts);
|
|
3506
|
+
});
|
|
3507
|
+
program.command("ssh <agent>").description("SSH to an agent by name or alias (juno, titus, scout)").option("-u, --user <user>", "SSH user").option("-c, --config <name>", "Config name (auto-detected if only one)").argument("[command...]", "Command to run on the agent").action(async (agent, commandArgs, opts) => {
|
|
3508
|
+
await sshCommand(agent, commandArgs, opts);
|
|
3509
|
+
});
|
|
3510
|
+
program.command("validate").description("Health check agents via Tailscale SSH").option("-t, --timeout <seconds>", "SSH timeout in seconds", "30").option("-c, --config <name>", "Config name (auto-detected if only one)").action(async (opts) => {
|
|
3511
|
+
await validateCommand(opts);
|
|
3512
|
+
});
|
|
3513
|
+
program.command("push").description("Push workspace files, skills, and config to running agents").option("--skills", "Sync skills to remote workspace").option("--workspace", "Sync workspace files from identity").option("--memory-reset", "Remove remote memory/ dir and MEMORY.md").option("--openclaw", "Upgrade openclaw to latest + restart gateway").option("--config-push", "Copy local openclaw.json to remote + restart gateway").option("-a, --agent <name>", "Target a single agent (name, role, or alias)").option("-c, --config <name>", "Config name (auto-detected if only one)").action(async (opts) => {
|
|
3514
|
+
await pushCommand({
|
|
3515
|
+
skills: opts.skills,
|
|
3516
|
+
workspace: opts.workspace,
|
|
3517
|
+
memoryReset: opts.memoryReset,
|
|
3518
|
+
openclaw: opts.openclaw,
|
|
3519
|
+
pushConfig: opts.configPush,
|
|
3520
|
+
agent: opts.agent,
|
|
3521
|
+
config: opts.config
|
|
3522
|
+
});
|
|
3523
|
+
});
|
|
3524
|
+
program.command("redeploy").description("Update agents in-place without destroying infrastructure").option("-y, --yes", "Skip confirmation prompt").option("-c, --config <name>", "Config name (auto-detected if only one)").action(async (opts) => {
|
|
3525
|
+
await redeployCommand(opts);
|
|
3526
|
+
});
|
|
3527
|
+
program.command("destroy").description("Tear down all resources with safety confirmations").option("-y, --yes", "Skip confirmation prompts (dangerous!)").option("-c, --config <name>", "Config name (auto-detected if only one)").action(async (opts) => {
|
|
3528
|
+
await destroyCommand(opts);
|
|
3529
|
+
});
|
|
3530
|
+
program.command("list").description("List all saved configs").option("--json", "Output as JSON").action(async (opts) => {
|
|
3531
|
+
await listCommand(opts);
|
|
3532
|
+
});
|
|
3533
|
+
var configCmd = program.command("config").description("View or modify config without re-running init");
|
|
3534
|
+
configCmd.command("show").description("Display current config").option("--json", "Output as JSON").option("-c, --config <name>", "Config name (auto-detected if only one)").action(async (opts) => {
|
|
3535
|
+
await configShowCommand(opts);
|
|
3536
|
+
});
|
|
3537
|
+
configCmd.command("set <key> <value>").description("Update a config value").option("-c, --config <name>", "Config name (auto-detected if only one)").option("-a, --agent <name>", "Target a specific agent").action(async (key, value, opts) => {
|
|
3538
|
+
await configSetCommand(key, value, opts);
|
|
3539
|
+
});
|
|
3540
|
+
configCmd.command("migrate").description("Migrate plugin config files into the manifest (one-time upgrade)").option("-c, --config <name>", "Config name (auto-detected if only one)").action(async (opts) => {
|
|
3541
|
+
await configMigrateCommand(opts);
|
|
3542
|
+
});
|
|
3543
|
+
var secretsCmd = program.command("secrets").description("View or update Pulumi secrets without re-running init");
|
|
3544
|
+
secretsCmd.command("set <key> <value>").description("Set a secret (e.g. braveApiKey, anthropicApiKey)").option("-c, --config <name>", "Config name (auto-detected if only one)").option("-a, --agent <role>", "Agent role for per-agent secrets (e.g. eng, pm, tester)").action(async (key, value, opts) => {
|
|
3545
|
+
await secretsSetCommand(key, value, opts);
|
|
3546
|
+
});
|
|
3547
|
+
secretsCmd.command("list").description("Show which secrets are configured (values redacted)").option("-c, --config <name>", "Config name (auto-detected if only one)").action(async (opts) => {
|
|
3548
|
+
await secretsListCommand(opts);
|
|
3549
|
+
});
|
|
3550
|
+
var webhooksCmd = program.command("webhooks").description("Manage agent webhooks");
|
|
3551
|
+
webhooksCmd.command("setup").description("Configure Linear webhooks for deployed agents").option("-c, --config <name>", "Config name (auto-detected if only one)").action(async (opts) => {
|
|
3552
|
+
await webhooksSetupCommand(opts);
|
|
3553
|
+
});
|
|
3554
|
+
program.command("update").description("Update clawup CLI to the latest version").action(async (opts) => {
|
|
3555
|
+
await updateCommand(opts);
|
|
3556
|
+
});
|
|
3557
|
+
checkForUpdates(pkgJson.version).catch(() => {
|
|
3558
|
+
});
|
|
220
3559
|
program.parse();
|
|
221
|
-
//# sourceMappingURL=bin.js.map
|
|
3560
|
+
//# sourceMappingURL=bin.js.map
|