issue-flow 0.13.0 → 0.15.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/agent-AT64RXTA.js +30 -0
- package/dist/{analyze-WS47UYGN.js → analyze-O2PYDIFK.js} +21 -18
- package/dist/{analyze-WS47UYGN.js.map → analyze-O2PYDIFK.js.map} +1 -1
- package/dist/{apply-OS62UWRN.js → apply-SSU6QCD4.js} +14 -11
- package/dist/{apply-OS62UWRN.js.map → apply-SSU6QCD4.js.map} +1 -1
- package/dist/{bench-SA3BOME5.js → bench-ODFPAGW6.js} +30 -26
- package/dist/{bench-SA3BOME5.js.map → bench-ODFPAGW6.js.map} +1 -1
- package/dist/{chunk-HJGGNLWI.js → chunk-2ZAM45PZ.js} +9 -9
- package/dist/{chunk-QCXPNSEL.js → chunk-3BWZO4YI.js} +2 -2
- package/dist/{chunk-J62QSVP6.js → chunk-4RXYFWP7.js} +8 -7
- package/dist/{chunk-J62QSVP6.js.map → chunk-4RXYFWP7.js.map} +1 -1
- package/dist/{chunk-ARFVHC2F.js → chunk-5ZKBIXET.js} +115 -23
- package/dist/chunk-5ZKBIXET.js.map +1 -0
- package/dist/{chunk-HEF2RMKN.js → chunk-6E2PEBFT.js} +18 -16
- package/dist/{chunk-HEF2RMKN.js.map → chunk-6E2PEBFT.js.map} +1 -1
- package/dist/{agent-BXRIEKVM.js → chunk-6FXOU2KR.js} +22 -22
- package/dist/chunk-6FXOU2KR.js.map +1 -0
- package/dist/{chunk-IYV6KNVF.js → chunk-6QYMSNVA.js} +66 -240
- package/dist/chunk-6QYMSNVA.js.map +1 -0
- package/dist/{chunk-UOCPC2Y2.js → chunk-6SRE4KD7.js} +4 -4
- package/dist/{chunk-MIVHK5YI.js → chunk-A35HE4V7.js} +3 -3
- package/dist/{chunk-TM67EHSV.js → chunk-ARWNYXFL.js} +19 -19
- package/dist/chunk-ARWNYXFL.js.map +1 -0
- package/dist/chunk-AYYMW33T.js +163 -0
- package/dist/chunk-AYYMW33T.js.map +1 -0
- package/dist/chunk-BMHGRYL6.js +890 -0
- package/dist/chunk-BMHGRYL6.js.map +1 -0
- package/dist/{chunk-PV4OX5TC.js → chunk-F4OK7KEP.js} +11 -11
- package/dist/{chunk-QXDNBYQF.js → chunk-FK4IDY6P.js} +21 -10
- package/dist/chunk-FK4IDY6P.js.map +1 -0
- package/dist/{chunk-KQHB7ZT3.js → chunk-GSZCQUCY.js} +82 -16
- package/dist/chunk-GSZCQUCY.js.map +1 -0
- package/dist/{chunk-TX5WIVYP.js → chunk-HAKDLR6N.js} +6 -1
- package/dist/chunk-HAKDLR6N.js.map +1 -0
- package/dist/{chunk-OPNGPUKJ.js → chunk-HGSYD57B.js} +143 -48
- package/dist/chunk-HGSYD57B.js.map +1 -0
- package/dist/{chunk-PIVGJM6K.js → chunk-KAHQ6F5C.js} +11 -11
- package/dist/{chunk-BHJCGWQX.js → chunk-MEFY4B7T.js} +9 -9
- package/dist/{chunk-BQDQTIM7.js → chunk-MGCOQU6N.js} +4 -4
- package/dist/{chunk-5TB6KLC2.js → chunk-MROS3SKL.js} +8 -8
- package/dist/chunk-NVZMBXTE.js +243 -0
- package/dist/chunk-NVZMBXTE.js.map +1 -0
- package/dist/{chunk-664Y3NDX.js → chunk-PCN7ZQND.js} +8 -8
- package/dist/{chunk-IV5SS4JN.js → chunk-QRCGW2ZO.js} +9 -7
- package/dist/{chunk-IV5SS4JN.js.map → chunk-QRCGW2ZO.js.map} +1 -1
- package/dist/{chunk-7KD66LIM.js → chunk-RPXDBX6R.js} +3 -3
- package/dist/{chunk-SNTTOQAD.js → chunk-RWEXK55D.js} +2 -2
- package/dist/{chunk-6FDFRNXZ.js → chunk-TBDP6QPJ.js} +25 -20
- package/dist/chunk-TBDP6QPJ.js.map +1 -0
- package/dist/chunk-UNQYMR3H.js +25 -0
- package/dist/chunk-UNQYMR3H.js.map +1 -0
- package/dist/{chunk-HQAHNIAR.js → chunk-VR34TXHF.js} +14 -14
- package/dist/{chunk-CNBYCMSU.js → chunk-WM3Y7Q4A.js} +27 -18
- package/dist/{chunk-CNBYCMSU.js.map → chunk-WM3Y7Q4A.js.map} +1 -1
- package/dist/{chunk-NEZZ6KDR.js → chunk-XDKGA6UX.js} +52 -3
- package/dist/chunk-XDKGA6UX.js.map +1 -0
- package/dist/{chunk-FZDR7CGH.js → chunk-XG2NYDRA.js} +3 -3
- package/dist/{chunk-3XLCNVMD.js → chunk-XLVGPXNL.js} +2 -2
- package/dist/{chunk-DIG5XKQQ.js → chunk-XQHFC62U.js} +2 -2
- package/dist/{chunk-UJ7XWK7J.js → chunk-XRLRKTQA.js} +16 -14
- package/dist/{chunk-UJ7XWK7J.js.map → chunk-XRLRKTQA.js.map} +1 -1
- package/dist/{chunk-LZ6GFQAG.js → chunk-ZFIXEYUJ.js} +44 -31
- package/dist/chunk-ZFIXEYUJ.js.map +1 -0
- package/dist/{chunk-37N2RJW2.js → chunk-ZPIVCM7Q.js} +3 -3
- package/dist/{chunk-TVN2JVOF.js → chunk-ZXPASFHG.js} +3 -3
- package/dist/cli.js +58 -64
- package/dist/cli.js.map +1 -1
- package/dist/{config-MEM2IWWN.js → config-UMP6DBJB.js} +12 -9
- package/dist/{conventions-WV4NDXGH.js → conventions-DKH7AE4T.js} +14 -11
- package/dist/{conventions-WV4NDXGH.js.map → conventions-DKH7AE4T.js.map} +1 -1
- package/dist/diagnostics-GOGCLTRS.js +23 -0
- package/dist/execute-E3BRMDAQ.js +35 -0
- package/dist/{generate-MVQI7A45.js → generate-AGBT7EHX.js} +23 -20
- package/dist/{generate-MVQI7A45.js.map → generate-AGBT7EHX.js.map} +1 -1
- package/dist/{git-YRVRUN3C.js → git-AC542GU2.js} +4 -2
- package/dist/init-LLBVJNPE.js +24 -0
- package/dist/{operations-LU6JA3BX.js → operations-JQYK2EMT.js} +17 -14
- package/dist/{operations-LU6JA3BX.js.map → operations-JQYK2EMT.js.map} +1 -1
- package/dist/{permissions-7NMDFU27.js → permissions-6HL52K54.js} +4 -4
- package/dist/plan-HSLWDJEZ.js +34 -0
- package/dist/{policy-JSYEK5B2.js → policy-IMRXGG7X.js} +13 -10
- package/dist/{policy-JSYEK5B2.js.map → policy-IMRXGG7X.js.map} +1 -1
- package/dist/pr-6TCZXG6K.js +39 -0
- package/dist/pr-review-6MWJO2NP.js +32 -0
- package/dist/prd-D6YDYAFX.js +33 -0
- package/dist/{ps-TNE44R5V.js → ps-KUUZ6TUL.js} +11 -8
- package/dist/{ps-TNE44R5V.js.map → ps-KUUZ6TUL.js.map} +1 -1
- package/dist/{recorder-XGF55WXI.js → recorder-BJ2KIAWT.js} +5 -4
- package/dist/registry-SB7XTMGE.js +15 -0
- package/dist/{resume-QMEPMSZW.js → resume-QNLQSCR5.js} +44 -40
- package/dist/{resume-QMEPMSZW.js.map → resume-QNLQSCR5.js.map} +1 -1
- package/dist/review-3KJ4BH2V.js +35 -0
- package/dist/{routing-2ZUMZPJB.js → routing-TN7GLMGU.js} +13 -10
- package/dist/{routing-2ZUMZPJB.js.map → routing-TN7GLMGU.js.map} +1 -1
- package/dist/run-5EHIFF2V.js +54 -0
- package/dist/run-5EHIFF2V.js.map +1 -0
- package/dist/session-publisher-T7EI3CIP.js +11 -0
- package/dist/session-publisher-T7EI3CIP.js.map +1 -0
- package/dist/{state-manager-X3PT3FGT.js → state-manager-ZEPWTCA6.js} +3 -3
- package/dist/state-manager-ZEPWTCA6.js.map +1 -0
- package/dist/{usage-VCFLDA5Y.js → usage-QL563I7F.js} +13 -10
- package/dist/{usage-VCFLDA5Y.js.map → usage-QL563I7F.js.map} +1 -1
- package/dist/{web-XVHLX62B.js → web-PPSMNTQ6.js} +22 -57
- package/dist/{web-XVHLX62B.js.map → web-PPSMNTQ6.js.map} +1 -1
- package/package.json +1 -1
- package/web/AGENTS.md +154 -11
- package/web/public/app.css +343 -110
- package/web/public/app.js +436 -27
- package/web/public/index.html +44 -1
- package/dist/agent-BXRIEKVM.js.map +0 -1
- package/dist/chunk-6FDFRNXZ.js.map +0 -1
- package/dist/chunk-ARFVHC2F.js.map +0 -1
- package/dist/chunk-IHIEXUTC.js +0 -483
- package/dist/chunk-IHIEXUTC.js.map +0 -1
- package/dist/chunk-IYV6KNVF.js.map +0 -1
- package/dist/chunk-KQHB7ZT3.js.map +0 -1
- package/dist/chunk-LZ6GFQAG.js.map +0 -1
- package/dist/chunk-NEZZ6KDR.js.map +0 -1
- package/dist/chunk-OPNGPUKJ.js.map +0 -1
- package/dist/chunk-QXDNBYQF.js.map +0 -1
- package/dist/chunk-TM67EHSV.js.map +0 -1
- package/dist/chunk-TX5WIVYP.js.map +0 -1
- package/dist/execute-NITIHHCK.js +0 -31
- package/dist/init-OKWEC3EV.js +0 -21
- package/dist/plan-LEJJF3DE.js +0 -31
- package/dist/pr-HPVUZ7S3.js +0 -36
- package/dist/pr-review-R2QZBT3F.js +0 -29
- package/dist/prd-5O6GYCC6.js +0 -30
- package/dist/registry-LGPE53QD.js +0 -15
- package/dist/review-YKWD6SNN.js +0 -32
- package/dist/run-YU4TR62R.js +0 -50
- /package/dist/{config-MEM2IWWN.js.map → agent-AT64RXTA.js.map} +0 -0
- /package/dist/{chunk-HJGGNLWI.js.map → chunk-2ZAM45PZ.js.map} +0 -0
- /package/dist/{chunk-QCXPNSEL.js.map → chunk-3BWZO4YI.js.map} +0 -0
- /package/dist/{chunk-UOCPC2Y2.js.map → chunk-6SRE4KD7.js.map} +0 -0
- /package/dist/{chunk-MIVHK5YI.js.map → chunk-A35HE4V7.js.map} +0 -0
- /package/dist/{chunk-PV4OX5TC.js.map → chunk-F4OK7KEP.js.map} +0 -0
- /package/dist/{chunk-PIVGJM6K.js.map → chunk-KAHQ6F5C.js.map} +0 -0
- /package/dist/{chunk-BHJCGWQX.js.map → chunk-MEFY4B7T.js.map} +0 -0
- /package/dist/{chunk-BQDQTIM7.js.map → chunk-MGCOQU6N.js.map} +0 -0
- /package/dist/{chunk-5TB6KLC2.js.map → chunk-MROS3SKL.js.map} +0 -0
- /package/dist/{chunk-664Y3NDX.js.map → chunk-PCN7ZQND.js.map} +0 -0
- /package/dist/{chunk-7KD66LIM.js.map → chunk-RPXDBX6R.js.map} +0 -0
- /package/dist/{chunk-SNTTOQAD.js.map → chunk-RWEXK55D.js.map} +0 -0
- /package/dist/{chunk-HQAHNIAR.js.map → chunk-VR34TXHF.js.map} +0 -0
- /package/dist/{chunk-FZDR7CGH.js.map → chunk-XG2NYDRA.js.map} +0 -0
- /package/dist/{chunk-3XLCNVMD.js.map → chunk-XLVGPXNL.js.map} +0 -0
- /package/dist/{chunk-DIG5XKQQ.js.map → chunk-XQHFC62U.js.map} +0 -0
- /package/dist/{chunk-37N2RJW2.js.map → chunk-ZPIVCM7Q.js.map} +0 -0
- /package/dist/{chunk-TVN2JVOF.js.map → chunk-ZXPASFHG.js.map} +0 -0
- /package/dist/{execute-NITIHHCK.js.map → config-UMP6DBJB.js.map} +0 -0
- /package/dist/{git-YRVRUN3C.js.map → diagnostics-GOGCLTRS.js.map} +0 -0
- /package/dist/{init-OKWEC3EV.js.map → execute-E3BRMDAQ.js.map} +0 -0
- /package/dist/{permissions-7NMDFU27.js.map → git-AC542GU2.js.map} +0 -0
- /package/dist/{plan-LEJJF3DE.js.map → init-LLBVJNPE.js.map} +0 -0
- /package/dist/{pr-HPVUZ7S3.js.map → permissions-6HL52K54.js.map} +0 -0
- /package/dist/{pr-review-R2QZBT3F.js.map → plan-HSLWDJEZ.js.map} +0 -0
- /package/dist/{prd-5O6GYCC6.js.map → pr-6TCZXG6K.js.map} +0 -0
- /package/dist/{recorder-XGF55WXI.js.map → pr-review-6MWJO2NP.js.map} +0 -0
- /package/dist/{registry-LGPE53QD.js.map → prd-D6YDYAFX.js.map} +0 -0
- /package/dist/{review-YKWD6SNN.js.map → recorder-BJ2KIAWT.js.map} +0 -0
- /package/dist/{run-YU4TR62R.js.map → registry-SB7XTMGE.js.map} +0 -0
- /package/dist/{state-manager-X3PT3FGT.js.map → review-3KJ4BH2V.js.map} +0 -0
|
@@ -0,0 +1,890 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
writeAgentPreference
|
|
4
|
+
} from "./chunk-6FXOU2KR.js";
|
|
5
|
+
import {
|
|
6
|
+
resolvePackageDir
|
|
7
|
+
} from "./chunk-A35HE4V7.js";
|
|
8
|
+
import {
|
|
9
|
+
printInfo,
|
|
10
|
+
printWarning
|
|
11
|
+
} from "./chunk-NVZMBXTE.js";
|
|
12
|
+
import {
|
|
13
|
+
NullPublisher
|
|
14
|
+
} from "./chunk-6QYMSNVA.js";
|
|
15
|
+
import {
|
|
16
|
+
readDiagnostics
|
|
17
|
+
} from "./chunk-AYYMW33T.js";
|
|
18
|
+
import {
|
|
19
|
+
getGlobalRoot
|
|
20
|
+
} from "./chunk-HAKDLR6N.js";
|
|
21
|
+
import {
|
|
22
|
+
isoNow
|
|
23
|
+
} from "./chunk-XQHFC62U.js";
|
|
24
|
+
import {
|
|
25
|
+
isAgentPhase,
|
|
26
|
+
isAgentProviderId,
|
|
27
|
+
isProcessAlive,
|
|
28
|
+
webLockSchema
|
|
29
|
+
} from "./chunk-XDKGA6UX.js";
|
|
30
|
+
|
|
31
|
+
// src/web/lock.ts
|
|
32
|
+
import { spawn as nodeSpawn } from "child_process";
|
|
33
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
34
|
+
import { mkdir, readFile as readFile2, rm, writeFile } from "fs/promises";
|
|
35
|
+
import { join as join2 } from "path";
|
|
36
|
+
import chalk2 from "chalk";
|
|
37
|
+
import { execa } from "execa";
|
|
38
|
+
|
|
39
|
+
// src/web/server.ts
|
|
40
|
+
import { createHash, randomUUID } from "crypto";
|
|
41
|
+
import { readFile } from "fs/promises";
|
|
42
|
+
import { createServer } from "http";
|
|
43
|
+
import { createRequire } from "module";
|
|
44
|
+
import { join } from "path";
|
|
45
|
+
import chalk from "chalk";
|
|
46
|
+
var require2 = createRequire(import.meta.url);
|
|
47
|
+
var SESSION_LIST_DESCRIPTION_MAX = 280;
|
|
48
|
+
function readPackageVersion() {
|
|
49
|
+
for (const candidate of ["../package.json", "../../package.json"]) {
|
|
50
|
+
try {
|
|
51
|
+
return require2(candidate).version;
|
|
52
|
+
} catch {
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return "0.0.0";
|
|
56
|
+
}
|
|
57
|
+
function truncateSessionDescription(text, max = SESSION_LIST_DESCRIPTION_MAX) {
|
|
58
|
+
if (text === null || text === void 0) return null;
|
|
59
|
+
const normalized = text.replace(/\s+/g, " ").trim();
|
|
60
|
+
if (normalized.length === 0) return normalized;
|
|
61
|
+
if (normalized.length <= max) return normalized;
|
|
62
|
+
return `${normalized.slice(0, max - 1).trimEnd()}\u2026`;
|
|
63
|
+
}
|
|
64
|
+
var JSON_TYPE = "application/json; charset=utf-8";
|
|
65
|
+
function publisherSessionSource(publisher) {
|
|
66
|
+
return {
|
|
67
|
+
list: () => {
|
|
68
|
+
const snapshot = publisher.snapshot();
|
|
69
|
+
return snapshot.sessionId === null ? [] : [snapshot];
|
|
70
|
+
},
|
|
71
|
+
get: (sessionId) => {
|
|
72
|
+
const snapshot = publisher.snapshot();
|
|
73
|
+
return snapshot.sessionId === sessionId ? snapshot : void 0;
|
|
74
|
+
},
|
|
75
|
+
events: async (sessionId) => publisher.snapshot().sessionId === sessionId ? [] : void 0
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function directorySessionSource(handle) {
|
|
79
|
+
return {
|
|
80
|
+
list: () => handle.sessions().map((entry) => entry.snapshot),
|
|
81
|
+
get: (sessionId) => handle.getSession(sessionId)?.snapshot,
|
|
82
|
+
events: (sessionId) => handle.events(sessionId)
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function resolveSessionSource(options) {
|
|
86
|
+
if (options.sessions) return directorySessionSource(options.sessions);
|
|
87
|
+
return publisherSessionSource(options.publisher ?? new NullPublisher());
|
|
88
|
+
}
|
|
89
|
+
var STATIC_ROUTES = {
|
|
90
|
+
"/": { file: "index.html", contentType: "text/html; charset=utf-8" },
|
|
91
|
+
"/app.css": { file: "app.css", contentType: "text/css; charset=utf-8" },
|
|
92
|
+
"/app.js": { file: "app.js", contentType: "text/javascript; charset=utf-8" }
|
|
93
|
+
};
|
|
94
|
+
async function loadStaticAssets(publicDir) {
|
|
95
|
+
const assets = /* @__PURE__ */ new Map();
|
|
96
|
+
if (publicDir === null) return assets;
|
|
97
|
+
for (const [route, { file, contentType }] of Object.entries(STATIC_ROUTES)) {
|
|
98
|
+
try {
|
|
99
|
+
const body = await readFile(join(publicDir, file), "utf-8");
|
|
100
|
+
assets.set(route, { body, contentType });
|
|
101
|
+
} catch {
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return assets;
|
|
105
|
+
}
|
|
106
|
+
function baseHeaders(res, instanceId) {
|
|
107
|
+
res.setHeader("Cache-Control", "no-store");
|
|
108
|
+
res.setHeader("X-Issue-Flow-Instance", instanceId);
|
|
109
|
+
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
110
|
+
res.setHeader("Referrer-Policy", "no-referrer");
|
|
111
|
+
res.setHeader("X-Robots-Tag", "noindex, nofollow");
|
|
112
|
+
}
|
|
113
|
+
function respond(res, status, contentType, body) {
|
|
114
|
+
res.statusCode = status;
|
|
115
|
+
res.setHeader("Content-Type", contentType);
|
|
116
|
+
res.end(body);
|
|
117
|
+
}
|
|
118
|
+
function respondJson(res, status, payload) {
|
|
119
|
+
respond(res, status, JSON_TYPE, JSON.stringify(payload));
|
|
120
|
+
}
|
|
121
|
+
function isLoopbackHost(host) {
|
|
122
|
+
return host === "127.0.0.1" || host === "::1" || host === "localhost";
|
|
123
|
+
}
|
|
124
|
+
async function readJsonBody(req) {
|
|
125
|
+
const chunks = [];
|
|
126
|
+
let size = 0;
|
|
127
|
+
for await (const chunk of req) {
|
|
128
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
129
|
+
size += buffer.length;
|
|
130
|
+
if (size > 64 * 1024) throw new Error("Request body too large.");
|
|
131
|
+
chunks.push(buffer);
|
|
132
|
+
}
|
|
133
|
+
return JSON.parse(Buffer.concat(chunks).toString("utf-8") || "{}");
|
|
134
|
+
}
|
|
135
|
+
async function startWebServer(options) {
|
|
136
|
+
const info = options.info ?? printInfo;
|
|
137
|
+
const warn = options.warn ?? printWarning;
|
|
138
|
+
const version = options.version ?? readPackageVersion();
|
|
139
|
+
const instanceId = options.instanceId ?? randomUUID();
|
|
140
|
+
const startedAtMs = Date.now();
|
|
141
|
+
const assets = await loadStaticAssets(
|
|
142
|
+
options.publicDir ?? resolvePackageDir(join("web", "public"))
|
|
143
|
+
);
|
|
144
|
+
const source = resolveSessionSource(options);
|
|
145
|
+
const statusCache = /* @__PURE__ */ new Map();
|
|
146
|
+
const statusPayload = (snapshot) => {
|
|
147
|
+
const key = snapshot.sessionId ?? "";
|
|
148
|
+
const body = JSON.stringify(snapshot);
|
|
149
|
+
const cached = statusCache.get(key);
|
|
150
|
+
if (cached !== void 0 && cached.body === body) return cached;
|
|
151
|
+
const etag = `"${createHash("sha1").update(body).digest("hex")}"`;
|
|
152
|
+
const entry = { body, etag };
|
|
153
|
+
statusCache.set(key, entry);
|
|
154
|
+
return entry;
|
|
155
|
+
};
|
|
156
|
+
const handleRequest = async (req, res) => {
|
|
157
|
+
baseHeaders(res, instanceId);
|
|
158
|
+
const requestUrl = new URL(req.url ?? "/", "http://localhost");
|
|
159
|
+
const path = requestUrl.pathname;
|
|
160
|
+
if (req.method === "POST" && path === "/api/config/agent") {
|
|
161
|
+
if (!isLoopbackHost(options.host)) {
|
|
162
|
+
respondJson(res, 403, {
|
|
163
|
+
error: "Configuration writes are disabled when the monitor is not bound to loopback."
|
|
164
|
+
});
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
const body = await readJsonBody(req);
|
|
168
|
+
if (body === null || typeof body !== "object" || Array.isArray(body)) {
|
|
169
|
+
respondJson(res, 400, { error: "Expected a JSON object." });
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
const input = body;
|
|
173
|
+
if (typeof input.provider !== "string" || !isAgentProviderId(input.provider)) {
|
|
174
|
+
respondJson(res, 400, { error: "Invalid provider." });
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
if (input.phase !== void 0 && (typeof input.phase !== "string" || !isAgentPhase(input.phase))) {
|
|
178
|
+
respondJson(res, 400, { error: "Invalid phase." });
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (input.model !== void 0 && typeof input.model !== "string") {
|
|
182
|
+
respondJson(res, 400, { error: "Invalid model." });
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
const file = await writeAgentPreference({
|
|
186
|
+
target: "global",
|
|
187
|
+
provider: input.provider,
|
|
188
|
+
...input.model ? { model: input.model } : {},
|
|
189
|
+
...input.phase ? { phase: input.phase } : {}
|
|
190
|
+
});
|
|
191
|
+
respondJson(res, 200, { ok: true, file, appliesTo: "future executions" });
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
195
|
+
respondJson(res, 404, { error: "Not found" });
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (path === "/api/status" || path === "/status.json") {
|
|
199
|
+
const sessionId = requestUrl.searchParams.get("session");
|
|
200
|
+
let snapshot;
|
|
201
|
+
if (sessionId !== null) {
|
|
202
|
+
snapshot = source.get(sessionId);
|
|
203
|
+
if (snapshot === void 0) {
|
|
204
|
+
respondJson(res, 404, { error: `No active session with id '${sessionId}'.` });
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
} else {
|
|
208
|
+
const active = source.list();
|
|
209
|
+
if (active.length === 0) {
|
|
210
|
+
respondJson(res, 404, {
|
|
211
|
+
error: "No active session. Pass ?session=<id> \u2014 see GET /api/sessions."
|
|
212
|
+
});
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
if (active.length > 1) {
|
|
216
|
+
respondJson(res, 409, {
|
|
217
|
+
error: "Multiple active sessions; specify ?session=<id>.",
|
|
218
|
+
sessions: active.map((entry) => entry.sessionId)
|
|
219
|
+
});
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
snapshot = active[0];
|
|
223
|
+
}
|
|
224
|
+
const { body, etag } = statusPayload(snapshot);
|
|
225
|
+
res.setHeader("ETag", etag);
|
|
226
|
+
if (req.headers["if-none-match"] === etag) {
|
|
227
|
+
res.statusCode = 304;
|
|
228
|
+
res.end();
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
respond(res, 200, JSON_TYPE, body);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
if (path === "/api/sessions") {
|
|
235
|
+
respondJson(
|
|
236
|
+
res,
|
|
237
|
+
200,
|
|
238
|
+
source.list().map((snapshot) => ({
|
|
239
|
+
sessionId: snapshot.sessionId,
|
|
240
|
+
issueNumber: snapshot.issue.number,
|
|
241
|
+
issueTitle: snapshot.issue.title,
|
|
242
|
+
issueDescription: truncateSessionDescription(snapshot.issue.description),
|
|
243
|
+
repositoryName: snapshot.repository.name,
|
|
244
|
+
currentPhase: snapshot.currentPhase,
|
|
245
|
+
progressPercent: snapshot.progress.percent,
|
|
246
|
+
elapsedSeconds: snapshot.elapsedSeconds,
|
|
247
|
+
status: snapshot.status,
|
|
248
|
+
startedAt: snapshot.startedAt,
|
|
249
|
+
updatedAt: snapshot.updatedAt,
|
|
250
|
+
// Resilience fields, for a card that has to answer "is this still
|
|
251
|
+
// moving" during a six-hour run. `updatedAt` is already the last
|
|
252
|
+
// activity; these two say how hard the run has had to work for it.
|
|
253
|
+
retries: snapshot.execution.retries,
|
|
254
|
+
correctionCycle: snapshot.execution.correctionCycle,
|
|
255
|
+
attempt: snapshot.resilience.attempt,
|
|
256
|
+
provider: snapshot.resilience.provider,
|
|
257
|
+
lastFailureKind: snapshot.resilience.lastFailureKind,
|
|
258
|
+
cooldownUntil: snapshot.resilience.cooldownUntil,
|
|
259
|
+
lastActivityAt: snapshot.resilience.lastActivityAt,
|
|
260
|
+
statusUrl: `/api/status?session=${encodeURIComponent(snapshot.sessionId ?? "")}`,
|
|
261
|
+
eventsUrl: `/api/events?session=${encodeURIComponent(snapshot.sessionId ?? "")}`
|
|
262
|
+
}))
|
|
263
|
+
);
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
if (path === "/api/events") {
|
|
267
|
+
const sessionId = requestUrl.searchParams.get("session");
|
|
268
|
+
if (sessionId === null) {
|
|
269
|
+
respondJson(res, 400, { error: "Pass ?session=<id>." });
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
const entries = await source.events(sessionId);
|
|
273
|
+
if (entries === void 0) {
|
|
274
|
+
respondJson(res, 404, { error: `No active session with id '${sessionId}'.` });
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
respondJson(res, 200, entries);
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
if (path === "/api/diagnostics") {
|
|
281
|
+
const sessionId = requestUrl.searchParams.get("session") ?? void 0;
|
|
282
|
+
respondJson(res, 200, await readDiagnostics({ sessionId, limit: 500 }));
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
if (path === "/api/config") {
|
|
286
|
+
const sessionId = requestUrl.searchParams.get("session");
|
|
287
|
+
const snapshot = sessionId === null ? void 0 : source.get(sessionId);
|
|
288
|
+
respondJson(res, 200, {
|
|
289
|
+
effective: snapshot?.configuration ?? null,
|
|
290
|
+
capturedForSession: snapshot?.sessionId ?? null,
|
|
291
|
+
writable: isLoopbackHost(options.host),
|
|
292
|
+
writeScope: "global preferences for future executions"
|
|
293
|
+
});
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
if (path === "/api/health") {
|
|
297
|
+
respondJson(res, 200, {
|
|
298
|
+
ok: true,
|
|
299
|
+
pid: process.pid,
|
|
300
|
+
instanceId,
|
|
301
|
+
startedAt: new Date(startedAtMs).toISOString(),
|
|
302
|
+
uptime: Math.round((Date.now() - startedAtMs) / 1e3),
|
|
303
|
+
version,
|
|
304
|
+
refreshSeconds: options.refreshSeconds ?? 5,
|
|
305
|
+
capabilities: isLoopbackHost(options.host) ? ["config:agent:write"] : []
|
|
306
|
+
});
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
const asset = assets.get(path);
|
|
310
|
+
if (asset) {
|
|
311
|
+
respond(res, 200, asset.contentType, asset.body);
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
respondJson(res, 404, { error: "Not found" });
|
|
315
|
+
};
|
|
316
|
+
const server = createServer((req, res) => {
|
|
317
|
+
void handleRequest(req, res).catch((err) => {
|
|
318
|
+
try {
|
|
319
|
+
respondJson(res, 500, { error: err instanceof Error ? err.message : String(err) });
|
|
320
|
+
} catch {
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
const listening = await new Promise((resolve) => {
|
|
325
|
+
const onError = (err) => {
|
|
326
|
+
if (err.code === "EADDRINUSE") {
|
|
327
|
+
warn(
|
|
328
|
+
`Web monitor port ${options.port} is already in use. Continuing without the web server.`
|
|
329
|
+
);
|
|
330
|
+
} else {
|
|
331
|
+
warn(`Web monitor failed to start (${err.message}). Continuing without the web server.`);
|
|
332
|
+
}
|
|
333
|
+
resolve(false);
|
|
334
|
+
};
|
|
335
|
+
server.once("error", onError);
|
|
336
|
+
server.listen(options.port, options.host, () => {
|
|
337
|
+
server.removeListener("error", onError);
|
|
338
|
+
resolve(true);
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
if (!listening) return null;
|
|
342
|
+
if (options.unref !== false) {
|
|
343
|
+
server.unref();
|
|
344
|
+
}
|
|
345
|
+
server.on("error", (err) => {
|
|
346
|
+
warn(`Web monitor server error: ${err.message}`);
|
|
347
|
+
});
|
|
348
|
+
const address = server.address();
|
|
349
|
+
const port = address.port;
|
|
350
|
+
const displayHost = options.host === "0.0.0.0" ? "localhost" : options.host;
|
|
351
|
+
const url = `http://${displayHost}:${port}`;
|
|
352
|
+
if (options.host === "0.0.0.0") {
|
|
353
|
+
warn("Web monitor bound to 0.0.0.0: anyone on your local network can view the session state.");
|
|
354
|
+
}
|
|
355
|
+
info(`Web monitor running at ${chalk.bold.cyan(url)}`);
|
|
356
|
+
let closed = false;
|
|
357
|
+
let closePromise = null;
|
|
358
|
+
const doClose = () => new Promise((resolve) => {
|
|
359
|
+
process.removeListener("SIGINT", onSignal);
|
|
360
|
+
process.removeListener("SIGTERM", onSignal);
|
|
361
|
+
server.close(() => resolve());
|
|
362
|
+
server.closeAllConnections();
|
|
363
|
+
});
|
|
364
|
+
const close = () => {
|
|
365
|
+
if (closed) return closePromise ?? Promise.resolve();
|
|
366
|
+
closed = true;
|
|
367
|
+
closePromise = doClose();
|
|
368
|
+
return closePromise;
|
|
369
|
+
};
|
|
370
|
+
const handle = { server, host: options.host, port, url, instanceId, close };
|
|
371
|
+
const onSignal = (signal) => {
|
|
372
|
+
void handle.close().finally(() => {
|
|
373
|
+
process.kill(process.pid, signal);
|
|
374
|
+
});
|
|
375
|
+
};
|
|
376
|
+
process.once("SIGINT", onSignal);
|
|
377
|
+
process.once("SIGTERM", onSignal);
|
|
378
|
+
return handle;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// src/web/lock.ts
|
|
382
|
+
var WEB_LOCK_FILENAME = "web.lock";
|
|
383
|
+
var WEB_RESTART_LOCK_FILENAME = "web.restart.lock";
|
|
384
|
+
var HEALTH_PROBE_TIMEOUT_MS = 1e3;
|
|
385
|
+
var LOCK_CLAIM_RETRIES = 5;
|
|
386
|
+
var LOCK_CLAIM_RETRY_DELAY_MS = 100;
|
|
387
|
+
function getWebLockFile(options = {}) {
|
|
388
|
+
return join2(getGlobalRoot(options), WEB_LOCK_FILENAME);
|
|
389
|
+
}
|
|
390
|
+
function getWebRestartLockFile(options = {}) {
|
|
391
|
+
return join2(getGlobalRoot(options), WEB_RESTART_LOCK_FILENAME);
|
|
392
|
+
}
|
|
393
|
+
function sleep(ms) {
|
|
394
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
395
|
+
}
|
|
396
|
+
function probeUrl(host, port) {
|
|
397
|
+
const probeHost = host === "0.0.0.0" ? "127.0.0.1" : host;
|
|
398
|
+
return `http://${probeHost}:${port}/api/health`;
|
|
399
|
+
}
|
|
400
|
+
async function probeHealth(host, port) {
|
|
401
|
+
try {
|
|
402
|
+
const res = await fetch(probeUrl(host, port), {
|
|
403
|
+
signal: AbortSignal.timeout(HEALTH_PROBE_TIMEOUT_MS)
|
|
404
|
+
});
|
|
405
|
+
if (!res.ok) return null;
|
|
406
|
+
const body = await res.json();
|
|
407
|
+
if (body.ok !== true) return null;
|
|
408
|
+
return {
|
|
409
|
+
ok: true,
|
|
410
|
+
...typeof body.pid === "number" && Number.isInteger(body.pid) && body.pid > 0 ? { pid: body.pid } : {},
|
|
411
|
+
...typeof body.instanceId === "string" && body.instanceId !== "" ? { instanceId: body.instanceId } : {},
|
|
412
|
+
...typeof body.startedAt === "string" && body.startedAt !== "" ? { startedAt: body.startedAt } : {},
|
|
413
|
+
...typeof body.uptime === "number" && Number.isFinite(body.uptime) ? { uptime: body.uptime } : {},
|
|
414
|
+
...typeof body.version === "string" && body.version !== "" ? { version: body.version } : {}
|
|
415
|
+
};
|
|
416
|
+
} catch {
|
|
417
|
+
return null;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
function sameLock(a, b) {
|
|
421
|
+
return a !== null && a.pid === b.pid && a.port === b.port && a.host === b.host && a.startedAt === b.startedAt && a.instanceId === b.instanceId;
|
|
422
|
+
}
|
|
423
|
+
async function removeWebLockIfOwned(lockFile, expected) {
|
|
424
|
+
if (sameLock(await readWebLock(lockFile), expected)) {
|
|
425
|
+
await removeWebLock(lockFile);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
async function inspectProcessCommand(pid) {
|
|
429
|
+
const invocation = process.platform === "win32" ? {
|
|
430
|
+
command: "powershell.exe",
|
|
431
|
+
args: [
|
|
432
|
+
"-NoProfile",
|
|
433
|
+
"-NonInteractive",
|
|
434
|
+
"-Command",
|
|
435
|
+
`(Get-CimInstance Win32_Process -Filter "ProcessId = ${pid}").CommandLine`
|
|
436
|
+
]
|
|
437
|
+
} : { command: "ps", args: ["-p", String(pid), "-o", "command="] };
|
|
438
|
+
try {
|
|
439
|
+
const result = await execa(invocation.command, invocation.args, { reject: false });
|
|
440
|
+
const command = result.stdout.trim();
|
|
441
|
+
return result.exitCode === 0 && command !== "" ? command : null;
|
|
442
|
+
} catch {
|
|
443
|
+
return null;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
async function findListeningProcess(port) {
|
|
447
|
+
try {
|
|
448
|
+
let pids;
|
|
449
|
+
if (process.platform === "win32") {
|
|
450
|
+
const result = await execa(
|
|
451
|
+
"powershell.exe",
|
|
452
|
+
[
|
|
453
|
+
"-NoProfile",
|
|
454
|
+
"-NonInteractive",
|
|
455
|
+
"-Command",
|
|
456
|
+
`(Get-NetTCPConnection -State Listen -LocalPort ${port} -ErrorAction SilentlyContinue | Select-Object -ExpandProperty OwningProcess -Unique) -join [Environment]::NewLine`
|
|
457
|
+
],
|
|
458
|
+
{ reject: false }
|
|
459
|
+
);
|
|
460
|
+
pids = result.stdout.split(/\s+/).map(Number).filter((pid2) => Number.isInteger(pid2) && pid2 > 0);
|
|
461
|
+
} else {
|
|
462
|
+
const result = await execa("lsof", ["-nP", "-t", `-iTCP:${port}`, "-sTCP:LISTEN"], {
|
|
463
|
+
reject: false
|
|
464
|
+
});
|
|
465
|
+
pids = result.stdout.split(/\s+/).map(Number).filter((pid2) => Number.isInteger(pid2) && pid2 > 0);
|
|
466
|
+
}
|
|
467
|
+
const unique = [...new Set(pids)];
|
|
468
|
+
if (unique.length !== 1) return null;
|
|
469
|
+
const pid = unique[0];
|
|
470
|
+
const command = await inspectProcessCommand(pid);
|
|
471
|
+
return command === null ? null : { pid, command };
|
|
472
|
+
} catch {
|
|
473
|
+
return null;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
function isIssueFlowWebServe(owner, port) {
|
|
477
|
+
const escapedPort = String(port).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
478
|
+
return /(?:^|\s)web\s+serve(?:\s|$)/.test(owner.command) && new RegExp(`(?:^|\\s)--port(?:=|\\s+)${escapedPort}(?:\\s|$)`).test(owner.command) && /issue-flow|(?:^|[/\\])dist[/\\]cli\.js/.test(owner.command);
|
|
479
|
+
}
|
|
480
|
+
async function readWebLock(lockFile) {
|
|
481
|
+
let raw;
|
|
482
|
+
try {
|
|
483
|
+
raw = await readFile2(lockFile, "utf-8");
|
|
484
|
+
} catch {
|
|
485
|
+
return null;
|
|
486
|
+
}
|
|
487
|
+
try {
|
|
488
|
+
const result = webLockSchema.safeParse(JSON.parse(raw));
|
|
489
|
+
return result.success ? result.data : null;
|
|
490
|
+
} catch {
|
|
491
|
+
return null;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
async function removeWebLock(lockFile) {
|
|
495
|
+
try {
|
|
496
|
+
await rm(lockFile, { force: true });
|
|
497
|
+
} catch {
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
async function detectActiveInstance(lockFile) {
|
|
501
|
+
const lock = await readWebLock(lockFile);
|
|
502
|
+
if (lock === null) return null;
|
|
503
|
+
const health = isProcessAlive(lock.pid) ? await probeHealth(lock.host, lock.port) : null;
|
|
504
|
+
if (health !== null && (lock.instanceId === void 0 || health.instanceId === lock.instanceId)) {
|
|
505
|
+
return lock;
|
|
506
|
+
}
|
|
507
|
+
await removeWebLockIfOwned(lockFile, lock);
|
|
508
|
+
return null;
|
|
509
|
+
}
|
|
510
|
+
async function claimWebLock(lockFile, lock) {
|
|
511
|
+
try {
|
|
512
|
+
await writeFile(lockFile, JSON.stringify(lock, null, 2), { encoding: "utf-8", flag: "wx" });
|
|
513
|
+
return true;
|
|
514
|
+
} catch (err) {
|
|
515
|
+
if (err.code === "EEXIST") return false;
|
|
516
|
+
throw err;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
async function readRestartLock(file) {
|
|
520
|
+
try {
|
|
521
|
+
const value = JSON.parse(await readFile2(file, "utf-8"));
|
|
522
|
+
return typeof value.pid === "number" && Number.isInteger(value.pid) && value.pid > 0 && typeof value.operationId === "string" && value.operationId !== "" && typeof value.startedAt === "string" && value.startedAt !== "" ? { pid: value.pid, operationId: value.operationId, startedAt: value.startedAt } : null;
|
|
523
|
+
} catch {
|
|
524
|
+
return null;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
async function removeRestartLockIfOwned(file, expected) {
|
|
528
|
+
const current = await readRestartLock(file);
|
|
529
|
+
if (current?.operationId === expected.operationId) {
|
|
530
|
+
await rm(file, { force: true }).catch(() => {
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
async function waitForRestart(file, timeoutMs) {
|
|
535
|
+
const deadline = Date.now() + timeoutMs;
|
|
536
|
+
while (Date.now() < deadline) {
|
|
537
|
+
const lock = await readRestartLock(file);
|
|
538
|
+
if (lock === null) {
|
|
539
|
+
return true;
|
|
540
|
+
}
|
|
541
|
+
if (!isProcessAlive(lock.pid)) {
|
|
542
|
+
await removeRestartLockIfOwned(file, lock);
|
|
543
|
+
return true;
|
|
544
|
+
}
|
|
545
|
+
await sleep(LOCK_CLAIM_RETRY_DELAY_MS);
|
|
546
|
+
}
|
|
547
|
+
return false;
|
|
548
|
+
}
|
|
549
|
+
async function acquireRestartLock(file, timeoutMs) {
|
|
550
|
+
const deadline = Date.now() + timeoutMs;
|
|
551
|
+
let waited = false;
|
|
552
|
+
while (Date.now() < deadline) {
|
|
553
|
+
const lock = {
|
|
554
|
+
pid: process.pid,
|
|
555
|
+
operationId: randomUUID2(),
|
|
556
|
+
startedAt: isoNow()
|
|
557
|
+
};
|
|
558
|
+
try {
|
|
559
|
+
await writeFile(file, JSON.stringify(lock, null, 2), { encoding: "utf-8", flag: "wx" });
|
|
560
|
+
return {
|
|
561
|
+
lock,
|
|
562
|
+
waited,
|
|
563
|
+
release: () => removeRestartLockIfOwned(file, lock)
|
|
564
|
+
};
|
|
565
|
+
} catch (err) {
|
|
566
|
+
if (err.code !== "EEXIST") return null;
|
|
567
|
+
}
|
|
568
|
+
waited = true;
|
|
569
|
+
const existing = await readRestartLock(file);
|
|
570
|
+
if (existing === null || !isProcessAlive(existing.pid)) {
|
|
571
|
+
if (existing === null) await rm(file, { force: true }).catch(() => {
|
|
572
|
+
});
|
|
573
|
+
else await removeRestartLockIfOwned(file, existing);
|
|
574
|
+
continue;
|
|
575
|
+
}
|
|
576
|
+
await sleep(LOCK_CLAIM_RETRY_DELAY_MS);
|
|
577
|
+
}
|
|
578
|
+
return null;
|
|
579
|
+
}
|
|
580
|
+
function reusedHandle(lock) {
|
|
581
|
+
const displayHost = lock.host === "0.0.0.0" ? "localhost" : lock.host;
|
|
582
|
+
return {
|
|
583
|
+
host: lock.host,
|
|
584
|
+
port: lock.port,
|
|
585
|
+
url: `http://${displayHost}:${lock.port}`,
|
|
586
|
+
instanceId: lock.instanceId ?? `legacy-${lock.pid}-${lock.startedAt}`,
|
|
587
|
+
close: async () => {
|
|
588
|
+
}
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
async function ensureSingleWebServer(options, lockOptions = {}) {
|
|
592
|
+
const warn = options.warn ?? printWarning;
|
|
593
|
+
const info = options.info ?? printInfo;
|
|
594
|
+
const lockFile = getWebLockFile(lockOptions);
|
|
595
|
+
for (let attempt = 0; attempt <= LOCK_CLAIM_RETRIES; attempt++) {
|
|
596
|
+
const active = await detectActiveInstance(lockFile);
|
|
597
|
+
if (active !== null) {
|
|
598
|
+
info(`Reusing existing web monitor at ${reusedHandle(active).url}`);
|
|
599
|
+
return reusedHandle(active);
|
|
600
|
+
}
|
|
601
|
+
const handle = await startWebServer(options);
|
|
602
|
+
if (handle === null) {
|
|
603
|
+
await sleep(LOCK_CLAIM_RETRY_DELAY_MS);
|
|
604
|
+
continue;
|
|
605
|
+
}
|
|
606
|
+
let claimed;
|
|
607
|
+
try {
|
|
608
|
+
claimed = await claimWebLock(lockFile, {
|
|
609
|
+
pid: process.pid,
|
|
610
|
+
port: handle.port,
|
|
611
|
+
host: handle.host,
|
|
612
|
+
startedAt: isoNow(),
|
|
613
|
+
instanceId: handle.instanceId
|
|
614
|
+
});
|
|
615
|
+
} catch (err) {
|
|
616
|
+
await handle.close();
|
|
617
|
+
throw err;
|
|
618
|
+
}
|
|
619
|
+
if (!claimed) {
|
|
620
|
+
await handle.close();
|
|
621
|
+
await sleep(LOCK_CLAIM_RETRY_DELAY_MS);
|
|
622
|
+
continue;
|
|
623
|
+
}
|
|
624
|
+
const originalClose = handle.close;
|
|
625
|
+
handle.close = async () => {
|
|
626
|
+
await originalClose();
|
|
627
|
+
await removeWebLock(lockFile);
|
|
628
|
+
};
|
|
629
|
+
return handle;
|
|
630
|
+
}
|
|
631
|
+
warn("Could not determine the web monitor lock ownership. Continuing without the web server.");
|
|
632
|
+
return null;
|
|
633
|
+
}
|
|
634
|
+
var DETACHED_CLAIM_TIMEOUT_MS = 5e3;
|
|
635
|
+
var DETACHED_CLAIM_POLL_MS = 150;
|
|
636
|
+
function inferredStartedAt(health) {
|
|
637
|
+
if (health.startedAt !== void 0) return health.startedAt;
|
|
638
|
+
if (health.uptime !== void 0) {
|
|
639
|
+
return new Date(Date.now() - Math.max(0, health.uptime) * 1e3).toISOString();
|
|
640
|
+
}
|
|
641
|
+
return isoNow();
|
|
642
|
+
}
|
|
643
|
+
async function discoverManagedInstance(lockFile, options, monitorOptions) {
|
|
644
|
+
const existingLock = await readWebLock(lockFile);
|
|
645
|
+
if (existingLock !== null) {
|
|
646
|
+
const health2 = isProcessAlive(existingLock.pid) ? await probeHealth(existingLock.host, existingLock.port) : null;
|
|
647
|
+
if (health2 !== null && (existingLock.instanceId === void 0 || health2.instanceId === existingLock.instanceId)) {
|
|
648
|
+
return { instance: { lock: existingLock, health: health2, source: "lock" }, unownedMonitor: false };
|
|
649
|
+
}
|
|
650
|
+
await removeWebLockIfOwned(lockFile, existingLock);
|
|
651
|
+
}
|
|
652
|
+
const health = await probeHealth(options.host, options.port);
|
|
653
|
+
if (health === null) return { instance: null, unownedMonitor: false };
|
|
654
|
+
const inspect = monitorOptions.inspectProcess ?? inspectProcessCommand;
|
|
655
|
+
let owner = null;
|
|
656
|
+
if (health.pid !== void 0) {
|
|
657
|
+
const command = await inspect(health.pid);
|
|
658
|
+
if (command !== null) owner = { pid: health.pid, command };
|
|
659
|
+
}
|
|
660
|
+
owner ??= await (monitorOptions.findPortOwner ?? findListeningProcess)(options.port);
|
|
661
|
+
if (owner === null || !isIssueFlowWebServe(owner, options.port)) {
|
|
662
|
+
return { instance: null, unownedMonitor: true };
|
|
663
|
+
}
|
|
664
|
+
const recovered = {
|
|
665
|
+
pid: owner.pid,
|
|
666
|
+
port: options.port,
|
|
667
|
+
host: options.host,
|
|
668
|
+
startedAt: inferredStartedAt(health),
|
|
669
|
+
...health.instanceId === void 0 ? {} : { instanceId: health.instanceId }
|
|
670
|
+
};
|
|
671
|
+
const claimed = await claimWebLock(lockFile, recovered).catch(() => false);
|
|
672
|
+
if (!claimed) {
|
|
673
|
+
const winner = await readWebLock(lockFile);
|
|
674
|
+
if (winner !== null) {
|
|
675
|
+
const winnerHealth = await probeHealth(winner.host, winner.port);
|
|
676
|
+
if (winnerHealth !== null) {
|
|
677
|
+
return {
|
|
678
|
+
instance: { lock: winner, health: winnerHealth, source: "lock" },
|
|
679
|
+
unownedMonitor: false
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
return { instance: { lock: recovered, health, source: "port" }, unownedMonitor: false };
|
|
685
|
+
}
|
|
686
|
+
async function stopManagedInstance(instance, lockFile, options, monitorOptions) {
|
|
687
|
+
const info = options.info ?? printInfo;
|
|
688
|
+
const warn = options.warn ?? printWarning;
|
|
689
|
+
const version = instance.health.version === void 0 ? "" : `, version ${instance.health.version}`;
|
|
690
|
+
info(`Web monitor: stopping previous instance (pid ${instance.lock.pid}${version}).`);
|
|
691
|
+
try {
|
|
692
|
+
(monitorOptions.kill ?? process.kill)(instance.lock.pid, "SIGTERM");
|
|
693
|
+
} catch (err) {
|
|
694
|
+
if (err.code === "ESRCH") {
|
|
695
|
+
await removeWebLockIfOwned(lockFile, instance.lock);
|
|
696
|
+
info(`Web monitor: previous pid ${instance.lock.pid} was already gone; stale lock removed.`);
|
|
697
|
+
return true;
|
|
698
|
+
}
|
|
699
|
+
warn(
|
|
700
|
+
`Web monitor: failed to stop pid ${instance.lock.pid} (${err instanceof Error ? err.message : String(err)}). Continuing the pipeline without restarting it.`
|
|
701
|
+
);
|
|
702
|
+
return false;
|
|
703
|
+
}
|
|
704
|
+
const isAlive = monitorOptions.isAlive ?? isProcessAlive;
|
|
705
|
+
const deadline = Date.now() + (monitorOptions.shutdownTimeoutMs ?? 5e3);
|
|
706
|
+
const pollMs = monitorOptions.claimPollIntervalMs ?? DETACHED_CLAIM_POLL_MS;
|
|
707
|
+
while (Date.now() < deadline) {
|
|
708
|
+
const alive = isAlive(instance.lock.pid);
|
|
709
|
+
const health = alive ? await probeHealth(instance.lock.host, instance.lock.port) : null;
|
|
710
|
+
if (!alive || health === null) {
|
|
711
|
+
await removeWebLockIfOwned(lockFile, instance.lock);
|
|
712
|
+
info(`Web monitor: previous instance stopped (pid ${instance.lock.pid}).`);
|
|
713
|
+
info(
|
|
714
|
+
"Web monitor: process-local static asset and ETag caches invalidated; no on-disk web build cache exists."
|
|
715
|
+
);
|
|
716
|
+
return true;
|
|
717
|
+
}
|
|
718
|
+
await sleep(pollMs);
|
|
719
|
+
}
|
|
720
|
+
warn(
|
|
721
|
+
`Web monitor: pid ${instance.lock.pid} did not confirm shutdown in time. Continuing the pipeline without starting a competing instance.`
|
|
722
|
+
);
|
|
723
|
+
return false;
|
|
724
|
+
}
|
|
725
|
+
async function stopWebMonitor(options, monitorOptions = {}) {
|
|
726
|
+
const info = options.info ?? printInfo;
|
|
727
|
+
const warn = options.warn ?? printWarning;
|
|
728
|
+
let lockFile;
|
|
729
|
+
let restartLockFile;
|
|
730
|
+
try {
|
|
731
|
+
await mkdir(getGlobalRoot(monitorOptions), { recursive: true });
|
|
732
|
+
lockFile = getWebLockFile(monitorOptions);
|
|
733
|
+
restartLockFile = getWebRestartLockFile(monitorOptions);
|
|
734
|
+
} catch (err) {
|
|
735
|
+
warn(
|
|
736
|
+
`Web monitor: global storage unavailable (${err instanceof Error ? err.message : String(err)}).`
|
|
737
|
+
);
|
|
738
|
+
return "failed";
|
|
739
|
+
}
|
|
740
|
+
const lease = await acquireRestartLock(
|
|
741
|
+
restartLockFile,
|
|
742
|
+
monitorOptions.restartLockTimeoutMs ?? 5e3
|
|
743
|
+
);
|
|
744
|
+
if (lease === null) {
|
|
745
|
+
warn("Web monitor: could not acquire the maintenance lock; no process was stopped.");
|
|
746
|
+
return "failed";
|
|
747
|
+
}
|
|
748
|
+
try {
|
|
749
|
+
const stale = await readWebLock(lockFile);
|
|
750
|
+
const discovered = await discoverManagedInstance(lockFile, options, monitorOptions);
|
|
751
|
+
if (discovered.instance === null) {
|
|
752
|
+
if (stale !== null && !isProcessAlive(stale.pid)) {
|
|
753
|
+
info(`Web monitor: stale lock for dead pid ${stale.pid} removed.`);
|
|
754
|
+
}
|
|
755
|
+
if (discovered.unownedMonitor) {
|
|
756
|
+
warn(
|
|
757
|
+
`Web monitor: port ${options.port} looks like an Issue Flow monitor, but its process ownership could not be verified; it was left untouched.`
|
|
758
|
+
);
|
|
759
|
+
return "unowned";
|
|
760
|
+
}
|
|
761
|
+
info("No web monitor is currently running.");
|
|
762
|
+
return "not-running";
|
|
763
|
+
}
|
|
764
|
+
return await stopManagedInstance(discovered.instance, lockFile, options, monitorOptions) ? "stopped" : "failed";
|
|
765
|
+
} finally {
|
|
766
|
+
await lease.release();
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
async function ensureWebMonitor(options, monitorOptions = {}) {
|
|
770
|
+
const warn = options.warn ?? printWarning;
|
|
771
|
+
const info = options.info ?? printInfo;
|
|
772
|
+
let lockFile;
|
|
773
|
+
let restartLockFile;
|
|
774
|
+
try {
|
|
775
|
+
await mkdir(getGlobalRoot(monitorOptions), { recursive: true });
|
|
776
|
+
lockFile = getWebLockFile(monitorOptions);
|
|
777
|
+
restartLockFile = getWebRestartLockFile(monitorOptions);
|
|
778
|
+
} catch (err) {
|
|
779
|
+
warn(
|
|
780
|
+
`Web monitor: global storage unavailable (${err instanceof Error ? err.message : String(err)}). Falling back to legacy in-process mode.`
|
|
781
|
+
);
|
|
782
|
+
return startWebServer(options);
|
|
783
|
+
}
|
|
784
|
+
const coordinationTimeout = monitorOptions.restartLockTimeoutMs ?? 5e3;
|
|
785
|
+
const restartRequestedAt = Date.now();
|
|
786
|
+
const restartLease = monitorOptions.restart ? await acquireRestartLock(restartLockFile, coordinationTimeout) : null;
|
|
787
|
+
if (monitorOptions.restart && restartLease === null) {
|
|
788
|
+
warn(
|
|
789
|
+
"Web monitor: could not acquire the restart lock. Continuing the pipeline without changing the server."
|
|
790
|
+
);
|
|
791
|
+
return null;
|
|
792
|
+
}
|
|
793
|
+
if (!monitorOptions.restart && !await waitForRestart(restartLockFile, coordinationTimeout)) {
|
|
794
|
+
warn(
|
|
795
|
+
"Web monitor: another restart is still in progress. Continuing the pipeline without starting a competing instance."
|
|
796
|
+
);
|
|
797
|
+
return null;
|
|
798
|
+
}
|
|
799
|
+
try {
|
|
800
|
+
const discovered = await discoverManagedInstance(lockFile, options, monitorOptions);
|
|
801
|
+
const active = discovered.instance;
|
|
802
|
+
if (monitorOptions.restart && restartLease?.waited === true && active !== null && Date.parse(active.lock.startedAt) >= restartRequestedAt) {
|
|
803
|
+
info(
|
|
804
|
+
`Web monitor: a concurrent restart already started pid ${active.lock.pid}; reusing ${reusedHandle(active.lock).url}`
|
|
805
|
+
);
|
|
806
|
+
return reusedHandle(active.lock);
|
|
807
|
+
}
|
|
808
|
+
if (active !== null && !monitorOptions.restart) {
|
|
809
|
+
if (active.source === "port") {
|
|
810
|
+
info(
|
|
811
|
+
`Recovered orphaned web monitor pid ${active.lock.pid} on port ${active.lock.port} and restored web.lock.`
|
|
812
|
+
);
|
|
813
|
+
}
|
|
814
|
+
info(`Reusing existing web monitor at ${reusedHandle(active.lock).url}`);
|
|
815
|
+
return reusedHandle(active.lock);
|
|
816
|
+
}
|
|
817
|
+
if (active !== null && monitorOptions.restart) {
|
|
818
|
+
if (!await stopManagedInstance(active, lockFile, options, monitorOptions)) return null;
|
|
819
|
+
} else if (discovered.unownedMonitor) {
|
|
820
|
+
warn(
|
|
821
|
+
`Web monitor: port ${options.port} answers an Issue Flow health check, but its owning process could not be verified. It was left untouched and no competing server was started.`
|
|
822
|
+
);
|
|
823
|
+
return null;
|
|
824
|
+
} else if (monitorOptions.restart) {
|
|
825
|
+
info("Web monitor: no previous managed instance was found; starting a fresh one.");
|
|
826
|
+
info("Web monitor: no on-disk web build cache exists; no files were removed.");
|
|
827
|
+
}
|
|
828
|
+
const execPath = monitorOptions.execPath ?? process.execPath;
|
|
829
|
+
const entryScript = monitorOptions.entryScript ?? process.argv[1];
|
|
830
|
+
if (entryScript === void 0) {
|
|
831
|
+
warn("Web monitor: could not determine the CLI entry point to spawn a detached server.");
|
|
832
|
+
return null;
|
|
833
|
+
}
|
|
834
|
+
const args = [
|
|
835
|
+
entryScript,
|
|
836
|
+
"web",
|
|
837
|
+
"serve",
|
|
838
|
+
"--port",
|
|
839
|
+
String(options.port),
|
|
840
|
+
"--host",
|
|
841
|
+
options.host
|
|
842
|
+
];
|
|
843
|
+
if (options.refreshSeconds !== void 0) {
|
|
844
|
+
args.push("--refresh", String(options.refreshSeconds));
|
|
845
|
+
}
|
|
846
|
+
const spawnFn = monitorOptions.spawn ?? nodeSpawn;
|
|
847
|
+
try {
|
|
848
|
+
const child = spawnFn(execPath, args, { detached: true, stdio: "ignore" });
|
|
849
|
+
child.unref();
|
|
850
|
+
} catch (err) {
|
|
851
|
+
warn(
|
|
852
|
+
`Web monitor: failed to start the detached server (${err instanceof Error ? err.message : String(err)}). Continuing without the web server.`
|
|
853
|
+
);
|
|
854
|
+
return null;
|
|
855
|
+
}
|
|
856
|
+
const timeoutMs = monitorOptions.claimTimeoutMs ?? DETACHED_CLAIM_TIMEOUT_MS;
|
|
857
|
+
const pollMs = monitorOptions.claimPollIntervalMs ?? DETACHED_CLAIM_POLL_MS;
|
|
858
|
+
const deadline = Date.now() + timeoutMs;
|
|
859
|
+
while (Date.now() < deadline) {
|
|
860
|
+
const claimed = await detectActiveInstance(lockFile);
|
|
861
|
+
if (claimed !== null) {
|
|
862
|
+
const handle = reusedHandle(claimed);
|
|
863
|
+
const health = await probeHealth(claimed.host, claimed.port);
|
|
864
|
+
if (monitorOptions.restart) {
|
|
865
|
+
const version = health?.version === void 0 ? "" : `, version ${health.version}`;
|
|
866
|
+
info(
|
|
867
|
+
`Web monitor: new instance started (pid ${claimed.pid}${version}) at ${chalk2.bold.cyan(handle.url)}`
|
|
868
|
+
);
|
|
869
|
+
} else {
|
|
870
|
+
info(`Web monitor running at ${chalk2.bold.cyan(handle.url)}`);
|
|
871
|
+
}
|
|
872
|
+
return handle;
|
|
873
|
+
}
|
|
874
|
+
await sleep(pollMs);
|
|
875
|
+
}
|
|
876
|
+
warn(
|
|
877
|
+
"Web monitor: the detached server did not start in time. Continuing without the web server."
|
|
878
|
+
);
|
|
879
|
+
return null;
|
|
880
|
+
} finally {
|
|
881
|
+
await restartLease?.release();
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
export {
|
|
886
|
+
ensureSingleWebServer,
|
|
887
|
+
stopWebMonitor,
|
|
888
|
+
ensureWebMonitor
|
|
889
|
+
};
|
|
890
|
+
//# sourceMappingURL=chunk-BMHGRYL6.js.map
|