@drisp/cli 0.4.5 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,395 @@
1
+ // src/app/dashboard/instanceSocketClient.ts
2
+ import { WebSocket } from "ws";
3
+ var DEFAULT_HEARTBEAT_MS = 3e4;
4
+ var DEFAULT_CONNECT_TIMEOUT_MS = 1e4;
5
+ function instanceSocketUrl(dashboardUrl, instanceId) {
6
+ const url = new URL(dashboardUrl);
7
+ url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
8
+ url.pathname = `/api/instances/${encodeURIComponent(instanceId)}/socket`;
9
+ url.search = "";
10
+ url.hash = "";
11
+ return url.toString();
12
+ }
13
+ function createInstanceSocketClient(opts) {
14
+ const heartbeatMs = opts.heartbeatIntervalMs ?? DEFAULT_HEARTBEAT_MS;
15
+ const connectTimeoutMs = opts.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS;
16
+ const log = opts.log ?? (() => {
17
+ });
18
+ const now = opts.now ?? (() => Date.now());
19
+ const makeWebSocket = opts.makeWebSocket ?? ((url, accessToken) => new WebSocket(url, [accessToken]));
20
+ const frameHandlers = /* @__PURE__ */ new Set();
21
+ const closeHandlers = /* @__PURE__ */ new Set();
22
+ let ws = null;
23
+ let heartbeat = null;
24
+ let droppedSinceClose = 0;
25
+ function send(frame) {
26
+ if (!ws || ws.readyState !== ws.OPEN) {
27
+ droppedSinceClose += 1;
28
+ if (droppedSinceClose === 1) {
29
+ log(
30
+ "warn",
31
+ `instance socket dropped frame (socket not open): type=${frame.type}`
32
+ );
33
+ }
34
+ return;
35
+ }
36
+ droppedSinceClose = 0;
37
+ try {
38
+ ws.send(JSON.stringify(frame));
39
+ } catch (err) {
40
+ log(
41
+ "warn",
42
+ `instance socket send failed: ${err instanceof Error ? err.message : String(err)}`
43
+ );
44
+ }
45
+ }
46
+ function startHeartbeat() {
47
+ stopHeartbeat();
48
+ const interval = setInterval(() => {
49
+ send({ type: "ping", ts: now() });
50
+ }, heartbeatMs);
51
+ interval.unref();
52
+ heartbeat = interval;
53
+ }
54
+ function stopHeartbeat() {
55
+ if (heartbeat) {
56
+ clearInterval(heartbeat);
57
+ heartbeat = null;
58
+ }
59
+ }
60
+ function emitClose(reason) {
61
+ stopHeartbeat();
62
+ for (const handler of [...closeHandlers]) {
63
+ try {
64
+ handler(reason);
65
+ } catch {
66
+ }
67
+ }
68
+ }
69
+ function handleFrame(parsed) {
70
+ if (parsed.type === "job_assignment") {
71
+ send({ type: "assignment_accepted", runId: parsed.runId });
72
+ log("info", `instance socket: assignment accepted runId=${parsed.runId}`);
73
+ }
74
+ for (const handler of [...frameHandlers]) {
75
+ try {
76
+ handler(parsed);
77
+ } catch (err) {
78
+ log(
79
+ "warn",
80
+ `instance socket frame handler threw: ${err instanceof Error ? err.message : String(err)}`
81
+ );
82
+ }
83
+ }
84
+ }
85
+ async function connect() {
86
+ if (ws) throw new Error("instance socket already connected");
87
+ const url = instanceSocketUrl(opts.dashboardUrl, opts.instanceId);
88
+ const next = makeWebSocket(url, opts.accessToken);
89
+ try {
90
+ await new Promise((resolve, reject) => {
91
+ let settled = false;
92
+ const cleanup = () => {
93
+ next.off("open", onOpen);
94
+ next.off("error", onError);
95
+ clearTimeout(timer);
96
+ };
97
+ const onOpen = () => {
98
+ if (settled) return;
99
+ settled = true;
100
+ cleanup();
101
+ resolve();
102
+ };
103
+ const onError = (err) => {
104
+ if (settled) return;
105
+ settled = true;
106
+ cleanup();
107
+ reject(new Error(`instance socket connect failed: ${err.message}`));
108
+ };
109
+ const timer = setTimeout(() => {
110
+ if (settled) return;
111
+ settled = true;
112
+ cleanup();
113
+ reject(
114
+ new Error(
115
+ `instance socket connect failed: timed out after ${connectTimeoutMs}ms`
116
+ )
117
+ );
118
+ }, connectTimeoutMs);
119
+ next.once("open", onOpen);
120
+ next.once("error", onError);
121
+ });
122
+ } catch (err) {
123
+ next.on("error", () => {
124
+ });
125
+ try {
126
+ next.terminate();
127
+ } catch {
128
+ }
129
+ throw err;
130
+ }
131
+ ws = next;
132
+ startHeartbeat();
133
+ next.on("message", (data) => {
134
+ let parsed;
135
+ try {
136
+ parsed = JSON.parse(String(data));
137
+ } catch (err) {
138
+ log(
139
+ "warn",
140
+ `instance socket frame parse failed: ${err instanceof Error ? err.message : String(err)}`
141
+ );
142
+ return;
143
+ }
144
+ handleFrame(parsed);
145
+ });
146
+ next.on("close", (_code, reasonBuf) => {
147
+ if (next !== ws) return;
148
+ ws = null;
149
+ const reason = reasonBuf.toString() || "closed";
150
+ emitClose(reason);
151
+ });
152
+ next.on("error", (err) => {
153
+ log("warn", `instance socket error: ${err.message}`);
154
+ });
155
+ }
156
+ function close(reason) {
157
+ stopHeartbeat();
158
+ if (ws) {
159
+ try {
160
+ ws.close(1e3, reason ?? "client closed");
161
+ } catch {
162
+ ws.terminate();
163
+ }
164
+ }
165
+ ws = null;
166
+ }
167
+ function onFrame(handler) {
168
+ frameHandlers.add(handler);
169
+ }
170
+ function onClose(handler) {
171
+ closeHandlers.add(handler);
172
+ }
173
+ function sendRunEvent(event) {
174
+ send({ type: "run_event", ...event });
175
+ }
176
+ function sendFeedEvent(event) {
177
+ send({ type: "feed_event", ...event });
178
+ }
179
+ return { connect, close, onFrame, onClose, sendRunEvent, sendFeedEvent };
180
+ }
181
+
182
+ // src/infra/daemon/pidLock.ts
183
+ import fs from "fs";
184
+ function acquirePidLock(pidPath) {
185
+ const ownPid = process.pid;
186
+ for (let attempt = 0; attempt < 2; attempt += 1) {
187
+ try {
188
+ const fd = fs.openSync(pidPath, "wx", 384);
189
+ try {
190
+ fs.writeSync(fd, `${ownPid}
191
+ `);
192
+ fs.fsyncSync(fd);
193
+ } finally {
194
+ fs.closeSync(fd);
195
+ }
196
+ return makeHandle(pidPath, ownPid);
197
+ } catch (err) {
198
+ if (err.code !== "EEXIST") throw err;
199
+ }
200
+ const existing = readPidLock(pidPath);
201
+ if (existing.state === "held") {
202
+ throw new Error(
203
+ `dashboard daemon is already running as pid ${existing.pid} (lock at ${pidPath}). Use "drisp dashboard daemon stop" to terminate it.`
204
+ );
205
+ }
206
+ if (existing.state === "stale") {
207
+ try {
208
+ fs.unlinkSync(pidPath);
209
+ } catch (err) {
210
+ if (err.code !== "ENOENT") throw err;
211
+ }
212
+ continue;
213
+ }
214
+ }
215
+ throw new Error(
216
+ `dashboard daemon: failed to acquire pid lock at ${pidPath} after retry`
217
+ );
218
+ }
219
+ function readPidLock(pidPath) {
220
+ let raw;
221
+ try {
222
+ raw = fs.readFileSync(pidPath, "utf-8");
223
+ } catch (err) {
224
+ if (err.code === "ENOENT") {
225
+ return { state: "absent" };
226
+ }
227
+ throw err;
228
+ }
229
+ const pid = Number.parseInt(raw.trim(), 10);
230
+ if (!Number.isFinite(pid) || pid <= 0) {
231
+ return { state: "stale", pid: 0 };
232
+ }
233
+ if (!isProcessAlive(pid)) {
234
+ return { state: "stale", pid };
235
+ }
236
+ return { state: "held", pid };
237
+ }
238
+ function makeHandle(pidPath, pid) {
239
+ let released = false;
240
+ return {
241
+ pid,
242
+ release() {
243
+ if (released) return;
244
+ released = true;
245
+ try {
246
+ const raw = fs.readFileSync(pidPath, "utf-8").trim();
247
+ if (raw === String(pid)) {
248
+ fs.unlinkSync(pidPath);
249
+ }
250
+ } catch (err) {
251
+ if (err.code !== "ENOENT") {
252
+ }
253
+ }
254
+ }
255
+ };
256
+ }
257
+ function isProcessAlive(pid) {
258
+ if (process.platform === "win32") {
259
+ return true;
260
+ }
261
+ try {
262
+ process.kill(pid, 0);
263
+ return true;
264
+ } catch (err) {
265
+ const code = err.code;
266
+ if (code === "EPERM") {
267
+ return true;
268
+ }
269
+ return false;
270
+ }
271
+ }
272
+
273
+ // src/infra/config/attachmentMirror.ts
274
+ import crypto from "crypto";
275
+ import fs2 from "fs";
276
+ import os from "os";
277
+ import path from "path";
278
+ function attachmentMirrorPath(env = process.env) {
279
+ const home = env["HOME"] ?? os.homedir();
280
+ return path.join(home, ".config", "athena", "attachments.json");
281
+ }
282
+ function readAttachmentMirror(env = process.env) {
283
+ const file = attachmentMirrorPath(env);
284
+ let raw;
285
+ try {
286
+ raw = fs2.readFileSync(file, "utf-8");
287
+ } catch (err) {
288
+ if (err.code === "ENOENT") return null;
289
+ throw err;
290
+ }
291
+ let parsed;
292
+ try {
293
+ parsed = JSON.parse(raw);
294
+ } catch (err) {
295
+ throw new Error(
296
+ `attachment mirror ${file} is invalid JSON: ${err instanceof Error ? err.message : String(err)}`
297
+ );
298
+ }
299
+ try {
300
+ return parseAttachmentMirror(parsed);
301
+ } catch (err) {
302
+ throw new Error(
303
+ `attachment mirror ${file} is invalid: ${err instanceof Error ? err.message : String(err)}`
304
+ );
305
+ }
306
+ }
307
+ function writeAttachmentMirror(mirror, env = process.env) {
308
+ const validated = parseAttachmentMirror(mirror);
309
+ const file = attachmentMirrorPath(env);
310
+ const dir = path.dirname(file);
311
+ fs2.mkdirSync(dir, { recursive: true, mode: 448 });
312
+ const tmp = `${file}.${process.pid}.${crypto.randomBytes(4).toString("hex")}.tmp`;
313
+ const fd = fs2.openSync(tmp, "w", 384);
314
+ try {
315
+ fs2.writeSync(fd, JSON.stringify(validated, null, 2) + "\n");
316
+ fs2.fsyncSync(fd);
317
+ } finally {
318
+ fs2.closeSync(fd);
319
+ }
320
+ try {
321
+ fs2.renameSync(tmp, file);
322
+ } catch (err) {
323
+ try {
324
+ fs2.unlinkSync(tmp);
325
+ } catch {
326
+ }
327
+ throw err;
328
+ }
329
+ if (process.platform !== "win32") {
330
+ try {
331
+ fs2.chmodSync(dir, 448);
332
+ fs2.chmodSync(file, 384);
333
+ } catch {
334
+ }
335
+ }
336
+ }
337
+ function removeAttachmentMirror(env = process.env) {
338
+ const file = attachmentMirrorPath(env);
339
+ try {
340
+ fs2.unlinkSync(file);
341
+ } catch (err) {
342
+ if (err.code !== "ENOENT") throw err;
343
+ }
344
+ }
345
+ function parseAttachmentMirror(raw) {
346
+ if (typeof raw !== "object" || raw === null) {
347
+ throw new Error("root must be an object");
348
+ }
349
+ const obj = raw;
350
+ if (typeof obj["instanceId"] !== "string" || obj["instanceId"].length === 0) {
351
+ throw new Error("instanceId must be a non-empty string");
352
+ }
353
+ if (typeof obj["fetchedAt"] !== "number") {
354
+ throw new Error("fetchedAt must be a number");
355
+ }
356
+ if (!Array.isArray(obj["attachments"])) {
357
+ throw new Error("attachments must be an array");
358
+ }
359
+ const attachments = obj["attachments"].map((entry, idx) => {
360
+ if (typeof entry !== "object" || entry === null) {
361
+ throw new Error(`attachments[${idx}] must be an object`);
362
+ }
363
+ const e = entry;
364
+ if (typeof e["runnerId"] !== "string" || e["runnerId"].length === 0) {
365
+ throw new Error(
366
+ `attachments[${idx}].runnerId must be a non-empty string`
367
+ );
368
+ }
369
+ const out = { runnerId: e["runnerId"] };
370
+ if (typeof e["name"] === "string") out.name = e["name"];
371
+ if (typeof e["executionTarget"] === "string") {
372
+ out.executionTarget = e["executionTarget"];
373
+ }
374
+ if (typeof e["remoteInstanceId"] === "string") {
375
+ out.remoteInstanceId = e["remoteInstanceId"];
376
+ }
377
+ return out;
378
+ });
379
+ return {
380
+ instanceId: obj["instanceId"],
381
+ fetchedAt: obj["fetchedAt"],
382
+ attachments
383
+ };
384
+ }
385
+
386
+ export {
387
+ createInstanceSocketClient,
388
+ attachmentMirrorPath,
389
+ readAttachmentMirror,
390
+ writeAttachmentMirror,
391
+ removeAttachmentMirror,
392
+ acquirePidLock,
393
+ readPidLock
394
+ };
395
+ //# sourceMappingURL=chunk-ZVOGOZNT.js.map