panopticon-cli 0.5.4 → 0.5.6
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/{agents-HNMF52RM.js → agents-5HWTDR4S.js} +12 -9
- package/dist/archive-planning-U3AZAKWI.js +16 -0
- package/dist/{chunk-KBHRXV5T.js → chunk-43F4LDZ4.js} +3 -3
- package/dist/chunk-6OYUJ4AJ.js +146 -0
- package/dist/chunk-6OYUJ4AJ.js.map +1 -0
- package/dist/{chunk-MOPGR3CL.js → chunk-AAP4G6U7.js} +1 -1
- package/dist/chunk-AAP4G6U7.js.map +1 -0
- package/dist/{chunk-4HST45MO.js → chunk-BYWVPPAZ.js} +19 -12
- package/dist/chunk-BYWVPPAZ.js.map +1 -0
- package/dist/{chunk-CFCUOV3Q.js → chunk-DMRTN432.js} +4 -1
- package/dist/chunk-DMRTN432.js.map +1 -0
- package/dist/{chunk-HOGYHJ2G.js → chunk-DW3PKGIS.js} +2 -2
- package/dist/{chunk-KY2E2Q3T.js → chunk-FUUP55PE.js} +104 -46
- package/dist/chunk-FUUP55PE.js.map +1 -0
- package/dist/chunk-GUV2EPBG.js +692 -0
- package/dist/chunk-GUV2EPBG.js.map +1 -0
- package/dist/{chunk-44EOY2ZL.js → chunk-HHL3AWXA.js} +46 -2
- package/dist/chunk-HHL3AWXA.js.map +1 -0
- package/dist/{chunk-6N2KBSJA.js → chunk-IZIXJYXZ.js} +40 -6
- package/dist/chunk-IZIXJYXZ.js.map +1 -0
- package/dist/chunk-MJXYTGK5.js +64 -0
- package/dist/chunk-MJXYTGK5.js.map +1 -0
- package/dist/chunk-OJF4QS3S.js +269 -0
- package/dist/chunk-OJF4QS3S.js.map +1 -0
- package/dist/{chunk-FQ66DECN.js → chunk-QAJAJBFW.js} +1 -1
- package/dist/chunk-QAJAJBFW.js.map +1 -0
- package/dist/chunk-R4KPLLRB.js +36 -0
- package/dist/chunk-R4KPLLRB.js.map +1 -0
- package/dist/{chunk-DFNVHK3N.js → chunk-SUM2WVPF.js} +4 -4
- package/dist/{chunk-T7BBPDEJ.js → chunk-UKSGE6RH.js} +45 -15
- package/dist/chunk-UKSGE6RH.js.map +1 -0
- package/dist/chunk-W2OTF6OS.js +201 -0
- package/dist/chunk-W2OTF6OS.js.map +1 -0
- package/dist/chunk-WEQW3EAT.js +78 -0
- package/dist/chunk-WEQW3EAT.js.map +1 -0
- package/dist/{chunk-ID4OYXVH.js → chunk-WJJ3ZIQ6.js} +112 -45
- package/dist/chunk-WJJ3ZIQ6.js.map +1 -0
- package/dist/chunk-YAAT66RT.js +70 -0
- package/dist/chunk-YAAT66RT.js.map +1 -0
- package/dist/{chunk-RLZQB7HS.js → chunk-ZMJFEHGF.js} +13 -1
- package/dist/chunk-ZMJFEHGF.js.map +1 -0
- package/dist/{chunk-HRU7S4TA.js → chunk-ZN5RHWGR.js} +18 -208
- package/dist/{chunk-HRU7S4TA.js.map → chunk-ZN5RHWGR.js.map} +1 -1
- package/dist/{chunk-ZTYHZMEC.js → chunk-ZWZNEA26.js} +2 -2
- package/dist/clean-planning-7Z5YY64X.js +9 -0
- package/dist/cli/index.js +1301 -2142
- package/dist/cli/index.js.map +1 -1
- package/dist/close-issue-CTZK777I.js +9 -0
- package/dist/compact-beads-72SHALOL.js +9 -0
- package/dist/{config-4CJNUE3O.js → config-FFTMBVHM.js} +2 -2
- package/dist/dashboard/public/assets/{index-DSvt5pPn.css → index-Bx4NCn9A.css} +1 -1
- package/dist/dashboard/public/assets/index-Db9NOz4z.js +756 -0
- package/dist/dashboard/public/index.html +3 -2
- package/dist/dashboard/server.js +34714 -34296
- package/dist/{feedback-writer-T43PI5S2.js → feedback-writer-T2WCT6EZ.js} +2 -2
- package/dist/{hume-CKJJ3OUU.js → hume-GVTB5BKW.js} +3 -3
- package/dist/index.d.ts +24 -16
- package/dist/index.js +4 -4
- package/dist/label-cleanup-4HJVX6NP.js +103 -0
- package/dist/label-cleanup-4HJVX6NP.js.map +1 -0
- package/dist/merge-agent-WM7ZKUET.js +1725 -0
- package/dist/merge-agent-WM7ZKUET.js.map +1 -0
- package/dist/{projects-KVM3MN3Y.js → projects-3CRF57ZU.js} +2 -2
- package/dist/{rally-RKFSWC7E.js → rally-LBY24P4C.js} +2 -2
- package/dist/{remote-agents-ULPD6C5U.js → remote-agents-3NZPSHYG.js} +2 -3
- package/dist/{remote-workspace-XX6ARE6I.js → remote-workspace-M4IULGFZ.js} +24 -49
- package/dist/remote-workspace-M4IULGFZ.js.map +1 -0
- package/dist/{review-status-XKUKZF6J.js → review-status-J2YJGL3E.js} +2 -2
- package/dist/{specialist-context-C66TEMXS.js → specialist-context-74RQF5SR.js} +7 -5
- package/dist/{specialist-context-C66TEMXS.js.map → specialist-context-74RQF5SR.js.map} +1 -1
- package/dist/{specialist-logs-CJKXM3SR.js → specialist-logs-T5GW7CSU.js} +6 -4
- package/dist/{specialists-NXYD4Z62.js → specialists-HTYYFXHQ.js} +6 -4
- package/dist/specialists-HTYYFXHQ.js.map +1 -0
- package/dist/tmux-X2I5SAIJ.js +31 -0
- package/dist/tmux-X2I5SAIJ.js.map +1 -0
- package/dist/{traefik-5GL3Q7DJ.js → traefik-QXLZ4PO2.js} +4 -4
- package/dist/traefik-QXLZ4PO2.js.map +1 -0
- package/dist/{tunnel-BKC7KLBX.js → tunnel-7IOSRZVH.js} +3 -3
- package/dist/tunnel-7IOSRZVH.js.map +1 -0
- package/dist/{workspace-manager-ALBR62AS.js → workspace-manager-G6TTBPC3.js} +6 -6
- package/dist/workspace-manager-G6TTBPC3.js.map +1 -0
- package/package.json +2 -2
- package/scripts/build-cost-script.mjs +17 -0
- package/scripts/heartbeat-hook +28 -8
- package/scripts/record-cost-event.js +46 -7
- package/scripts/record-cost-event.ts +2 -1
- package/dist/chunk-44EOY2ZL.js.map +0 -1
- package/dist/chunk-4HST45MO.js.map +0 -1
- package/dist/chunk-565HZ6VV.js +0 -159
- package/dist/chunk-565HZ6VV.js.map +0 -1
- package/dist/chunk-6N2KBSJA.js.map +0 -1
- package/dist/chunk-CFCUOV3Q.js.map +0 -1
- package/dist/chunk-FQ66DECN.js.map +0 -1
- package/dist/chunk-ID4OYXVH.js.map +0 -1
- package/dist/chunk-KY2E2Q3T.js.map +0 -1
- package/dist/chunk-MOPGR3CL.js.map +0 -1
- package/dist/chunk-RLZQB7HS.js.map +0 -1
- package/dist/chunk-T7BBPDEJ.js.map +0 -1
- package/dist/chunk-ZDNQFWR5.js +0 -650
- package/dist/chunk-ZDNQFWR5.js.map +0 -1
- package/dist/dashboard/public/assets/index-DA6pnizT.js +0 -767
- package/dist/remote-workspace-XX6ARE6I.js.map +0 -1
- /package/dist/{agents-HNMF52RM.js.map → agents-5HWTDR4S.js.map} +0 -0
- /package/dist/{config-4CJNUE3O.js.map → archive-planning-U3AZAKWI.js.map} +0 -0
- /package/dist/{chunk-KBHRXV5T.js.map → chunk-43F4LDZ4.js.map} +0 -0
- /package/dist/{chunk-HOGYHJ2G.js.map → chunk-DW3PKGIS.js.map} +0 -0
- /package/dist/{chunk-DFNVHK3N.js.map → chunk-SUM2WVPF.js.map} +0 -0
- /package/dist/{chunk-ZTYHZMEC.js.map → chunk-ZWZNEA26.js.map} +0 -0
- /package/dist/{hume-CKJJ3OUU.js.map → clean-planning-7Z5YY64X.js.map} +0 -0
- /package/dist/{projects-KVM3MN3Y.js.map → close-issue-CTZK777I.js.map} +0 -0
- /package/dist/{rally-RKFSWC7E.js.map → compact-beads-72SHALOL.js.map} +0 -0
- /package/dist/{remote-agents-ULPD6C5U.js.map → config-FFTMBVHM.js.map} +0 -0
- /package/dist/{feedback-writer-T43PI5S2.js.map → feedback-writer-T2WCT6EZ.js.map} +0 -0
- /package/dist/{review-status-XKUKZF6J.js.map → hume-GVTB5BKW.js.map} +0 -0
- /package/dist/{specialist-logs-CJKXM3SR.js.map → projects-3CRF57ZU.js.map} +0 -0
- /package/dist/{specialists-NXYD4Z62.js.map → rally-LBY24P4C.js.map} +0 -0
- /package/dist/{traefik-5GL3Q7DJ.js.map → remote-agents-3NZPSHYG.js.map} +0 -0
- /package/dist/{tunnel-BKC7KLBX.js.map → review-status-J2YJGL3E.js.map} +0 -0
- /package/dist/{workspace-manager-ALBR62AS.js.map → specialist-logs-T5GW7CSU.js.map} +0 -0
package/dist/chunk-ZDNQFWR5.js
DELETED
|
@@ -1,650 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
__esm,
|
|
3
|
-
init_esm_shims
|
|
4
|
-
} from "./chunk-ZHC57RCV.js";
|
|
5
|
-
|
|
6
|
-
// src/lib/remote/exe-provider.ts
|
|
7
|
-
import { exec, spawn } from "child_process";
|
|
8
|
-
import { promisify } from "util";
|
|
9
|
-
import { existsSync, readFileSync, readdirSync, statSync } from "fs";
|
|
10
|
-
import { join } from "path";
|
|
11
|
-
import { homedir } from "os";
|
|
12
|
-
function parseVmList(output) {
|
|
13
|
-
const vms = [];
|
|
14
|
-
const lines = output.trim().split("\n");
|
|
15
|
-
for (const line of lines) {
|
|
16
|
-
const trimmed = line.trim();
|
|
17
|
-
if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith("Your VMs")) continue;
|
|
18
|
-
const match = trimmed.match(/[•\-]\s*([a-z0-9-]+)\.exe\.xyz\s*-\s*(\w+)/i);
|
|
19
|
-
if (match) {
|
|
20
|
-
const name = match[1];
|
|
21
|
-
const statusText = match[2].toLowerCase();
|
|
22
|
-
const status = statusText === "running" ? "running" : statusText === "stopped" ? "stopped" : "unknown";
|
|
23
|
-
vms.push({ name, status });
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
return vms;
|
|
27
|
-
}
|
|
28
|
-
async function canSshToExeDev() {
|
|
29
|
-
try {
|
|
30
|
-
const { stdout } = await execAsync("ssh -o BatchMode=yes -o ConnectTimeout=5 exe.dev ls 2>&1", {
|
|
31
|
-
timeout: 1e4
|
|
32
|
-
});
|
|
33
|
-
return !stdout.includes("Permission denied") && !stdout.includes("Host key verification failed");
|
|
34
|
-
} catch (error) {
|
|
35
|
-
if (error.stdout && !error.stdout.includes("Permission denied")) {
|
|
36
|
-
return true;
|
|
37
|
-
}
|
|
38
|
-
return false;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
function createExeProvider(config) {
|
|
42
|
-
return new ExeProvider(config);
|
|
43
|
-
}
|
|
44
|
-
var execAsync, ExeProvider;
|
|
45
|
-
var init_exe_provider = __esm({
|
|
46
|
-
"src/lib/remote/exe-provider.ts"() {
|
|
47
|
-
"use strict";
|
|
48
|
-
init_esm_shims();
|
|
49
|
-
execAsync = promisify(exec);
|
|
50
|
-
ExeProvider = class {
|
|
51
|
-
name = "exe";
|
|
52
|
-
config;
|
|
53
|
-
constructor(config = {}) {
|
|
54
|
-
this.config = config;
|
|
55
|
-
}
|
|
56
|
-
/**
|
|
57
|
-
* Check if user is authenticated with exe.dev
|
|
58
|
-
* This checks if SSH key is configured and can connect
|
|
59
|
-
*/
|
|
60
|
-
async isAuthenticated() {
|
|
61
|
-
return canSshToExeDev();
|
|
62
|
-
}
|
|
63
|
-
/**
|
|
64
|
-
* Execute a command on the exe.dev CLI (via ssh exe.dev)
|
|
65
|
-
*/
|
|
66
|
-
async exeCmd(command) {
|
|
67
|
-
try {
|
|
68
|
-
const { stdout, stderr } = await execAsync(`ssh exe.dev ${command}`, {
|
|
69
|
-
timeout: 6e4,
|
|
70
|
-
maxBuffer: 10 * 1024 * 1024
|
|
71
|
-
});
|
|
72
|
-
return { stdout, stderr, exitCode: 0 };
|
|
73
|
-
} catch (error) {
|
|
74
|
-
return {
|
|
75
|
-
stdout: error.stdout || "",
|
|
76
|
-
stderr: error.stderr || error.message,
|
|
77
|
-
exitCode: error.code || 1
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
/**
|
|
82
|
-
* Create a new VM on exe.dev
|
|
83
|
-
*/
|
|
84
|
-
async createVm(name) {
|
|
85
|
-
try {
|
|
86
|
-
const result = await this.exeCmd(`new --name=${name}`);
|
|
87
|
-
if (result.exitCode !== 0) {
|
|
88
|
-
throw new Error(`Failed to create VM: ${result.stderr}`);
|
|
89
|
-
}
|
|
90
|
-
await new Promise((resolve) => setTimeout(resolve, 5e3));
|
|
91
|
-
return { name, status: "running" };
|
|
92
|
-
} catch (error) {
|
|
93
|
-
throw new Error(`Failed to create VM ${name}: ${error.message}`);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
/**
|
|
97
|
-
* Delete a VM on exe.dev
|
|
98
|
-
*/
|
|
99
|
-
async deleteVm(name) {
|
|
100
|
-
try {
|
|
101
|
-
const result = await this.exeCmd(`rm ${name}`);
|
|
102
|
-
if (result.exitCode !== 0 && !result.stderr.includes("not found")) {
|
|
103
|
-
throw new Error(result.stderr);
|
|
104
|
-
}
|
|
105
|
-
} catch (error) {
|
|
106
|
-
throw new Error(`Failed to delete VM ${name}: ${error.message}`);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
/**
|
|
110
|
-
* List all VMs
|
|
111
|
-
*/
|
|
112
|
-
async listVms() {
|
|
113
|
-
try {
|
|
114
|
-
const result = await this.exeCmd("ls");
|
|
115
|
-
if (result.exitCode !== 0) {
|
|
116
|
-
throw new Error(result.stderr);
|
|
117
|
-
}
|
|
118
|
-
return parseVmList(result.stdout);
|
|
119
|
-
} catch (error) {
|
|
120
|
-
throw new Error(`Failed to list VMs: ${error.message}`);
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
/**
|
|
124
|
-
* Get VM status
|
|
125
|
-
*/
|
|
126
|
-
async getStatus(name) {
|
|
127
|
-
try {
|
|
128
|
-
const vms = await this.listVms();
|
|
129
|
-
const vm = vms.find((v) => v.name === name);
|
|
130
|
-
return vm?.status || "unknown";
|
|
131
|
-
} catch {
|
|
132
|
-
return "unknown";
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
/**
|
|
136
|
-
* Get detailed VM info
|
|
137
|
-
*/
|
|
138
|
-
async getVmInfo(name) {
|
|
139
|
-
try {
|
|
140
|
-
const vms = await this.listVms();
|
|
141
|
-
return vms.find((v) => v.name === name) || null;
|
|
142
|
-
} catch {
|
|
143
|
-
return null;
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
/**
|
|
147
|
-
* Start a stopped VM
|
|
148
|
-
* Note: exe.dev VMs are persistent and always running
|
|
149
|
-
*/
|
|
150
|
-
async startVm(name) {
|
|
151
|
-
const status = await this.getStatus(name);
|
|
152
|
-
if (status === "unknown") {
|
|
153
|
-
throw new Error(`VM ${name} not found`);
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
/**
|
|
157
|
-
* Stop a running VM
|
|
158
|
-
* Note: exe.dev VMs are persistent - use rm to delete
|
|
159
|
-
*/
|
|
160
|
-
async stopVm(name) {
|
|
161
|
-
console.warn(`exe.dev VMs cannot be stopped, only deleted. VM ${name} continues running.`);
|
|
162
|
-
}
|
|
163
|
-
/**
|
|
164
|
-
* Execute a command on VM via SSH
|
|
165
|
-
* SSH to vmname.exe.xyz
|
|
166
|
-
*/
|
|
167
|
-
async ssh(vm, command) {
|
|
168
|
-
try {
|
|
169
|
-
const sshHost = `${vm}.exe.xyz`;
|
|
170
|
-
const escapedCmd = command.replace(/"/g, '\\"');
|
|
171
|
-
const { stdout, stderr } = await execAsync(`ssh -A ${sshHost} "${escapedCmd}"`, {
|
|
172
|
-
timeout: 3e5,
|
|
173
|
-
// 5 minutes
|
|
174
|
-
maxBuffer: 50 * 1024 * 1024
|
|
175
|
-
// 50MB
|
|
176
|
-
});
|
|
177
|
-
return { stdout, stderr, exitCode: 0 };
|
|
178
|
-
} catch (error) {
|
|
179
|
-
return {
|
|
180
|
-
stdout: error.stdout || "",
|
|
181
|
-
stderr: error.stderr || error.message,
|
|
182
|
-
exitCode: error.code || 1
|
|
183
|
-
};
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
/**
|
|
187
|
-
* Execute a command and stream output
|
|
188
|
-
*/
|
|
189
|
-
async *sshStream(vm, command) {
|
|
190
|
-
const sshHost = `${vm}.exe.xyz`;
|
|
191
|
-
const child = spawn("ssh", ["-A", sshHost, command], {
|
|
192
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
193
|
-
});
|
|
194
|
-
for await (const chunk of child.stdout) {
|
|
195
|
-
yield chunk.toString();
|
|
196
|
-
}
|
|
197
|
-
for await (const chunk of child.stderr) {
|
|
198
|
-
yield chunk.toString();
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
/**
|
|
202
|
-
* Copy file to VM
|
|
203
|
-
*/
|
|
204
|
-
async copyToVm(vm, localPath, remotePath) {
|
|
205
|
-
try {
|
|
206
|
-
const sshHost = `${vm}.exe.xyz`;
|
|
207
|
-
await execAsync(`scp "${localPath}" ${sshHost}:${remotePath}`, { timeout: 3e5 });
|
|
208
|
-
} catch (error) {
|
|
209
|
-
throw new Error(`Failed to copy ${localPath} to ${vm}:${remotePath}: ${error.message}`);
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
/**
|
|
213
|
-
* Copy file from VM
|
|
214
|
-
*/
|
|
215
|
-
async copyFromVm(vm, remotePath, localPath) {
|
|
216
|
-
try {
|
|
217
|
-
const sshHost = `${vm}.exe.xyz`;
|
|
218
|
-
await execAsync(`scp ${sshHost}:${remotePath} "${localPath}"`, { timeout: 3e5 });
|
|
219
|
-
} catch (error) {
|
|
220
|
-
throw new Error(`Failed to copy ${vm}:${remotePath} to ${localPath}: ${error.message}`);
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
/**
|
|
224
|
-
* Expose a port on VM (returns public URL)
|
|
225
|
-
*
|
|
226
|
-
* exe.dev provides automatic HTTPS URLs for services:
|
|
227
|
-
* https://vmname.exe.xyz:PORT
|
|
228
|
-
*/
|
|
229
|
-
async exposePort(vm, port) {
|
|
230
|
-
if (port === 80 || port === 443) {
|
|
231
|
-
return `https://${vm}.exe.xyz`;
|
|
232
|
-
}
|
|
233
|
-
return `https://${vm}.exe.xyz:${port}`;
|
|
234
|
-
}
|
|
235
|
-
/**
|
|
236
|
-
* Create SSH tunnel to VM
|
|
237
|
-
*/
|
|
238
|
-
async tunnel(vm, remotePort, localPort) {
|
|
239
|
-
const sshHost = `${vm}.exe.xyz`;
|
|
240
|
-
const child = spawn("ssh", ["-N", "-L", `${localPort}:localhost:${remotePort}`, sshHost], {
|
|
241
|
-
stdio: "ignore",
|
|
242
|
-
detached: true
|
|
243
|
-
});
|
|
244
|
-
child.unref();
|
|
245
|
-
return {
|
|
246
|
-
close: () => {
|
|
247
|
-
child.kill();
|
|
248
|
-
}
|
|
249
|
-
};
|
|
250
|
-
}
|
|
251
|
-
/**
|
|
252
|
-
* Get the configured infrastructure VM name
|
|
253
|
-
*/
|
|
254
|
-
getInfraVm() {
|
|
255
|
-
return this.config.infraVm;
|
|
256
|
-
}
|
|
257
|
-
/**
|
|
258
|
-
* Initialize the shared infrastructure VM
|
|
259
|
-
*
|
|
260
|
-
* Sets up postgres, redis, and traefik on a dedicated VM.
|
|
261
|
-
*/
|
|
262
|
-
async initInfrastructure(vmName) {
|
|
263
|
-
let status = await this.getStatus(vmName);
|
|
264
|
-
if (status === "unknown") {
|
|
265
|
-
await this.createVm(vmName);
|
|
266
|
-
await new Promise((resolve) => setTimeout(resolve, 1e4));
|
|
267
|
-
}
|
|
268
|
-
const dockerCheck = await this.ssh(vmName, "which docker");
|
|
269
|
-
if (dockerCheck.exitCode !== 0) {
|
|
270
|
-
await this.ssh(vmName, "curl -fsSL https://get.docker.com | sh");
|
|
271
|
-
await this.ssh(vmName, "sudo usermod -aG docker $USER");
|
|
272
|
-
}
|
|
273
|
-
const composeContent = `
|
|
274
|
-
version: '3.8'
|
|
275
|
-
services:
|
|
276
|
-
postgres:
|
|
277
|
-
image: postgres:16
|
|
278
|
-
restart: unless-stopped
|
|
279
|
-
environment:
|
|
280
|
-
POSTGRES_PASSWORD: \${PAN_POSTGRES_PASSWORD:-panopticon}
|
|
281
|
-
volumes:
|
|
282
|
-
- postgres_data:/var/lib/postgresql/data
|
|
283
|
-
ports:
|
|
284
|
-
- "5432:5432"
|
|
285
|
-
|
|
286
|
-
redis:
|
|
287
|
-
image: redis:7
|
|
288
|
-
restart: unless-stopped
|
|
289
|
-
volumes:
|
|
290
|
-
- redis_data:/data
|
|
291
|
-
ports:
|
|
292
|
-
- "6379:6379"
|
|
293
|
-
|
|
294
|
-
traefik:
|
|
295
|
-
image: traefik:v3.0
|
|
296
|
-
restart: unless-stopped
|
|
297
|
-
ports:
|
|
298
|
-
- "80:80"
|
|
299
|
-
- "443:443"
|
|
300
|
-
- "8080:8080"
|
|
301
|
-
volumes:
|
|
302
|
-
- /var/run/docker.sock:/var/run/docker.sock:ro
|
|
303
|
-
command:
|
|
304
|
-
- --api.dashboard=true
|
|
305
|
-
- --providers.docker=true
|
|
306
|
-
- --providers.docker.exposedbydefault=false
|
|
307
|
-
- --entrypoints.web.address=:80
|
|
308
|
-
- --entrypoints.websecure.address=:443
|
|
309
|
-
|
|
310
|
-
volumes:
|
|
311
|
-
postgres_data:
|
|
312
|
-
redis_data:
|
|
313
|
-
`;
|
|
314
|
-
await this.ssh(vmName, `mkdir -p /opt/panopticon && cat > /opt/panopticon/docker-compose.yml << 'COMPOSE_EOF'
|
|
315
|
-
${composeContent}
|
|
316
|
-
COMPOSE_EOF`);
|
|
317
|
-
await this.ssh(vmName, "cd /opt/panopticon && docker compose up -d");
|
|
318
|
-
this.config.infraVm = vmName;
|
|
319
|
-
}
|
|
320
|
-
/**
|
|
321
|
-
* Sync Claude Code credentials from local macOS Keychain to remote VM
|
|
322
|
-
*
|
|
323
|
-
* This should be called before spawning agents to ensure fresh credentials.
|
|
324
|
-
* Credentials can expire, and re-running this ensures the VM has valid auth.
|
|
325
|
-
*
|
|
326
|
-
* @returns true if credentials were synced, false if not available
|
|
327
|
-
*/
|
|
328
|
-
async syncClaudeCredentials(vmName) {
|
|
329
|
-
try {
|
|
330
|
-
const { stdout: credentials } = await execAsync(
|
|
331
|
-
'security find-generic-password -s "Claude Code-credentials" -w 2>/dev/null',
|
|
332
|
-
{ encoding: "utf-8" }
|
|
333
|
-
);
|
|
334
|
-
if (!credentials || !credentials.trim()) {
|
|
335
|
-
return false;
|
|
336
|
-
}
|
|
337
|
-
await this.ssh(vmName, "mkdir -p ~/.claude");
|
|
338
|
-
const credsBase64 = Buffer.from(credentials.trim()).toString("base64");
|
|
339
|
-
const result = await this.ssh(vmName, `echo '${credsBase64}' | base64 -d > ~/.claude/.credentials.json`);
|
|
340
|
-
return result.exitCode === 0;
|
|
341
|
-
} catch (error) {
|
|
342
|
-
return false;
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
/**
|
|
346
|
-
* Sync GitHub CLI authentication from local macOS to remote VM
|
|
347
|
-
*
|
|
348
|
-
* GitHub CLI on macOS stores tokens in Keychain. On Linux VMs, it stores
|
|
349
|
-
* them directly in ~/.config/gh/hosts.yml. This extracts from Keychain
|
|
350
|
-
* and writes to the VM's config file.
|
|
351
|
-
*
|
|
352
|
-
* @returns true if auth was synced, false if not available
|
|
353
|
-
*/
|
|
354
|
-
async syncGitHubAuth(vmName) {
|
|
355
|
-
try {
|
|
356
|
-
const { stdout: tokenRaw } = await execAsync(
|
|
357
|
-
'security find-generic-password -s "gh:github.com" -w 2>/dev/null',
|
|
358
|
-
{ encoding: "utf-8" }
|
|
359
|
-
);
|
|
360
|
-
if (!tokenRaw || !tokenRaw.trim()) {
|
|
361
|
-
return false;
|
|
362
|
-
}
|
|
363
|
-
let token = tokenRaw.trim();
|
|
364
|
-
if (token.startsWith("go-keyring-base64:")) {
|
|
365
|
-
token = Buffer.from(token.replace("go-keyring-base64:", ""), "base64").toString("utf-8");
|
|
366
|
-
}
|
|
367
|
-
let username = "user";
|
|
368
|
-
try {
|
|
369
|
-
const { stdout: configOutput } = await execAsync("cat ~/.config/gh/hosts.yml 2>/dev/null", { encoding: "utf-8" });
|
|
370
|
-
const userMatch = configOutput.match(/user:\s*(\S+)/);
|
|
371
|
-
if (userMatch) {
|
|
372
|
-
username = userMatch[1];
|
|
373
|
-
}
|
|
374
|
-
} catch {
|
|
375
|
-
}
|
|
376
|
-
const hostsYml = `github.com:
|
|
377
|
-
oauth_token: ${token}
|
|
378
|
-
git_protocol: ssh
|
|
379
|
-
user: ${username}
|
|
380
|
-
`;
|
|
381
|
-
await this.ssh(vmName, "mkdir -p ~/.config/gh");
|
|
382
|
-
const hostsBase64 = Buffer.from(hostsYml).toString("base64");
|
|
383
|
-
const result = await this.ssh(vmName, `echo '${hostsBase64}' | base64 -d > ~/.config/gh/hosts.yml && chmod 600 ~/.config/gh/hosts.yml`);
|
|
384
|
-
return result.exitCode === 0;
|
|
385
|
-
} catch (error) {
|
|
386
|
-
return false;
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
/**
|
|
390
|
-
* Sync GitLab CLI (glab) authentication to a remote VM
|
|
391
|
-
*
|
|
392
|
-
* Copies the glab config from local machine to the remote VM.
|
|
393
|
-
* This allows the remote agent to use glab commands for MRs, etc.
|
|
394
|
-
*/
|
|
395
|
-
async syncGitLabAuth(vmName) {
|
|
396
|
-
try {
|
|
397
|
-
const glabConfigPath = join(homedir(), ".config", "glab-cli", "config.yml");
|
|
398
|
-
if (!existsSync(glabConfigPath)) {
|
|
399
|
-
console.log(`[exe-provider] No glab config found at ${glabConfigPath}`);
|
|
400
|
-
return false;
|
|
401
|
-
}
|
|
402
|
-
const glabConfig = readFileSync(glabConfigPath, "utf-8");
|
|
403
|
-
await this.ssh(vmName, "mkdir -p ~/.config/glab-cli");
|
|
404
|
-
const configBase64 = Buffer.from(glabConfig).toString("base64");
|
|
405
|
-
const result = await this.ssh(vmName, `echo '${configBase64}' | base64 -d > ~/.config/glab-cli/config.yml && chmod 600 ~/.config/glab-cli/config.yml`);
|
|
406
|
-
if (result.exitCode === 0) {
|
|
407
|
-
console.log(`[exe-provider] GitLab auth synced to ${vmName}`);
|
|
408
|
-
return true;
|
|
409
|
-
}
|
|
410
|
-
return false;
|
|
411
|
-
} catch (error) {
|
|
412
|
-
console.error(`[exe-provider] Failed to sync GitLab auth: ${error.message}`);
|
|
413
|
-
return false;
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
/**
|
|
417
|
-
* Sync all credentials needed for remote workspace operation
|
|
418
|
-
*
|
|
419
|
-
* This syncs:
|
|
420
|
-
* - Claude Code OAuth credentials
|
|
421
|
-
* - GitHub CLI authentication
|
|
422
|
-
*
|
|
423
|
-
* Call this before spawning agents to ensure fresh credentials.
|
|
424
|
-
*
|
|
425
|
-
* @returns object with sync status for each credential type
|
|
426
|
-
*/
|
|
427
|
-
async syncAllCredentials(vmName) {
|
|
428
|
-
const [claude, github] = await Promise.all([
|
|
429
|
-
this.syncClaudeCredentials(vmName),
|
|
430
|
-
this.syncGitHubAuth(vmName)
|
|
431
|
-
]);
|
|
432
|
-
return { claude, github };
|
|
433
|
-
}
|
|
434
|
-
/**
|
|
435
|
-
* Install beads CLI (bd) on a remote VM
|
|
436
|
-
*
|
|
437
|
-
* Beads is required for planning agents to create task breakdowns.
|
|
438
|
-
* Downloads the binary from GitHub releases since npm/node are often
|
|
439
|
-
* not available on exe.dev VMs.
|
|
440
|
-
*
|
|
441
|
-
* @returns true if bd is available (already installed or just installed)
|
|
442
|
-
*/
|
|
443
|
-
async installBeads(vmName) {
|
|
444
|
-
const FALLBACK_BEADS_VERSION = "0.49.4";
|
|
445
|
-
try {
|
|
446
|
-
const checkResult = await this.ssh(vmName, "which bd");
|
|
447
|
-
if (checkResult.exitCode === 0 && checkResult.stdout.trim()) {
|
|
448
|
-
console.log(`[exe-provider] bd already installed on ${vmName}`);
|
|
449
|
-
return true;
|
|
450
|
-
}
|
|
451
|
-
console.log(`[exe-provider] Installing bd (beads CLI) on ${vmName}...`);
|
|
452
|
-
const installCmd = `
|
|
453
|
-
cd /tmp && BEADS_VERSION=$(curl -sI "https://github.com/steveyegge/beads/releases/latest" 2>/dev/null | grep -i '^location:' | sed 's|.*/v||' | tr -d '\\r\\n') && if [ -z "$BEADS_VERSION" ]; then BEADS_VERSION="${FALLBACK_BEADS_VERSION}"; echo "Using fallback version: $BEADS_VERSION"; else echo "Detected version: $BEADS_VERSION"; fi && echo "Downloading beads v$BEADS_VERSION..." && curl -sL "https://github.com/steveyegge/beads/releases/download/v\${BEADS_VERSION}/beads_\${BEADS_VERSION}_linux_amd64.tar.gz" -o beads.tar.gz && tar -xzf beads.tar.gz && sudo mv bd /usr/local/bin/ && rm -f beads.tar.gz CHANGELOG.md LICENSE README.md
|
|
454
|
-
`.trim().replace(/\n\s+/g, " ");
|
|
455
|
-
const installResult = await this.ssh(vmName, installCmd);
|
|
456
|
-
if (installResult.stderr) {
|
|
457
|
-
console.log(`[exe-provider] bd install stderr on ${vmName}: ${installResult.stderr}`);
|
|
458
|
-
}
|
|
459
|
-
if (installResult.stdout) {
|
|
460
|
-
console.log(`[exe-provider] bd install output on ${vmName}: ${installResult.stdout.trim()}`);
|
|
461
|
-
}
|
|
462
|
-
const verifyResult = await this.ssh(vmName, "which bd && bd --version");
|
|
463
|
-
if (verifyResult.exitCode === 0 && verifyResult.stdout.includes("bd version")) {
|
|
464
|
-
console.log(`[exe-provider] bd installed successfully on ${vmName}: ${verifyResult.stdout.trim()}`);
|
|
465
|
-
return true;
|
|
466
|
-
}
|
|
467
|
-
console.warn(`[exe-provider] Failed to install bd on ${vmName} - verify exitCode: ${verifyResult.exitCode}, stdout: ${verifyResult.stdout}, stderr: ${verifyResult.stderr}`);
|
|
468
|
-
return false;
|
|
469
|
-
} catch (error) {
|
|
470
|
-
console.error(`[exe-provider] Error installing bd on ${vmName}:`, error.message);
|
|
471
|
-
return false;
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
/**
|
|
475
|
-
* Initialize beads in a workspace on a remote VM
|
|
476
|
-
*
|
|
477
|
-
* Creates .beads/ directory and initializes the database if needed.
|
|
478
|
-
*/
|
|
479
|
-
async initBeads(vmName, workspacePath = "/workspace") {
|
|
480
|
-
try {
|
|
481
|
-
const checkResult = await this.ssh(vmName, `test -d ${workspacePath}/.beads && echo "exists"`);
|
|
482
|
-
if (checkResult.stdout.includes("exists")) {
|
|
483
|
-
console.log(`[exe-provider] .beads already initialized on ${vmName}`);
|
|
484
|
-
return true;
|
|
485
|
-
}
|
|
486
|
-
console.log(`[exe-provider] Initializing beads on ${vmName}...`);
|
|
487
|
-
const initResult = await this.ssh(vmName, `cd ${workspacePath} && bd init 2>/dev/null || true`);
|
|
488
|
-
const verifyResult = await this.ssh(vmName, `test -d ${workspacePath}/.beads && echo "ok"`);
|
|
489
|
-
if (verifyResult.stdout.includes("ok")) {
|
|
490
|
-
console.log(`[exe-provider] beads initialized on ${vmName}`);
|
|
491
|
-
return true;
|
|
492
|
-
}
|
|
493
|
-
await this.ssh(vmName, `mkdir -p ${workspacePath}/.beads`);
|
|
494
|
-
return true;
|
|
495
|
-
} catch (error) {
|
|
496
|
-
console.error(`[exe-provider] Error initializing beads on ${vmName}:`, error.message);
|
|
497
|
-
return false;
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
/**
|
|
501
|
-
* Sync beads from remote VM to git
|
|
502
|
-
*
|
|
503
|
-
* Exports beads database to JSONL, commits, and pushes.
|
|
504
|
-
* This should be called after planning completes to persist tasks.
|
|
505
|
-
*/
|
|
506
|
-
async syncBeadsToGit(vmName, workspacePath = "/workspace", commitMessage) {
|
|
507
|
-
try {
|
|
508
|
-
console.log(`[exe-provider] Syncing beads to git on ${vmName}...`);
|
|
509
|
-
const syncResult = await this.ssh(vmName, `cd ${workspacePath} && bd sync 2>/dev/null || true`);
|
|
510
|
-
const statusResult = await this.ssh(vmName, `cd ${workspacePath} && git status --porcelain .beads/ .planning/ 2>/dev/null`);
|
|
511
|
-
if (!statusResult.stdout.trim()) {
|
|
512
|
-
console.log(`[exe-provider] No beads changes to commit on ${vmName}`);
|
|
513
|
-
return true;
|
|
514
|
-
}
|
|
515
|
-
const msg = commitMessage || "Sync beads from planning session";
|
|
516
|
-
await this.ssh(vmName, `cd ${workspacePath} && git add .beads/ && git add -f .planning/ 2>/dev/null || true`);
|
|
517
|
-
await this.ssh(vmName, `cd ${workspacePath} && git commit -m "${msg}" 2>/dev/null || true`);
|
|
518
|
-
const branchResult = await this.ssh(vmName, `cd ${workspacePath} && git branch --show-current`);
|
|
519
|
-
const branch = branchResult.stdout.trim() || "main";
|
|
520
|
-
const pushResult = await this.ssh(vmName, `cd ${workspacePath} && git push -u origin ${branch} 2>&1`);
|
|
521
|
-
if (pushResult.exitCode !== 0) {
|
|
522
|
-
console.warn(`[exe-provider] Git push failed on ${vmName}: ${pushResult.stderr || pushResult.stdout}`);
|
|
523
|
-
return false;
|
|
524
|
-
}
|
|
525
|
-
console.log(`[exe-provider] Beads synced and pushed from ${vmName}`);
|
|
526
|
-
return true;
|
|
527
|
-
} catch (error) {
|
|
528
|
-
console.error(`[exe-provider] Error syncing beads on ${vmName}:`, error.message);
|
|
529
|
-
return false;
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
/**
|
|
533
|
-
* Query beads on a remote VM
|
|
534
|
-
*
|
|
535
|
-
* Runs bd search on the remote and returns results.
|
|
536
|
-
*/
|
|
537
|
-
async queryBeads(vmName, searchTerm, workspacePath = "/workspace") {
|
|
538
|
-
try {
|
|
539
|
-
const result = await this.ssh(vmName, `cd ${workspacePath} && bd search "${searchTerm}" --json 2>/dev/null || echo "[]"`);
|
|
540
|
-
if (result.exitCode !== 0 || !result.stdout.trim()) {
|
|
541
|
-
return [];
|
|
542
|
-
}
|
|
543
|
-
try {
|
|
544
|
-
return JSON.parse(result.stdout.trim());
|
|
545
|
-
} catch {
|
|
546
|
-
return [];
|
|
547
|
-
}
|
|
548
|
-
} catch (error) {
|
|
549
|
-
console.error(`[exe-provider] Error querying beads on ${vmName}:`, error.message);
|
|
550
|
-
return [];
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
|
-
/**
|
|
554
|
-
* Configure Claude Code on a VM for autonomous operation
|
|
555
|
-
*
|
|
556
|
-
* Sets up:
|
|
557
|
-
* - ~/.claude.json with bypass permissions and onboarding settings
|
|
558
|
-
* - ~/.bashrc with proper terminal environment (TERM, LANG, etc.)
|
|
559
|
-
*
|
|
560
|
-
* This is required for remote agents to run without human interaction.
|
|
561
|
-
*/
|
|
562
|
-
async configureClaudeCode(vmName) {
|
|
563
|
-
const setupScript = `
|
|
564
|
-
import json, os
|
|
565
|
-
path = os.path.expanduser("~/.claude.json")
|
|
566
|
-
data = {}
|
|
567
|
-
if os.path.exists(path):
|
|
568
|
-
with open(path, "r") as f:
|
|
569
|
-
data = json.load(f)
|
|
570
|
-
data["bypassPermissionsModeAccepted"] = True
|
|
571
|
-
data["hasCompletedOnboarding"] = True
|
|
572
|
-
with open(path, "w") as f:
|
|
573
|
-
json.dump(data, f, indent=2)
|
|
574
|
-
`;
|
|
575
|
-
const scriptBase64 = Buffer.from(setupScript).toString("base64");
|
|
576
|
-
const result = await this.ssh(vmName, `echo '${scriptBase64}' | base64 -d | python3`);
|
|
577
|
-
if (result.exitCode !== 0) {
|
|
578
|
-
throw new Error(`Failed to configure Claude Code on ${vmName}: ${result.stderr}`);
|
|
579
|
-
}
|
|
580
|
-
const termEnv = `
|
|
581
|
-
# Panopticon terminal settings
|
|
582
|
-
export TERM=xterm-256color
|
|
583
|
-
export LANG=C.UTF-8
|
|
584
|
-
export LC_ALL=C.UTF-8
|
|
585
|
-
export COLORTERM=truecolor
|
|
586
|
-
`;
|
|
587
|
-
const termEnvBase64 = Buffer.from(termEnv).toString("base64");
|
|
588
|
-
await this.ssh(vmName, `grep -q "Panopticon terminal settings" ~/.bashrc 2>/dev/null || echo '${termEnvBase64}' | base64 -d >> ~/.bashrc`);
|
|
589
|
-
}
|
|
590
|
-
/**
|
|
591
|
-
* Copy essential skills from local ~/.panopticon/skills/ to remote VM ~/.claude/skills/
|
|
592
|
-
*
|
|
593
|
-
* Skills are read locally and written to the VM via base64-encoded SSH.
|
|
594
|
-
* Only copies SKILL.md and resource files (not symlinks or deep directories).
|
|
595
|
-
*/
|
|
596
|
-
async copySkillsToVm(vmName) {
|
|
597
|
-
const essentialSkills = [
|
|
598
|
-
"beads",
|
|
599
|
-
"beads-completion-check",
|
|
600
|
-
"beads-panopticon-guide",
|
|
601
|
-
"work-complete",
|
|
602
|
-
"session-health"
|
|
603
|
-
];
|
|
604
|
-
const skillsSourceDir = join(homedir(), ".panopticon", "skills");
|
|
605
|
-
if (!existsSync(skillsSourceDir)) {
|
|
606
|
-
console.log(`[exe-provider] No skills source directory at ${skillsSourceDir}`);
|
|
607
|
-
return;
|
|
608
|
-
}
|
|
609
|
-
await this.ssh(vmName, "mkdir -p ~/.claude/skills");
|
|
610
|
-
for (const skillName of essentialSkills) {
|
|
611
|
-
const skillDir = join(skillsSourceDir, skillName);
|
|
612
|
-
if (!existsSync(skillDir)) {
|
|
613
|
-
continue;
|
|
614
|
-
}
|
|
615
|
-
try {
|
|
616
|
-
await this.ssh(vmName, `mkdir -p ~/.claude/skills/${skillName}`);
|
|
617
|
-
const skillMdPath = join(skillDir, "SKILL.md");
|
|
618
|
-
if (existsSync(skillMdPath)) {
|
|
619
|
-
const content = readFileSync(skillMdPath, "utf-8");
|
|
620
|
-
const contentBase64 = Buffer.from(content).toString("base64");
|
|
621
|
-
await this.ssh(vmName, `echo '${contentBase64}' | base64 -d > ~/.claude/skills/${skillName}/SKILL.md`);
|
|
622
|
-
}
|
|
623
|
-
const resourcesDir = join(skillDir, "resources");
|
|
624
|
-
if (existsSync(resourcesDir) && statSync(resourcesDir).isDirectory()) {
|
|
625
|
-
await this.ssh(vmName, `mkdir -p ~/.claude/skills/${skillName}/resources`);
|
|
626
|
-
const resourceFiles = readdirSync(resourcesDir).filter((f) => f.endsWith(".md"));
|
|
627
|
-
for (const resourceFile of resourceFiles) {
|
|
628
|
-
const resourcePath = join(resourcesDir, resourceFile);
|
|
629
|
-
if (statSync(resourcePath).isFile()) {
|
|
630
|
-
const resourceContent = readFileSync(resourcePath, "utf-8");
|
|
631
|
-
const resourceBase64 = Buffer.from(resourceContent).toString("base64");
|
|
632
|
-
await this.ssh(vmName, `echo '${resourceBase64}' | base64 -d > ~/.claude/skills/${skillName}/resources/${resourceFile}`);
|
|
633
|
-
}
|
|
634
|
-
}
|
|
635
|
-
}
|
|
636
|
-
console.log(`[exe-provider] Copied skill ${skillName} to ${vmName}`);
|
|
637
|
-
} catch (error) {
|
|
638
|
-
console.warn(`[exe-provider] Failed to copy skill ${skillName} to ${vmName}: ${error.message}`);
|
|
639
|
-
}
|
|
640
|
-
}
|
|
641
|
-
}
|
|
642
|
-
};
|
|
643
|
-
}
|
|
644
|
-
});
|
|
645
|
-
|
|
646
|
-
export {
|
|
647
|
-
createExeProvider,
|
|
648
|
-
init_exe_provider
|
|
649
|
-
};
|
|
650
|
-
//# sourceMappingURL=chunk-ZDNQFWR5.js.map
|