@victor-software-house/pi-acp 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{client-CTg5Oiz5.mjs → client-BjK2BnaD.mjs} +4 -4
- package/dist/{client-CTg5Oiz5.mjs.map → client-BjK2BnaD.mjs.map} +1 -1
- package/dist/{serve-DLukbpF4.mjs → daemon-tHPrf3qs.mjs} +801 -26
- package/dist/daemon-tHPrf3qs.mjs.map +1 -0
- package/dist/index.mjs +20 -23
- package/dist/index.mjs.map +1 -1
- package/dist/operator-AtBT_SZT.mjs +52 -0
- package/dist/operator-AtBT_SZT.mjs.map +1 -0
- package/dist/pi-package-DFOfbtij.mjs +3 -0
- package/dist/pi-package-aHs6rWNo.mjs +29 -0
- package/dist/pi-package-aHs6rWNo.mjs.map +1 -0
- package/dist/{socket-BUNWxnAN.mjs → socket-wvV053VI.mjs} +28 -25
- package/dist/socket-wvV053VI.mjs.map +1 -0
- package/package.json +5 -3
- package/dist/daemon-irIzm1zJ.mjs +0 -189
- package/dist/daemon-irIzm1zJ.mjs.map +0 -1
- package/dist/in-process-DcAV6Sgx.mjs +0 -31
- package/dist/in-process-DcAV6Sgx.mjs.map +0 -1
- package/dist/serve-DLukbpF4.mjs.map +0 -1
- package/dist/socket-BUNWxnAN.mjs.map +0 -1
|
@@ -1,12 +1,189 @@
|
|
|
1
|
-
#!/usr/bin/env
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { a as removeStaleSocketIfAny, i as releaseLock, n as controlSocketPath, o as socketPath, r as ensureSocketParentDir, t as acquireLock } from "./socket-wvV053VI.mjs";
|
|
3
|
+
import { t as piChangelogPath } from "./pi-package-aHs6rWNo.mjs";
|
|
4
|
+
import { createServer } from "node:net";
|
|
5
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
6
|
import { homedir } from "node:os";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
7
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
8
|
+
import { Hono } from "hono";
|
|
6
9
|
import { AgentSideConnection, RequestError, ndJsonStream } from "@agentclientprotocol/sdk";
|
|
7
10
|
import { randomUUID } from "node:crypto";
|
|
8
|
-
import { SessionManager, createAgentSession } from "@earendil-works/pi-coding-agent";
|
|
11
|
+
import { DefaultResourceLoader, SessionManager, createAgentSession, getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
9
12
|
import * as z from "zod";
|
|
13
|
+
import { parse } from "yaml";
|
|
14
|
+
import { $ } from "bun";
|
|
15
|
+
//#region src/daemon/session-registry.ts
|
|
16
|
+
function createSessionRegistry() {
|
|
17
|
+
const map = /* @__PURE__ */ new Map();
|
|
18
|
+
return {
|
|
19
|
+
register(input) {
|
|
20
|
+
const entry = {
|
|
21
|
+
sessionId: input.sessionId,
|
|
22
|
+
piSession: input.piSession,
|
|
23
|
+
ownerConnectionId: input.ownerConnectionId,
|
|
24
|
+
alsoHeldBy: /* @__PURE__ */ new Set(),
|
|
25
|
+
cwd: input.cwd,
|
|
26
|
+
sessionFile: input.sessionFile,
|
|
27
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
28
|
+
};
|
|
29
|
+
map.set(input.sessionId, entry);
|
|
30
|
+
},
|
|
31
|
+
attach(sessionId, connectionId) {
|
|
32
|
+
const entry = map.get(sessionId);
|
|
33
|
+
if (entry === void 0) return void 0;
|
|
34
|
+
if (entry.ownerConnectionId !== connectionId) {
|
|
35
|
+
entry.alsoHeldBy.add(connectionId);
|
|
36
|
+
entry.updatedAt = /* @__PURE__ */ new Date();
|
|
37
|
+
}
|
|
38
|
+
return entry;
|
|
39
|
+
},
|
|
40
|
+
release(sessionId, connectionId) {
|
|
41
|
+
const entry = map.get(sessionId);
|
|
42
|
+
if (entry === void 0) return { kind: "unknown" };
|
|
43
|
+
if (entry.alsoHeldBy.delete(connectionId)) {
|
|
44
|
+
entry.updatedAt = /* @__PURE__ */ new Date();
|
|
45
|
+
if (entry.ownerConnectionId === connectionId && entry.alsoHeldBy.size === 0) {
|
|
46
|
+
map.delete(sessionId);
|
|
47
|
+
return {
|
|
48
|
+
kind: "disposed",
|
|
49
|
+
entry
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
kind: "still-held",
|
|
54
|
+
entry
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
if (entry.ownerConnectionId === connectionId) {
|
|
58
|
+
if (entry.alsoHeldBy.size > 0) {
|
|
59
|
+
const next = entry.alsoHeldBy.values().next().value;
|
|
60
|
+
if (next !== void 0) {
|
|
61
|
+
entry.alsoHeldBy.delete(next);
|
|
62
|
+
entry.ownerConnectionId = next;
|
|
63
|
+
entry.updatedAt = /* @__PURE__ */ new Date();
|
|
64
|
+
return {
|
|
65
|
+
kind: "still-held",
|
|
66
|
+
entry
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
map.delete(sessionId);
|
|
71
|
+
return {
|
|
72
|
+
kind: "disposed",
|
|
73
|
+
entry
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
kind: "still-held",
|
|
78
|
+
entry
|
|
79
|
+
};
|
|
80
|
+
},
|
|
81
|
+
get(sessionId) {
|
|
82
|
+
return map.get(sessionId);
|
|
83
|
+
},
|
|
84
|
+
listAll() {
|
|
85
|
+
return Array.from(map.values());
|
|
86
|
+
},
|
|
87
|
+
listOwnedBy(connectionId) {
|
|
88
|
+
return Array.from(map.values()).filter((e) => e.ownerConnectionId === connectionId || e.alsoHeldBy.has(connectionId));
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
//#endregion
|
|
93
|
+
//#region src/daemon/context.ts
|
|
94
|
+
/**
|
|
95
|
+
* Daemon-level shared state injected into per-connection PiAcpAgent instances.
|
|
96
|
+
*
|
|
97
|
+
* Phase 1 landed the interface + stub IdleTracker.
|
|
98
|
+
* Phase 2 wires the real SessionRegistry.
|
|
99
|
+
* Phase 3 will replace IdleTracker.
|
|
100
|
+
*/
|
|
101
|
+
function createNoopIdleTracker() {
|
|
102
|
+
return {
|
|
103
|
+
bump() {},
|
|
104
|
+
dispose() {}
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
function createDaemonContext() {
|
|
108
|
+
return {
|
|
109
|
+
sessionRegistry: createSessionRegistry(),
|
|
110
|
+
idleTracker: createNoopIdleTracker()
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
//#endregion
|
|
114
|
+
//#region src/daemon/control.ts
|
|
115
|
+
function buildControlApp(control) {
|
|
116
|
+
const app = new Hono();
|
|
117
|
+
app.get("/status", (c) => c.json({
|
|
118
|
+
uptimeSeconds: Math.round((Date.now() - control.startedAt) / 1e3),
|
|
119
|
+
connections: control.activeConnections(),
|
|
120
|
+
sessions: control.ctx.sessionRegistry.listAll().length,
|
|
121
|
+
pid: control.pid,
|
|
122
|
+
version: control.version
|
|
123
|
+
}));
|
|
124
|
+
app.post("/shutdown", (c) => {
|
|
125
|
+
setImmediate(control.onShutdown);
|
|
126
|
+
return c.json({ ok: true });
|
|
127
|
+
});
|
|
128
|
+
app.get("/sessions", (c) => c.json({ sessions: control.ctx.sessionRegistry.listAll().map((entry) => ({
|
|
129
|
+
sessionId: entry.sessionId,
|
|
130
|
+
cwd: entry.cwd,
|
|
131
|
+
owner: entry.ownerConnectionId,
|
|
132
|
+
alsoHeldBy: [...entry.alsoHeldBy],
|
|
133
|
+
updatedAt: entry.updatedAt
|
|
134
|
+
})) }));
|
|
135
|
+
return app;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Bind the control app to a Unix domain socket. Uses Bun.serve's `unix` option.
|
|
139
|
+
*/
|
|
140
|
+
function serveControl(app, socketPath) {
|
|
141
|
+
const server = Bun.serve({
|
|
142
|
+
unix: socketPath,
|
|
143
|
+
fetch: app.fetch
|
|
144
|
+
});
|
|
145
|
+
return { stop() {
|
|
146
|
+
try {
|
|
147
|
+
server.stop(true);
|
|
148
|
+
} catch {}
|
|
149
|
+
} };
|
|
150
|
+
}
|
|
151
|
+
//#endregion
|
|
152
|
+
//#region src/daemon/idle.ts
|
|
153
|
+
const DEFAULT_IDLE_SECONDS = 600;
|
|
154
|
+
function createIdleTracker(opts) {
|
|
155
|
+
let active = 0;
|
|
156
|
+
let timer = null;
|
|
157
|
+
const startTimer = () => {
|
|
158
|
+
if (timer !== null) return;
|
|
159
|
+
timer = setTimeout(opts.onIdle, opts.idleMs);
|
|
160
|
+
timer.unref?.();
|
|
161
|
+
};
|
|
162
|
+
const stopTimer = () => {
|
|
163
|
+
if (timer === null) return;
|
|
164
|
+
clearTimeout(timer);
|
|
165
|
+
timer = null;
|
|
166
|
+
};
|
|
167
|
+
startTimer();
|
|
168
|
+
return {
|
|
169
|
+
bump(delta) {
|
|
170
|
+
active = Math.max(0, active + delta);
|
|
171
|
+
if (active === 0) startTimer();
|
|
172
|
+
else stopTimer();
|
|
173
|
+
},
|
|
174
|
+
dispose() {
|
|
175
|
+
stopTimer();
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
function resolveIdleMs() {
|
|
180
|
+
const raw = process.env["PI_ACP_DAEMON_IDLE_SECONDS"];
|
|
181
|
+
if (raw === void 0 || raw === "") return DEFAULT_IDLE_SECONDS * 1e3;
|
|
182
|
+
const n = Number.parseInt(raw, 10);
|
|
183
|
+
if (!Number.isFinite(n) || n <= 0) return DEFAULT_IDLE_SECONDS * 1e3;
|
|
184
|
+
return n * 1e3;
|
|
185
|
+
}
|
|
186
|
+
//#endregion
|
|
10
187
|
//#region src/acp/auth.ts
|
|
11
188
|
const AUTH_METHOD_ID = "pi_terminal_login";
|
|
12
189
|
function buildAuthMethods(opts) {
|
|
@@ -1136,9 +1313,449 @@ function acpPromptToPiMessage(blocks) {
|
|
|
1136
1313
|
};
|
|
1137
1314
|
}
|
|
1138
1315
|
//#endregion
|
|
1316
|
+
//#region src/resources/sources/local.ts
|
|
1317
|
+
/**
|
|
1318
|
+
* LocalBackend: wraps pi's DefaultResourceLoader for one (cwd, agentDir) root.
|
|
1319
|
+
* Phase 4 skeleton — manifest support (multiple local roots) lands in Phase 5.
|
|
1320
|
+
*/
|
|
1321
|
+
var LocalBackend = class {
|
|
1322
|
+
id;
|
|
1323
|
+
kind = "local";
|
|
1324
|
+
loader;
|
|
1325
|
+
constructor(opts) {
|
|
1326
|
+
this.id = opts.id ?? "local";
|
|
1327
|
+
this.loader = new DefaultResourceLoader({
|
|
1328
|
+
cwd: opts.cwd,
|
|
1329
|
+
agentDir: opts.agentDir
|
|
1330
|
+
});
|
|
1331
|
+
}
|
|
1332
|
+
async reload() {
|
|
1333
|
+
await this.loader.reload();
|
|
1334
|
+
}
|
|
1335
|
+
getAgentsFiles() {
|
|
1336
|
+
return this.loader.getAgentsFiles().agentsFiles;
|
|
1337
|
+
}
|
|
1338
|
+
getSkills() {
|
|
1339
|
+
return this.loader.getSkills();
|
|
1340
|
+
}
|
|
1341
|
+
getPrompts() {
|
|
1342
|
+
return this.loader.getPrompts();
|
|
1343
|
+
}
|
|
1344
|
+
getExtensions() {
|
|
1345
|
+
return this.loader.getExtensions();
|
|
1346
|
+
}
|
|
1347
|
+
getSystemPrompt() {
|
|
1348
|
+
return this.loader.getSystemPrompt();
|
|
1349
|
+
}
|
|
1350
|
+
getAppendSystemPrompt() {
|
|
1351
|
+
return this.loader.getAppendSystemPrompt();
|
|
1352
|
+
}
|
|
1353
|
+
/**
|
|
1354
|
+
* Expose the wrapped DefaultResourceLoader for VirtualResourceLoader's
|
|
1355
|
+
* extension/theme passthrough. Other backends don't expose this.
|
|
1356
|
+
*/
|
|
1357
|
+
inner() {
|
|
1358
|
+
return this.loader;
|
|
1359
|
+
}
|
|
1360
|
+
};
|
|
1361
|
+
//#endregion
|
|
1362
|
+
//#region src/resources/loader.ts
|
|
1363
|
+
var VirtualResourceLoader = class {
|
|
1364
|
+
sources;
|
|
1365
|
+
mergeStrategy;
|
|
1366
|
+
primary;
|
|
1367
|
+
constructor(opts) {
|
|
1368
|
+
if (opts.sources.length === 0) throw new Error("VirtualResourceLoader requires at least one source");
|
|
1369
|
+
this.sources = opts.sources;
|
|
1370
|
+
this.mergeStrategy = opts.mergeStrategy ?? "append";
|
|
1371
|
+
const primary = resolvePrimary(opts.sources, opts.primarySourceId);
|
|
1372
|
+
this.primary = primary;
|
|
1373
|
+
}
|
|
1374
|
+
async reload() {
|
|
1375
|
+
await Promise.all(this.sources.map((s) => s.reload()));
|
|
1376
|
+
}
|
|
1377
|
+
getAgentsFiles() {
|
|
1378
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1379
|
+
const merged = [];
|
|
1380
|
+
for (const source of this.sources) for (const file of source.getAgentsFiles()) {
|
|
1381
|
+
if (seen.has(file.path)) continue;
|
|
1382
|
+
seen.add(file.path);
|
|
1383
|
+
merged.push(file);
|
|
1384
|
+
}
|
|
1385
|
+
return { agentsFiles: merged };
|
|
1386
|
+
}
|
|
1387
|
+
getSkills() {
|
|
1388
|
+
const merge = createMerger(this.mergeStrategy, (s) => s.name);
|
|
1389
|
+
const diagnostics = [];
|
|
1390
|
+
for (const source of this.sources) {
|
|
1391
|
+
const result = source.getSkills();
|
|
1392
|
+
merge.absorb(result.skills);
|
|
1393
|
+
diagnostics.push(...result.diagnostics);
|
|
1394
|
+
}
|
|
1395
|
+
return {
|
|
1396
|
+
skills: merge.list(),
|
|
1397
|
+
diagnostics
|
|
1398
|
+
};
|
|
1399
|
+
}
|
|
1400
|
+
getPrompts() {
|
|
1401
|
+
const merge = createMerger(this.mergeStrategy, (p) => p.name);
|
|
1402
|
+
const diagnostics = [];
|
|
1403
|
+
for (const source of this.sources) {
|
|
1404
|
+
const result = source.getPrompts();
|
|
1405
|
+
merge.absorb(result.prompts);
|
|
1406
|
+
diagnostics.push(...result.diagnostics);
|
|
1407
|
+
}
|
|
1408
|
+
return {
|
|
1409
|
+
prompts: merge.list(),
|
|
1410
|
+
diagnostics
|
|
1411
|
+
};
|
|
1412
|
+
}
|
|
1413
|
+
getThemes() {
|
|
1414
|
+
return this.primary.inner().getThemes();
|
|
1415
|
+
}
|
|
1416
|
+
getExtensions() {
|
|
1417
|
+
return this.primary.getExtensions();
|
|
1418
|
+
}
|
|
1419
|
+
getSystemPrompt() {
|
|
1420
|
+
for (const source of this.sources) {
|
|
1421
|
+
const sp = source.getSystemPrompt();
|
|
1422
|
+
if (sp !== void 0) return sp;
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
getAppendSystemPrompt() {
|
|
1426
|
+
const merged = [];
|
|
1427
|
+
for (const source of this.sources) merged.push(...source.getAppendSystemPrompt());
|
|
1428
|
+
return merged;
|
|
1429
|
+
}
|
|
1430
|
+
extendResources(paths) {
|
|
1431
|
+
this.primary.inner().extendResources(paths);
|
|
1432
|
+
}
|
|
1433
|
+
/** Returns the active source list. Useful for diagnostics. */
|
|
1434
|
+
listSources() {
|
|
1435
|
+
return [...this.sources];
|
|
1436
|
+
}
|
|
1437
|
+
};
|
|
1438
|
+
function resolvePrimary(sources, preferredId) {
|
|
1439
|
+
if (preferredId !== void 0) {
|
|
1440
|
+
const found = sources.find((s) => s.id === preferredId);
|
|
1441
|
+
if (found === void 0) throw new Error(`VirtualResourceLoader: primarySourceId "${preferredId}" not in sources`);
|
|
1442
|
+
if (!(found instanceof LocalBackend)) throw new Error(`VirtualResourceLoader: primary source "${preferredId}" must be a LocalBackend`);
|
|
1443
|
+
return found;
|
|
1444
|
+
}
|
|
1445
|
+
const firstLocal = sources.find((s) => s instanceof LocalBackend);
|
|
1446
|
+
if (firstLocal === void 0) throw new Error("VirtualResourceLoader: at least one LocalBackend is required (for extensions + themes)");
|
|
1447
|
+
return firstLocal;
|
|
1448
|
+
}
|
|
1449
|
+
function createMerger(strategy, key) {
|
|
1450
|
+
if (strategy === "append") {
|
|
1451
|
+
const out = [];
|
|
1452
|
+
return {
|
|
1453
|
+
absorb(items) {
|
|
1454
|
+
out.push(...items);
|
|
1455
|
+
},
|
|
1456
|
+
list() {
|
|
1457
|
+
return out;
|
|
1458
|
+
}
|
|
1459
|
+
};
|
|
1460
|
+
}
|
|
1461
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
1462
|
+
return {
|
|
1463
|
+
absorb(items) {
|
|
1464
|
+
for (const item of items) byKey.set(key(item), item);
|
|
1465
|
+
},
|
|
1466
|
+
list() {
|
|
1467
|
+
return Array.from(byKey.values());
|
|
1468
|
+
}
|
|
1469
|
+
};
|
|
1470
|
+
}
|
|
1471
|
+
//#endregion
|
|
1472
|
+
//#region src/resources/manifest.schema.ts
|
|
1473
|
+
/**
|
|
1474
|
+
* Zod schema for the `.pi-acp.yaml` resource composition manifest (ADR-0008).
|
|
1475
|
+
*
|
|
1476
|
+
* Backend kinds: `local`, `ssh`, `http`, `acp-fs`. Phase 5 ships parsing and
|
|
1477
|
+
* validation for all four; only `local` is honored by the loader until the
|
|
1478
|
+
* remote-backend phases land — unknown kinds parse fine and surface as a
|
|
1479
|
+
* diagnostic at load time.
|
|
1480
|
+
*/
|
|
1481
|
+
const IdSchema = z.string().trim().min(1, "id is required");
|
|
1482
|
+
const LocalPathsSchema = z.object({
|
|
1483
|
+
cwd: z.string().trim().optional(),
|
|
1484
|
+
agentDir: z.string().trim().optional()
|
|
1485
|
+
}).strict();
|
|
1486
|
+
const RemotePathsSchema = z.object({
|
|
1487
|
+
skills: z.string().trim().optional(),
|
|
1488
|
+
prompts: z.string().trim().optional(),
|
|
1489
|
+
agentsFiles: z.array(z.string().trim()).optional(),
|
|
1490
|
+
extensions: z.string().trim().optional()
|
|
1491
|
+
}).strict();
|
|
1492
|
+
const LocalRootSchema = z.object({
|
|
1493
|
+
id: IdSchema,
|
|
1494
|
+
kind: z.literal("local"),
|
|
1495
|
+
paths: LocalPathsSchema.default({})
|
|
1496
|
+
}).strict();
|
|
1497
|
+
const SshRootSchema = z.object({
|
|
1498
|
+
id: IdSchema,
|
|
1499
|
+
kind: z.literal("ssh"),
|
|
1500
|
+
host: z.string().trim().min(1),
|
|
1501
|
+
user: z.string().trim().optional(),
|
|
1502
|
+
paths: RemotePathsSchema.default({})
|
|
1503
|
+
}).strict();
|
|
1504
|
+
const HttpRootSchema = z.object({
|
|
1505
|
+
id: IdSchema,
|
|
1506
|
+
kind: z.literal("http"),
|
|
1507
|
+
baseUrl: z.url().refine((u) => u.startsWith("https://"), { error: "baseUrl must use https://" }),
|
|
1508
|
+
cache: z.object({ ttl: z.int().nonnegative() }).strict().optional(),
|
|
1509
|
+
paths: RemotePathsSchema.default({})
|
|
1510
|
+
}).strict();
|
|
1511
|
+
const AcpFsRootSchema = z.object({
|
|
1512
|
+
id: IdSchema,
|
|
1513
|
+
kind: z.literal("acp-fs"),
|
|
1514
|
+
paths: RemotePathsSchema.default({})
|
|
1515
|
+
}).strict();
|
|
1516
|
+
const RootSchema = z.discriminatedUnion("kind", [
|
|
1517
|
+
LocalRootSchema,
|
|
1518
|
+
SshRootSchema,
|
|
1519
|
+
HttpRootSchema,
|
|
1520
|
+
AcpFsRootSchema
|
|
1521
|
+
]);
|
|
1522
|
+
const AutoImportSchema = z.object({
|
|
1523
|
+
source: IdSchema,
|
|
1524
|
+
paths: z.array(z.string().trim()).min(1)
|
|
1525
|
+
}).strict();
|
|
1526
|
+
const ManifestSchema = z.object({
|
|
1527
|
+
version: z.literal(1),
|
|
1528
|
+
mode: z.enum([
|
|
1529
|
+
"local",
|
|
1530
|
+
"overlay",
|
|
1531
|
+
"none"
|
|
1532
|
+
]).default("local"),
|
|
1533
|
+
roots: z.array(RootSchema).default([]),
|
|
1534
|
+
mergeStrategy: z.enum(["append", "override-by-name"]).default("append"),
|
|
1535
|
+
autoImport: z.array(AutoImportSchema).optional(),
|
|
1536
|
+
diagnostics: z.boolean().default(false)
|
|
1537
|
+
}).strict();
|
|
1538
|
+
const DEFAULT_MANIFEST = {
|
|
1539
|
+
version: 1,
|
|
1540
|
+
mode: "local",
|
|
1541
|
+
roots: [],
|
|
1542
|
+
mergeStrategy: "append",
|
|
1543
|
+
diagnostics: false
|
|
1544
|
+
};
|
|
1545
|
+
//#endregion
|
|
1546
|
+
//#region src/resources/manifest.ts
|
|
1547
|
+
/**
|
|
1548
|
+
* Cascade resolver for the `.pi-acp.yaml` resource composition manifest
|
|
1549
|
+
* (ADR-0008, PRD-002 §FR-3).
|
|
1550
|
+
*
|
|
1551
|
+
* Precedence (highest first):
|
|
1552
|
+
* 1. ACP session params: `params._meta.piAcp.manifest`
|
|
1553
|
+
* — either an inline manifest object or a string path to a YAML file
|
|
1554
|
+
* 2. Project-level: `<cwd>/.pi-acp.yaml`
|
|
1555
|
+
* 3. User-global: `~/.pi-acp/config.yaml`
|
|
1556
|
+
* 4. Synthesized default
|
|
1557
|
+
*
|
|
1558
|
+
* Parse errors at any layer fall through to the next; the caller never gets
|
|
1559
|
+
* an exception. Errors collect into the returned `diagnostics` list so they
|
|
1560
|
+
* can be surfaced to the operator.
|
|
1561
|
+
*/
|
|
1562
|
+
const USER_MANIFEST_PATH = join(homedir(), ".pi-acp", "config.yaml");
|
|
1563
|
+
const PROJECT_MANIFEST_BASENAME = ".pi-acp.yaml";
|
|
1564
|
+
async function loadManifest(input) {
|
|
1565
|
+
const diagnostics = [];
|
|
1566
|
+
const fromParams = await tryFromSessionParams(input.sessionParams, diagnostics);
|
|
1567
|
+
if (fromParams !== null) {
|
|
1568
|
+
const result = {
|
|
1569
|
+
manifest: fromParams.manifest,
|
|
1570
|
+
source: "session-params",
|
|
1571
|
+
diagnostics
|
|
1572
|
+
};
|
|
1573
|
+
if (fromParams.path !== void 0) result.path = fromParams.path;
|
|
1574
|
+
return result;
|
|
1575
|
+
}
|
|
1576
|
+
const projectPath = join(input.cwd, PROJECT_MANIFEST_BASENAME);
|
|
1577
|
+
const fromProject = tryFromFile(projectPath, "project", diagnostics);
|
|
1578
|
+
if (fromProject !== null) return {
|
|
1579
|
+
manifest: fromProject,
|
|
1580
|
+
source: "project",
|
|
1581
|
+
path: projectPath,
|
|
1582
|
+
diagnostics
|
|
1583
|
+
};
|
|
1584
|
+
const fromUser = tryFromFile(USER_MANIFEST_PATH, "user-global", diagnostics);
|
|
1585
|
+
if (fromUser !== null) return {
|
|
1586
|
+
manifest: fromUser,
|
|
1587
|
+
source: "user-global",
|
|
1588
|
+
path: USER_MANIFEST_PATH,
|
|
1589
|
+
diagnostics
|
|
1590
|
+
};
|
|
1591
|
+
return {
|
|
1592
|
+
manifest: DEFAULT_MANIFEST,
|
|
1593
|
+
source: "default",
|
|
1594
|
+
diagnostics
|
|
1595
|
+
};
|
|
1596
|
+
}
|
|
1597
|
+
async function tryFromSessionParams(params, diagnostics) {
|
|
1598
|
+
if (typeof params !== "object" || params === null) return null;
|
|
1599
|
+
const meta = params._meta;
|
|
1600
|
+
if (typeof meta !== "object" || meta === null) return null;
|
|
1601
|
+
const piAcp = meta.piAcp;
|
|
1602
|
+
if (typeof piAcp !== "object" || piAcp === null) return null;
|
|
1603
|
+
const manifestRef = piAcp.manifest;
|
|
1604
|
+
if (manifestRef === void 0) return null;
|
|
1605
|
+
if (typeof manifestRef === "string") {
|
|
1606
|
+
const parsed = tryFromFile(manifestRef, "session-params", diagnostics);
|
|
1607
|
+
if (parsed !== null) return {
|
|
1608
|
+
manifest: parsed,
|
|
1609
|
+
path: manifestRef
|
|
1610
|
+
};
|
|
1611
|
+
return null;
|
|
1612
|
+
}
|
|
1613
|
+
const result = ManifestSchema.safeParse(manifestRef);
|
|
1614
|
+
if (result.success) return { manifest: result.data };
|
|
1615
|
+
diagnostics.push({
|
|
1616
|
+
source: "session-params",
|
|
1617
|
+
message: `inline manifest validation failed: ${result.error.message}`
|
|
1618
|
+
});
|
|
1619
|
+
return null;
|
|
1620
|
+
}
|
|
1621
|
+
function tryFromFile(path, source, diagnostics) {
|
|
1622
|
+
if (!existsSync(path)) return null;
|
|
1623
|
+
let raw;
|
|
1624
|
+
try {
|
|
1625
|
+
raw = readFileSync(path, "utf8");
|
|
1626
|
+
} catch (err) {
|
|
1627
|
+
diagnostics.push({
|
|
1628
|
+
source,
|
|
1629
|
+
path,
|
|
1630
|
+
message: `read failed: ${err instanceof Error ? err.message : String(err)}`
|
|
1631
|
+
});
|
|
1632
|
+
return null;
|
|
1633
|
+
}
|
|
1634
|
+
let parsed;
|
|
1635
|
+
try {
|
|
1636
|
+
parsed = parse(raw);
|
|
1637
|
+
} catch (err) {
|
|
1638
|
+
diagnostics.push({
|
|
1639
|
+
source,
|
|
1640
|
+
path,
|
|
1641
|
+
message: `YAML parse failed: ${err instanceof Error ? err.message : String(err)}`
|
|
1642
|
+
});
|
|
1643
|
+
return null;
|
|
1644
|
+
}
|
|
1645
|
+
const result = ManifestSchema.safeParse(parsed);
|
|
1646
|
+
if (result.success) return result.data;
|
|
1647
|
+
diagnostics.push({
|
|
1648
|
+
source,
|
|
1649
|
+
path,
|
|
1650
|
+
message: `schema validation failed: ${result.error.message}`
|
|
1651
|
+
});
|
|
1652
|
+
return null;
|
|
1653
|
+
}
|
|
1654
|
+
//#endregion
|
|
1655
|
+
//#region src/resources/sources/ssh.ts
|
|
1656
|
+
const DEFAULT_TIMEOUT_MS = 5e3;
|
|
1657
|
+
var SshBackend = class {
|
|
1658
|
+
id;
|
|
1659
|
+
kind = "ssh";
|
|
1660
|
+
host;
|
|
1661
|
+
user;
|
|
1662
|
+
paths;
|
|
1663
|
+
timeoutMs;
|
|
1664
|
+
sshCommand;
|
|
1665
|
+
cache = null;
|
|
1666
|
+
constructor(opts) {
|
|
1667
|
+
this.id = opts.id;
|
|
1668
|
+
this.host = opts.host;
|
|
1669
|
+
this.user = opts.user;
|
|
1670
|
+
this.paths = opts.paths ?? {};
|
|
1671
|
+
this.timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
1672
|
+
this.sshCommand = opts.sshCommand ?? "ssh";
|
|
1673
|
+
}
|
|
1674
|
+
async reload() {
|
|
1675
|
+
const diagnostics = [];
|
|
1676
|
+
for (const kind of [
|
|
1677
|
+
"skills",
|
|
1678
|
+
"prompts",
|
|
1679
|
+
"extensions"
|
|
1680
|
+
]) if (this.paths[kind] !== void 0) diagnostics.push(this.unsupportedDiagnostic(kind));
|
|
1681
|
+
const list = this.paths.agentsFiles ?? [];
|
|
1682
|
+
const files = [];
|
|
1683
|
+
if (list.length > 0) {
|
|
1684
|
+
const results = await Promise.all(list.map((path) => this.cat(path).then((content) => ({
|
|
1685
|
+
path,
|
|
1686
|
+
content,
|
|
1687
|
+
error: null
|
|
1688
|
+
}), (err) => ({
|
|
1689
|
+
path,
|
|
1690
|
+
content: null,
|
|
1691
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1692
|
+
}))));
|
|
1693
|
+
for (const r of results) {
|
|
1694
|
+
if (r.content !== null) {
|
|
1695
|
+
files.push({
|
|
1696
|
+
path: this.qualifyPath(r.path),
|
|
1697
|
+
content: r.content
|
|
1698
|
+
});
|
|
1699
|
+
continue;
|
|
1700
|
+
}
|
|
1701
|
+
diagnostics.push({
|
|
1702
|
+
type: "warning",
|
|
1703
|
+
message: `pi-acp ssh source '${this.id}' (${this.target()}): agentsFile '${r.path}' unreadable — ${r.error ?? "(unknown)"}`,
|
|
1704
|
+
path: r.path
|
|
1705
|
+
});
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
this.cache = {
|
|
1709
|
+
files,
|
|
1710
|
+
diagnostics
|
|
1711
|
+
};
|
|
1712
|
+
}
|
|
1713
|
+
getAgentsFiles() {
|
|
1714
|
+
return this.cache?.files ?? [];
|
|
1715
|
+
}
|
|
1716
|
+
getSkills() {
|
|
1717
|
+
return {
|
|
1718
|
+
skills: [],
|
|
1719
|
+
diagnostics: this.cache?.diagnostics ?? []
|
|
1720
|
+
};
|
|
1721
|
+
}
|
|
1722
|
+
getPrompts() {
|
|
1723
|
+
return {
|
|
1724
|
+
prompts: [],
|
|
1725
|
+
diagnostics: []
|
|
1726
|
+
};
|
|
1727
|
+
}
|
|
1728
|
+
getSystemPrompt() {}
|
|
1729
|
+
getAppendSystemPrompt() {
|
|
1730
|
+
return [];
|
|
1731
|
+
}
|
|
1732
|
+
target() {
|
|
1733
|
+
return this.user !== void 0 && this.user.length > 0 ? `${this.user}@${this.host}` : this.host;
|
|
1734
|
+
}
|
|
1735
|
+
qualifyPath(path) {
|
|
1736
|
+
return `ssh://${this.target()}/${path.replace(/^\//, "")}`;
|
|
1737
|
+
}
|
|
1738
|
+
unsupportedDiagnostic(kind) {
|
|
1739
|
+
return {
|
|
1740
|
+
type: "warning",
|
|
1741
|
+
message: `pi-acp ssh source '${this.id}' (${this.target()}): ${kind} discovery over SSH not yet implemented — declare individual files via paths.agentsFiles for now, or omit paths.${kind}.`
|
|
1742
|
+
};
|
|
1743
|
+
}
|
|
1744
|
+
async cat(path) {
|
|
1745
|
+
const seconds = Math.max(1, Math.ceil(this.timeoutMs / 1e3));
|
|
1746
|
+
const aliveCount = Math.max(1, Math.floor(seconds / 2));
|
|
1747
|
+
const result = await $`${this.sshCommand} -o BatchMode=yes -o ConnectTimeout=${seconds} -o ServerAliveInterval=2 -o ServerAliveCountMax=${aliveCount} ${this.target()} -- cat ${path}`.quiet().nothrow();
|
|
1748
|
+
if (result.exitCode !== 0) {
|
|
1749
|
+
const stderr = result.stderr.toString().trim();
|
|
1750
|
+
throw new Error(`ssh exited ${result.exitCode}: ${stderr || "(no stderr)"}`);
|
|
1751
|
+
}
|
|
1752
|
+
return result.stdout.toString();
|
|
1753
|
+
}
|
|
1754
|
+
};
|
|
1755
|
+
//#endregion
|
|
1139
1756
|
//#region package.json
|
|
1140
1757
|
var name = "@victor-software-house/pi-acp";
|
|
1141
|
-
var version = "0.
|
|
1758
|
+
var version = "0.9.0";
|
|
1142
1759
|
//#endregion
|
|
1143
1760
|
//#region src/acp/agent.ts
|
|
1144
1761
|
/** Builtin ACP slash commands handled directly by the adapter. */
|
|
@@ -1259,6 +1876,64 @@ var PiAcpAgent = class {
|
|
|
1259
1876
|
if (this.daemonContext === void 0) return { disposed: true };
|
|
1260
1877
|
return { disposed: this.daemonContext.sessionRegistry.release(sessionId, this.connectionId).kind === "disposed" };
|
|
1261
1878
|
}
|
|
1879
|
+
/**
|
|
1880
|
+
* Build a VirtualResourceLoader for a new pi session. Reads the
|
|
1881
|
+
* `.pi-acp.yaml` manifest cascade (ACP params > project > user-global >
|
|
1882
|
+
* default), turns each declared root into a ResourceSource, and ensures at
|
|
1883
|
+
* least one LocalBackend is present for the extension / theme passthrough.
|
|
1884
|
+
*
|
|
1885
|
+
* Phase 5 materializes `kind: "local"`. Phase 6 adds `kind: "ssh"`.
|
|
1886
|
+
* `http` and `acp-fs` still parse fine but surface as diagnostics until
|
|
1887
|
+
* their backends land in subsequent phases.
|
|
1888
|
+
*/
|
|
1889
|
+
async buildResourceLoader(cwd, sessionParams) {
|
|
1890
|
+
const loaded = await loadManifest({
|
|
1891
|
+
cwd,
|
|
1892
|
+
sessionParams
|
|
1893
|
+
});
|
|
1894
|
+
const diagnostics = [...loaded.diagnostics];
|
|
1895
|
+
const sources = [];
|
|
1896
|
+
for (const root of loaded.manifest.roots) {
|
|
1897
|
+
if (root.kind === "local") {
|
|
1898
|
+
sources.push(new LocalBackend({
|
|
1899
|
+
id: root.id,
|
|
1900
|
+
cwd: root.paths.cwd ?? cwd,
|
|
1901
|
+
agentDir: root.paths.agentDir ?? getAgentDir()
|
|
1902
|
+
}));
|
|
1903
|
+
continue;
|
|
1904
|
+
}
|
|
1905
|
+
if (root.kind === "ssh") {
|
|
1906
|
+
const sshOpts = {
|
|
1907
|
+
id: root.id,
|
|
1908
|
+
host: root.host,
|
|
1909
|
+
paths: root.paths
|
|
1910
|
+
};
|
|
1911
|
+
if (root.user !== void 0) sshOpts.user = root.user;
|
|
1912
|
+
sources.push(new SshBackend(sshOpts));
|
|
1913
|
+
continue;
|
|
1914
|
+
}
|
|
1915
|
+
const diag = {
|
|
1916
|
+
source: loaded.source,
|
|
1917
|
+
message: `root "${root.id}" kind="${root.kind}" not yet supported in this build (skipped)`
|
|
1918
|
+
};
|
|
1919
|
+
if (loaded.path !== void 0) diag.path = loaded.path;
|
|
1920
|
+
diagnostics.push(diag);
|
|
1921
|
+
}
|
|
1922
|
+
if (!sources.some((s) => s.kind === "local")) sources.unshift(new LocalBackend({
|
|
1923
|
+
cwd,
|
|
1924
|
+
agentDir: getAgentDir()
|
|
1925
|
+
}));
|
|
1926
|
+
const loader = new VirtualResourceLoader({
|
|
1927
|
+
sources,
|
|
1928
|
+
mergeStrategy: loaded.manifest.mergeStrategy
|
|
1929
|
+
});
|
|
1930
|
+
await loader.reload();
|
|
1931
|
+
if (diagnostics.length > 0 && process.env["PI_ACP_DAEMON_DEBUG"] === "1") for (const d of diagnostics) {
|
|
1932
|
+
const where = d.path !== void 0 ? ` ${d.path}` : "";
|
|
1933
|
+
process.stderr.write(`pi-acp manifest [${d.source}${where}]: ${d.message}\n`);
|
|
1934
|
+
}
|
|
1935
|
+
return loader;
|
|
1936
|
+
}
|
|
1262
1937
|
async initialize(params) {
|
|
1263
1938
|
const supportedVersion = 1;
|
|
1264
1939
|
const requested = params.protocolVersion;
|
|
@@ -1295,7 +1970,11 @@ var PiAcpAgent = class {
|
|
|
1295
1970
|
if (!isAbsolute(params.cwd)) throw RequestError.invalidParams(`cwd must be an absolute path: ${params.cwd}`);
|
|
1296
1971
|
let result;
|
|
1297
1972
|
try {
|
|
1298
|
-
|
|
1973
|
+
const resourceLoader = await this.buildResourceLoader(params.cwd, params);
|
|
1974
|
+
result = await createAgentSession({
|
|
1975
|
+
cwd: params.cwd,
|
|
1976
|
+
resourceLoader
|
|
1977
|
+
});
|
|
1299
1978
|
} catch (e) {
|
|
1300
1979
|
const authErr = detectAuthError(e);
|
|
1301
1980
|
if (authErr !== null) throw authErr;
|
|
@@ -1565,9 +2244,11 @@ var PiAcpAgent = class {
|
|
|
1565
2244
|
let result;
|
|
1566
2245
|
try {
|
|
1567
2246
|
const sm = SessionManager.open(sessionFile);
|
|
2247
|
+
const resourceLoader = await this.buildResourceLoader(params.cwd, params);
|
|
1568
2248
|
result = await createAgentSession({
|
|
1569
2249
|
cwd: params.cwd,
|
|
1570
|
-
sessionManager: sm
|
|
2250
|
+
sessionManager: sm,
|
|
2251
|
+
resourceLoader
|
|
1571
2252
|
});
|
|
1572
2253
|
} catch (e) {
|
|
1573
2254
|
const authErr = detectAuthError(e);
|
|
@@ -1663,9 +2344,11 @@ var PiAcpAgent = class {
|
|
|
1663
2344
|
let result;
|
|
1664
2345
|
try {
|
|
1665
2346
|
const sm = SessionManager.open(sessionFile);
|
|
2347
|
+
const resourceLoader = await this.buildResourceLoader(params.cwd, params);
|
|
1666
2348
|
result = await createAgentSession({
|
|
1667
2349
|
cwd: params.cwd,
|
|
1668
|
-
sessionManager: sm
|
|
2350
|
+
sessionManager: sm,
|
|
2351
|
+
resourceLoader
|
|
1669
2352
|
});
|
|
1670
2353
|
} catch (e) {
|
|
1671
2354
|
const authErr = detectAuthError(e);
|
|
@@ -1720,9 +2403,11 @@ var PiAcpAgent = class {
|
|
|
1720
2403
|
let result;
|
|
1721
2404
|
try {
|
|
1722
2405
|
const sm = SessionManager.forkFrom(sourceFile, params.cwd);
|
|
2406
|
+
const resourceLoader = await this.buildResourceLoader(params.cwd, params);
|
|
1723
2407
|
result = await createAgentSession({
|
|
1724
2408
|
cwd: params.cwd,
|
|
1725
|
-
sessionManager: sm
|
|
2409
|
+
sessionManager: sm,
|
|
2410
|
+
resourceLoader
|
|
1726
2411
|
});
|
|
1727
2412
|
} catch (e) {
|
|
1728
2413
|
const authErr = detectAuthError(e);
|
|
@@ -2191,20 +2876,8 @@ function buildCommandList(piSession, enableSkillCommands) {
|
|
|
2191
2876
|
}
|
|
2192
2877
|
function findChangelog() {
|
|
2193
2878
|
try {
|
|
2194
|
-
const
|
|
2195
|
-
|
|
2196
|
-
if (piPath !== void 0 && piPath !== "") {
|
|
2197
|
-
const p = join(dirname(dirname(realpathSync(piPath))), "CHANGELOG.md");
|
|
2198
|
-
if (existsSync(p)) return p;
|
|
2199
|
-
}
|
|
2200
|
-
} catch {}
|
|
2201
|
-
try {
|
|
2202
|
-
const npmRoot = spawnSync("npm", ["root", "-g"], { encoding: "utf-8" });
|
|
2203
|
-
const root = String(npmRoot.stdout ?? "").trim();
|
|
2204
|
-
if (root) {
|
|
2205
|
-
const p = join(root, "@mariozechner", "pi-coding-agent", "CHANGELOG.md");
|
|
2206
|
-
if (existsSync(p)) return p;
|
|
2207
|
-
}
|
|
2879
|
+
const p = piChangelogPath();
|
|
2880
|
+
if (existsSync(p)) return p;
|
|
2208
2881
|
} catch {}
|
|
2209
2882
|
return null;
|
|
2210
2883
|
}
|
|
@@ -2258,6 +2931,108 @@ function toWebWritable(dst) {
|
|
|
2258
2931
|
} });
|
|
2259
2932
|
}
|
|
2260
2933
|
//#endregion
|
|
2261
|
-
|
|
2934
|
+
//#region src/daemon/index.ts
|
|
2935
|
+
/**
|
|
2936
|
+
* Daemon entry point. Invoked when pi-acp is launched with `--daemon`.
|
|
2937
|
+
*
|
|
2938
|
+
* Lifecycle:
|
|
2939
|
+
* 1. Acquire per-UID lockfile (refuses if another daemon alive).
|
|
2940
|
+
* 2. Remove stale socket files left by a dead prior daemon.
|
|
2941
|
+
* 3. Construct DaemonContext shared singletons.
|
|
2942
|
+
* 4. Bind ACP socket (raw NDJSON via node:net).
|
|
2943
|
+
* 5. Bind control socket (HTTP via Bun.serve + Hono).
|
|
2944
|
+
* 6. SIGINT/SIGTERM/idle-timeout trigger graceful shutdown.
|
|
2945
|
+
*/
|
|
2946
|
+
async function runDaemon() {
|
|
2947
|
+
const lockResult = acquireLock();
|
|
2948
|
+
if (!lockResult.ok) {
|
|
2949
|
+
process.stderr.write(`pi-acp daemon: already running (pid ${lockResult.heldByPid ?? "unknown"})\n`);
|
|
2950
|
+
process.exit(1);
|
|
2951
|
+
}
|
|
2952
|
+
ensureSocketParentDir();
|
|
2953
|
+
removeStaleSocketIfAny();
|
|
2954
|
+
const connections = /* @__PURE__ */ new Set();
|
|
2955
|
+
let shuttingDown = false;
|
|
2956
|
+
const startedAt = Date.now();
|
|
2957
|
+
const shutdown = () => {
|
|
2958
|
+
if (shuttingDown) return;
|
|
2959
|
+
shuttingDown = true;
|
|
2960
|
+
server.close();
|
|
2961
|
+
controlServer.stop();
|
|
2962
|
+
for (const entry of connections) {
|
|
2963
|
+
try {
|
|
2964
|
+
entry.handle.dispose();
|
|
2965
|
+
} catch {}
|
|
2966
|
+
try {
|
|
2967
|
+
entry.socket.destroy();
|
|
2968
|
+
} catch {}
|
|
2969
|
+
}
|
|
2970
|
+
connections.clear();
|
|
2971
|
+
ctx.idleTracker.dispose();
|
|
2972
|
+
removeStaleSocketIfAny();
|
|
2973
|
+
releaseLock();
|
|
2974
|
+
process.exit(0);
|
|
2975
|
+
};
|
|
2976
|
+
const ctx = createDaemonContext();
|
|
2977
|
+
ctx.idleTracker = createIdleTracker({
|
|
2978
|
+
idleMs: resolveIdleMs(),
|
|
2979
|
+
onIdle: shutdown
|
|
2980
|
+
});
|
|
2981
|
+
const controlCtx = {
|
|
2982
|
+
ctx,
|
|
2983
|
+
startedAt,
|
|
2984
|
+
pid: process.pid,
|
|
2985
|
+
version,
|
|
2986
|
+
activeConnections: () => connections.size,
|
|
2987
|
+
onShutdown: shutdown
|
|
2988
|
+
};
|
|
2989
|
+
const server = createServer((socket) => {
|
|
2990
|
+
if (shuttingDown) {
|
|
2991
|
+
socket.destroy();
|
|
2992
|
+
return;
|
|
2993
|
+
}
|
|
2994
|
+
onAccept(socket);
|
|
2995
|
+
});
|
|
2996
|
+
const onAccept = (socket) => {
|
|
2997
|
+
const handle = serveAcp({
|
|
2998
|
+
input: socket,
|
|
2999
|
+
output: socket,
|
|
3000
|
+
daemonContext: ctx
|
|
3001
|
+
});
|
|
3002
|
+
const entry = {
|
|
3003
|
+
socket,
|
|
3004
|
+
handle
|
|
3005
|
+
};
|
|
3006
|
+
connections.add(entry);
|
|
3007
|
+
ctx.idleTracker.bump(1);
|
|
3008
|
+
const cleanup = () => {
|
|
3009
|
+
if (!connections.delete(entry)) return;
|
|
3010
|
+
try {
|
|
3011
|
+
handle.dispose();
|
|
3012
|
+
} catch {}
|
|
3013
|
+
ctx.idleTracker.bump(-1);
|
|
3014
|
+
};
|
|
3015
|
+
socket.on("close", cleanup);
|
|
3016
|
+
socket.on("error", cleanup);
|
|
3017
|
+
};
|
|
3018
|
+
server.on("error", (err) => {
|
|
3019
|
+
process.stderr.write(`pi-acp daemon: server error: ${err.message}\n`);
|
|
3020
|
+
});
|
|
3021
|
+
await new Promise((resolve, reject) => {
|
|
3022
|
+
const path = socketPath();
|
|
3023
|
+
server.listen(path, () => resolve());
|
|
3024
|
+
server.once("error", reject);
|
|
3025
|
+
});
|
|
3026
|
+
const controlServer = serveControl(buildControlApp(controlCtx), controlSocketPath());
|
|
3027
|
+
if (process.env["PI_ACP_DAEMON_DEBUG"] === "1") process.stderr.write(`pi-acp daemon: acp=${socketPath()} control=${controlSocketPath()} pid=${process.pid}\n`);
|
|
3028
|
+
process.on("SIGINT", () => {
|
|
3029
|
+
shutdown();
|
|
3030
|
+
});
|
|
3031
|
+
process.on("SIGTERM", () => {
|
|
3032
|
+
shutdown();
|
|
3033
|
+
});
|
|
3034
|
+
}
|
|
3035
|
+
//#endregion
|
|
3036
|
+
export { runDaemon };
|
|
2262
3037
|
|
|
2263
|
-
//# sourceMappingURL=
|
|
3038
|
+
//# sourceMappingURL=daemon-tHPrf3qs.mjs.map
|