@gh-symphony/cli 0.0.19 → 0.0.21
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/README.md +30 -2
- package/dist/chunk-A67CMOYE.js +684 -0
- package/dist/{chunk-TILHWBP6.js → chunk-C67H3OUL.js} +239 -36
- package/dist/{chunk-RN2PACNV.js → chunk-JN3TQVFV.js} +721 -74
- package/dist/{chunk-GKENCODJ.js → chunk-KY6WKH66.js} +437 -101
- package/dist/{chunk-6CI3UUMH.js → chunk-MYVJ6HK4.js} +950 -1240
- package/dist/{chunk-M3IFVLQS.js → chunk-QEONJ5DZ.js} +978 -72
- package/dist/{chunk-H2YXSYOZ.js → chunk-S6VIK4FF.js} +59 -31
- package/dist/chunk-SXGT7LOF.js +1060 -0
- package/dist/{doctor-IYHCFXOZ.js → doctor-4HBRICHP.js} +102 -37
- package/dist/index.js +38 -17
- package/dist/{init-KZT6YNOH.js → init-HZ3JEDGQ.js} +7 -2
- package/dist/{project-DNALEWO3.js → project-25NQ4J4Y.js} +8 -6
- package/dist/{recover-C3V2QAUB.js → recover-L3MJHHDA.js} +4 -2
- package/dist/{repo-HDDE7OUI.js → repo-TDCWQR6P.js} +72 -14
- package/dist/{run-XI2S5Y4V.js → run-XJQ6BF7U.js} +4 -2
- package/dist/{setup-K4CYYJBF.js → setup-B2SVLW2R.js} +46 -8
- package/dist/{start-M6IQGRFO.js → start-I2CC7BLW.js} +6 -4
- package/dist/{upgrade-F4VE4XBS.js → upgrade-OJXPZRYE.js} +2 -2
- package/dist/{version-Y5RYNWMF.js → version-TBDCTKDO.js} +1 -1
- package/dist/worker-entry.js +522 -867
- package/dist/{workflow-TBIFY5MO.js → workflow-BLJH2HC3.js} +176 -10
- package/package.json +5 -3
|
@@ -5,7 +5,10 @@ import {
|
|
|
5
5
|
createStore,
|
|
6
6
|
releaseProjectLock,
|
|
7
7
|
resolveOrchestratorLogLevel
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-MYVJ6HK4.js";
|
|
9
|
+
import {
|
|
10
|
+
getGhToken
|
|
11
|
+
} from "./chunk-C67H3OUL.js";
|
|
9
12
|
import {
|
|
10
13
|
deriveIssueWorkspaceKeyFromIdentifier,
|
|
11
14
|
isFileMissing,
|
|
@@ -14,10 +17,7 @@ import {
|
|
|
14
17
|
parseRecentEvents,
|
|
15
18
|
readJsonFile,
|
|
16
19
|
safeReadDir
|
|
17
|
-
} from "./chunk-
|
|
18
|
-
import {
|
|
19
|
-
getGhToken
|
|
20
|
-
} from "./chunk-TILHWBP6.js";
|
|
20
|
+
} from "./chunk-QEONJ5DZ.js";
|
|
21
21
|
import {
|
|
22
22
|
bold,
|
|
23
23
|
cyan,
|
|
@@ -42,12 +42,12 @@ import {
|
|
|
42
42
|
} from "./chunk-ROGRTUFI.js";
|
|
43
43
|
|
|
44
44
|
// src/commands/start.ts
|
|
45
|
-
import { writeFile, mkdir, readFile, rm } from "fs/promises";
|
|
46
|
-
import { dirname, join as
|
|
45
|
+
import { writeFile, mkdir, readFile as readFile2, rm } from "fs/promises";
|
|
46
|
+
import { dirname as dirname2, join as join3 } from "path";
|
|
47
47
|
import { spawn } from "child_process";
|
|
48
|
-
import { createServer as
|
|
48
|
+
import { createServer as createServer3 } from "http";
|
|
49
49
|
|
|
50
|
-
// ../dashboard/
|
|
50
|
+
// ../dashboard/src/store.ts
|
|
51
51
|
import { open } from "fs/promises";
|
|
52
52
|
import { join, resolve } from "path";
|
|
53
53
|
var DEFAULT_RECENT_EVENT_LIMIT = 20;
|
|
@@ -55,15 +55,13 @@ var RECENT_EVENT_CHUNK_SIZE = 4096;
|
|
|
55
55
|
var MAX_RECENT_EVENT_SCAN_BYTES = 64 * 1024;
|
|
56
56
|
var RUN_RECORD_LOAD_CONCURRENCY = 8;
|
|
57
57
|
var DashboardFsReader = class {
|
|
58
|
-
runtimeRoot;
|
|
59
|
-
projectId;
|
|
60
|
-
resolvedRuntimeRoot;
|
|
61
58
|
constructor(runtimeRoot, projectId) {
|
|
62
59
|
this.runtimeRoot = runtimeRoot;
|
|
63
60
|
this.projectId = projectId;
|
|
64
61
|
assertValidDashboardProjectId(projectId);
|
|
65
62
|
this.resolvedRuntimeRoot = resolve(runtimeRoot);
|
|
66
63
|
}
|
|
64
|
+
resolvedRuntimeRoot;
|
|
67
65
|
projectDir() {
|
|
68
66
|
return join(this.resolvedRuntimeRoot, "projects", this.projectId);
|
|
69
67
|
}
|
|
@@ -72,7 +70,9 @@ var DashboardFsReader = class {
|
|
|
72
70
|
return join(this.projectDir(), "runs", runId);
|
|
73
71
|
}
|
|
74
72
|
async loadProjectStatus() {
|
|
75
|
-
return readJsonFile(
|
|
73
|
+
return readJsonFile(
|
|
74
|
+
join(this.projectDir(), "status.json")
|
|
75
|
+
);
|
|
76
76
|
}
|
|
77
77
|
async loadProjectState() {
|
|
78
78
|
const snapshot = await this.loadProjectStatus();
|
|
@@ -87,7 +87,9 @@ var DashboardFsReader = class {
|
|
|
87
87
|
};
|
|
88
88
|
}
|
|
89
89
|
async loadProjectIssueOrchestrations() {
|
|
90
|
-
const issues = await readJsonFile(
|
|
90
|
+
const issues = await readJsonFile(
|
|
91
|
+
join(this.projectDir(), "issues.json")
|
|
92
|
+
);
|
|
91
93
|
if (issues) {
|
|
92
94
|
return issues.map((issue) => ({
|
|
93
95
|
...issue,
|
|
@@ -99,7 +101,9 @@ var DashboardFsReader = class {
|
|
|
99
101
|
return legacyLeases.map((lease) => ({
|
|
100
102
|
issueId: lease.issueId,
|
|
101
103
|
identifier: lease.issueIdentifier,
|
|
102
|
-
workspaceKey: deriveIssueWorkspaceKeyFromIdentifier(
|
|
104
|
+
workspaceKey: deriveIssueWorkspaceKeyFromIdentifier(
|
|
105
|
+
lease.issueIdentifier
|
|
106
|
+
),
|
|
103
107
|
completedOnce: false,
|
|
104
108
|
failureRetryCount: 0,
|
|
105
109
|
state: lease.status === "active" ? "claimed" : "released",
|
|
@@ -109,29 +113,39 @@ var DashboardFsReader = class {
|
|
|
109
113
|
}));
|
|
110
114
|
}
|
|
111
115
|
async loadRun(runId) {
|
|
112
|
-
return readJsonFile(
|
|
116
|
+
return readJsonFile(
|
|
117
|
+
join(this.runDir(runId), "run.json")
|
|
118
|
+
);
|
|
113
119
|
}
|
|
114
120
|
async loadAllRuns() {
|
|
115
121
|
const runIds = await safeReadDir(join(this.projectDir(), "runs"));
|
|
116
|
-
const runs = await mapWithConcurrency(
|
|
122
|
+
const runs = await mapWithConcurrency(
|
|
123
|
+
runIds,
|
|
124
|
+
RUN_RECORD_LOAD_CONCURRENCY,
|
|
125
|
+
(runId) => this.loadRun(runId)
|
|
126
|
+
);
|
|
117
127
|
return runs.filter((run) => Boolean(run));
|
|
118
128
|
}
|
|
119
129
|
async loadRunsForIssue(issueId, issueIdentifier) {
|
|
120
130
|
const runIds = await safeReadDir(join(this.projectDir(), "runs"));
|
|
121
|
-
const runs = await mapWithConcurrency(
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
131
|
+
const runs = await mapWithConcurrency(
|
|
132
|
+
runIds,
|
|
133
|
+
RUN_RECORD_LOAD_CONCURRENCY,
|
|
134
|
+
async (runId) => {
|
|
135
|
+
try {
|
|
136
|
+
const run = await this.loadRun(runId);
|
|
137
|
+
if (!run) {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
return run.issueId === issueId || run.issueIdentifier === issueIdentifier ? run : null;
|
|
141
|
+
} catch (error) {
|
|
142
|
+
if (isFileMissing(error)) {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
130
145
|
return null;
|
|
131
146
|
}
|
|
132
|
-
return null;
|
|
133
147
|
}
|
|
134
|
-
|
|
148
|
+
);
|
|
135
149
|
return runs.filter((run) => Boolean(run));
|
|
136
150
|
}
|
|
137
151
|
async loadRecentRunEvents(runId, limit = DEFAULT_RECENT_EVENT_LIMIT) {
|
|
@@ -148,7 +162,11 @@ var DashboardFsReader = class {
|
|
|
148
162
|
let newlineCount = 0;
|
|
149
163
|
const chunks = [];
|
|
150
164
|
while (position > 0 && bytesScanned < MAX_RECENT_EVENT_SCAN_BYTES && newlineCount <= limit) {
|
|
151
|
-
const readSize = Math.min(
|
|
165
|
+
const readSize = Math.min(
|
|
166
|
+
position,
|
|
167
|
+
RECENT_EVENT_CHUNK_SIZE,
|
|
168
|
+
MAX_RECENT_EVENT_SCAN_BYTES - bytesScanned
|
|
169
|
+
);
|
|
152
170
|
position -= readSize;
|
|
153
171
|
const chunk = Buffer.allocUnsafe(readSize);
|
|
154
172
|
const { bytesRead } = await handle.read(chunk, 0, readSize, position);
|
|
@@ -160,9 +178,13 @@ var DashboardFsReader = class {
|
|
|
160
178
|
bytesScanned += bytesRead;
|
|
161
179
|
newlineCount += countNewlines(populatedChunk);
|
|
162
180
|
}
|
|
163
|
-
return parseRecentEvents(
|
|
164
|
-
|
|
165
|
-
|
|
181
|
+
return parseRecentEvents(
|
|
182
|
+
Buffer.concat(chunks).toString("utf8"),
|
|
183
|
+
limit,
|
|
184
|
+
{
|
|
185
|
+
allowPartialFirstLine: position > 0
|
|
186
|
+
}
|
|
187
|
+
);
|
|
166
188
|
} finally {
|
|
167
189
|
await handle.close();
|
|
168
190
|
}
|
|
@@ -185,12 +207,19 @@ function countNewlines(chunk) {
|
|
|
185
207
|
}
|
|
186
208
|
async function statusForIssue(reader, issueIdentifier) {
|
|
187
209
|
const issueRecords = await reader.loadProjectIssueOrchestrations();
|
|
188
|
-
const issueRecord = issueRecords.find(
|
|
210
|
+
const issueRecord = issueRecords.find(
|
|
211
|
+
(record) => record.identifier === issueIdentifier
|
|
212
|
+
);
|
|
189
213
|
if (!issueRecord) {
|
|
190
214
|
return null;
|
|
191
215
|
}
|
|
192
216
|
const currentRunCandidate = issueRecord.currentRunId ? await reader.loadRun(issueRecord.currentRunId) : null;
|
|
193
|
-
const currentRun = isMatchingIssueRun(
|
|
217
|
+
const currentRun = isMatchingIssueRun(
|
|
218
|
+
currentRunCandidate,
|
|
219
|
+
reader.projectId,
|
|
220
|
+
issueRecord.issueId,
|
|
221
|
+
issueIdentifier
|
|
222
|
+
) ? currentRunCandidate : null;
|
|
194
223
|
const issueRuns = currentRun === null ? await reader.loadRunsForIssue(issueRecord.issueId, issueIdentifier) : currentRun.tokenUsage ? await reader.loadRunsForIssue(issueRecord.issueId, issueIdentifier) : null;
|
|
195
224
|
const resolvedRun = currentRun ?? findLatestRunForIssue(issueRuns ?? []);
|
|
196
225
|
const recentEvents = resolvedRun === null ? [] : await reader.loadRecentRunEvents(resolvedRun.runId);
|
|
@@ -252,28 +281,37 @@ async function statusForIssue(reader, issueIdentifier) {
|
|
|
252
281
|
};
|
|
253
282
|
}
|
|
254
283
|
function aggregateIssueTokenUsage(runs) {
|
|
255
|
-
return runs.reduce(
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
284
|
+
return runs.reduce(
|
|
285
|
+
(total, run) => ({
|
|
286
|
+
inputTokens: total.inputTokens + (run.tokenUsage?.inputTokens ?? 0),
|
|
287
|
+
outputTokens: total.outputTokens + (run.tokenUsage?.outputTokens ?? 0),
|
|
288
|
+
totalTokens: total.totalTokens + (run.tokenUsage?.totalTokens ?? 0)
|
|
289
|
+
}),
|
|
290
|
+
{
|
|
291
|
+
inputTokens: 0,
|
|
292
|
+
outputTokens: 0,
|
|
293
|
+
totalTokens: 0
|
|
294
|
+
}
|
|
295
|
+
);
|
|
264
296
|
}
|
|
265
297
|
function findLatestRunForIssue(matchingRuns) {
|
|
266
|
-
const sortedRuns = [...matchingRuns].sort(
|
|
298
|
+
const sortedRuns = [...matchingRuns].sort(
|
|
299
|
+
(left, right) => new Date(right.updatedAt).getTime() - new Date(left.updatedAt).getTime()
|
|
300
|
+
);
|
|
267
301
|
return sortedRuns[0] ?? null;
|
|
268
302
|
}
|
|
269
303
|
function assertValidDashboardProjectId(projectId) {
|
|
270
304
|
if (projectId.length === 0 || projectId === "." || projectId === ".." || projectId.includes("/") || projectId.includes("\\")) {
|
|
271
|
-
throw new Error(
|
|
305
|
+
throw new Error(
|
|
306
|
+
`Invalid project ID "${projectId}". Project IDs must not contain path separators or traversal segments.`
|
|
307
|
+
);
|
|
272
308
|
}
|
|
273
309
|
}
|
|
274
310
|
function assertValidDashboardRunId(runId) {
|
|
275
311
|
if (runId.length === 0 || runId === "." || runId === ".." || runId.includes("/") || runId.includes("\\")) {
|
|
276
|
-
throw new Error(
|
|
312
|
+
throw new Error(
|
|
313
|
+
`Invalid run ID "${runId}". Run IDs must not contain path separators or traversal segments.`
|
|
314
|
+
);
|
|
277
315
|
}
|
|
278
316
|
}
|
|
279
317
|
async function mapWithConcurrency(items, concurrency, mapper) {
|
|
@@ -291,8 +329,10 @@ async function mapWithConcurrency(items, concurrency, mapper) {
|
|
|
291
329
|
return results;
|
|
292
330
|
}
|
|
293
331
|
|
|
294
|
-
// ../dashboard/
|
|
295
|
-
import {
|
|
332
|
+
// ../dashboard/src/server.ts
|
|
333
|
+
import {
|
|
334
|
+
createServer
|
|
335
|
+
} from "http";
|
|
296
336
|
async function resolveDashboardResponse(options) {
|
|
297
337
|
const method = options.method ?? "GET";
|
|
298
338
|
if (options.pathname === "/healthz") {
|
|
@@ -371,6 +411,267 @@ async function resolveDashboardResponse(options) {
|
|
|
371
411
|
};
|
|
372
412
|
}
|
|
373
413
|
|
|
414
|
+
// ../control-plane/src/server.ts
|
|
415
|
+
import {
|
|
416
|
+
createServer as createServer2
|
|
417
|
+
} from "http";
|
|
418
|
+
import { readFile, stat } from "fs/promises";
|
|
419
|
+
import { dirname, extname, join as join2, resolve as resolve2, sep } from "path";
|
|
420
|
+
import { fileURLToPath } from "url";
|
|
421
|
+
var CLIENT_DIST_DIR = join2(
|
|
422
|
+
dirname(fileURLToPath(import.meta.url)),
|
|
423
|
+
"../client/dist"
|
|
424
|
+
);
|
|
425
|
+
var BUNDLED_CLIENT_DIST_DIR = join2(
|
|
426
|
+
dirname(fileURLToPath(import.meta.url)),
|
|
427
|
+
"../../control-plane/client/dist"
|
|
428
|
+
);
|
|
429
|
+
var WORKSPACE_CLIENT_DIST_DIR = join2(
|
|
430
|
+
process.cwd(),
|
|
431
|
+
"packages/control-plane/client/dist"
|
|
432
|
+
);
|
|
433
|
+
var NODE_MODULES_CLIENT_DIST_DIR = join2(
|
|
434
|
+
process.cwd(),
|
|
435
|
+
"node_modules/@gh-symphony/control-plane/client/dist"
|
|
436
|
+
);
|
|
437
|
+
var CLIENT_DIST_DIR_CANDIDATES = [
|
|
438
|
+
CLIENT_DIST_DIR,
|
|
439
|
+
BUNDLED_CLIENT_DIST_DIR,
|
|
440
|
+
WORKSPACE_CLIENT_DIST_DIR,
|
|
441
|
+
NODE_MODULES_CLIENT_DIST_DIR
|
|
442
|
+
];
|
|
443
|
+
var clientDistDirPromise;
|
|
444
|
+
var TEXT_CONTENT_TYPES = /* @__PURE__ */ new Set([
|
|
445
|
+
"application/javascript",
|
|
446
|
+
"application/json",
|
|
447
|
+
"image/svg+xml",
|
|
448
|
+
"text/css",
|
|
449
|
+
"text/html",
|
|
450
|
+
"text/plain"
|
|
451
|
+
]);
|
|
452
|
+
var CONTENT_TYPES = {
|
|
453
|
+
".css": "text/css",
|
|
454
|
+
".gif": "image/gif",
|
|
455
|
+
".html": "text/html",
|
|
456
|
+
".ico": "image/x-icon",
|
|
457
|
+
".jpeg": "image/jpeg",
|
|
458
|
+
".jpg": "image/jpeg",
|
|
459
|
+
".js": "application/javascript",
|
|
460
|
+
".json": "application/json",
|
|
461
|
+
".map": "application/json",
|
|
462
|
+
".png": "image/png",
|
|
463
|
+
".svg": "image/svg+xml",
|
|
464
|
+
".txt": "text/plain",
|
|
465
|
+
".webp": "image/webp"
|
|
466
|
+
};
|
|
467
|
+
function createControlPlaneHandler(options) {
|
|
468
|
+
return async (request, response) => {
|
|
469
|
+
try {
|
|
470
|
+
const method = request.method ?? "GET";
|
|
471
|
+
const url = new URL(request.url ?? "/", "http://127.0.0.1");
|
|
472
|
+
if (url.pathname === "/api/v1/refresh") {
|
|
473
|
+
await handleRefreshRequest(method, request, response, options);
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
if (isDashboardRequest(url.pathname)) {
|
|
477
|
+
const resolved = await resolveDashboardResponse({
|
|
478
|
+
pathname: url.pathname,
|
|
479
|
+
method,
|
|
480
|
+
reader: options.reader
|
|
481
|
+
});
|
|
482
|
+
respondJson(response, resolved.status, resolved.payload);
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
if (!isStaticRequestMethod(method)) {
|
|
486
|
+
respondJson(response, 405, { error: "Method not allowed" });
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
const asset = await resolveStaticAsset(url.pathname);
|
|
490
|
+
if (!asset) {
|
|
491
|
+
respondJson(response, 404, { error: "Not found" });
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
if (asset.kind === "error") {
|
|
495
|
+
respondJson(response, asset.status, { error: "Bad request" });
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
await respondFile(response, asset.path, method, asset.fallback);
|
|
499
|
+
} catch (error) {
|
|
500
|
+
console.error("Control plane request failed.", error);
|
|
501
|
+
if (!response.headersSent) {
|
|
502
|
+
respondJson(response, 500, { error: "Internal server error" });
|
|
503
|
+
} else {
|
|
504
|
+
response.end();
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
async function startControlPlaneServer(options) {
|
|
510
|
+
const reader = new DashboardFsReader(options.runtimeRoot, options.projectId);
|
|
511
|
+
const handler2 = createControlPlaneHandler({
|
|
512
|
+
reader,
|
|
513
|
+
onRefreshRequest: options.onRefreshRequest
|
|
514
|
+
});
|
|
515
|
+
for (let port = options.port; port <= 65535; port += 1) {
|
|
516
|
+
const server = createServer2((request, response) => {
|
|
517
|
+
void handler2(request, response);
|
|
518
|
+
});
|
|
519
|
+
try {
|
|
520
|
+
await new Promise((resolveReady, rejectReady) => {
|
|
521
|
+
const cleanup = () => {
|
|
522
|
+
server.off("listening", handleListening);
|
|
523
|
+
server.off("error", handleError);
|
|
524
|
+
};
|
|
525
|
+
const handleListening = () => {
|
|
526
|
+
cleanup();
|
|
527
|
+
resolveReady();
|
|
528
|
+
};
|
|
529
|
+
const handleError = (error) => {
|
|
530
|
+
cleanup();
|
|
531
|
+
rejectReady(error);
|
|
532
|
+
};
|
|
533
|
+
server.once("listening", handleListening);
|
|
534
|
+
server.once("error", handleError);
|
|
535
|
+
server.listen(port, options.host);
|
|
536
|
+
});
|
|
537
|
+
const address = server.address();
|
|
538
|
+
const boundPort = address && typeof address !== "string" ? address.port : port;
|
|
539
|
+
return {
|
|
540
|
+
server,
|
|
541
|
+
port: boundPort,
|
|
542
|
+
url: formatBoundUrl(server)
|
|
543
|
+
};
|
|
544
|
+
} catch (error) {
|
|
545
|
+
await closeServer(server).catch(() => {
|
|
546
|
+
});
|
|
547
|
+
if (error.code === "EADDRINUSE") {
|
|
548
|
+
continue;
|
|
549
|
+
}
|
|
550
|
+
throw error;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
throw new Error(
|
|
554
|
+
`Unable to bind control plane server starting from port ${options.port}`
|
|
555
|
+
);
|
|
556
|
+
}
|
|
557
|
+
async function handleRefreshRequest(method, request, response, options) {
|
|
558
|
+
if (method !== "POST") {
|
|
559
|
+
respondJson(response, 405, { error: "Method not allowed" });
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
request.resume();
|
|
563
|
+
options.onRefreshRequest?.();
|
|
564
|
+
respondJson(response, 202, { ok: true });
|
|
565
|
+
}
|
|
566
|
+
function isDashboardRequest(pathname) {
|
|
567
|
+
return pathname === "/healthz" || pathname === "/api/v1/state" || pathname.startsWith("/api/v1/");
|
|
568
|
+
}
|
|
569
|
+
function isStaticRequestMethod(method) {
|
|
570
|
+
return method === "GET" || method === "HEAD";
|
|
571
|
+
}
|
|
572
|
+
async function resolveStaticAsset(pathname) {
|
|
573
|
+
const clientDistDir = await resolveClientDistDir();
|
|
574
|
+
if (!clientDistDir) {
|
|
575
|
+
return null;
|
|
576
|
+
}
|
|
577
|
+
const indexPath = join2(clientDistDir, "index.html");
|
|
578
|
+
if (pathname === "/") {
|
|
579
|
+
return await existsAsFile(indexPath) ? { kind: "asset", path: indexPath, fallback: true } : null;
|
|
580
|
+
}
|
|
581
|
+
let decodedPathname;
|
|
582
|
+
try {
|
|
583
|
+
decodedPathname = decodeURIComponent(pathname);
|
|
584
|
+
} catch {
|
|
585
|
+
return { kind: "error", status: 400 };
|
|
586
|
+
}
|
|
587
|
+
const resolvedPath = resolve2(clientDistDir, `.${decodedPathname}`);
|
|
588
|
+
if (!isPathInsideClientDist(clientDistDir, resolvedPath)) {
|
|
589
|
+
return null;
|
|
590
|
+
}
|
|
591
|
+
if (await existsAsFile(resolvedPath)) {
|
|
592
|
+
return { kind: "asset", path: resolvedPath, fallback: false };
|
|
593
|
+
}
|
|
594
|
+
if (hasFileExtension(decodedPathname)) {
|
|
595
|
+
return null;
|
|
596
|
+
}
|
|
597
|
+
return await existsAsFile(indexPath) ? { kind: "asset", path: indexPath, fallback: true } : null;
|
|
598
|
+
}
|
|
599
|
+
function isPathInsideClientDist(clientDistDir, path) {
|
|
600
|
+
return path === clientDistDir || path.startsWith(`${clientDistDir}${sep}`);
|
|
601
|
+
}
|
|
602
|
+
function hasFileExtension(pathname) {
|
|
603
|
+
const lastSegment = pathname.split("/").pop() ?? "";
|
|
604
|
+
return lastSegment.includes(".");
|
|
605
|
+
}
|
|
606
|
+
async function existsAsFile(path) {
|
|
607
|
+
try {
|
|
608
|
+
return (await stat(path)).isFile();
|
|
609
|
+
} catch {
|
|
610
|
+
return false;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
async function resolveClientDistDir() {
|
|
614
|
+
clientDistDirPromise ??= (async () => {
|
|
615
|
+
for (const candidate of CLIENT_DIST_DIR_CANDIDATES) {
|
|
616
|
+
try {
|
|
617
|
+
if ((await stat(candidate)).isDirectory()) {
|
|
618
|
+
return candidate;
|
|
619
|
+
}
|
|
620
|
+
} catch {
|
|
621
|
+
continue;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
return null;
|
|
625
|
+
})();
|
|
626
|
+
return clientDistDirPromise;
|
|
627
|
+
}
|
|
628
|
+
async function respondFile(response, path, method, fallback) {
|
|
629
|
+
const contentType = contentTypeForPath(path);
|
|
630
|
+
const body = method === "HEAD" ? void 0 : await readFile(path);
|
|
631
|
+
const cacheControl = fallback || path.endsWith(`${sep}index.html`) ? "no-cache" : "public, max-age=31536000, immutable";
|
|
632
|
+
response.writeHead(200, {
|
|
633
|
+
"cache-control": cacheControl,
|
|
634
|
+
"content-type": contentType
|
|
635
|
+
});
|
|
636
|
+
response.end(body);
|
|
637
|
+
}
|
|
638
|
+
function contentTypeForPath(path) {
|
|
639
|
+
const contentType = CONTENT_TYPES[extname(path).toLowerCase()];
|
|
640
|
+
if (!contentType) {
|
|
641
|
+
return "application/octet-stream";
|
|
642
|
+
}
|
|
643
|
+
if (TEXT_CONTENT_TYPES.has(contentType)) {
|
|
644
|
+
return `${contentType}; charset=utf-8`;
|
|
645
|
+
}
|
|
646
|
+
return contentType;
|
|
647
|
+
}
|
|
648
|
+
function respondJson(response, status, payload) {
|
|
649
|
+
response.writeHead(status, {
|
|
650
|
+
"content-type": "application/json; charset=utf-8"
|
|
651
|
+
});
|
|
652
|
+
response.end(JSON.stringify(payload));
|
|
653
|
+
}
|
|
654
|
+
function formatBoundUrl(server) {
|
|
655
|
+
const address = server.address();
|
|
656
|
+
if (!address || typeof address === "string") {
|
|
657
|
+
return "http://localhost";
|
|
658
|
+
}
|
|
659
|
+
const host = address.address === "::" || address.address === "::1" || address.address === "0.0.0.0" || address.address === "127.0.0.1" ? "localhost" : address.address;
|
|
660
|
+
const urlHost = host.includes(":") ? `[${host}]` : host;
|
|
661
|
+
return `http://${urlHost}:${address.port}`;
|
|
662
|
+
}
|
|
663
|
+
async function closeServer(server) {
|
|
664
|
+
await new Promise((resolveClose, rejectClose) => {
|
|
665
|
+
server.close((error) => {
|
|
666
|
+
if (error) {
|
|
667
|
+
rejectClose(error);
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
resolveClose();
|
|
671
|
+
});
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
|
|
374
675
|
// src/commands/start.ts
|
|
375
676
|
function timestamp() {
|
|
376
677
|
const now = /* @__PURE__ */ new Date();
|
|
@@ -410,6 +711,16 @@ function parseStartArgs(args) {
|
|
|
410
711
|
i += 1;
|
|
411
712
|
continue;
|
|
412
713
|
}
|
|
714
|
+
if (arg === "--web") {
|
|
715
|
+
const value = args[i + 1];
|
|
716
|
+
if (!value || value.startsWith("-")) {
|
|
717
|
+
parsed.webPort = DEFAULT_HTTP_PORT;
|
|
718
|
+
continue;
|
|
719
|
+
}
|
|
720
|
+
parsed.webPort = parsePort(value, arg);
|
|
721
|
+
i += 1;
|
|
722
|
+
continue;
|
|
723
|
+
}
|
|
413
724
|
if (arg === "--project" || arg === "--project-id") {
|
|
414
725
|
const value = args[i + 1];
|
|
415
726
|
if (!value || value.startsWith("-")) {
|
|
@@ -435,6 +746,9 @@ function parseStartArgs(args) {
|
|
|
435
746
|
return parsed;
|
|
436
747
|
}
|
|
437
748
|
}
|
|
749
|
+
if (parsed.httpPort !== void 0 && parsed.webPort !== void 0) {
|
|
750
|
+
parsed.error = "Options '--http' and '--web' cannot be used together";
|
|
751
|
+
}
|
|
438
752
|
return parsed;
|
|
439
753
|
}
|
|
440
754
|
function logTickResult(snapshot, prevSnapshot, isFirst) {
|
|
@@ -530,13 +844,13 @@ function parsePort(value, optionName) {
|
|
|
530
844
|
}
|
|
531
845
|
return parsed;
|
|
532
846
|
}
|
|
533
|
-
function
|
|
847
|
+
function respondJson2(response, status, payload) {
|
|
534
848
|
response.writeHead(status, {
|
|
535
849
|
"content-type": "application/json"
|
|
536
850
|
});
|
|
537
851
|
response.end(JSON.stringify(payload));
|
|
538
852
|
}
|
|
539
|
-
function
|
|
853
|
+
function formatBoundUrl2(server) {
|
|
540
854
|
const address = server.address();
|
|
541
855
|
if (!address || typeof address === "string") {
|
|
542
856
|
return `http://${HTTP_HOST}`;
|
|
@@ -573,14 +887,14 @@ async function removeHttpBindingState(configDir, projectId) {
|
|
|
573
887
|
async function startHttpServer(input) {
|
|
574
888
|
const reader = new DashboardFsReader(input.runtimeRoot, input.projectId);
|
|
575
889
|
for (let port = input.initialPort; port <= 65535; port += 1) {
|
|
576
|
-
const server =
|
|
890
|
+
const server = createServer3((request, response) => {
|
|
577
891
|
void (async () => {
|
|
578
892
|
try {
|
|
579
893
|
const url = new URL(request.url ?? "/", `http://${HTTP_HOST}`);
|
|
580
894
|
if (request.method === "POST" && url.pathname === "/api/v1/refresh") {
|
|
581
895
|
request.resume();
|
|
582
896
|
input.service.requestReconcile();
|
|
583
|
-
|
|
897
|
+
respondJson2(response, 202, { ok: true });
|
|
584
898
|
return;
|
|
585
899
|
}
|
|
586
900
|
const resolved = await resolveDashboardResponse({
|
|
@@ -588,11 +902,11 @@ async function startHttpServer(input) {
|
|
|
588
902
|
method: request.method ?? "GET",
|
|
589
903
|
reader
|
|
590
904
|
});
|
|
591
|
-
|
|
905
|
+
respondJson2(response, resolved.status, resolved.payload);
|
|
592
906
|
} catch (error) {
|
|
593
907
|
logHttpRequestError(error);
|
|
594
908
|
if (!response.headersSent) {
|
|
595
|
-
|
|
909
|
+
respondJson2(response, 500, {
|
|
596
910
|
error: "Internal server error"
|
|
597
911
|
});
|
|
598
912
|
} else {
|
|
@@ -622,7 +936,7 @@ async function startHttpServer(input) {
|
|
|
622
936
|
return {
|
|
623
937
|
server,
|
|
624
938
|
port,
|
|
625
|
-
url:
|
|
939
|
+
url: formatBoundUrl2(server)
|
|
626
940
|
};
|
|
627
941
|
} catch (error) {
|
|
628
942
|
await closeHttpServer(server).catch(() => {
|
|
@@ -654,7 +968,7 @@ var handler = async (args, options) => {
|
|
|
654
968
|
process.stderr.write(`${parsed.error}
|
|
655
969
|
`);
|
|
656
970
|
process.stderr.write(
|
|
657
|
-
"Usage: gh-symphony start --project-id <project-id> [--daemon] [--once] [--http [port]]\n"
|
|
971
|
+
"Usage: gh-symphony start --project-id <project-id> [--daemon] [--once] [--http [port]] [--web [port]]\n"
|
|
658
972
|
);
|
|
659
973
|
process.exitCode = 2;
|
|
660
974
|
return;
|
|
@@ -688,7 +1002,13 @@ var handler = async (args, options) => {
|
|
|
688
1002
|
return;
|
|
689
1003
|
}
|
|
690
1004
|
if (parsed.daemon) {
|
|
691
|
-
await startDaemon(
|
|
1005
|
+
await startDaemon(
|
|
1006
|
+
options,
|
|
1007
|
+
projectId,
|
|
1008
|
+
parsed.logLevel,
|
|
1009
|
+
parsed.httpPort,
|
|
1010
|
+
parsed.webPort
|
|
1011
|
+
);
|
|
692
1012
|
return;
|
|
693
1013
|
}
|
|
694
1014
|
if (!process.env.GITHUB_GRAPHQL_TOKEN) {
|
|
@@ -739,45 +1059,10 @@ var handler = async (args, options) => {
|
|
|
739
1059
|
}
|
|
740
1060
|
}
|
|
741
1061
|
});
|
|
742
|
-
const httpServer = parsed.httpPort !== void 0 ? await startHttpServer({
|
|
743
|
-
runtimeRoot,
|
|
744
|
-
projectId,
|
|
745
|
-
initialPort: parsed.httpPort,
|
|
746
|
-
service
|
|
747
|
-
}) : null;
|
|
748
|
-
if (httpServer) {
|
|
749
|
-
try {
|
|
750
|
-
await writeHttpBindingState(options.configDir, projectId, {
|
|
751
|
-
host: HTTP_HOST,
|
|
752
|
-
port: httpServer.port,
|
|
753
|
-
endpoint: httpServer.url
|
|
754
|
-
});
|
|
755
|
-
} catch (error) {
|
|
756
|
-
logLine(
|
|
757
|
-
yellow("\u26A0"),
|
|
758
|
-
yellow(
|
|
759
|
-
`Failed to persist HTTP binding state (http.json): ${error instanceof Error ? error.message : "Unknown error"}`
|
|
760
|
-
)
|
|
761
|
-
);
|
|
762
|
-
}
|
|
763
|
-
}
|
|
764
|
-
logLine(
|
|
765
|
-
green("\u25B2"),
|
|
766
|
-
`Starting orchestrator for project: ${bold(projectId)}`
|
|
767
|
-
);
|
|
768
|
-
if (httpServer) {
|
|
769
|
-
logLine(
|
|
770
|
-
cyan("\u25A1"),
|
|
771
|
-
`HTTP dashboard listening on ${httpServer.url}`
|
|
772
|
-
);
|
|
773
|
-
}
|
|
774
|
-
logLine(
|
|
775
|
-
dim("\xB7"),
|
|
776
|
-
dim(parsed.once ? "Running one orchestration tick" : "Press Ctrl+C to stop")
|
|
777
|
-
);
|
|
778
1062
|
let shuttingDown = false;
|
|
779
1063
|
let shutdownPromise = null;
|
|
780
1064
|
let keepHttpAliveResolve = null;
|
|
1065
|
+
let httpServer = null;
|
|
781
1066
|
const shutdown = async () => {
|
|
782
1067
|
if (shuttingDown) {
|
|
783
1068
|
return shutdownPromise;
|
|
@@ -805,17 +1090,67 @@ var handler = async (args, options) => {
|
|
|
805
1090
|
process.on("SIGINT", handleSigint);
|
|
806
1091
|
process.on("SIGTERM", handleSigterm);
|
|
807
1092
|
try {
|
|
1093
|
+
httpServer = parsed.webPort !== void 0 ? await startControlPlaneServer({
|
|
1094
|
+
host: HTTP_HOST,
|
|
1095
|
+
port: parsed.webPort,
|
|
1096
|
+
runtimeRoot,
|
|
1097
|
+
projectId,
|
|
1098
|
+
onRefreshRequest: () => service.requestReconcile()
|
|
1099
|
+
}) : parsed.httpPort !== void 0 ? await startHttpServer({
|
|
1100
|
+
runtimeRoot,
|
|
1101
|
+
projectId,
|
|
1102
|
+
initialPort: parsed.httpPort,
|
|
1103
|
+
service
|
|
1104
|
+
}) : null;
|
|
1105
|
+
if (httpServer) {
|
|
1106
|
+
try {
|
|
1107
|
+
await writeHttpBindingState(options.configDir, projectId, {
|
|
1108
|
+
host: HTTP_HOST,
|
|
1109
|
+
port: httpServer.port,
|
|
1110
|
+
endpoint: httpServer.url
|
|
1111
|
+
});
|
|
1112
|
+
} catch (error) {
|
|
1113
|
+
logLine(
|
|
1114
|
+
yellow("\u26A0"),
|
|
1115
|
+
yellow(
|
|
1116
|
+
`Failed to persist HTTP binding state (http.json): ${error instanceof Error ? error.message : "Unknown error"}`
|
|
1117
|
+
)
|
|
1118
|
+
);
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
logLine(
|
|
1122
|
+
green("\u25B2"),
|
|
1123
|
+
`Starting orchestrator for project: ${bold(projectId)}`
|
|
1124
|
+
);
|
|
1125
|
+
if (httpServer) {
|
|
1126
|
+
logLine(
|
|
1127
|
+
cyan("\u25A1"),
|
|
1128
|
+
parsed.webPort !== void 0 ? `Web dashboard listening on ${httpServer.url}` : `HTTP dashboard listening on ${httpServer.url}`
|
|
1129
|
+
);
|
|
1130
|
+
}
|
|
1131
|
+
logLine(
|
|
1132
|
+
dim("\xB7"),
|
|
1133
|
+
dim(
|
|
1134
|
+
parsed.once ? "Running one orchestration tick" : "Press Ctrl+C to stop"
|
|
1135
|
+
)
|
|
1136
|
+
);
|
|
808
1137
|
while (!shuttingDown) {
|
|
809
1138
|
try {
|
|
810
1139
|
await service.run({ once: parsed.once });
|
|
1140
|
+
if (shuttingDown) {
|
|
1141
|
+
break;
|
|
1142
|
+
}
|
|
811
1143
|
if (parsed.once) {
|
|
812
1144
|
if (httpServer) {
|
|
813
1145
|
logLine(
|
|
814
1146
|
cyan("\u25A1"),
|
|
815
|
-
"One-shot tick completed; HTTP dashboard remains available until Ctrl+C"
|
|
1147
|
+
parsed.webPort !== void 0 ? "One-shot tick completed; web dashboard remains available until Ctrl+C" : "One-shot tick completed; HTTP dashboard remains available until Ctrl+C"
|
|
816
1148
|
);
|
|
817
|
-
|
|
818
|
-
|
|
1149
|
+
if (shuttingDown) {
|
|
1150
|
+
break;
|
|
1151
|
+
}
|
|
1152
|
+
await new Promise((resolve3) => {
|
|
1153
|
+
keepHttpAliveResolve = resolve3;
|
|
819
1154
|
});
|
|
820
1155
|
} else {
|
|
821
1156
|
await shutdown();
|
|
@@ -905,8 +1240,8 @@ async function shutdownForegroundOrchestrator(input) {
|
|
|
905
1240
|
}
|
|
906
1241
|
async function tailWorkerLog(runtimeRoot, projectId, runId, issueIdentifier) {
|
|
907
1242
|
try {
|
|
908
|
-
const logPath =
|
|
909
|
-
const content = await
|
|
1243
|
+
const logPath = join3(runtimeRoot, "projects", projectId, "runs", runId, "worker.log");
|
|
1244
|
+
const content = await readFile2(logPath, "utf8");
|
|
910
1245
|
const lines = content.split("\n").filter((l) => l.trim());
|
|
911
1246
|
if (lines.length === 0) return;
|
|
912
1247
|
const tail = lines.slice(-30);
|
|
@@ -919,9 +1254,9 @@ async function tailWorkerLog(runtimeRoot, projectId, runId, issueIdentifier) {
|
|
|
919
1254
|
}
|
|
920
1255
|
}
|
|
921
1256
|
var start_default = handler;
|
|
922
|
-
async function startDaemon(options, projectId, logLevel, httpPort) {
|
|
1257
|
+
async function startDaemon(options, projectId, logLevel, httpPort, webPort) {
|
|
923
1258
|
const logPath = orchestratorLogPath(options.configDir, projectId);
|
|
924
|
-
await mkdir(
|
|
1259
|
+
await mkdir(dirname2(logPath), { recursive: true });
|
|
925
1260
|
const { openSync } = await import("fs");
|
|
926
1261
|
const logFd = openSync(logPath, "a");
|
|
927
1262
|
const child = spawn(
|
|
@@ -932,6 +1267,7 @@ async function startDaemon(options, projectId, logLevel, httpPort) {
|
|
|
932
1267
|
"--project",
|
|
933
1268
|
projectId,
|
|
934
1269
|
...httpPort !== void 0 ? ["--http", String(httpPort)] : [],
|
|
1270
|
+
...webPort !== void 0 ? ["--web", String(webPort)] : [],
|
|
935
1271
|
...logLevel ? ["--log-level", logLevel] : []
|
|
936
1272
|
],
|
|
937
1273
|
{
|
|
@@ -945,7 +1281,7 @@ async function startDaemon(options, projectId, logLevel, httpPort) {
|
|
|
945
1281
|
}
|
|
946
1282
|
);
|
|
947
1283
|
const pidPath = daemonPidPath(options.configDir, projectId);
|
|
948
|
-
await mkdir(
|
|
1284
|
+
await mkdir(dirname2(pidPath), { recursive: true });
|
|
949
1285
|
await writeFile(pidPath, String(child.pid), "utf8");
|
|
950
1286
|
child.unref();
|
|
951
1287
|
const { closeSync } = await import("fs");
|