panopticon-cli 0.5.3 → 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-DMPT32H7.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-D67AQTHF.js → chunk-FUUP55PE.js} +108 -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-2V2DQ3IX.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 +1314 -2146
- 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-BJKEp64j.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 +34785 -34330
- 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-53AWO6AE.js → specialist-context-74RQF5SR.js} +7 -5
- package/dist/{specialist-context-53AWO6AE.js.map → specialist-context-74RQF5SR.js.map} +1 -1
- package/dist/{specialist-logs-QREUJ4HN.js → specialist-logs-T5GW7CSU.js} +6 -4
- package/dist/{specialists-2DBBXRCK.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/scripts/recover-costs-deep.mjs +209 -0
- package/scripts/recover-costs-proportional.mjs +206 -0
- package/scripts/recover-costs.mjs +169 -0
- package/dist/chunk-2V2DQ3IX.js.map +0 -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-D67AQTHF.js.map +0 -1
- package/dist/chunk-FQ66DECN.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-CgJjqjAV.js +0 -767
- package/dist/remote-workspace-XX6ARE6I.js.map +0 -1
- /package/dist/{agents-DMPT32H7.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-QREUJ4HN.js.map → projects-3CRF57ZU.js.map} +0 -0
- /package/dist/{specialists-2DBBXRCK.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
|
@@ -0,0 +1,692 @@
|
|
|
1
|
+
import {
|
|
2
|
+
__esm,
|
|
3
|
+
init_esm_shims
|
|
4
|
+
} from "./chunk-ZHC57RCV.js";
|
|
5
|
+
|
|
6
|
+
// src/lib/remote/fly-api.ts
|
|
7
|
+
function createFlyApiClient(token) {
|
|
8
|
+
const tok = token ?? process.env.FLY_API_TOKEN;
|
|
9
|
+
if (!tok) {
|
|
10
|
+
throw new Error(
|
|
11
|
+
"Fly API token not found. Set FLY_API_TOKEN environment variable or run: fly auth login"
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
return new FlyApiClient(tok);
|
|
15
|
+
}
|
|
16
|
+
var FlyApiError, BASE_URL, FlyApiClient;
|
|
17
|
+
var init_fly_api = __esm({
|
|
18
|
+
"src/lib/remote/fly-api.ts"() {
|
|
19
|
+
"use strict";
|
|
20
|
+
init_esm_shims();
|
|
21
|
+
FlyApiError = class extends Error {
|
|
22
|
+
constructor(message, statusCode, body) {
|
|
23
|
+
super(message);
|
|
24
|
+
this.statusCode = statusCode;
|
|
25
|
+
this.body = body;
|
|
26
|
+
this.name = "FlyApiError";
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
BASE_URL = "https://api.machines.dev/v1";
|
|
30
|
+
FlyApiClient = class {
|
|
31
|
+
token;
|
|
32
|
+
constructor(token) {
|
|
33
|
+
this.token = token;
|
|
34
|
+
}
|
|
35
|
+
async request(method, path, body) {
|
|
36
|
+
const url = `${BASE_URL}${path}`;
|
|
37
|
+
const headers = {
|
|
38
|
+
Authorization: `Bearer ${this.token}`,
|
|
39
|
+
"Content-Type": "application/json"
|
|
40
|
+
};
|
|
41
|
+
const response = await fetch(url, {
|
|
42
|
+
method,
|
|
43
|
+
headers,
|
|
44
|
+
body: body !== void 0 ? JSON.stringify(body) : void 0
|
|
45
|
+
});
|
|
46
|
+
const text = await response.text();
|
|
47
|
+
if (!response.ok) {
|
|
48
|
+
throw new FlyApiError(
|
|
49
|
+
`Fly API error ${response.status} for ${method} ${path}: ${text}`,
|
|
50
|
+
response.status,
|
|
51
|
+
text
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
if (!text) return void 0;
|
|
55
|
+
try {
|
|
56
|
+
return JSON.parse(text);
|
|
57
|
+
} catch {
|
|
58
|
+
return text;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/** Create a machine in an app */
|
|
62
|
+
async createMachine(appName, name, config) {
|
|
63
|
+
return this.request("POST", `/apps/${appName}/machines`, {
|
|
64
|
+
name,
|
|
65
|
+
config: {
|
|
66
|
+
image: config.image,
|
|
67
|
+
env: config.env,
|
|
68
|
+
guest: config.size ? { cpu_kind: "shared", cpus: 2, memory_mb: config.memory ?? 1024 } : void 0,
|
|
69
|
+
restart: config.restart ?? { policy: "no" },
|
|
70
|
+
auto_destroy: config.auto_destroy,
|
|
71
|
+
metadata: config.metadata
|
|
72
|
+
},
|
|
73
|
+
region: config.region
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
/** Destroy a machine (force=true for immediate) */
|
|
77
|
+
async destroyMachine(appName, machineId) {
|
|
78
|
+
await this.request(
|
|
79
|
+
"DELETE",
|
|
80
|
+
`/apps/${appName}/machines/${machineId}?force=true`
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
/** Start a stopped machine */
|
|
84
|
+
async startMachine(appName, machineId) {
|
|
85
|
+
await this.request(
|
|
86
|
+
"POST",
|
|
87
|
+
`/apps/${appName}/machines/${machineId}/start`
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
/** Stop a running machine */
|
|
91
|
+
async stopMachine(appName, machineId, signal, timeout) {
|
|
92
|
+
await this.request(
|
|
93
|
+
"POST",
|
|
94
|
+
`/apps/${appName}/machines/${machineId}/stop`,
|
|
95
|
+
signal || timeout ? { signal, timeout } : void 0
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
/** Get a machine by ID */
|
|
99
|
+
async getMachine(appName, machineId) {
|
|
100
|
+
return this.request(
|
|
101
|
+
"GET",
|
|
102
|
+
`/apps/${appName}/machines/${machineId}`
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
/** List all machines in an app */
|
|
106
|
+
async listMachines(appName) {
|
|
107
|
+
const result = await this.request(
|
|
108
|
+
"GET",
|
|
109
|
+
`/apps/${appName}/machines`
|
|
110
|
+
);
|
|
111
|
+
return result ?? [];
|
|
112
|
+
}
|
|
113
|
+
/** Execute a command inside a running machine */
|
|
114
|
+
async execCommand(appName, machineId, command, timeout = 30) {
|
|
115
|
+
return this.request(
|
|
116
|
+
"POST",
|
|
117
|
+
`/apps/${appName}/machines/${machineId}/exec`,
|
|
118
|
+
{ command, timeout }
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
/** Wait for a machine to reach a target state */
|
|
122
|
+
async waitForState(appName, machineId, state, timeout = 60) {
|
|
123
|
+
await this.request(
|
|
124
|
+
"GET",
|
|
125
|
+
`/apps/${appName}/machines/${machineId}/wait?state=${state}&timeout=${timeout}`
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
/** Create a Fly app if it doesn't exist */
|
|
129
|
+
async ensureApp(appName, orgSlug) {
|
|
130
|
+
try {
|
|
131
|
+
await this.request("GET", `/apps/${appName}`);
|
|
132
|
+
} catch (err) {
|
|
133
|
+
if (err instanceof FlyApiError && err.statusCode === 404) {
|
|
134
|
+
await this.request("POST", "/apps", {
|
|
135
|
+
app_name: appName,
|
|
136
|
+
org_slug: orgSlug,
|
|
137
|
+
network: "default"
|
|
138
|
+
});
|
|
139
|
+
} else {
|
|
140
|
+
throw err;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// src/lib/remote/fly-provider.ts
|
|
149
|
+
import { exec, spawn } from "child_process";
|
|
150
|
+
import { promisify } from "util";
|
|
151
|
+
import { existsSync, readFileSync, readdirSync } from "fs";
|
|
152
|
+
import { join } from "path";
|
|
153
|
+
import { homedir } from "os";
|
|
154
|
+
import { parse } from "yaml";
|
|
155
|
+
function mapFlyStateToVmStatus(state) {
|
|
156
|
+
switch (state) {
|
|
157
|
+
case "started":
|
|
158
|
+
return "running";
|
|
159
|
+
case "stopped":
|
|
160
|
+
case "suspended":
|
|
161
|
+
return "stopped";
|
|
162
|
+
case "created":
|
|
163
|
+
case "replacing":
|
|
164
|
+
return "creating";
|
|
165
|
+
case "destroying":
|
|
166
|
+
case "destroyed":
|
|
167
|
+
return "deleting";
|
|
168
|
+
default:
|
|
169
|
+
return "unknown";
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
function createFlyProvider(config) {
|
|
173
|
+
return new FlyProvider(config);
|
|
174
|
+
}
|
|
175
|
+
var execAsync, FlyProvider;
|
|
176
|
+
var init_fly_provider = __esm({
|
|
177
|
+
"src/lib/remote/fly-provider.ts"() {
|
|
178
|
+
"use strict";
|
|
179
|
+
init_esm_shims();
|
|
180
|
+
init_fly_api();
|
|
181
|
+
execAsync = promisify(exec);
|
|
182
|
+
FlyProvider = class {
|
|
183
|
+
name = "fly";
|
|
184
|
+
config;
|
|
185
|
+
api = null;
|
|
186
|
+
constructor(config = {}) {
|
|
187
|
+
this.config = {
|
|
188
|
+
app: config.app ?? "pan-workspaces",
|
|
189
|
+
org: config.org ?? "personal",
|
|
190
|
+
region: config.region ?? "iad",
|
|
191
|
+
vmSize: config.vmSize ?? "shared-cpu-2x",
|
|
192
|
+
vmMemory: config.vmMemory ?? 1024,
|
|
193
|
+
image: config.image ?? "registry.fly.io/pan-workspace:latest",
|
|
194
|
+
apiToken: config.apiToken ?? process.env.FLY_API_TOKEN ?? ""
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
getApi() {
|
|
198
|
+
if (!this.api) {
|
|
199
|
+
this.api = createFlyApiClient(this.config.apiToken || void 0);
|
|
200
|
+
}
|
|
201
|
+
return this.api;
|
|
202
|
+
}
|
|
203
|
+
async isAuthenticated() {
|
|
204
|
+
if (this.config.apiToken || process.env.FLY_API_TOKEN) {
|
|
205
|
+
try {
|
|
206
|
+
await this.getApi().listMachines(this.config.app);
|
|
207
|
+
return true;
|
|
208
|
+
} catch {
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
try {
|
|
212
|
+
const result = await execAsync("fly auth whoami", { timeout: 1e4 });
|
|
213
|
+
return !result.stdout.includes("not logged in");
|
|
214
|
+
} catch {
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Resolve vmName to {appName, machineId} by scanning workspace metadata.
|
|
220
|
+
* Falls back to listing machines in the app.
|
|
221
|
+
*/
|
|
222
|
+
async resolveVm(vmName) {
|
|
223
|
+
const workspacesDir = join(homedir(), ".panopticon", "workspaces");
|
|
224
|
+
if (existsSync(workspacesDir)) {
|
|
225
|
+
for (const file of readdirSync(workspacesDir)) {
|
|
226
|
+
if (!file.endsWith(".yaml")) continue;
|
|
227
|
+
try {
|
|
228
|
+
const content = readFileSync(join(workspacesDir, file), "utf-8");
|
|
229
|
+
const metadata = parse(content);
|
|
230
|
+
if (metadata.vmName === vmName && metadata.machineId && metadata.appName) {
|
|
231
|
+
return { appName: metadata.appName, machineId: metadata.machineId };
|
|
232
|
+
}
|
|
233
|
+
} catch {
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
const machines = await this.getApi().listMachines(this.config.app);
|
|
238
|
+
const machine = machines.find((m) => m.name === vmName);
|
|
239
|
+
if (!machine) {
|
|
240
|
+
throw new Error(`No Fly machine found for VM name: ${vmName}`);
|
|
241
|
+
}
|
|
242
|
+
return { appName: this.config.app, machineId: machine.id };
|
|
243
|
+
}
|
|
244
|
+
async createVm(name) {
|
|
245
|
+
const api = this.getApi();
|
|
246
|
+
await api.ensureApp(this.config.app, this.config.org);
|
|
247
|
+
const machine = await api.createMachine(this.config.app, name, {
|
|
248
|
+
image: this.config.image,
|
|
249
|
+
size: this.config.vmSize,
|
|
250
|
+
memory: this.config.vmMemory,
|
|
251
|
+
region: this.config.region,
|
|
252
|
+
restart: { policy: "no" },
|
|
253
|
+
auto_destroy: false
|
|
254
|
+
});
|
|
255
|
+
try {
|
|
256
|
+
await api.waitForState(this.config.app, machine.id, "started", 120);
|
|
257
|
+
} catch {
|
|
258
|
+
}
|
|
259
|
+
return {
|
|
260
|
+
name,
|
|
261
|
+
status: mapFlyStateToVmStatus(machine.state),
|
|
262
|
+
machineId: machine.id,
|
|
263
|
+
ipAddress: machine.private_ip,
|
|
264
|
+
created: machine.created_at ? new Date(machine.created_at) : void 0
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
async deleteVm(name) {
|
|
268
|
+
const { appName, machineId } = await this.resolveVm(name);
|
|
269
|
+
await this.getApi().destroyMachine(appName, machineId);
|
|
270
|
+
}
|
|
271
|
+
async listVms() {
|
|
272
|
+
const machines = await this.getApi().listMachines(this.config.app);
|
|
273
|
+
return machines.map((m) => ({
|
|
274
|
+
name: m.name,
|
|
275
|
+
status: mapFlyStateToVmStatus(m.state),
|
|
276
|
+
machineId: m.id,
|
|
277
|
+
ipAddress: m.private_ip,
|
|
278
|
+
created: m.created_at ? new Date(m.created_at) : void 0
|
|
279
|
+
}));
|
|
280
|
+
}
|
|
281
|
+
async getStatus(name) {
|
|
282
|
+
try {
|
|
283
|
+
const { appName, machineId } = await this.resolveVm(name);
|
|
284
|
+
const machine = await this.getApi().getMachine(appName, machineId);
|
|
285
|
+
return mapFlyStateToVmStatus(machine.state);
|
|
286
|
+
} catch {
|
|
287
|
+
return "unknown";
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
async getVmInfo(name) {
|
|
291
|
+
try {
|
|
292
|
+
const { appName, machineId } = await this.resolveVm(name);
|
|
293
|
+
const machine = await this.getApi().getMachine(appName, machineId);
|
|
294
|
+
return {
|
|
295
|
+
name,
|
|
296
|
+
status: mapFlyStateToVmStatus(machine.state),
|
|
297
|
+
machineId: machine.id,
|
|
298
|
+
ipAddress: machine.private_ip,
|
|
299
|
+
created: machine.created_at ? new Date(machine.created_at) : void 0,
|
|
300
|
+
memoryTotal: machine.config?.guest?.memory_mb
|
|
301
|
+
};
|
|
302
|
+
} catch {
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
async startVm(name) {
|
|
307
|
+
const { appName, machineId } = await this.resolveVm(name);
|
|
308
|
+
await this.getApi().startMachine(appName, machineId);
|
|
309
|
+
await this.getApi().waitForState(appName, machineId, "started", 60);
|
|
310
|
+
}
|
|
311
|
+
async stopVm(name) {
|
|
312
|
+
const { appName, machineId } = await this.resolveVm(name);
|
|
313
|
+
await this.getApi().stopMachine(appName, machineId);
|
|
314
|
+
}
|
|
315
|
+
/** Execute a command on the VM via Fly Machines exec API */
|
|
316
|
+
async ssh(vm, command) {
|
|
317
|
+
const { appName, machineId } = await this.resolveVm(vm);
|
|
318
|
+
try {
|
|
319
|
+
const result = await this.getApi().execCommand(
|
|
320
|
+
appName,
|
|
321
|
+
machineId,
|
|
322
|
+
["/bin/sh", "-c", command],
|
|
323
|
+
60
|
|
324
|
+
);
|
|
325
|
+
return {
|
|
326
|
+
stdout: result.stdout ?? "",
|
|
327
|
+
stderr: result.stderr ?? "",
|
|
328
|
+
exitCode: result.exit_code ?? 0
|
|
329
|
+
};
|
|
330
|
+
} catch (err) {
|
|
331
|
+
if (err instanceof FlyApiError) {
|
|
332
|
+
return { stdout: "", stderr: err.message, exitCode: 1 };
|
|
333
|
+
}
|
|
334
|
+
throw err;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
/** Stream command output via fly SSH console */
|
|
338
|
+
async *sshStream(vm, command) {
|
|
339
|
+
const { appName } = await this.resolveVm(vm);
|
|
340
|
+
const child = spawn("fly", ["ssh", "console", "-a", appName, "-C", command], {
|
|
341
|
+
env: { ...process.env }
|
|
342
|
+
});
|
|
343
|
+
for await (const chunk of child.stdout) {
|
|
344
|
+
yield chunk.toString();
|
|
345
|
+
}
|
|
346
|
+
for await (const chunk of child.stderr) {
|
|
347
|
+
yield chunk.toString();
|
|
348
|
+
}
|
|
349
|
+
await new Promise((resolve, reject) => {
|
|
350
|
+
child.on("close", resolve);
|
|
351
|
+
child.on("error", reject);
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
/** Copy a local file to VM using base64 encoding */
|
|
355
|
+
async copyToVm(vm, localPath, remotePath) {
|
|
356
|
+
const content = readFileSync(localPath);
|
|
357
|
+
const b64 = content.toString("base64");
|
|
358
|
+
const dirPath = remotePath.substring(0, remotePath.lastIndexOf("/"));
|
|
359
|
+
if (dirPath) {
|
|
360
|
+
await this.ssh(vm, `mkdir -p ${JSON.stringify(dirPath)}`);
|
|
361
|
+
}
|
|
362
|
+
await this.ssh(vm, `echo '${b64}' | base64 -d > ${JSON.stringify(remotePath)}`);
|
|
363
|
+
}
|
|
364
|
+
/** Copy a file from VM to local path */
|
|
365
|
+
async copyFromVm(vm, remotePath, localPath) {
|
|
366
|
+
const { appName } = await this.resolveVm(vm);
|
|
367
|
+
await execAsync(
|
|
368
|
+
`fly ssh sftp get -a ${JSON.stringify(appName)} ${JSON.stringify(remotePath)} ${JSON.stringify(localPath)}`,
|
|
369
|
+
{ timeout: 6e4 }
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
/** Expose a port — not supported by Fly.io provider */
|
|
373
|
+
async exposePort(_vm, _port) {
|
|
374
|
+
throw new Error(
|
|
375
|
+
"exposePort is not supported by the Fly.io provider. Configure services in fly.toml or via the Fly Machines API config."
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
/** Create a fly proxy tunnel to the machine */
|
|
379
|
+
async tunnel(vm, remotePort, localPort) {
|
|
380
|
+
const { appName } = await this.resolveVm(vm);
|
|
381
|
+
const child = spawn("fly", ["proxy", `${localPort}:${remotePort}`, "-a", appName], {
|
|
382
|
+
env: { ...process.env }
|
|
383
|
+
});
|
|
384
|
+
return {
|
|
385
|
+
close: () => {
|
|
386
|
+
child.kill();
|
|
387
|
+
}
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
// ============================================================================
|
|
391
|
+
// Credential Sync & Configuration (ported from ExeProvider)
|
|
392
|
+
// ============================================================================
|
|
393
|
+
/** Sync Claude Code credentials from local macOS Keychain to remote VM */
|
|
394
|
+
async syncClaudeCredentials(vmName) {
|
|
395
|
+
try {
|
|
396
|
+
const { stdout: credentials } = await execAsync(
|
|
397
|
+
'security find-generic-password -s "Claude Code-credentials" -w 2>/dev/null',
|
|
398
|
+
{ encoding: "utf-8", timeout: 1e4 }
|
|
399
|
+
);
|
|
400
|
+
if (!credentials?.trim()) return false;
|
|
401
|
+
const b64 = Buffer.from(credentials.trim()).toString("base64");
|
|
402
|
+
await this.ssh(vmName, `mkdir -p ~/.claude && echo '${b64}' | base64 -d > ~/.claude/.credentials.json`);
|
|
403
|
+
return true;
|
|
404
|
+
} catch {
|
|
405
|
+
return false;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
/** Sync GitHub CLI authentication to the remote VM */
|
|
409
|
+
async syncGitHubAuth(vmName) {
|
|
410
|
+
const ghConfigPath = join(homedir(), ".config", "gh", "hosts.yml");
|
|
411
|
+
if (!existsSync(ghConfigPath)) return false;
|
|
412
|
+
try {
|
|
413
|
+
const content = readFileSync(ghConfigPath, "utf-8");
|
|
414
|
+
const b64 = Buffer.from(content).toString("base64");
|
|
415
|
+
await this.ssh(vmName, `mkdir -p ~/.config/gh && echo '${b64}' | base64 -d > ~/.config/gh/hosts.yml`);
|
|
416
|
+
return true;
|
|
417
|
+
} catch {
|
|
418
|
+
return false;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
/** Sync GitLab CLI (glab) authentication to the remote VM */
|
|
422
|
+
async syncGitLabAuth(vmName) {
|
|
423
|
+
const glabConfigPath = join(homedir(), ".config", "glab-cli", "config.yml");
|
|
424
|
+
if (!existsSync(glabConfigPath)) return false;
|
|
425
|
+
try {
|
|
426
|
+
const content = readFileSync(glabConfigPath, "utf-8");
|
|
427
|
+
const b64 = Buffer.from(content).toString("base64");
|
|
428
|
+
await this.ssh(vmName, `mkdir -p ~/.config/glab-cli && echo '${b64}' | base64 -d > ~/.config/glab-cli/config.yml`);
|
|
429
|
+
return true;
|
|
430
|
+
} catch {
|
|
431
|
+
return false;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
/** Sync all credentials needed for remote workspace operation */
|
|
435
|
+
async syncAllCredentials(vmName) {
|
|
436
|
+
const [claude, github] = await Promise.all([
|
|
437
|
+
this.syncClaudeCredentials(vmName),
|
|
438
|
+
this.syncGitHubAuth(vmName)
|
|
439
|
+
]);
|
|
440
|
+
return { claude, github };
|
|
441
|
+
}
|
|
442
|
+
/** Install beads CLI (bd) on a remote VM */
|
|
443
|
+
async installBeads(vmName) {
|
|
444
|
+
const check = await this.ssh(vmName, "which bd 2>/dev/null");
|
|
445
|
+
if (check.exitCode === 0 && check.stdout.trim()) return true;
|
|
446
|
+
const result = await this.ssh(vmName, "npm install -g @beads-dev/beads 2>&1");
|
|
447
|
+
if (result.exitCode !== 0) {
|
|
448
|
+
const alt = await this.ssh(
|
|
449
|
+
vmName,
|
|
450
|
+
"curl -fsSL https://raw.githubusercontent.com/beads-dev/beads/main/install.sh | bash 2>&1"
|
|
451
|
+
);
|
|
452
|
+
return alt.exitCode === 0;
|
|
453
|
+
}
|
|
454
|
+
return true;
|
|
455
|
+
}
|
|
456
|
+
/** Initialize beads in a workspace on a remote VM */
|
|
457
|
+
async initBeads(vmName, workspacePath = "/workspace") {
|
|
458
|
+
const result = await this.ssh(
|
|
459
|
+
vmName,
|
|
460
|
+
`cd ${workspacePath} && bd init --prefix PAN 2>&1 || bd init 2>&1`
|
|
461
|
+
);
|
|
462
|
+
return result.exitCode === 0;
|
|
463
|
+
}
|
|
464
|
+
/** Configure Claude Code on a VM for autonomous operation */
|
|
465
|
+
async configureClaudeCode(vmName) {
|
|
466
|
+
await this.ssh(vmName, "mkdir -p ~/.claude");
|
|
467
|
+
const onboardingScript = `
|
|
468
|
+
import json, os
|
|
469
|
+
path = os.path.expanduser("~/.claude.json")
|
|
470
|
+
data = {}
|
|
471
|
+
if os.path.exists(path):
|
|
472
|
+
with open(path) as f:
|
|
473
|
+
data = json.load(f)
|
|
474
|
+
data["hasCompletedOnboarding"] = True
|
|
475
|
+
data["lastOnboardingVersion"] = "2.0.50"
|
|
476
|
+
with open(path, "w") as f:
|
|
477
|
+
json.dump(data, f, indent=2)
|
|
478
|
+
`;
|
|
479
|
+
const scriptB64 = Buffer.from(onboardingScript).toString("base64");
|
|
480
|
+
await this.ssh(vmName, `echo '${scriptB64}' | base64 -d | python3`);
|
|
481
|
+
const settings = JSON.stringify({
|
|
482
|
+
theme: "dark",
|
|
483
|
+
permissions: { defaultMode: "bypassPermissions" }
|
|
484
|
+
});
|
|
485
|
+
const settingsB64 = Buffer.from(settings).toString("base64");
|
|
486
|
+
await this.ssh(vmName, `echo '${settingsB64}' | base64 -d > ~/.claude/settings.json`);
|
|
487
|
+
}
|
|
488
|
+
/** Copy essential skills from local ~/.panopticon/skills/ to remote VM */
|
|
489
|
+
async copySkillsToVm(vmName) {
|
|
490
|
+
const skillsDir = join(homedir(), ".panopticon", "skills");
|
|
491
|
+
if (!existsSync(skillsDir)) return;
|
|
492
|
+
await this.ssh(vmName, "mkdir -p ~/.claude/skills");
|
|
493
|
+
try {
|
|
494
|
+
const entries = readdirSync(skillsDir, { withFileTypes: true });
|
|
495
|
+
for (const entry of entries) {
|
|
496
|
+
if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
|
|
497
|
+
const localPath = join(skillsDir, entry.name);
|
|
498
|
+
const content = readFileSync(localPath, "utf-8");
|
|
499
|
+
const b64 = Buffer.from(content).toString("base64");
|
|
500
|
+
await this.ssh(vmName, `echo '${b64}' | base64 -d > ~/.claude/skills/${entry.name}`);
|
|
501
|
+
}
|
|
502
|
+
} catch {
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
/** Sync beads from remote VM to git: exports JSONL, commits, and pushes */
|
|
506
|
+
async syncBeadsToGit(vmName, workspacePath = "/workspace", commitMessage) {
|
|
507
|
+
const msg = commitMessage ?? "chore: sync beads from remote";
|
|
508
|
+
const exportResult = await this.ssh(
|
|
509
|
+
vmName,
|
|
510
|
+
`cd ${workspacePath} && bd export --output .beads/issues.jsonl 2>&1`
|
|
511
|
+
);
|
|
512
|
+
if (exportResult.exitCode !== 0) {
|
|
513
|
+
return false;
|
|
514
|
+
}
|
|
515
|
+
const gitResult = await this.ssh(
|
|
516
|
+
vmName,
|
|
517
|
+
`cd ${workspacePath} && git add .beads/ && git diff --cached --quiet || (git commit -m ${JSON.stringify(msg)} && git push origin HEAD) 2>&1`
|
|
518
|
+
);
|
|
519
|
+
return gitResult.exitCode === 0;
|
|
520
|
+
}
|
|
521
|
+
/** Query beads on a remote VM via bd search */
|
|
522
|
+
async queryBeads(vmName, searchTerm, workspacePath = "/workspace") {
|
|
523
|
+
const result = await this.ssh(
|
|
524
|
+
vmName,
|
|
525
|
+
`cd ${workspacePath} && bd search ${JSON.stringify(searchTerm)} --json 2>/dev/null || echo '[]'`
|
|
526
|
+
);
|
|
527
|
+
try {
|
|
528
|
+
return JSON.parse(result.stdout.trim() || "[]");
|
|
529
|
+
} catch {
|
|
530
|
+
return [];
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
/** Get the configured app name */
|
|
534
|
+
getAppName() {
|
|
535
|
+
return this.config.app;
|
|
536
|
+
}
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
// src/lib/remote/remote-agents.ts
|
|
542
|
+
import { join as join2 } from "path";
|
|
543
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync, mkdirSync } from "fs";
|
|
544
|
+
import { homedir as homedir2 } from "os";
|
|
545
|
+
function getRemoteAgentStateFile(agentId) {
|
|
546
|
+
return join2(AGENTS_DIR, agentId, "remote-state.json");
|
|
547
|
+
}
|
|
548
|
+
function saveRemoteAgentState(state) {
|
|
549
|
+
const dir = join2(AGENTS_DIR, state.id);
|
|
550
|
+
if (!existsSync2(dir)) {
|
|
551
|
+
mkdirSync(dir, { recursive: true });
|
|
552
|
+
}
|
|
553
|
+
writeFileSync(getRemoteAgentStateFile(state.id), JSON.stringify(state, null, 2));
|
|
554
|
+
}
|
|
555
|
+
function loadRemoteAgentState(agentId) {
|
|
556
|
+
const file = getRemoteAgentStateFile(agentId);
|
|
557
|
+
if (!existsSync2(file)) return null;
|
|
558
|
+
try {
|
|
559
|
+
return JSON.parse(readFileSync2(file, "utf-8"));
|
|
560
|
+
} catch {
|
|
561
|
+
return null;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
async function remoteSessionExists(provider, vmName, sessionName) {
|
|
565
|
+
const result = await provider.ssh(vmName, `tmux has-session -t ${sessionName} 2>/dev/null && echo exists || echo not-found`);
|
|
566
|
+
return result.stdout.trim() === "exists";
|
|
567
|
+
}
|
|
568
|
+
async function spawnRemoteAgent(options) {
|
|
569
|
+
const { issueId, workspace, model = "claude-sonnet-4-6", prompt } = options;
|
|
570
|
+
const agentId = `agent-${issueId.toLowerCase()}`;
|
|
571
|
+
const vmName = workspace.vmName;
|
|
572
|
+
const fly = createFlyProvider();
|
|
573
|
+
const vmStatus = await fly.getStatus(vmName);
|
|
574
|
+
if (vmStatus !== "running") {
|
|
575
|
+
throw new Error(`VM ${vmName} is not running. Start it with: pan workspace start ${issueId}`);
|
|
576
|
+
}
|
|
577
|
+
if (await remoteSessionExists(fly, vmName, agentId)) {
|
|
578
|
+
throw new Error(`Agent ${agentId} already running on ${vmName}. Use 'pan work tell' to message it.`);
|
|
579
|
+
}
|
|
580
|
+
const state = {
|
|
581
|
+
id: agentId,
|
|
582
|
+
issueId,
|
|
583
|
+
vmName,
|
|
584
|
+
model,
|
|
585
|
+
status: "starting",
|
|
586
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
587
|
+
location: "remote"
|
|
588
|
+
};
|
|
589
|
+
saveRemoteAgentState(state);
|
|
590
|
+
let claudeCmd;
|
|
591
|
+
if (prompt) {
|
|
592
|
+
const promptFile = `/workspace/.panopticon/prompts/${agentId}.md`;
|
|
593
|
+
await fly.ssh(vmName, `mkdir -p /workspace/.panopticon/prompts`);
|
|
594
|
+
const promptBase64 = Buffer.from(prompt).toString("base64");
|
|
595
|
+
await fly.ssh(vmName, `echo '${promptBase64}' | base64 -d > ${promptFile}`);
|
|
596
|
+
const launcherScript = `/workspace/.panopticon/prompts/${agentId}-launcher.sh`;
|
|
597
|
+
const launcherContent = `#!/bin/bash
|
|
598
|
+
export PATH="/usr/local/bin:$PATH"
|
|
599
|
+
prompt=$(cat "${promptFile}")
|
|
600
|
+
exec claude --dangerously-skip-permissions --model ${model} "$prompt"
|
|
601
|
+
`;
|
|
602
|
+
const launcherBase64 = Buffer.from(launcherContent).toString("base64");
|
|
603
|
+
await fly.ssh(vmName, `echo '${launcherBase64}' | base64 -d > ${launcherScript} && chmod +x ${launcherScript}`);
|
|
604
|
+
claudeCmd = `bash ${launcherScript}`;
|
|
605
|
+
} else {
|
|
606
|
+
claudeCmd = `claude --dangerously-skip-permissions --model ${model}`;
|
|
607
|
+
}
|
|
608
|
+
const tmuxCmd = `tmux new-session -d -s ${agentId} -c /workspace '${claudeCmd}'`;
|
|
609
|
+
const result = await fly.ssh(vmName, tmuxCmd);
|
|
610
|
+
if (result.exitCode !== 0) {
|
|
611
|
+
state.status = "error";
|
|
612
|
+
saveRemoteAgentState(state);
|
|
613
|
+
throw new Error(`Failed to start agent: ${result.stderr}`);
|
|
614
|
+
}
|
|
615
|
+
state.status = "running";
|
|
616
|
+
saveRemoteAgentState(state);
|
|
617
|
+
return state;
|
|
618
|
+
}
|
|
619
|
+
async function getRemoteAgentOutput(agentId, vmName, lines = 100) {
|
|
620
|
+
const fly = createFlyProvider();
|
|
621
|
+
const result = await fly.ssh(vmName, `tmux capture-pane -t ${agentId} -p -S -${lines}`);
|
|
622
|
+
return result.stdout;
|
|
623
|
+
}
|
|
624
|
+
async function sendToRemoteAgent(agentId, vmName, message) {
|
|
625
|
+
const fly = createFlyProvider();
|
|
626
|
+
const escapedMessage = message.replace(/'/g, "'\\''");
|
|
627
|
+
await fly.ssh(vmName, `tmux send-keys -t ${agentId} '${escapedMessage}'`);
|
|
628
|
+
await fly.ssh(vmName, `tmux send-keys -t ${agentId} C-m`);
|
|
629
|
+
}
|
|
630
|
+
async function isRemoteAgentRunning(agentId, vmName) {
|
|
631
|
+
const fly = createFlyProvider();
|
|
632
|
+
return remoteSessionExists(fly, vmName, agentId);
|
|
633
|
+
}
|
|
634
|
+
async function killRemoteAgent(agentId, vmName) {
|
|
635
|
+
const fly = createFlyProvider();
|
|
636
|
+
await fly.ssh(vmName, `tmux kill-session -t ${agentId} 2>/dev/null || true`);
|
|
637
|
+
const state = loadRemoteAgentState(agentId);
|
|
638
|
+
if (state) {
|
|
639
|
+
state.status = "stopped";
|
|
640
|
+
saveRemoteAgentState(state);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
async function listRemoteAgents(vmName) {
|
|
644
|
+
const fly = createFlyProvider();
|
|
645
|
+
const result = await fly.ssh(vmName, `tmux list-sessions -F "#{session_name}" 2>/dev/null | grep "^agent-" || true`);
|
|
646
|
+
if (!result.stdout.trim()) {
|
|
647
|
+
return [];
|
|
648
|
+
}
|
|
649
|
+
return result.stdout.trim().split("\n").filter(Boolean);
|
|
650
|
+
}
|
|
651
|
+
async function pollRemoteAgentStatus(agentId, vmName) {
|
|
652
|
+
const fly = createFlyProvider();
|
|
653
|
+
const isRunning = await remoteSessionExists(fly, vmName, agentId);
|
|
654
|
+
if (!isRunning) {
|
|
655
|
+
return { isRunning: false, lastOutput: "", toolUses: [] };
|
|
656
|
+
}
|
|
657
|
+
const output = await getRemoteAgentOutput(agentId, vmName, 50);
|
|
658
|
+
const toolUses = [];
|
|
659
|
+
const toolPattern = /(?:Using|Calling|Running)\s+(\w+)\s+tool/gi;
|
|
660
|
+
let match;
|
|
661
|
+
while ((match = toolPattern.exec(output)) !== null) {
|
|
662
|
+
toolUses.push(match[1]);
|
|
663
|
+
}
|
|
664
|
+
return {
|
|
665
|
+
isRunning,
|
|
666
|
+
lastOutput: output,
|
|
667
|
+
toolUses
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
var AGENTS_DIR;
|
|
671
|
+
var init_remote_agents = __esm({
|
|
672
|
+
"src/lib/remote/remote-agents.ts"() {
|
|
673
|
+
init_esm_shims();
|
|
674
|
+
init_fly_provider();
|
|
675
|
+
AGENTS_DIR = join2(homedir2(), ".panopticon", "agents");
|
|
676
|
+
}
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
export {
|
|
680
|
+
createFlyProvider,
|
|
681
|
+
init_fly_provider,
|
|
682
|
+
loadRemoteAgentState,
|
|
683
|
+
spawnRemoteAgent,
|
|
684
|
+
getRemoteAgentOutput,
|
|
685
|
+
sendToRemoteAgent,
|
|
686
|
+
isRemoteAgentRunning,
|
|
687
|
+
killRemoteAgent,
|
|
688
|
+
listRemoteAgents,
|
|
689
|
+
pollRemoteAgentStatus,
|
|
690
|
+
init_remote_agents
|
|
691
|
+
};
|
|
692
|
+
//# sourceMappingURL=chunk-GUV2EPBG.js.map
|