@gh-symphony/cli 0.0.22 → 0.1.3
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 +72 -77
- package/dist/{chunk-HMLBBZNY.js → chunk-2YF7PQUC.js} +16 -71
- package/dist/{chunk-IWFX2FMA.js → chunk-6I753NYO.js} +4 -1
- package/dist/{chunk-2TSM3INR.js → chunk-HQ7A3C7K.js} +575 -12
- package/dist/{chunk-36KYEDEO.js → chunk-MVRF7BES.js} +1 -10
- package/dist/{workflow-L3KT6HB7.js → chunk-NESHTYXQ.js} +27 -19
- package/dist/{chunk-2UW7NQLX.js → chunk-PEZUBHWJ.js} +1 -1
- package/dist/chunk-PG332ZS4.js +238 -0
- package/dist/{chunk-EEQQWTXS.js → chunk-WCOIVNHH.js} +213 -82
- package/dist/{chunk-QIRE2VXS.js → chunk-WOVNN5NW.js} +16 -17
- package/dist/{chunk-C67H3OUL.js → chunk-Z3NZOPLZ.js} +0 -81
- package/dist/{config-cmd-Z3A7V6NC.js → config-cmd-2ADPUYWA.js} +1 -1
- package/dist/{doctor-EJUMPBMW.js → doctor-2AXHIEAP.js} +464 -40
- package/dist/index.js +340 -294
- package/dist/{chunk-PUDXVBSN.js → repo-SUXYT4OK.js} +6272 -2996
- package/dist/{setup-TZJSM3QV.js → setup-UBHOMXUG.js} +57 -92
- package/dist/{upgrade-O33S2SJK.js → upgrade-355SQJ5P.js} +2 -2
- package/dist/{version-CW54Q7BK.js → version-4ILSDZQH.js} +1 -1
- package/dist/worker-entry.js +10 -5
- package/dist/workflow-S6YSZPQT.js +22 -0
- package/package.json +4 -4
- package/dist/chunk-DDL4BWSL.js +0 -146
- package/dist/chunk-DFLXHNYQ.js +0 -482
- package/dist/chunk-E7HYEEZD.js +0 -1318
- package/dist/chunk-GDE6FYN4.js +0 -26
- package/dist/chunk-GSX2FV3M.js +0 -103
- package/dist/chunk-ZHOKYUO3.js +0 -1047
- package/dist/init-54HMKNYI.js +0 -38
- package/dist/logs-GTZ4U5JE.js +0 -188
- package/dist/project-RMYMZSFV.js +0 -25
- package/dist/recover-LTLKMTRX.js +0 -133
- package/dist/repo-WI7GF6XQ.js +0 -749
- package/dist/run-IHN3ZL35.js +0 -122
- package/dist/start-RTAHQMR2.js +0 -19
- package/dist/status-F4D52OVK.js +0 -12
- package/dist/stop-MDKMJPVR.js +0 -10
package/dist/chunk-E7HYEEZD.js
DELETED
|
@@ -1,1318 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
OrchestratorService,
|
|
4
|
-
acquireProjectLock,
|
|
5
|
-
createStore,
|
|
6
|
-
releaseProjectLock,
|
|
7
|
-
resolveOrchestratorLogLevel
|
|
8
|
-
} from "./chunk-PUDXVBSN.js";
|
|
9
|
-
import {
|
|
10
|
-
getGhToken
|
|
11
|
-
} from "./chunk-C67H3OUL.js";
|
|
12
|
-
import {
|
|
13
|
-
deriveIssueWorkspaceKeyFromIdentifier,
|
|
14
|
-
isFileMissing,
|
|
15
|
-
isMatchingIssueRun,
|
|
16
|
-
mapIssueOrchestrationStateToStatus,
|
|
17
|
-
parseRecentEvents,
|
|
18
|
-
readJsonFile,
|
|
19
|
-
safeReadDir
|
|
20
|
-
} from "./chunk-EEQQWTXS.js";
|
|
21
|
-
import {
|
|
22
|
-
bold,
|
|
23
|
-
cyan,
|
|
24
|
-
dim,
|
|
25
|
-
formatRepositoryDisplay,
|
|
26
|
-
green,
|
|
27
|
-
red,
|
|
28
|
-
setNoColor,
|
|
29
|
-
yellow
|
|
30
|
-
} from "./chunk-36KYEDEO.js";
|
|
31
|
-
import {
|
|
32
|
-
resolveRuntimeRoot
|
|
33
|
-
} from "./chunk-IWFX2FMA.js";
|
|
34
|
-
import {
|
|
35
|
-
rejectRemovedProjectId
|
|
36
|
-
} from "./chunk-GDE6FYN4.js";
|
|
37
|
-
import {
|
|
38
|
-
handleMissingManagedProjectConfig,
|
|
39
|
-
resolveManagedProjectConfig
|
|
40
|
-
} from "./chunk-DDL4BWSL.js";
|
|
41
|
-
import {
|
|
42
|
-
daemonPidPath,
|
|
43
|
-
httpStatusPath,
|
|
44
|
-
orchestratorLogPath,
|
|
45
|
-
writeJsonFile
|
|
46
|
-
} from "./chunk-QIRE2VXS.js";
|
|
47
|
-
|
|
48
|
-
// src/commands/start.ts
|
|
49
|
-
import { writeFile, mkdir, readFile as readFile2, rm } from "fs/promises";
|
|
50
|
-
import { dirname as dirname2, join as join3 } from "path";
|
|
51
|
-
import { spawn } from "child_process";
|
|
52
|
-
import { createServer as createServer3 } from "http";
|
|
53
|
-
|
|
54
|
-
// ../dashboard/src/store.ts
|
|
55
|
-
import { open } from "fs/promises";
|
|
56
|
-
import { join, resolve } from "path";
|
|
57
|
-
var DEFAULT_RECENT_EVENT_LIMIT = 20;
|
|
58
|
-
var RECENT_EVENT_CHUNK_SIZE = 4096;
|
|
59
|
-
var MAX_RECENT_EVENT_SCAN_BYTES = 64 * 1024;
|
|
60
|
-
var RUN_RECORD_LOAD_CONCURRENCY = 8;
|
|
61
|
-
var DashboardFsReader = class {
|
|
62
|
-
constructor(runtimeRoot) {
|
|
63
|
-
this.runtimeRoot = runtimeRoot;
|
|
64
|
-
this.resolvedRuntimeRoot = resolve(runtimeRoot);
|
|
65
|
-
}
|
|
66
|
-
resolvedRuntimeRoot;
|
|
67
|
-
projectDir() {
|
|
68
|
-
return this.resolvedRuntimeRoot;
|
|
69
|
-
}
|
|
70
|
-
runDir(runId) {
|
|
71
|
-
assertValidDashboardRunId(runId);
|
|
72
|
-
return join(this.resolvedRuntimeRoot, "runs", runId);
|
|
73
|
-
}
|
|
74
|
-
async loadProjectStatus() {
|
|
75
|
-
const snapshot = await readJsonFile(join(this.projectDir(), "status.json"));
|
|
76
|
-
if (!snapshot) {
|
|
77
|
-
return null;
|
|
78
|
-
}
|
|
79
|
-
const status = { ...snapshot };
|
|
80
|
-
delete status.projectId;
|
|
81
|
-
delete status.slug;
|
|
82
|
-
if (!isRepositoryRef(status.repository)) {
|
|
83
|
-
return null;
|
|
84
|
-
}
|
|
85
|
-
return status;
|
|
86
|
-
}
|
|
87
|
-
async loadProjectState() {
|
|
88
|
-
const snapshot = await this.loadProjectStatus();
|
|
89
|
-
if (!snapshot) {
|
|
90
|
-
return null;
|
|
91
|
-
}
|
|
92
|
-
const issues = await this.loadProjectIssueOrchestrations();
|
|
93
|
-
return {
|
|
94
|
-
...snapshot,
|
|
95
|
-
completedCount: issues.filter((issue) => issue.completedOnce).length,
|
|
96
|
-
issues
|
|
97
|
-
};
|
|
98
|
-
}
|
|
99
|
-
async loadProjectIssueOrchestrations() {
|
|
100
|
-
const issues = await readJsonFile(
|
|
101
|
-
join(this.projectDir(), "issues.json")
|
|
102
|
-
);
|
|
103
|
-
if (issues) {
|
|
104
|
-
return issues.map((issue) => ({
|
|
105
|
-
...issue,
|
|
106
|
-
completedOnce: issue.completedOnce ?? false,
|
|
107
|
-
failureRetryCount: issue.failureRetryCount ?? 0
|
|
108
|
-
}));
|
|
109
|
-
}
|
|
110
|
-
const legacyLeases = await readJsonFile(join(this.projectDir(), "leases.json")) ?? [];
|
|
111
|
-
return legacyLeases.map((lease) => ({
|
|
112
|
-
issueId: lease.issueId,
|
|
113
|
-
identifier: lease.issueIdentifier,
|
|
114
|
-
workspaceKey: deriveIssueWorkspaceKeyFromIdentifier(
|
|
115
|
-
lease.issueIdentifier
|
|
116
|
-
),
|
|
117
|
-
completedOnce: false,
|
|
118
|
-
failureRetryCount: 0,
|
|
119
|
-
state: lease.status === "active" ? "claimed" : "released",
|
|
120
|
-
currentRunId: lease.status === "active" ? lease.runId : null,
|
|
121
|
-
retryEntry: null,
|
|
122
|
-
updatedAt: lease.updatedAt
|
|
123
|
-
}));
|
|
124
|
-
}
|
|
125
|
-
async loadRun(runId) {
|
|
126
|
-
return readJsonFile(
|
|
127
|
-
join(this.runDir(runId), "run.json")
|
|
128
|
-
);
|
|
129
|
-
}
|
|
130
|
-
async loadAllRuns() {
|
|
131
|
-
const runIds = await safeReadDir(join(this.projectDir(), "runs"));
|
|
132
|
-
const runs = await mapWithConcurrency(
|
|
133
|
-
runIds,
|
|
134
|
-
RUN_RECORD_LOAD_CONCURRENCY,
|
|
135
|
-
(runId) => this.loadRun(runId)
|
|
136
|
-
);
|
|
137
|
-
return runs.filter((run) => Boolean(run));
|
|
138
|
-
}
|
|
139
|
-
async loadRunsForIssue(issueId, issueIdentifier) {
|
|
140
|
-
const runIds = await safeReadDir(join(this.projectDir(), "runs"));
|
|
141
|
-
const runs = await mapWithConcurrency(
|
|
142
|
-
runIds,
|
|
143
|
-
RUN_RECORD_LOAD_CONCURRENCY,
|
|
144
|
-
async (runId) => {
|
|
145
|
-
try {
|
|
146
|
-
const run = await this.loadRun(runId);
|
|
147
|
-
if (!run) {
|
|
148
|
-
return null;
|
|
149
|
-
}
|
|
150
|
-
return run.issueId === issueId || run.issueIdentifier === issueIdentifier ? run : null;
|
|
151
|
-
} catch (error) {
|
|
152
|
-
if (isFileMissing(error)) {
|
|
153
|
-
return null;
|
|
154
|
-
}
|
|
155
|
-
return null;
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
);
|
|
159
|
-
return runs.filter((run) => Boolean(run));
|
|
160
|
-
}
|
|
161
|
-
async loadRecentRunEvents(runId, limit = DEFAULT_RECENT_EVENT_LIMIT) {
|
|
162
|
-
if (limit <= 0) {
|
|
163
|
-
return [];
|
|
164
|
-
}
|
|
165
|
-
const path = join(this.runDir(runId), "events.ndjson");
|
|
166
|
-
try {
|
|
167
|
-
const handle = await open(path, "r");
|
|
168
|
-
try {
|
|
169
|
-
const stats = await handle.stat();
|
|
170
|
-
let position = stats.size;
|
|
171
|
-
let bytesScanned = 0;
|
|
172
|
-
let newlineCount = 0;
|
|
173
|
-
const chunks = [];
|
|
174
|
-
while (position > 0 && bytesScanned < MAX_RECENT_EVENT_SCAN_BYTES && newlineCount <= limit) {
|
|
175
|
-
const readSize = Math.min(
|
|
176
|
-
position,
|
|
177
|
-
RECENT_EVENT_CHUNK_SIZE,
|
|
178
|
-
MAX_RECENT_EVENT_SCAN_BYTES - bytesScanned
|
|
179
|
-
);
|
|
180
|
-
position -= readSize;
|
|
181
|
-
const chunk = Buffer.allocUnsafe(readSize);
|
|
182
|
-
const { bytesRead } = await handle.read(chunk, 0, readSize, position);
|
|
183
|
-
if (bytesRead === 0) {
|
|
184
|
-
break;
|
|
185
|
-
}
|
|
186
|
-
const populatedChunk = chunk.subarray(0, bytesRead);
|
|
187
|
-
chunks.unshift(populatedChunk);
|
|
188
|
-
bytesScanned += bytesRead;
|
|
189
|
-
newlineCount += countNewlines(populatedChunk);
|
|
190
|
-
}
|
|
191
|
-
return parseRecentEvents(
|
|
192
|
-
Buffer.concat(chunks).toString("utf8"),
|
|
193
|
-
limit,
|
|
194
|
-
{
|
|
195
|
-
allowPartialFirstLine: position > 0
|
|
196
|
-
}
|
|
197
|
-
);
|
|
198
|
-
} finally {
|
|
199
|
-
await handle.close();
|
|
200
|
-
}
|
|
201
|
-
} catch (error) {
|
|
202
|
-
if (isFileMissing(error)) {
|
|
203
|
-
return [];
|
|
204
|
-
}
|
|
205
|
-
throw error;
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
};
|
|
209
|
-
function countNewlines(chunk) {
|
|
210
|
-
let count = 0;
|
|
211
|
-
for (const byte of chunk) {
|
|
212
|
-
if (byte === 10) {
|
|
213
|
-
count += 1;
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
return count;
|
|
217
|
-
}
|
|
218
|
-
async function statusForIssue(reader, issueIdentifier) {
|
|
219
|
-
const issueRecords = await reader.loadProjectIssueOrchestrations();
|
|
220
|
-
const issueRecord = issueRecords.find(
|
|
221
|
-
(record) => record.identifier === issueIdentifier
|
|
222
|
-
);
|
|
223
|
-
if (!issueRecord) {
|
|
224
|
-
return null;
|
|
225
|
-
}
|
|
226
|
-
const currentRunCandidate = issueRecord.currentRunId ? await reader.loadRun(issueRecord.currentRunId) : null;
|
|
227
|
-
const currentRun = isMatchingIssueRun(
|
|
228
|
-
currentRunCandidate,
|
|
229
|
-
issueRecord.issueId,
|
|
230
|
-
issueIdentifier
|
|
231
|
-
) ? currentRunCandidate : null;
|
|
232
|
-
const issueRuns = currentRun === null ? await reader.loadRunsForIssue(issueRecord.issueId, issueIdentifier) : currentRun.tokenUsage ? await reader.loadRunsForIssue(issueRecord.issueId, issueIdentifier) : null;
|
|
233
|
-
const resolvedRun = currentRun ?? findLatestRunForIssue(issueRuns ?? []);
|
|
234
|
-
const recentEvents = resolvedRun === null ? [] : await reader.loadRecentRunEvents(resolvedRun.runId);
|
|
235
|
-
const cumulativeTokens = aggregateIssueTokenUsage(issueRuns ?? []);
|
|
236
|
-
const latestEventMessage = recentEvents[recentEvents.length - 1]?.message ?? null;
|
|
237
|
-
const currentAttempt = resolvedRun?.attempt ?? issueRecord.retryEntry?.attempt ?? 0;
|
|
238
|
-
return {
|
|
239
|
-
issue_identifier: issueRecord.identifier,
|
|
240
|
-
issue_id: issueRecord.issueId,
|
|
241
|
-
status: resolvedRun?.status ?? mapIssueOrchestrationStateToStatus(issueRecord.state),
|
|
242
|
-
workspace: {
|
|
243
|
-
path: resolvedRun?.workingDirectory ?? null
|
|
244
|
-
},
|
|
245
|
-
attempts: {
|
|
246
|
-
restart_count: Math.max(0, currentAttempt - 1),
|
|
247
|
-
current_retry_attempt: currentAttempt
|
|
248
|
-
},
|
|
249
|
-
running: resolvedRun === null ? null : {
|
|
250
|
-
session_id: resolvedRun.runtimeSession?.sessionId ?? null,
|
|
251
|
-
turn_count: resolvedRun.turnCount ?? null,
|
|
252
|
-
state: resolvedRun.issueState ?? null,
|
|
253
|
-
started_at: resolvedRun.startedAt ?? null,
|
|
254
|
-
last_event: resolvedRun.lastEvent ?? null,
|
|
255
|
-
last_message: latestEventMessage,
|
|
256
|
-
last_event_at: resolvedRun.lastEventAt ?? null,
|
|
257
|
-
tokens: resolvedRun.tokenUsage ? {
|
|
258
|
-
input_tokens: resolvedRun.tokenUsage.inputTokens,
|
|
259
|
-
output_tokens: resolvedRun.tokenUsage.outputTokens,
|
|
260
|
-
total_tokens: resolvedRun.tokenUsage.totalTokens,
|
|
261
|
-
cumulative_input_tokens: cumulativeTokens.inputTokens,
|
|
262
|
-
cumulative_output_tokens: cumulativeTokens.outputTokens,
|
|
263
|
-
cumulative_total_tokens: cumulativeTokens.totalTokens
|
|
264
|
-
} : null
|
|
265
|
-
},
|
|
266
|
-
retry: resolvedRun?.nextRetryAt ?? issueRecord.retryEntry?.dueAt ? {
|
|
267
|
-
due_at: resolvedRun?.nextRetryAt ?? issueRecord.retryEntry?.dueAt ?? "",
|
|
268
|
-
kind: resolvedRun?.retryKind ?? null,
|
|
269
|
-
error: resolvedRun?.lastError ?? issueRecord.retryEntry?.error ?? null
|
|
270
|
-
} : null,
|
|
271
|
-
logs: {
|
|
272
|
-
codex_session_logs: resolvedRun === null ? [] : [
|
|
273
|
-
{
|
|
274
|
-
label: "worker",
|
|
275
|
-
path: join(reader.runDir(resolvedRun.runId), "worker.log"),
|
|
276
|
-
url: null
|
|
277
|
-
}
|
|
278
|
-
]
|
|
279
|
-
},
|
|
280
|
-
recent_events: recentEvents,
|
|
281
|
-
last_error: resolvedRun?.lastError ?? issueRecord.retryEntry?.error ?? null,
|
|
282
|
-
tracked: {
|
|
283
|
-
issue_orchestration_state: issueRecord.state,
|
|
284
|
-
current_run_id: issueRecord.currentRunId,
|
|
285
|
-
workspace_key: issueRecord.workspaceKey,
|
|
286
|
-
completed_once: issueRecord.completedOnce,
|
|
287
|
-
run_phase: resolvedRun?.runPhase ?? null,
|
|
288
|
-
execution_phase: resolvedRun?.executionPhase ?? null
|
|
289
|
-
}
|
|
290
|
-
};
|
|
291
|
-
}
|
|
292
|
-
function aggregateIssueTokenUsage(runs) {
|
|
293
|
-
return runs.reduce(
|
|
294
|
-
(total, run) => ({
|
|
295
|
-
inputTokens: total.inputTokens + (run.tokenUsage?.inputTokens ?? 0),
|
|
296
|
-
outputTokens: total.outputTokens + (run.tokenUsage?.outputTokens ?? 0),
|
|
297
|
-
totalTokens: total.totalTokens + (run.tokenUsage?.totalTokens ?? 0)
|
|
298
|
-
}),
|
|
299
|
-
{
|
|
300
|
-
inputTokens: 0,
|
|
301
|
-
outputTokens: 0,
|
|
302
|
-
totalTokens: 0
|
|
303
|
-
}
|
|
304
|
-
);
|
|
305
|
-
}
|
|
306
|
-
function findLatestRunForIssue(matchingRuns) {
|
|
307
|
-
const sortedRuns = [...matchingRuns].sort(
|
|
308
|
-
(left, right) => new Date(right.updatedAt).getTime() - new Date(left.updatedAt).getTime()
|
|
309
|
-
);
|
|
310
|
-
return sortedRuns[0] ?? null;
|
|
311
|
-
}
|
|
312
|
-
function assertValidDashboardRunId(runId) {
|
|
313
|
-
if (runId.length === 0 || runId === "." || runId === ".." || runId.includes("/") || runId.includes("\\")) {
|
|
314
|
-
throw new Error(
|
|
315
|
-
`Invalid run ID "${runId}". Run IDs must not contain path separators or traversal segments.`
|
|
316
|
-
);
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
function isRepositoryRef(value) {
|
|
320
|
-
if (!value || typeof value !== "object") {
|
|
321
|
-
return false;
|
|
322
|
-
}
|
|
323
|
-
const repository = value;
|
|
324
|
-
return typeof repository.owner === "string" && repository.owner.length > 0 && typeof repository.name === "string" && repository.name.length > 0 && typeof repository.cloneUrl === "string";
|
|
325
|
-
}
|
|
326
|
-
async function mapWithConcurrency(items, concurrency, mapper) {
|
|
327
|
-
const results = new Array(items.length);
|
|
328
|
-
let nextIndex = 0;
|
|
329
|
-
const worker = async () => {
|
|
330
|
-
while (nextIndex < items.length) {
|
|
331
|
-
const currentIndex = nextIndex;
|
|
332
|
-
nextIndex += 1;
|
|
333
|
-
results[currentIndex] = await mapper(items[currentIndex]);
|
|
334
|
-
}
|
|
335
|
-
};
|
|
336
|
-
const workerCount = Math.min(Math.max(concurrency, 1), items.length);
|
|
337
|
-
await Promise.all(Array.from({ length: workerCount }, () => worker()));
|
|
338
|
-
return results;
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
// ../dashboard/src/server.ts
|
|
342
|
-
import {
|
|
343
|
-
createServer
|
|
344
|
-
} from "http";
|
|
345
|
-
async function resolveDashboardResponse(options) {
|
|
346
|
-
const method = options.method ?? "GET";
|
|
347
|
-
if (options.pathname === "/healthz") {
|
|
348
|
-
return {
|
|
349
|
-
status: 200,
|
|
350
|
-
payload: { ok: true }
|
|
351
|
-
};
|
|
352
|
-
}
|
|
353
|
-
if (options.pathname === "/api/v1/state") {
|
|
354
|
-
if (method !== "GET") {
|
|
355
|
-
return {
|
|
356
|
-
status: 405,
|
|
357
|
-
payload: { error: "Method not allowed" }
|
|
358
|
-
};
|
|
359
|
-
}
|
|
360
|
-
const snapshot = await options.reader.loadProjectState();
|
|
361
|
-
if (!snapshot) {
|
|
362
|
-
return {
|
|
363
|
-
status: 404,
|
|
364
|
-
payload: { error: "Project status not found." }
|
|
365
|
-
};
|
|
366
|
-
}
|
|
367
|
-
return {
|
|
368
|
-
status: 200,
|
|
369
|
-
payload: snapshot
|
|
370
|
-
};
|
|
371
|
-
}
|
|
372
|
-
if (options.pathname.startsWith("/api/v1/")) {
|
|
373
|
-
if (method !== "GET") {
|
|
374
|
-
return {
|
|
375
|
-
status: 405,
|
|
376
|
-
payload: { error: "Method not allowed" }
|
|
377
|
-
};
|
|
378
|
-
}
|
|
379
|
-
const rawIdentifier = options.pathname.slice("/api/v1/".length);
|
|
380
|
-
if (!rawIdentifier || rawIdentifier === "state") {
|
|
381
|
-
return {
|
|
382
|
-
status: 404,
|
|
383
|
-
payload: { error: "Not found" }
|
|
384
|
-
};
|
|
385
|
-
}
|
|
386
|
-
let issueIdentifier;
|
|
387
|
-
try {
|
|
388
|
-
issueIdentifier = decodeURIComponent(rawIdentifier);
|
|
389
|
-
} catch {
|
|
390
|
-
return {
|
|
391
|
-
status: 400,
|
|
392
|
-
payload: {
|
|
393
|
-
error: {
|
|
394
|
-
code: "invalid_issue_identifier",
|
|
395
|
-
message: "Issue identifier path segment is not valid URL encoding."
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
};
|
|
399
|
-
}
|
|
400
|
-
const issueStatus = await statusForIssue(options.reader, issueIdentifier);
|
|
401
|
-
if (!issueStatus) {
|
|
402
|
-
return {
|
|
403
|
-
status: 404,
|
|
404
|
-
payload: {
|
|
405
|
-
error: {
|
|
406
|
-
code: "issue_not_found",
|
|
407
|
-
message: `Issue "${issueIdentifier}" is unknown to the current filesystem state.`
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
};
|
|
411
|
-
}
|
|
412
|
-
return {
|
|
413
|
-
status: 200,
|
|
414
|
-
payload: issueStatus
|
|
415
|
-
};
|
|
416
|
-
}
|
|
417
|
-
return {
|
|
418
|
-
status: 404,
|
|
419
|
-
payload: { error: "Not found" }
|
|
420
|
-
};
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
// ../control-plane/src/server.ts
|
|
424
|
-
import {
|
|
425
|
-
createServer as createServer2
|
|
426
|
-
} from "http";
|
|
427
|
-
import { readFile, stat } from "fs/promises";
|
|
428
|
-
import { dirname, extname, join as join2, resolve as resolve2, sep } from "path";
|
|
429
|
-
import { fileURLToPath } from "url";
|
|
430
|
-
var CLIENT_DIST_DIR = join2(
|
|
431
|
-
dirname(fileURLToPath(import.meta.url)),
|
|
432
|
-
"../client/dist"
|
|
433
|
-
);
|
|
434
|
-
var BUNDLED_CLIENT_DIST_DIR = join2(
|
|
435
|
-
dirname(fileURLToPath(import.meta.url)),
|
|
436
|
-
"../../control-plane/client/dist"
|
|
437
|
-
);
|
|
438
|
-
var WORKSPACE_CLIENT_DIST_DIR = join2(
|
|
439
|
-
process.cwd(),
|
|
440
|
-
"packages/control-plane/client/dist"
|
|
441
|
-
);
|
|
442
|
-
var NODE_MODULES_CLIENT_DIST_DIR = join2(
|
|
443
|
-
process.cwd(),
|
|
444
|
-
"node_modules/@gh-symphony/control-plane/client/dist"
|
|
445
|
-
);
|
|
446
|
-
var CLIENT_DIST_DIR_CANDIDATES = [
|
|
447
|
-
CLIENT_DIST_DIR,
|
|
448
|
-
BUNDLED_CLIENT_DIST_DIR,
|
|
449
|
-
WORKSPACE_CLIENT_DIST_DIR,
|
|
450
|
-
NODE_MODULES_CLIENT_DIST_DIR
|
|
451
|
-
];
|
|
452
|
-
var clientDistDirPromise;
|
|
453
|
-
var TEXT_CONTENT_TYPES = /* @__PURE__ */ new Set([
|
|
454
|
-
"application/javascript",
|
|
455
|
-
"application/json",
|
|
456
|
-
"image/svg+xml",
|
|
457
|
-
"text/css",
|
|
458
|
-
"text/html",
|
|
459
|
-
"text/plain"
|
|
460
|
-
]);
|
|
461
|
-
var CONTENT_TYPES = {
|
|
462
|
-
".css": "text/css",
|
|
463
|
-
".gif": "image/gif",
|
|
464
|
-
".html": "text/html",
|
|
465
|
-
".ico": "image/x-icon",
|
|
466
|
-
".jpeg": "image/jpeg",
|
|
467
|
-
".jpg": "image/jpeg",
|
|
468
|
-
".js": "application/javascript",
|
|
469
|
-
".json": "application/json",
|
|
470
|
-
".map": "application/json",
|
|
471
|
-
".png": "image/png",
|
|
472
|
-
".svg": "image/svg+xml",
|
|
473
|
-
".txt": "text/plain",
|
|
474
|
-
".webp": "image/webp"
|
|
475
|
-
};
|
|
476
|
-
function createControlPlaneHandler(options) {
|
|
477
|
-
return async (request, response) => {
|
|
478
|
-
try {
|
|
479
|
-
const method = request.method ?? "GET";
|
|
480
|
-
const url = new URL(request.url ?? "/", "http://127.0.0.1");
|
|
481
|
-
if (url.pathname === "/api/v1/refresh") {
|
|
482
|
-
await handleRefreshRequest(method, request, response, options);
|
|
483
|
-
return;
|
|
484
|
-
}
|
|
485
|
-
if (isDashboardRequest(url.pathname)) {
|
|
486
|
-
const resolved = await resolveDashboardResponse({
|
|
487
|
-
pathname: url.pathname,
|
|
488
|
-
method,
|
|
489
|
-
reader: options.reader
|
|
490
|
-
});
|
|
491
|
-
respondJson(response, resolved.status, resolved.payload);
|
|
492
|
-
return;
|
|
493
|
-
}
|
|
494
|
-
if (!isStaticRequestMethod(method)) {
|
|
495
|
-
respondJson(response, 405, { error: "Method not allowed" });
|
|
496
|
-
return;
|
|
497
|
-
}
|
|
498
|
-
const asset = await resolveStaticAsset(url.pathname);
|
|
499
|
-
if (!asset) {
|
|
500
|
-
respondJson(response, 404, { error: "Not found" });
|
|
501
|
-
return;
|
|
502
|
-
}
|
|
503
|
-
if (asset.kind === "error") {
|
|
504
|
-
respondJson(response, asset.status, { error: "Bad request" });
|
|
505
|
-
return;
|
|
506
|
-
}
|
|
507
|
-
await respondFile(response, asset.path, method, asset.fallback);
|
|
508
|
-
} catch (error) {
|
|
509
|
-
console.error("Control plane request failed.", error);
|
|
510
|
-
if (!response.headersSent) {
|
|
511
|
-
respondJson(response, 500, { error: "Internal server error" });
|
|
512
|
-
} else {
|
|
513
|
-
response.end();
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
};
|
|
517
|
-
}
|
|
518
|
-
async function startControlPlaneServer(options) {
|
|
519
|
-
const reader = new DashboardFsReader(options.runtimeRoot);
|
|
520
|
-
const handler2 = createControlPlaneHandler({
|
|
521
|
-
reader,
|
|
522
|
-
onRefreshRequest: options.onRefreshRequest
|
|
523
|
-
});
|
|
524
|
-
for (let port = options.port; port <= 65535; port += 1) {
|
|
525
|
-
const server = createServer2((request, response) => {
|
|
526
|
-
void handler2(request, response);
|
|
527
|
-
});
|
|
528
|
-
try {
|
|
529
|
-
await new Promise((resolveReady, rejectReady) => {
|
|
530
|
-
const cleanup = () => {
|
|
531
|
-
server.off("listening", handleListening);
|
|
532
|
-
server.off("error", handleError);
|
|
533
|
-
};
|
|
534
|
-
const handleListening = () => {
|
|
535
|
-
cleanup();
|
|
536
|
-
resolveReady();
|
|
537
|
-
};
|
|
538
|
-
const handleError = (error) => {
|
|
539
|
-
cleanup();
|
|
540
|
-
rejectReady(error);
|
|
541
|
-
};
|
|
542
|
-
server.once("listening", handleListening);
|
|
543
|
-
server.once("error", handleError);
|
|
544
|
-
server.listen(port, options.host);
|
|
545
|
-
});
|
|
546
|
-
const address = server.address();
|
|
547
|
-
const boundPort = address && typeof address !== "string" ? address.port : port;
|
|
548
|
-
return {
|
|
549
|
-
server,
|
|
550
|
-
port: boundPort,
|
|
551
|
-
url: formatBoundUrl(server)
|
|
552
|
-
};
|
|
553
|
-
} catch (error) {
|
|
554
|
-
await closeServer(server).catch(() => {
|
|
555
|
-
});
|
|
556
|
-
if (error.code === "EADDRINUSE") {
|
|
557
|
-
continue;
|
|
558
|
-
}
|
|
559
|
-
throw error;
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
throw new Error(
|
|
563
|
-
`Unable to bind control plane server starting from port ${options.port}`
|
|
564
|
-
);
|
|
565
|
-
}
|
|
566
|
-
async function handleRefreshRequest(method, request, response, options) {
|
|
567
|
-
if (method !== "POST") {
|
|
568
|
-
respondJson(response, 405, { error: "Method not allowed" });
|
|
569
|
-
return;
|
|
570
|
-
}
|
|
571
|
-
request.resume();
|
|
572
|
-
options.onRefreshRequest?.();
|
|
573
|
-
respondJson(response, 202, { ok: true });
|
|
574
|
-
}
|
|
575
|
-
function isDashboardRequest(pathname) {
|
|
576
|
-
return pathname === "/healthz" || pathname === "/api/v1/state" || pathname.startsWith("/api/v1/");
|
|
577
|
-
}
|
|
578
|
-
function isStaticRequestMethod(method) {
|
|
579
|
-
return method === "GET" || method === "HEAD";
|
|
580
|
-
}
|
|
581
|
-
async function resolveStaticAsset(pathname) {
|
|
582
|
-
const clientDistDir = await resolveClientDistDir();
|
|
583
|
-
if (!clientDistDir) {
|
|
584
|
-
return null;
|
|
585
|
-
}
|
|
586
|
-
const indexPath = join2(clientDistDir, "index.html");
|
|
587
|
-
if (pathname === "/") {
|
|
588
|
-
return await existsAsFile(indexPath) ? { kind: "asset", path: indexPath, fallback: true } : null;
|
|
589
|
-
}
|
|
590
|
-
let decodedPathname;
|
|
591
|
-
try {
|
|
592
|
-
decodedPathname = decodeURIComponent(pathname);
|
|
593
|
-
} catch {
|
|
594
|
-
return { kind: "error", status: 400 };
|
|
595
|
-
}
|
|
596
|
-
const resolvedPath = resolve2(clientDistDir, `.${decodedPathname}`);
|
|
597
|
-
if (!isPathInsideClientDist(clientDistDir, resolvedPath)) {
|
|
598
|
-
return null;
|
|
599
|
-
}
|
|
600
|
-
if (await existsAsFile(resolvedPath)) {
|
|
601
|
-
return { kind: "asset", path: resolvedPath, fallback: false };
|
|
602
|
-
}
|
|
603
|
-
if (hasFileExtension(decodedPathname)) {
|
|
604
|
-
return null;
|
|
605
|
-
}
|
|
606
|
-
return await existsAsFile(indexPath) ? { kind: "asset", path: indexPath, fallback: true } : null;
|
|
607
|
-
}
|
|
608
|
-
function isPathInsideClientDist(clientDistDir, path) {
|
|
609
|
-
return path === clientDistDir || path.startsWith(`${clientDistDir}${sep}`);
|
|
610
|
-
}
|
|
611
|
-
function hasFileExtension(pathname) {
|
|
612
|
-
const lastSegment = pathname.split("/").pop() ?? "";
|
|
613
|
-
return lastSegment.includes(".");
|
|
614
|
-
}
|
|
615
|
-
async function existsAsFile(path) {
|
|
616
|
-
try {
|
|
617
|
-
return (await stat(path)).isFile();
|
|
618
|
-
} catch {
|
|
619
|
-
return false;
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
async function resolveClientDistDir() {
|
|
623
|
-
clientDistDirPromise ??= (async () => {
|
|
624
|
-
for (const candidate of CLIENT_DIST_DIR_CANDIDATES) {
|
|
625
|
-
try {
|
|
626
|
-
if ((await stat(candidate)).isDirectory()) {
|
|
627
|
-
return candidate;
|
|
628
|
-
}
|
|
629
|
-
} catch {
|
|
630
|
-
continue;
|
|
631
|
-
}
|
|
632
|
-
}
|
|
633
|
-
return null;
|
|
634
|
-
})();
|
|
635
|
-
return clientDistDirPromise;
|
|
636
|
-
}
|
|
637
|
-
async function respondFile(response, path, method, fallback) {
|
|
638
|
-
const contentType = contentTypeForPath(path);
|
|
639
|
-
const body = method === "HEAD" ? void 0 : await readFile(path);
|
|
640
|
-
const cacheControl = fallback || path.endsWith(`${sep}index.html`) ? "no-cache" : "public, max-age=31536000, immutable";
|
|
641
|
-
response.writeHead(200, {
|
|
642
|
-
"cache-control": cacheControl,
|
|
643
|
-
"content-type": contentType
|
|
644
|
-
});
|
|
645
|
-
response.end(body);
|
|
646
|
-
}
|
|
647
|
-
function contentTypeForPath(path) {
|
|
648
|
-
const contentType = CONTENT_TYPES[extname(path).toLowerCase()];
|
|
649
|
-
if (!contentType) {
|
|
650
|
-
return "application/octet-stream";
|
|
651
|
-
}
|
|
652
|
-
if (TEXT_CONTENT_TYPES.has(contentType)) {
|
|
653
|
-
return `${contentType}; charset=utf-8`;
|
|
654
|
-
}
|
|
655
|
-
return contentType;
|
|
656
|
-
}
|
|
657
|
-
function respondJson(response, status, payload) {
|
|
658
|
-
response.writeHead(status, {
|
|
659
|
-
"content-type": "application/json; charset=utf-8"
|
|
660
|
-
});
|
|
661
|
-
response.end(JSON.stringify(payload));
|
|
662
|
-
}
|
|
663
|
-
function formatBoundUrl(server) {
|
|
664
|
-
const address = server.address();
|
|
665
|
-
if (!address || typeof address === "string") {
|
|
666
|
-
return "http://localhost";
|
|
667
|
-
}
|
|
668
|
-
const host = address.address === "::" || address.address === "::1" || address.address === "0.0.0.0" || address.address === "127.0.0.1" ? "localhost" : address.address;
|
|
669
|
-
const urlHost = host.includes(":") ? `[${host}]` : host;
|
|
670
|
-
return `http://${urlHost}:${address.port}`;
|
|
671
|
-
}
|
|
672
|
-
async function closeServer(server) {
|
|
673
|
-
await new Promise((resolveClose, rejectClose) => {
|
|
674
|
-
server.close((error) => {
|
|
675
|
-
if (error) {
|
|
676
|
-
rejectClose(error);
|
|
677
|
-
return;
|
|
678
|
-
}
|
|
679
|
-
resolveClose();
|
|
680
|
-
});
|
|
681
|
-
});
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
// src/commands/start.ts
|
|
685
|
-
function timestamp() {
|
|
686
|
-
const now = /* @__PURE__ */ new Date();
|
|
687
|
-
const hh = String(now.getHours()).padStart(2, "0");
|
|
688
|
-
const mm = String(now.getMinutes()).padStart(2, "0");
|
|
689
|
-
const ss = String(now.getSeconds()).padStart(2, "0");
|
|
690
|
-
return dim(`${hh}:${mm}:${ss}`);
|
|
691
|
-
}
|
|
692
|
-
function logLine(icon, msg) {
|
|
693
|
-
process.stdout.write(`${timestamp()} ${icon} ${msg}
|
|
694
|
-
`);
|
|
695
|
-
}
|
|
696
|
-
var DEFAULT_HTTP_PORT = 4680;
|
|
697
|
-
var HTTP_HOST = "0.0.0.0";
|
|
698
|
-
function parseStartArgs(args) {
|
|
699
|
-
const parsed = {
|
|
700
|
-
daemon: false,
|
|
701
|
-
once: false
|
|
702
|
-
};
|
|
703
|
-
for (let i = 0; i < args.length; i += 1) {
|
|
704
|
-
const arg = args[i];
|
|
705
|
-
if (arg === "--daemon" || arg === "-d") {
|
|
706
|
-
parsed.daemon = true;
|
|
707
|
-
continue;
|
|
708
|
-
}
|
|
709
|
-
if (arg === "--once") {
|
|
710
|
-
parsed.once = true;
|
|
711
|
-
continue;
|
|
712
|
-
}
|
|
713
|
-
if (arg === "--http") {
|
|
714
|
-
const value = args[i + 1];
|
|
715
|
-
if (!value || value.startsWith("-")) {
|
|
716
|
-
parsed.httpPort = DEFAULT_HTTP_PORT;
|
|
717
|
-
continue;
|
|
718
|
-
}
|
|
719
|
-
parsed.httpPort = parsePort(value, arg);
|
|
720
|
-
i += 1;
|
|
721
|
-
continue;
|
|
722
|
-
}
|
|
723
|
-
if (arg === "--web") {
|
|
724
|
-
const value = args[i + 1];
|
|
725
|
-
if (!value || value.startsWith("-")) {
|
|
726
|
-
parsed.webPort = DEFAULT_HTTP_PORT;
|
|
727
|
-
continue;
|
|
728
|
-
}
|
|
729
|
-
parsed.webPort = parsePort(value, arg);
|
|
730
|
-
i += 1;
|
|
731
|
-
continue;
|
|
732
|
-
}
|
|
733
|
-
if (arg === "--log-level") {
|
|
734
|
-
const value = args[i + 1];
|
|
735
|
-
if (!value || value.startsWith("-")) {
|
|
736
|
-
parsed.error = `Option '${arg}' argument missing`;
|
|
737
|
-
return parsed;
|
|
738
|
-
}
|
|
739
|
-
parsed.logLevel = value;
|
|
740
|
-
i += 1;
|
|
741
|
-
continue;
|
|
742
|
-
}
|
|
743
|
-
if (arg?.startsWith("-")) {
|
|
744
|
-
parsed.error = `Unknown option '${arg}'`;
|
|
745
|
-
return parsed;
|
|
746
|
-
}
|
|
747
|
-
}
|
|
748
|
-
if (parsed.httpPort !== void 0 && parsed.webPort !== void 0) {
|
|
749
|
-
parsed.error = "Options '--http' and '--web' cannot be used together";
|
|
750
|
-
}
|
|
751
|
-
return parsed;
|
|
752
|
-
}
|
|
753
|
-
function logTickResult(snapshot, prevSnapshot, isFirst) {
|
|
754
|
-
if (isFirst) {
|
|
755
|
-
const healthColor = snapshot.health === "degraded" ? red : snapshot.health === "running" ? green : cyan;
|
|
756
|
-
logLine(
|
|
757
|
-
green("\u25CF"),
|
|
758
|
-
`Repository ${bold(formatRepositoryDisplay(snapshot))} connected ${dim(
|
|
759
|
-
"("
|
|
760
|
-
)}${healthColor(snapshot.health)}${dim(")")}`
|
|
761
|
-
);
|
|
762
|
-
if (snapshot.summary.activeRuns > 0) {
|
|
763
|
-
logLine(cyan("\u25B8"), `${snapshot.summary.activeRuns} active run(s)`);
|
|
764
|
-
}
|
|
765
|
-
return;
|
|
766
|
-
}
|
|
767
|
-
if (prevSnapshot && prevSnapshot.health !== snapshot.health) {
|
|
768
|
-
const icon = snapshot.health === "degraded" ? red("\u25CF") : green("\u25CF");
|
|
769
|
-
logLine(
|
|
770
|
-
icon,
|
|
771
|
-
`Health changed: ${prevSnapshot.health} \u2192 ${bold(snapshot.health)}`
|
|
772
|
-
);
|
|
773
|
-
}
|
|
774
|
-
if (snapshot.lastError && snapshot.lastError !== prevSnapshot?.lastError) {
|
|
775
|
-
logLine(red("\u2717"), red(snapshot.lastError));
|
|
776
|
-
}
|
|
777
|
-
if (!snapshot.lastError && prevSnapshot?.lastError) {
|
|
778
|
-
logLine(green("\u2713"), green("Error cleared"));
|
|
779
|
-
}
|
|
780
|
-
const prevDispatched = prevSnapshot?.summary.dispatched ?? 0;
|
|
781
|
-
if (snapshot.summary.dispatched > prevDispatched) {
|
|
782
|
-
const delta = snapshot.summary.dispatched - prevDispatched;
|
|
783
|
-
logLine(yellow("\u25B8"), `Dispatched ${bold(String(delta))} new run(s)`);
|
|
784
|
-
}
|
|
785
|
-
const prevRunIds = new Set(
|
|
786
|
-
prevSnapshot?.activeRuns.map((run) => run.runId) ?? []
|
|
787
|
-
);
|
|
788
|
-
for (const run of snapshot.activeRuns) {
|
|
789
|
-
if (!prevRunIds.has(run.runId)) {
|
|
790
|
-
logLine(
|
|
791
|
-
cyan("\u25B8"),
|
|
792
|
-
`Run started: ${bold(run.issueIdentifier)} ${dim("state=")}${run.issueState} ${dim("status=")}${run.status}`
|
|
793
|
-
);
|
|
794
|
-
}
|
|
795
|
-
}
|
|
796
|
-
const currentRunIds = new Set(snapshot.activeRuns.map((run) => run.runId));
|
|
797
|
-
for (const prevRun of prevSnapshot?.activeRuns ?? []) {
|
|
798
|
-
if (!currentRunIds.has(prevRun.runId)) {
|
|
799
|
-
logLine(
|
|
800
|
-
green("\u2713"),
|
|
801
|
-
`Run finished: ${bold(prevRun.issueIdentifier)} ${dim("(")}${prevRun.status}${dim(")")}`
|
|
802
|
-
);
|
|
803
|
-
}
|
|
804
|
-
}
|
|
805
|
-
const prevSuppressed = prevSnapshot?.summary.suppressed ?? 0;
|
|
806
|
-
if (snapshot.summary.suppressed > prevSuppressed) {
|
|
807
|
-
const delta = snapshot.summary.suppressed - prevSuppressed;
|
|
808
|
-
logLine(
|
|
809
|
-
dim("\u25CB"),
|
|
810
|
-
dim(`${delta} issue(s) suppressed (already running or at limit)`)
|
|
811
|
-
);
|
|
812
|
-
}
|
|
813
|
-
const prevRecovered = prevSnapshot?.summary.recovered ?? 0;
|
|
814
|
-
if (snapshot.summary.recovered > prevRecovered) {
|
|
815
|
-
const delta = snapshot.summary.recovered - prevRecovered;
|
|
816
|
-
logLine(
|
|
817
|
-
yellow("\u21BA"),
|
|
818
|
-
`Recovered ${bold(String(delta))} stalled run(s)`
|
|
819
|
-
);
|
|
820
|
-
}
|
|
821
|
-
const prevRetryCount = prevSnapshot?.retryQueue.length ?? 0;
|
|
822
|
-
if (snapshot.retryQueue.length > prevRetryCount) {
|
|
823
|
-
const delta = snapshot.retryQueue.length - prevRetryCount;
|
|
824
|
-
logLine(yellow("\u25CC"), `${delta} run(s) queued for retry`);
|
|
825
|
-
}
|
|
826
|
-
const changed = snapshot.health !== prevSnapshot?.health || snapshot.lastError !== prevSnapshot?.lastError || snapshot.summary.dispatched !== prevSnapshot?.summary.dispatched || snapshot.summary.suppressed !== prevSnapshot?.summary.suppressed || snapshot.summary.recovered !== prevSnapshot?.summary.recovered || snapshot.activeRuns.length !== (prevSnapshot?.activeRuns.length ?? 0) || snapshot.retryQueue.length !== (prevSnapshot?.retryQueue.length ?? 0);
|
|
827
|
-
if (!changed) {
|
|
828
|
-
logLine(
|
|
829
|
-
dim("\xB7"),
|
|
830
|
-
dim(
|
|
831
|
-
`tick \u2014 ${snapshot.summary.activeRuns} active, ${snapshot.health}`
|
|
832
|
-
)
|
|
833
|
-
);
|
|
834
|
-
}
|
|
835
|
-
}
|
|
836
|
-
function parsePort(value, optionName) {
|
|
837
|
-
if (!/^\d+$/.test(value)) {
|
|
838
|
-
throw new Error(`Option '${optionName}' must be an integer port number`);
|
|
839
|
-
}
|
|
840
|
-
const parsed = Number.parseInt(value, 10);
|
|
841
|
-
if (!Number.isSafeInteger(parsed) || parsed < 0 || parsed > 65535) {
|
|
842
|
-
throw new Error(
|
|
843
|
-
`Option '${optionName}' must be a port number between 0 and 65535`
|
|
844
|
-
);
|
|
845
|
-
}
|
|
846
|
-
return parsed;
|
|
847
|
-
}
|
|
848
|
-
function respondJson2(response, status, payload) {
|
|
849
|
-
response.writeHead(status, {
|
|
850
|
-
"content-type": "application/json"
|
|
851
|
-
});
|
|
852
|
-
response.end(JSON.stringify(payload));
|
|
853
|
-
}
|
|
854
|
-
function formatBoundUrl2(server) {
|
|
855
|
-
const address = server.address();
|
|
856
|
-
if (!address || typeof address === "string") {
|
|
857
|
-
return `http://${HTTP_HOST}`;
|
|
858
|
-
}
|
|
859
|
-
const host = address.address === "::" || address.address === "::1" || address.address === "0.0.0.0" || address.address === "127.0.0.1" ? "localhost" : address.address;
|
|
860
|
-
const urlHost = host.includes(":") ? `[${host}]` : host;
|
|
861
|
-
return `http://${urlHost}:${address.port}`;
|
|
862
|
-
}
|
|
863
|
-
function logHttpRequestError(error) {
|
|
864
|
-
const message = error instanceof Error ? error.stack ?? error.message : String(error);
|
|
865
|
-
process.stderr.write(`[start] HTTP request failed: ${message}
|
|
866
|
-
`);
|
|
867
|
-
}
|
|
868
|
-
async function closeHttpServer(server) {
|
|
869
|
-
if (!server) {
|
|
870
|
-
return;
|
|
871
|
-
}
|
|
872
|
-
await new Promise((resolveClose, rejectClose) => {
|
|
873
|
-
server.close((error) => {
|
|
874
|
-
if (error) {
|
|
875
|
-
rejectClose(error);
|
|
876
|
-
return;
|
|
877
|
-
}
|
|
878
|
-
resolveClose();
|
|
879
|
-
});
|
|
880
|
-
});
|
|
881
|
-
}
|
|
882
|
-
async function writeHttpBindingState(configDir, projectId, binding) {
|
|
883
|
-
await writeJsonFile(httpStatusPath(configDir, projectId), binding);
|
|
884
|
-
}
|
|
885
|
-
async function removeHttpBindingState(configDir, projectId) {
|
|
886
|
-
await rm(httpStatusPath(configDir, projectId), { force: true });
|
|
887
|
-
}
|
|
888
|
-
async function startHttpServer(input) {
|
|
889
|
-
const reader = new DashboardFsReader(input.runtimeRoot);
|
|
890
|
-
for (let port = input.initialPort; port <= 65535; port += 1) {
|
|
891
|
-
const server = createServer3((request, response) => {
|
|
892
|
-
void (async () => {
|
|
893
|
-
try {
|
|
894
|
-
const url = new URL(request.url ?? "/", `http://${HTTP_HOST}`);
|
|
895
|
-
if (request.method === "POST" && url.pathname === "/api/v1/refresh") {
|
|
896
|
-
request.resume();
|
|
897
|
-
input.service.requestReconcile();
|
|
898
|
-
respondJson2(response, 202, { ok: true });
|
|
899
|
-
return;
|
|
900
|
-
}
|
|
901
|
-
const resolved = await resolveDashboardResponse({
|
|
902
|
-
pathname: url.pathname,
|
|
903
|
-
method: request.method ?? "GET",
|
|
904
|
-
reader
|
|
905
|
-
});
|
|
906
|
-
respondJson2(response, resolved.status, resolved.payload);
|
|
907
|
-
} catch (error) {
|
|
908
|
-
logHttpRequestError(error);
|
|
909
|
-
if (!response.headersSent) {
|
|
910
|
-
respondJson2(response, 500, {
|
|
911
|
-
error: "Internal server error"
|
|
912
|
-
});
|
|
913
|
-
} else {
|
|
914
|
-
response.end();
|
|
915
|
-
}
|
|
916
|
-
}
|
|
917
|
-
})();
|
|
918
|
-
});
|
|
919
|
-
try {
|
|
920
|
-
await new Promise((resolveReady, rejectReady) => {
|
|
921
|
-
const handleListening = () => {
|
|
922
|
-
cleanup();
|
|
923
|
-
resolveReady();
|
|
924
|
-
};
|
|
925
|
-
const handleError = (error) => {
|
|
926
|
-
cleanup();
|
|
927
|
-
rejectReady(error);
|
|
928
|
-
};
|
|
929
|
-
const cleanup = () => {
|
|
930
|
-
server.off("listening", handleListening);
|
|
931
|
-
server.off("error", handleError);
|
|
932
|
-
};
|
|
933
|
-
server.once("listening", handleListening);
|
|
934
|
-
server.once("error", handleError);
|
|
935
|
-
server.listen(port, HTTP_HOST);
|
|
936
|
-
});
|
|
937
|
-
return {
|
|
938
|
-
server,
|
|
939
|
-
port,
|
|
940
|
-
url: formatBoundUrl2(server)
|
|
941
|
-
};
|
|
942
|
-
} catch (error) {
|
|
943
|
-
await closeHttpServer(server).catch(() => {
|
|
944
|
-
});
|
|
945
|
-
if (error?.code === "EADDRINUSE") {
|
|
946
|
-
continue;
|
|
947
|
-
}
|
|
948
|
-
throw error;
|
|
949
|
-
}
|
|
950
|
-
}
|
|
951
|
-
throw new Error(
|
|
952
|
-
`Unable to bind HTTP server starting from port ${input.initialPort}`
|
|
953
|
-
);
|
|
954
|
-
}
|
|
955
|
-
var handler = async (args, options) => {
|
|
956
|
-
setNoColor(options.noColor);
|
|
957
|
-
let parsed;
|
|
958
|
-
try {
|
|
959
|
-
if (rejectRemovedProjectId(args)) {
|
|
960
|
-
return;
|
|
961
|
-
}
|
|
962
|
-
parsed = parseStartArgs(args);
|
|
963
|
-
} catch (error) {
|
|
964
|
-
process.stderr.write(
|
|
965
|
-
`${error instanceof Error ? error.message : "Invalid arguments"}
|
|
966
|
-
`
|
|
967
|
-
);
|
|
968
|
-
process.exitCode = 2;
|
|
969
|
-
return;
|
|
970
|
-
}
|
|
971
|
-
if (parsed.error) {
|
|
972
|
-
process.stderr.write(`${parsed.error}
|
|
973
|
-
`);
|
|
974
|
-
process.stderr.write(
|
|
975
|
-
"Usage: gh-symphony start [--daemon] [--once] [--http [port]] [--web [port]]\n"
|
|
976
|
-
);
|
|
977
|
-
process.exitCode = 2;
|
|
978
|
-
return;
|
|
979
|
-
}
|
|
980
|
-
if (parsed.daemon && parsed.once) {
|
|
981
|
-
process.stderr.write(
|
|
982
|
-
"Options '--daemon' and '--once' cannot be used together\n"
|
|
983
|
-
);
|
|
984
|
-
process.exitCode = 2;
|
|
985
|
-
return;
|
|
986
|
-
}
|
|
987
|
-
const projectConfig = await resolveManagedProjectConfig({
|
|
988
|
-
configDir: options.configDir,
|
|
989
|
-
requestedProjectId: void 0
|
|
990
|
-
});
|
|
991
|
-
if (!projectConfig) {
|
|
992
|
-
handleMissingManagedProjectConfig();
|
|
993
|
-
return;
|
|
994
|
-
}
|
|
995
|
-
if (!hasConfiguredRepository(projectConfig)) {
|
|
996
|
-
process.stderr.write(
|
|
997
|
-
"No repository is configured in this project. Run 'gh-symphony repo add owner/name' first.\n"
|
|
998
|
-
);
|
|
999
|
-
process.exitCode = 1;
|
|
1000
|
-
return;
|
|
1001
|
-
}
|
|
1002
|
-
const runtimeRoot = resolveRuntimeRoot(options.configDir);
|
|
1003
|
-
const projectId = projectConfig.projectId;
|
|
1004
|
-
let logLevel;
|
|
1005
|
-
try {
|
|
1006
|
-
logLevel = resolveOrchestratorLogLevel(
|
|
1007
|
-
parsed.logLevel ?? process.env.SYMPHONY_LOG_LEVEL
|
|
1008
|
-
);
|
|
1009
|
-
} catch (error) {
|
|
1010
|
-
process.stderr.write(
|
|
1011
|
-
`${error instanceof Error ? error.message : "Unsupported log level"}
|
|
1012
|
-
`
|
|
1013
|
-
);
|
|
1014
|
-
process.exitCode = 2;
|
|
1015
|
-
return;
|
|
1016
|
-
}
|
|
1017
|
-
if (parsed.daemon) {
|
|
1018
|
-
await startDaemon(
|
|
1019
|
-
options,
|
|
1020
|
-
projectId,
|
|
1021
|
-
parsed.logLevel,
|
|
1022
|
-
parsed.httpPort,
|
|
1023
|
-
parsed.webPort
|
|
1024
|
-
);
|
|
1025
|
-
return;
|
|
1026
|
-
}
|
|
1027
|
-
if (!process.env.GITHUB_GRAPHQL_TOKEN) {
|
|
1028
|
-
try {
|
|
1029
|
-
process.env.GITHUB_GRAPHQL_TOKEN = getGhToken();
|
|
1030
|
-
} catch {
|
|
1031
|
-
}
|
|
1032
|
-
}
|
|
1033
|
-
let projectLock = null;
|
|
1034
|
-
try {
|
|
1035
|
-
projectLock = await acquireProjectLock({
|
|
1036
|
-
runtimeRoot,
|
|
1037
|
-
projectId
|
|
1038
|
-
});
|
|
1039
|
-
await removeHttpBindingState(options.configDir, projectId);
|
|
1040
|
-
const store = createStore(runtimeRoot);
|
|
1041
|
-
let prevSnapshot = null;
|
|
1042
|
-
let isFirst = true;
|
|
1043
|
-
const service = new OrchestratorService(store, projectConfig, {
|
|
1044
|
-
logLevel,
|
|
1045
|
-
onTick: async (snapshot) => {
|
|
1046
|
-
try {
|
|
1047
|
-
logTickResult(snapshot, prevSnapshot, isFirst);
|
|
1048
|
-
if (!isFirst) {
|
|
1049
|
-
const currentRunIds = new Set(
|
|
1050
|
-
snapshot.activeRuns.map((run) => run.runId)
|
|
1051
|
-
);
|
|
1052
|
-
for (const prevRun of prevSnapshot?.activeRuns ?? []) {
|
|
1053
|
-
if (!currentRunIds.has(prevRun.runId)) {
|
|
1054
|
-
await tailWorkerLog(
|
|
1055
|
-
runtimeRoot,
|
|
1056
|
-
projectId,
|
|
1057
|
-
prevRun.runId,
|
|
1058
|
-
prevRun.issueIdentifier
|
|
1059
|
-
);
|
|
1060
|
-
}
|
|
1061
|
-
}
|
|
1062
|
-
}
|
|
1063
|
-
prevSnapshot = snapshot;
|
|
1064
|
-
isFirst = false;
|
|
1065
|
-
} catch (error) {
|
|
1066
|
-
logLine(
|
|
1067
|
-
red("\u2717"),
|
|
1068
|
-
red(
|
|
1069
|
-
`Tick error: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
1070
|
-
)
|
|
1071
|
-
);
|
|
1072
|
-
}
|
|
1073
|
-
}
|
|
1074
|
-
});
|
|
1075
|
-
let shuttingDown = false;
|
|
1076
|
-
let shutdownPromise = null;
|
|
1077
|
-
let keepHttpAliveResolve = null;
|
|
1078
|
-
let httpServer = null;
|
|
1079
|
-
const shutdown = async () => {
|
|
1080
|
-
if (shuttingDown) {
|
|
1081
|
-
return shutdownPromise;
|
|
1082
|
-
}
|
|
1083
|
-
shuttingDown = true;
|
|
1084
|
-
keepHttpAliveResolve?.();
|
|
1085
|
-
keepHttpAliveResolve = null;
|
|
1086
|
-
const heldLock = projectLock;
|
|
1087
|
-
projectLock = null;
|
|
1088
|
-
shutdownPromise = shutdownForegroundOrchestrator({
|
|
1089
|
-
configDir: options.configDir,
|
|
1090
|
-
projectId,
|
|
1091
|
-
httpServer: httpServer?.server,
|
|
1092
|
-
projectLock: heldLock,
|
|
1093
|
-
service
|
|
1094
|
-
});
|
|
1095
|
-
return shutdownPromise;
|
|
1096
|
-
};
|
|
1097
|
-
const handleSigint = () => {
|
|
1098
|
-
void shutdown();
|
|
1099
|
-
};
|
|
1100
|
-
const handleSigterm = () => {
|
|
1101
|
-
void shutdown();
|
|
1102
|
-
};
|
|
1103
|
-
process.on("SIGINT", handleSigint);
|
|
1104
|
-
process.on("SIGTERM", handleSigterm);
|
|
1105
|
-
try {
|
|
1106
|
-
httpServer = parsed.webPort !== void 0 ? await startControlPlaneServer({
|
|
1107
|
-
host: HTTP_HOST,
|
|
1108
|
-
port: parsed.webPort,
|
|
1109
|
-
runtimeRoot,
|
|
1110
|
-
onRefreshRequest: () => service.requestReconcile()
|
|
1111
|
-
}) : parsed.httpPort !== void 0 ? await startHttpServer({
|
|
1112
|
-
runtimeRoot,
|
|
1113
|
-
projectId,
|
|
1114
|
-
initialPort: parsed.httpPort,
|
|
1115
|
-
service
|
|
1116
|
-
}) : null;
|
|
1117
|
-
if (httpServer) {
|
|
1118
|
-
try {
|
|
1119
|
-
await writeHttpBindingState(options.configDir, projectId, {
|
|
1120
|
-
host: HTTP_HOST,
|
|
1121
|
-
port: httpServer.port,
|
|
1122
|
-
endpoint: httpServer.url
|
|
1123
|
-
});
|
|
1124
|
-
} catch (error) {
|
|
1125
|
-
logLine(
|
|
1126
|
-
yellow("\u26A0"),
|
|
1127
|
-
yellow(
|
|
1128
|
-
`Failed to persist HTTP binding state (http.json): ${error instanceof Error ? error.message : "Unknown error"}`
|
|
1129
|
-
)
|
|
1130
|
-
);
|
|
1131
|
-
}
|
|
1132
|
-
}
|
|
1133
|
-
logLine(
|
|
1134
|
-
green("\u25B2"),
|
|
1135
|
-
`Starting orchestrator for project: ${bold(projectId)}`
|
|
1136
|
-
);
|
|
1137
|
-
if (httpServer) {
|
|
1138
|
-
logLine(
|
|
1139
|
-
cyan("\u25A1"),
|
|
1140
|
-
parsed.webPort !== void 0 ? `Web dashboard listening on ${httpServer.url}` : `HTTP dashboard listening on ${httpServer.url}`
|
|
1141
|
-
);
|
|
1142
|
-
}
|
|
1143
|
-
logLine(
|
|
1144
|
-
dim("\xB7"),
|
|
1145
|
-
dim(
|
|
1146
|
-
parsed.once ? "Running one orchestration tick" : "Press Ctrl+C to stop"
|
|
1147
|
-
)
|
|
1148
|
-
);
|
|
1149
|
-
while (!shuttingDown) {
|
|
1150
|
-
try {
|
|
1151
|
-
await service.run({ once: parsed.once });
|
|
1152
|
-
if (shuttingDown) {
|
|
1153
|
-
break;
|
|
1154
|
-
}
|
|
1155
|
-
if (parsed.once) {
|
|
1156
|
-
if (httpServer) {
|
|
1157
|
-
logLine(
|
|
1158
|
-
cyan("\u25A1"),
|
|
1159
|
-
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"
|
|
1160
|
-
);
|
|
1161
|
-
if (shuttingDown) {
|
|
1162
|
-
break;
|
|
1163
|
-
}
|
|
1164
|
-
await new Promise((resolve3) => {
|
|
1165
|
-
keepHttpAliveResolve = resolve3;
|
|
1166
|
-
});
|
|
1167
|
-
} else {
|
|
1168
|
-
await shutdown();
|
|
1169
|
-
}
|
|
1170
|
-
}
|
|
1171
|
-
break;
|
|
1172
|
-
} catch (error) {
|
|
1173
|
-
if (shuttingDown) {
|
|
1174
|
-
break;
|
|
1175
|
-
}
|
|
1176
|
-
logLine(
|
|
1177
|
-
red("\u2717"),
|
|
1178
|
-
red(
|
|
1179
|
-
`${parsed.once ? "One-shot run failed" : "Run loop error"}: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
1180
|
-
)
|
|
1181
|
-
);
|
|
1182
|
-
if (parsed.once) {
|
|
1183
|
-
process.exitCode = 1;
|
|
1184
|
-
await closeHttpServer(httpServer?.server).catch((closeError) => {
|
|
1185
|
-
logLine(
|
|
1186
|
-
yellow("\u26A0"),
|
|
1187
|
-
`Failed to stop HTTP server: ${closeError instanceof Error ? closeError.message : "Unknown error"}`
|
|
1188
|
-
);
|
|
1189
|
-
});
|
|
1190
|
-
await removeHttpBindingState(options.configDir, projectId).catch(
|
|
1191
|
-
(removeError) => {
|
|
1192
|
-
logLine(
|
|
1193
|
-
yellow("\u26A0"),
|
|
1194
|
-
`Failed to remove HTTP state: ${removeError instanceof Error ? removeError.message : "Unknown error"}`
|
|
1195
|
-
);
|
|
1196
|
-
}
|
|
1197
|
-
);
|
|
1198
|
-
return;
|
|
1199
|
-
}
|
|
1200
|
-
}
|
|
1201
|
-
}
|
|
1202
|
-
} finally {
|
|
1203
|
-
process.off("SIGINT", handleSigint);
|
|
1204
|
-
process.off("SIGTERM", handleSigterm);
|
|
1205
|
-
if (shutdownPromise) {
|
|
1206
|
-
await shutdownPromise;
|
|
1207
|
-
}
|
|
1208
|
-
}
|
|
1209
|
-
} finally {
|
|
1210
|
-
await releaseProjectLock(projectLock);
|
|
1211
|
-
}
|
|
1212
|
-
};
|
|
1213
|
-
async function shutdownForegroundOrchestrator(input) {
|
|
1214
|
-
logLine(yellow("\u25BC"), "Shutting down...");
|
|
1215
|
-
if (input.service) {
|
|
1216
|
-
try {
|
|
1217
|
-
await input.service.shutdown();
|
|
1218
|
-
} catch (error) {
|
|
1219
|
-
logLine(
|
|
1220
|
-
red("\u2717"),
|
|
1221
|
-
red(
|
|
1222
|
-
`Failed to shut down workers: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
1223
|
-
)
|
|
1224
|
-
);
|
|
1225
|
-
}
|
|
1226
|
-
}
|
|
1227
|
-
try {
|
|
1228
|
-
await closeHttpServer(input.httpServer);
|
|
1229
|
-
} catch (error) {
|
|
1230
|
-
logLine(
|
|
1231
|
-
yellow("\u26A0"),
|
|
1232
|
-
`Failed to stop HTTP server: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
1233
|
-
);
|
|
1234
|
-
}
|
|
1235
|
-
try {
|
|
1236
|
-
await removeHttpBindingState(input.configDir, input.projectId);
|
|
1237
|
-
} catch (error) {
|
|
1238
|
-
logLine(
|
|
1239
|
-
yellow("\u26A0"),
|
|
1240
|
-
`Failed to remove HTTP state: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
1241
|
-
);
|
|
1242
|
-
}
|
|
1243
|
-
try {
|
|
1244
|
-
await (input.releaseLock ?? releaseProjectLock)(input.projectLock);
|
|
1245
|
-
} catch (error) {
|
|
1246
|
-
logLine(
|
|
1247
|
-
yellow("\u26A0"),
|
|
1248
|
-
`Failed to release project lock: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
1249
|
-
);
|
|
1250
|
-
}
|
|
1251
|
-
return (input.exit ?? process.exit)(0);
|
|
1252
|
-
}
|
|
1253
|
-
function hasConfiguredRepository(config) {
|
|
1254
|
-
return Boolean(config.repository?.owner && config.repository.name);
|
|
1255
|
-
}
|
|
1256
|
-
async function tailWorkerLog(runtimeRoot, projectId, runId, issueIdentifier) {
|
|
1257
|
-
for (const logPath of [
|
|
1258
|
-
join3(runtimeRoot, "runs", runId, "worker.log"),
|
|
1259
|
-
join3(runtimeRoot, "projects", projectId, "runs", runId, "worker.log")
|
|
1260
|
-
]) {
|
|
1261
|
-
try {
|
|
1262
|
-
const content = await readFile2(logPath, "utf8");
|
|
1263
|
-
const lines = content.split("\n").filter((l) => l.trim());
|
|
1264
|
-
if (lines.length === 0) return;
|
|
1265
|
-
const tail = lines.slice(-30);
|
|
1266
|
-
logLine(red("\u2717"), red(`Worker stderr (${issueIdentifier}):`));
|
|
1267
|
-
for (const line of tail) {
|
|
1268
|
-
process.stdout.write(` ${dim(line)}
|
|
1269
|
-
`);
|
|
1270
|
-
}
|
|
1271
|
-
return;
|
|
1272
|
-
} catch {
|
|
1273
|
-
}
|
|
1274
|
-
}
|
|
1275
|
-
}
|
|
1276
|
-
var start_default = handler;
|
|
1277
|
-
async function startDaemon(options, projectId, logLevel, httpPort, webPort) {
|
|
1278
|
-
const logPath = orchestratorLogPath(options.configDir, projectId);
|
|
1279
|
-
await mkdir(dirname2(logPath), { recursive: true });
|
|
1280
|
-
const { openSync } = await import("fs");
|
|
1281
|
-
const logFd = openSync(logPath, "a");
|
|
1282
|
-
const child = spawn(
|
|
1283
|
-
process.execPath,
|
|
1284
|
-
[
|
|
1285
|
-
process.argv[1],
|
|
1286
|
-
"start",
|
|
1287
|
-
...httpPort !== void 0 ? ["--http", String(httpPort)] : [],
|
|
1288
|
-
...webPort !== void 0 ? ["--web", String(webPort)] : [],
|
|
1289
|
-
...logLevel ? ["--log-level", logLevel] : []
|
|
1290
|
-
],
|
|
1291
|
-
{
|
|
1292
|
-
cwd: process.cwd(),
|
|
1293
|
-
env: {
|
|
1294
|
-
...process.env,
|
|
1295
|
-
GH_SYMPHONY_CONFIG_DIR: options.configDir
|
|
1296
|
-
},
|
|
1297
|
-
detached: true,
|
|
1298
|
-
stdio: ["ignore", logFd, logFd]
|
|
1299
|
-
}
|
|
1300
|
-
);
|
|
1301
|
-
const pidPath = daemonPidPath(options.configDir, projectId);
|
|
1302
|
-
await mkdir(dirname2(pidPath), { recursive: true });
|
|
1303
|
-
await writeFile(pidPath, String(child.pid), "utf8");
|
|
1304
|
-
child.unref();
|
|
1305
|
-
const { closeSync } = await import("fs");
|
|
1306
|
-
closeSync(logFd);
|
|
1307
|
-
process.stdout.write(
|
|
1308
|
-
`Orchestrator started in background (PID: ${child.pid}).
|
|
1309
|
-
Logs: ${logPath}
|
|
1310
|
-
Stop with: gh-symphony repo stop
|
|
1311
|
-
`
|
|
1312
|
-
);
|
|
1313
|
-
}
|
|
1314
|
-
|
|
1315
|
-
export {
|
|
1316
|
-
shutdownForegroundOrchestrator,
|
|
1317
|
-
start_default
|
|
1318
|
-
};
|