@gh-symphony/cli 0.1.4 → 0.2.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/README.md +19 -0
- package/dist/{chunk-E7OCBNB2.js → chunk-B6OHDUSH.js} +1 -1
- package/dist/{chunk-DW63WPRE.js → chunk-C44DYDNU.js} +12 -1
- package/dist/{repo-IH6UWE4H.js → chunk-CTTFIZYG.js} +5391 -6261
- package/dist/{chunk-RHLUIMBN.js → chunk-F46FTZJE.js} +17 -1
- package/dist/{chunk-HT3FAJAO.js → chunk-JU3WSGMZ.js} +77 -8
- package/dist/{chunk-EWTMSDCE.js → chunk-Q3UEPUE3.js} +4 -0
- package/dist/{doctor-I32MANQ4.js → doctor-JPNA7OCD.js} +5 -4
- package/dist/index.js +6 -6
- package/dist/repo-OJLSMOR3.js +2693 -0
- package/dist/{setup-UJC2WYHQ.js → setup-PD27LSPP.js} +3 -3
- package/dist/{upgrade-XYHCUGHT.js → upgrade-HRI3KEO7.js} +2 -2
- package/dist/{version-B2AYYGLM.js → version-JSBTKS6Q.js} +1 -1
- package/dist/worker-entry.js +2 -2
- package/dist/{workflow-WSXHMO5B.js → workflow-KB3TX5Z4.js} +5 -4
- package/package.json +4 -4
- package/dist/chunk-YIARPBOR.js +0 -1648
|
@@ -0,0 +1,2693 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
blue,
|
|
4
|
+
bold,
|
|
5
|
+
clearScreen,
|
|
6
|
+
cyan,
|
|
7
|
+
dim,
|
|
8
|
+
green,
|
|
9
|
+
hideCursor,
|
|
10
|
+
magenta,
|
|
11
|
+
red,
|
|
12
|
+
setNoColor,
|
|
13
|
+
showCursor,
|
|
14
|
+
stripAnsi,
|
|
15
|
+
yellow
|
|
16
|
+
} from "./chunk-MVRF7BES.js";
|
|
17
|
+
import {
|
|
18
|
+
initRepoRuntime,
|
|
19
|
+
parseRepoRuntimeFlags
|
|
20
|
+
} from "./chunk-C44DYDNU.js";
|
|
21
|
+
import {
|
|
22
|
+
OrchestratorService,
|
|
23
|
+
acquireProjectLock,
|
|
24
|
+
createStore,
|
|
25
|
+
explainIssueDispatch,
|
|
26
|
+
findGithubProjectIssue,
|
|
27
|
+
handleMissingManagedProjectConfig,
|
|
28
|
+
isActiveRunRecordStatus,
|
|
29
|
+
parseIssueIdentifier,
|
|
30
|
+
releaseProjectLock,
|
|
31
|
+
resolveCanonicalSubjectIssues,
|
|
32
|
+
resolveManagedProjectConfig,
|
|
33
|
+
resolveOrchestratorLogLevel,
|
|
34
|
+
resolveTrackerAdapter,
|
|
35
|
+
runCli
|
|
36
|
+
} from "./chunk-CTTFIZYG.js";
|
|
37
|
+
import "./chunk-B6OHDUSH.js";
|
|
38
|
+
import {
|
|
39
|
+
resolveRepoRuntimeRoot,
|
|
40
|
+
resolveRuntimeRoot
|
|
41
|
+
} from "./chunk-6I753NYO.js";
|
|
42
|
+
import {
|
|
43
|
+
GhAuthError,
|
|
44
|
+
getGhToken
|
|
45
|
+
} from "./chunk-Z3NZOPLZ.js";
|
|
46
|
+
import {
|
|
47
|
+
WorkflowConfigStore,
|
|
48
|
+
deriveIssueWorkspaceKeyFromIdentifier,
|
|
49
|
+
isFileMissing,
|
|
50
|
+
isMatchingIssueRun,
|
|
51
|
+
mapIssueOrchestrationStateToStatus,
|
|
52
|
+
parseRecentEvents,
|
|
53
|
+
readJsonFile,
|
|
54
|
+
safeReadDir
|
|
55
|
+
} from "./chunk-Q3UEPUE3.js";
|
|
56
|
+
import {
|
|
57
|
+
daemonPidPath,
|
|
58
|
+
httpStatusPath,
|
|
59
|
+
loadActiveProjectConfig,
|
|
60
|
+
orchestratorLogPath,
|
|
61
|
+
writeJsonFile
|
|
62
|
+
} from "./chunk-WOVNN5NW.js";
|
|
63
|
+
|
|
64
|
+
// src/commands/logs.ts
|
|
65
|
+
import { readFile, readdir, stat } from "fs/promises";
|
|
66
|
+
import { join, resolve } from "path";
|
|
67
|
+
import { createReadStream } from "fs";
|
|
68
|
+
import { createInterface } from "readline";
|
|
69
|
+
function parseLogsArgs(args) {
|
|
70
|
+
const parsed = { follow: false };
|
|
71
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
72
|
+
const arg = args[i];
|
|
73
|
+
if (arg === "--follow" || arg === "-f") {
|
|
74
|
+
parsed.follow = true;
|
|
75
|
+
}
|
|
76
|
+
if (arg === "--issue") {
|
|
77
|
+
parsed.issue = args[i + 1];
|
|
78
|
+
i += 1;
|
|
79
|
+
}
|
|
80
|
+
if (arg === "--run") {
|
|
81
|
+
parsed.run = args[i + 1];
|
|
82
|
+
i += 1;
|
|
83
|
+
}
|
|
84
|
+
if (arg === "--level") {
|
|
85
|
+
parsed.level = args[i + 1];
|
|
86
|
+
i += 1;
|
|
87
|
+
}
|
|
88
|
+
if (arg === "--project" || arg === "--project-id") {
|
|
89
|
+
parsed.projectId = args[i + 1];
|
|
90
|
+
i += 1;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return parsed;
|
|
94
|
+
}
|
|
95
|
+
var handler = async (args, options) => {
|
|
96
|
+
const parsed = parseLogsArgs(args);
|
|
97
|
+
const runtimeRoot = resolve(options.configDir);
|
|
98
|
+
if (parsed.run) {
|
|
99
|
+
const eventsPath = parsed.projectId ? join(
|
|
100
|
+
runtimeRoot,
|
|
101
|
+
"projects",
|
|
102
|
+
parsed.projectId,
|
|
103
|
+
"runs",
|
|
104
|
+
parsed.run,
|
|
105
|
+
"events.ndjson"
|
|
106
|
+
) : await resolveRunEventsPath(runtimeRoot, parsed.run);
|
|
107
|
+
if (!eventsPath) {
|
|
108
|
+
process.stderr.write(`No events found for run: ${parsed.run}
|
|
109
|
+
`);
|
|
110
|
+
process.exitCode = 1;
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
try {
|
|
114
|
+
const content = await readFile(eventsPath, "utf8");
|
|
115
|
+
const lines = content.trim().split("\n").filter(Boolean);
|
|
116
|
+
for (const line of lines) {
|
|
117
|
+
const event = JSON.parse(line);
|
|
118
|
+
if (parsed.projectId && event.projectId !== parsed.projectId) continue;
|
|
119
|
+
if (parsed.level && event.level !== parsed.level) continue;
|
|
120
|
+
if (parsed.issue && event.issueIdentifier !== parsed.issue) continue;
|
|
121
|
+
process.stdout.write(formatEvent(event) + "\n");
|
|
122
|
+
}
|
|
123
|
+
} catch {
|
|
124
|
+
process.stderr.write(`No events found for run: ${parsed.run}
|
|
125
|
+
`);
|
|
126
|
+
process.exitCode = 1;
|
|
127
|
+
}
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (parsed.follow) {
|
|
131
|
+
const projectConfig = await resolveManagedProjectConfig({
|
|
132
|
+
configDir: options.configDir,
|
|
133
|
+
requestedProjectId: parsed.projectId
|
|
134
|
+
});
|
|
135
|
+
if (!projectConfig) {
|
|
136
|
+
handleMissingManagedProjectConfig();
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const logPath = orchestratorLogPath(
|
|
140
|
+
options.configDir,
|
|
141
|
+
projectConfig.projectId
|
|
142
|
+
);
|
|
143
|
+
try {
|
|
144
|
+
const stream = createReadStream(logPath, { encoding: "utf8" });
|
|
145
|
+
const rl = createInterface({ input: stream });
|
|
146
|
+
for await (const line of rl) {
|
|
147
|
+
process.stdout.write(line + "\n");
|
|
148
|
+
}
|
|
149
|
+
const { watchFile } = await import("fs");
|
|
150
|
+
let lastSize = 0;
|
|
151
|
+
watchFile(logPath, { interval: 1e3 }, async (curr) => {
|
|
152
|
+
if (curr.size > lastSize) {
|
|
153
|
+
const fd = await import("fs/promises");
|
|
154
|
+
const handle = await fd.open(logPath, "r");
|
|
155
|
+
const buf = Buffer.alloc(curr.size - lastSize);
|
|
156
|
+
await handle.read(buf, 0, buf.length, lastSize);
|
|
157
|
+
await handle.close();
|
|
158
|
+
process.stdout.write(buf.toString("utf8"));
|
|
159
|
+
lastSize = curr.size;
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
await new Promise(() => {
|
|
163
|
+
});
|
|
164
|
+
} catch {
|
|
165
|
+
process.stderr.write(
|
|
166
|
+
"No log file found. Start the orchestrator first.\n"
|
|
167
|
+
);
|
|
168
|
+
process.exitCode = 1;
|
|
169
|
+
}
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
const runRoots = parsed.projectId ? [join(runtimeRoot, "projects", parsed.projectId, "runs")] : await listProjectRunRoots(runtimeRoot);
|
|
173
|
+
let foundRuns = false;
|
|
174
|
+
try {
|
|
175
|
+
for (const runsDir of runRoots) {
|
|
176
|
+
const entries = await safeReadDir2(runsDir);
|
|
177
|
+
if (entries.length === 0) {
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
foundRuns = true;
|
|
181
|
+
for (const entry of entries) {
|
|
182
|
+
const eventsPath = join(runsDir, entry, "events.ndjson");
|
|
183
|
+
try {
|
|
184
|
+
const content = await readFile(eventsPath, "utf8");
|
|
185
|
+
const lines = content.trim().split("\n").filter(Boolean);
|
|
186
|
+
for (const line of lines) {
|
|
187
|
+
const event = JSON.parse(line);
|
|
188
|
+
if (parsed.projectId && event.projectId !== parsed.projectId)
|
|
189
|
+
continue;
|
|
190
|
+
if (parsed.level && event.level !== parsed.level) continue;
|
|
191
|
+
if (parsed.issue && event.issueIdentifier !== parsed.issue) continue;
|
|
192
|
+
process.stdout.write(formatEvent(event) + "\n");
|
|
193
|
+
}
|
|
194
|
+
} catch {
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
} catch {
|
|
199
|
+
}
|
|
200
|
+
if (!foundRuns) {
|
|
201
|
+
process.stderr.write("No runs found. Start the orchestrator first.\n");
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
var logs_default = handler;
|
|
205
|
+
function formatEvent(event) {
|
|
206
|
+
const at = event.at ?? "";
|
|
207
|
+
const eventType = event.event ?? "unknown";
|
|
208
|
+
const issue = event.issueIdentifier ?? "";
|
|
209
|
+
const extra = event.error ? ` error=${event.error}` : "";
|
|
210
|
+
return `[${at}] ${eventType} ${issue}${extra}`;
|
|
211
|
+
}
|
|
212
|
+
async function listProjectRunRoots(runtimeRoot) {
|
|
213
|
+
const roots = [join(runtimeRoot, "runs")];
|
|
214
|
+
try {
|
|
215
|
+
const projectIds = await readdir(join(runtimeRoot, "projects"));
|
|
216
|
+
roots.push(
|
|
217
|
+
...projectIds.map(
|
|
218
|
+
(projectId) => join(runtimeRoot, "projects", projectId, "runs")
|
|
219
|
+
)
|
|
220
|
+
);
|
|
221
|
+
} catch {
|
|
222
|
+
}
|
|
223
|
+
return roots;
|
|
224
|
+
}
|
|
225
|
+
async function resolveRunEventsPath(runtimeRoot, runId) {
|
|
226
|
+
for (const runsDir of await listProjectRunRoots(runtimeRoot)) {
|
|
227
|
+
const eventsPath = join(runsDir, runId, "events.ndjson");
|
|
228
|
+
try {
|
|
229
|
+
await stat(eventsPath);
|
|
230
|
+
return eventsPath;
|
|
231
|
+
} catch {
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
async function safeReadDir2(path) {
|
|
237
|
+
try {
|
|
238
|
+
return await readdir(path);
|
|
239
|
+
} catch {
|
|
240
|
+
return [];
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// src/commands/recover.ts
|
|
245
|
+
import { readFile as readFile2, readdir as readdir2 } from "fs/promises";
|
|
246
|
+
import { join as join2 } from "path";
|
|
247
|
+
function parseRecoverArgs(args) {
|
|
248
|
+
const parsed = { dryRun: false };
|
|
249
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
250
|
+
const arg = args[i];
|
|
251
|
+
if (arg === "--dry-run") {
|
|
252
|
+
parsed.dryRun = true;
|
|
253
|
+
}
|
|
254
|
+
if (arg === "--project" || arg === "--project-id") {
|
|
255
|
+
parsed.projectId = args[i + 1];
|
|
256
|
+
i += 1;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return parsed;
|
|
260
|
+
}
|
|
261
|
+
var handler2 = async (args, options) => {
|
|
262
|
+
const parsed = parseRecoverArgs(args);
|
|
263
|
+
const projectConfig = await resolveManagedProjectConfig({
|
|
264
|
+
configDir: options.configDir,
|
|
265
|
+
requestedProjectId: parsed.projectId
|
|
266
|
+
});
|
|
267
|
+
if (!projectConfig) {
|
|
268
|
+
handleMissingManagedProjectConfig();
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
const runtimeRoot = resolveRuntimeRoot(options.configDir);
|
|
272
|
+
const projectId = projectConfig.projectId;
|
|
273
|
+
if (parsed.dryRun) {
|
|
274
|
+
process.stdout.write("Dry run \u2014 scanning for stalled runs...\n");
|
|
275
|
+
const candidates = await listRecoverCandidates(runtimeRoot, projectId);
|
|
276
|
+
if (options.json) {
|
|
277
|
+
process.stdout.write(JSON.stringify(candidates, null, 2) + "\n");
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
if (candidates.length === 0) {
|
|
281
|
+
process.stdout.write("No recoverable runs found.\n");
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
for (const candidate of candidates) {
|
|
285
|
+
process.stdout.write(
|
|
286
|
+
`${candidate.issueIdentifier} (${candidate.runId}) \u2014 ${candidate.reason}
|
|
287
|
+
`
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
process.stdout.write("Recovering stalled runs...\n");
|
|
293
|
+
await runCli([
|
|
294
|
+
"recover",
|
|
295
|
+
"--runtime-root",
|
|
296
|
+
runtimeRoot,
|
|
297
|
+
"--project-id",
|
|
298
|
+
projectId
|
|
299
|
+
]);
|
|
300
|
+
};
|
|
301
|
+
var recover_default = handler2;
|
|
302
|
+
async function listRecoverCandidates(runtimeRoot, projectId) {
|
|
303
|
+
const runRoots = [
|
|
304
|
+
join2(runtimeRoot, "runs"),
|
|
305
|
+
join2(runtimeRoot, "projects", projectId, "runs")
|
|
306
|
+
];
|
|
307
|
+
const candidates = [];
|
|
308
|
+
for (const runsDir of runRoots) {
|
|
309
|
+
let entries = [];
|
|
310
|
+
try {
|
|
311
|
+
entries = await readdir2(runsDir);
|
|
312
|
+
} catch {
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
for (const entry of entries) {
|
|
316
|
+
const runPath = join2(runsDir, entry, "run.json");
|
|
317
|
+
try {
|
|
318
|
+
const raw = await readFile2(runPath, "utf8");
|
|
319
|
+
const run = JSON.parse(raw);
|
|
320
|
+
if (run.projectId && run.projectId !== projectId) {
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
const reason = detectRecoveryReason(run);
|
|
324
|
+
if (!reason) {
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
if (!candidates.some((candidate) => candidate.runId === run.runId)) {
|
|
328
|
+
candidates.push({
|
|
329
|
+
runId: run.runId,
|
|
330
|
+
issueIdentifier: run.issueIdentifier,
|
|
331
|
+
status: run.status,
|
|
332
|
+
reason
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
} catch {
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return candidates;
|
|
340
|
+
}
|
|
341
|
+
function detectRecoveryReason(run) {
|
|
342
|
+
if (run.processId) {
|
|
343
|
+
const startedAt = run.startedAt ? new Date(run.startedAt).getTime() : 0;
|
|
344
|
+
const runningForMs = Date.now() - startedAt;
|
|
345
|
+
if (isProcessRunning(run.processId) && runningForMs > 30 * 60 * 1e3) {
|
|
346
|
+
return "worker appears stuck";
|
|
347
|
+
}
|
|
348
|
+
if (!isProcessRunning(run.processId)) {
|
|
349
|
+
return "worker process is no longer running";
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
if (run.status === "retrying" && run.nextRetryAt && new Date(run.nextRetryAt).getTime() <= Date.now()) {
|
|
353
|
+
return "retry window has elapsed";
|
|
354
|
+
}
|
|
355
|
+
return null;
|
|
356
|
+
}
|
|
357
|
+
function isProcessRunning(pid) {
|
|
358
|
+
try {
|
|
359
|
+
process.kill(pid, 0);
|
|
360
|
+
return true;
|
|
361
|
+
} catch {
|
|
362
|
+
return false;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// src/commands/repo-explain.ts
|
|
367
|
+
import { readdir as readdir3, readFile as readFile3 } from "fs/promises";
|
|
368
|
+
import { join as join3, resolve as resolve2 } from "path";
|
|
369
|
+
function parseRepoExplainFlags(args) {
|
|
370
|
+
const parsed = {};
|
|
371
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
372
|
+
const arg = args[i];
|
|
373
|
+
if (arg === "--workflow" || arg === "--workflow-path") {
|
|
374
|
+
const value = args[i + 1];
|
|
375
|
+
if (!value || value.startsWith("-")) {
|
|
376
|
+
parsed.error = `Option '${arg}' argument missing`;
|
|
377
|
+
return parsed;
|
|
378
|
+
}
|
|
379
|
+
parsed.workflowPath = value;
|
|
380
|
+
i += 1;
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
if (arg?.startsWith("-")) {
|
|
384
|
+
parsed.error = `Unknown option '${arg}'`;
|
|
385
|
+
return parsed;
|
|
386
|
+
}
|
|
387
|
+
if (parsed.identifier) {
|
|
388
|
+
parsed.error = "Only one issue identifier can be explained at a time";
|
|
389
|
+
return parsed;
|
|
390
|
+
}
|
|
391
|
+
parsed.identifier = arg;
|
|
392
|
+
}
|
|
393
|
+
if (!parsed.identifier) {
|
|
394
|
+
parsed.error = "Issue identifier argument missing";
|
|
395
|
+
} else if (!parseIssueIdentifier(parsed.identifier)) {
|
|
396
|
+
parsed.error = "Issue identifier must use the form <owner>/<repo>#<number>";
|
|
397
|
+
}
|
|
398
|
+
return parsed;
|
|
399
|
+
}
|
|
400
|
+
var handler3 = async (args, options) => {
|
|
401
|
+
const parsed = parseRepoExplainFlags(args);
|
|
402
|
+
if (parsed.error) {
|
|
403
|
+
process.stderr.write(`${parsed.error}
|
|
404
|
+
`);
|
|
405
|
+
process.stderr.write(
|
|
406
|
+
"Usage: gh-symphony repo explain <owner/repo#number> [--workflow <path>]\n"
|
|
407
|
+
);
|
|
408
|
+
process.exitCode = 2;
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
const projectConfig = await loadActiveProjectConfig(options.configDir);
|
|
412
|
+
if (!projectConfig) {
|
|
413
|
+
process.stderr.write(
|
|
414
|
+
"No repository runtime configured. Run 'gh-symphony repo init' in the target repository.\n"
|
|
415
|
+
);
|
|
416
|
+
process.exitCode = 1;
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
const identifier = parsed.identifier;
|
|
420
|
+
const parsedIdentifier = parseIssueIdentifier(identifier);
|
|
421
|
+
const fallbackRepository = {
|
|
422
|
+
owner: parsedIdentifier.owner,
|
|
423
|
+
name: parsedIdentifier.name,
|
|
424
|
+
cloneUrl: `https://github.com/${parsedIdentifier.owner}/${parsedIdentifier.name}.git`
|
|
425
|
+
};
|
|
426
|
+
const workflowRepository = projectConfig.repository ?? fallbackRepository;
|
|
427
|
+
let token;
|
|
428
|
+
try {
|
|
429
|
+
token = getGhToken();
|
|
430
|
+
} catch (error) {
|
|
431
|
+
if (error instanceof GhAuthError) {
|
|
432
|
+
process.stderr.write(
|
|
433
|
+
`Error: GitHub authentication is required for repo explain. ${error.message}
|
|
434
|
+
`
|
|
435
|
+
);
|
|
436
|
+
process.stderr.write(
|
|
437
|
+
"Run 'gh auth login --scopes repo,read:org,project' or set GITHUB_GRAPHQL_TOKEN, then re-run this command.\n"
|
|
438
|
+
);
|
|
439
|
+
process.exitCode = 2;
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
throw error;
|
|
443
|
+
}
|
|
444
|
+
const trackerAdapter = resolveTrackerAdapter(projectConfig.tracker);
|
|
445
|
+
const orchestratorProject = {
|
|
446
|
+
...projectConfig,
|
|
447
|
+
repository: workflowRepository
|
|
448
|
+
};
|
|
449
|
+
const trackerDependencies = {
|
|
450
|
+
token,
|
|
451
|
+
projectItemsCache: createProjectItemsCache()
|
|
452
|
+
};
|
|
453
|
+
const runtimeRoot = options.configDir;
|
|
454
|
+
const issuesPromise = trackerAdapter.listIssues(
|
|
455
|
+
orchestratorProject,
|
|
456
|
+
trackerDependencies
|
|
457
|
+
);
|
|
458
|
+
const issuePromise = projectConfig.tracker.adapter === "github-project" ? findGithubProjectIssue(
|
|
459
|
+
orchestratorProject,
|
|
460
|
+
identifier,
|
|
461
|
+
trackerDependencies
|
|
462
|
+
) : issuesPromise.then(
|
|
463
|
+
(issues2) => issues2.find(
|
|
464
|
+
(candidate) => candidate.identifier.trim().toLowerCase() === identifier.trim().toLowerCase()
|
|
465
|
+
) ?? null
|
|
466
|
+
);
|
|
467
|
+
const [issues, issue, issueRecords, runs, snapshot] = await Promise.all([
|
|
468
|
+
issuesPromise,
|
|
469
|
+
issuePromise,
|
|
470
|
+
readJsonFile2(join3(runtimeRoot, "issues.json")),
|
|
471
|
+
readRuns(runtimeRoot, projectConfig.projectId),
|
|
472
|
+
readJsonFile2(join3(runtimeRoot, "status.json"))
|
|
473
|
+
]);
|
|
474
|
+
const canonicalIssues = resolveCanonicalSubjectIssues(issues);
|
|
475
|
+
const canonicalIssue = canonicalIssues.find(
|
|
476
|
+
(candidate) => matchesExplainIdentifier(candidate, identifier)
|
|
477
|
+
) ?? issue;
|
|
478
|
+
let workflow;
|
|
479
|
+
try {
|
|
480
|
+
workflow = await loadExplainWorkflow({
|
|
481
|
+
explicitWorkflowPath: parsed.workflowPath,
|
|
482
|
+
repository: workflowRepository,
|
|
483
|
+
runs
|
|
484
|
+
});
|
|
485
|
+
} catch (error) {
|
|
486
|
+
if (error instanceof RepoExplainWorkflowError) {
|
|
487
|
+
process.stderr.write(`Error: ${error.message}
|
|
488
|
+
`);
|
|
489
|
+
process.stderr.write(
|
|
490
|
+
"Hint: pass --workflow <path-to-WORKFLOW.md> or run 'gh-symphony workflow preview --file <path>' to verify the workflow file.\n"
|
|
491
|
+
);
|
|
492
|
+
process.exitCode = 2;
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
throw error;
|
|
496
|
+
}
|
|
497
|
+
const activeRunCount = runs.filter(
|
|
498
|
+
(run) => isActiveRunRecordStatus(run.status)
|
|
499
|
+
).length;
|
|
500
|
+
const report = explainIssueDispatch({
|
|
501
|
+
identifier,
|
|
502
|
+
issue: canonicalIssue,
|
|
503
|
+
projectRepository: projectConfig.repository ?? null,
|
|
504
|
+
allIssues: canonicalIssues,
|
|
505
|
+
lifecycle: workflow.lifecycle,
|
|
506
|
+
issueRecords: issueRecords ?? [],
|
|
507
|
+
runs,
|
|
508
|
+
activeRunCount,
|
|
509
|
+
maxConcurrentAgents: workflow.maxConcurrentAgents,
|
|
510
|
+
maxConcurrentAgentsByState: workflow.maxConcurrentAgentsByState
|
|
511
|
+
});
|
|
512
|
+
const enrichedReport = {
|
|
513
|
+
...report,
|
|
514
|
+
project: {
|
|
515
|
+
id: projectConfig.projectId,
|
|
516
|
+
slug: projectConfig.slug,
|
|
517
|
+
tracker: projectConfig.tracker,
|
|
518
|
+
lastTickAt: snapshot?.lastTickAt ?? null,
|
|
519
|
+
health: snapshot?.health ?? null
|
|
520
|
+
}
|
|
521
|
+
};
|
|
522
|
+
if (options.json) {
|
|
523
|
+
process.stdout.write(JSON.stringify(enrichedReport, null, 2) + "\n");
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
process.stdout.write(renderRepoExplainReport(report, options.noColor));
|
|
527
|
+
};
|
|
528
|
+
var repo_explain_default = handler3;
|
|
529
|
+
function matchesExplainIdentifier(issue, identifier) {
|
|
530
|
+
const normalizedIdentifier = identifier.trim().toLowerCase();
|
|
531
|
+
if (issue.identifier.trim().toLowerCase() === normalizedIdentifier) {
|
|
532
|
+
return true;
|
|
533
|
+
}
|
|
534
|
+
const linkedPullRequests = Array.isArray(issue.metadata.linkedPullRequests) ? issue.metadata.linkedPullRequests : [];
|
|
535
|
+
return linkedPullRequests.some(
|
|
536
|
+
(pullRequest) => typeof pullRequest === "object" && pullRequest !== null && "identifier" in pullRequest && typeof pullRequest.identifier === "string" && pullRequest.identifier.trim().toLowerCase() === normalizedIdentifier
|
|
537
|
+
);
|
|
538
|
+
}
|
|
539
|
+
var RepoExplainWorkflowError = class extends Error {
|
|
540
|
+
constructor(message) {
|
|
541
|
+
super(message);
|
|
542
|
+
this.name = "RepoExplainWorkflowError";
|
|
543
|
+
}
|
|
544
|
+
};
|
|
545
|
+
async function loadExplainWorkflow(input) {
|
|
546
|
+
const workflowPaths = resolveExplainWorkflowCandidates(input);
|
|
547
|
+
if (workflowPaths.length === 0) {
|
|
548
|
+
throw new RepoExplainWorkflowError(
|
|
549
|
+
"No WORKFLOW.md path could be resolved from --workflow, the configured repository path, or previous run records."
|
|
550
|
+
);
|
|
551
|
+
}
|
|
552
|
+
const failures = [];
|
|
553
|
+
for (const workflowPath of workflowPaths) {
|
|
554
|
+
try {
|
|
555
|
+
const resolution = await new WorkflowConfigStore().load(workflowPath);
|
|
556
|
+
return {
|
|
557
|
+
lifecycle: resolution.lifecycle,
|
|
558
|
+
maxConcurrentAgents: resolution.workflow.agent.maxConcurrentAgents,
|
|
559
|
+
maxConcurrentAgentsByState: resolution.workflow.agent.maxConcurrentAgentsByState
|
|
560
|
+
};
|
|
561
|
+
} catch (error) {
|
|
562
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
563
|
+
failures.push(`${workflowPath}: ${message}`);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
throw new RepoExplainWorkflowError(
|
|
567
|
+
`Unable to load WORKFLOW.md for repo explain. Checked: ${failures.join("; ")}`
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
function resolveExplainWorkflowCandidates(input) {
|
|
571
|
+
const paths = [];
|
|
572
|
+
if (input.explicitWorkflowPath) {
|
|
573
|
+
paths.push(resolve2(input.explicitWorkflowPath));
|
|
574
|
+
}
|
|
575
|
+
if (input.repository.path) {
|
|
576
|
+
paths.push(join3(resolve2(input.repository.path), "WORKFLOW.md"));
|
|
577
|
+
}
|
|
578
|
+
const newestRuns = [...input.runs].sort(
|
|
579
|
+
(left, right) => (Date.parse(right.updatedAt) || 0) - (Date.parse(left.updatedAt) || 0)
|
|
580
|
+
);
|
|
581
|
+
for (const run of newestRuns) {
|
|
582
|
+
if (run.workflowPath) {
|
|
583
|
+
paths.push(resolve2(run.workflowPath));
|
|
584
|
+
}
|
|
585
|
+
if (run.workingDirectory) {
|
|
586
|
+
paths.push(join3(resolve2(run.workingDirectory), "WORKFLOW.md"));
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
return [...new Set(paths)];
|
|
590
|
+
}
|
|
591
|
+
function createProjectItemsCache() {
|
|
592
|
+
const entries = /* @__PURE__ */ new Map();
|
|
593
|
+
return {
|
|
594
|
+
getOrLoad(key, load) {
|
|
595
|
+
const cached = entries.get(key);
|
|
596
|
+
if (cached) {
|
|
597
|
+
return cached;
|
|
598
|
+
}
|
|
599
|
+
const pending = load().catch((error) => {
|
|
600
|
+
entries.delete(key);
|
|
601
|
+
throw error;
|
|
602
|
+
});
|
|
603
|
+
entries.set(key, pending);
|
|
604
|
+
return pending;
|
|
605
|
+
}
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
async function readRuns(runtimeRoot, projectId) {
|
|
609
|
+
let runIds;
|
|
610
|
+
try {
|
|
611
|
+
runIds = await readdir3(join3(runtimeRoot, "runs"));
|
|
612
|
+
} catch {
|
|
613
|
+
return [];
|
|
614
|
+
}
|
|
615
|
+
const runs = await Promise.all(
|
|
616
|
+
runIds.map(
|
|
617
|
+
(runId) => readJsonFile2(
|
|
618
|
+
join3(runtimeRoot, "runs", runId, "run.json")
|
|
619
|
+
)
|
|
620
|
+
)
|
|
621
|
+
);
|
|
622
|
+
return runs.filter(
|
|
623
|
+
(run) => run !== null && (!run.projectId || run.projectId === projectId)
|
|
624
|
+
);
|
|
625
|
+
}
|
|
626
|
+
async function readJsonFile2(path) {
|
|
627
|
+
try {
|
|
628
|
+
return JSON.parse(await readFile3(path, "utf8"));
|
|
629
|
+
} catch {
|
|
630
|
+
return null;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
function renderRepoExplainReport(report, noColor) {
|
|
634
|
+
const apply = noColor ? (value) => stripAnsi(value) : (value) => value;
|
|
635
|
+
const lines = [
|
|
636
|
+
apply(bold(`Issue dispatch explanation: ${report.issue.identifier}`)),
|
|
637
|
+
report.summary,
|
|
638
|
+
"",
|
|
639
|
+
`State: ${report.issue.state ?? "unknown"}`,
|
|
640
|
+
`Repository: ${report.issue.repository}`,
|
|
641
|
+
"",
|
|
642
|
+
"Checks:"
|
|
643
|
+
];
|
|
644
|
+
for (const check of report.checks) {
|
|
645
|
+
const marker = check.status === "pass" ? green("\u2713") : check.status === "warn" ? yellow("!") : red("\u2717");
|
|
646
|
+
lines.push(` ${apply(marker)} ${check.message}`);
|
|
647
|
+
if (check.hint) {
|
|
648
|
+
lines.push(` Hint: ${check.hint}`);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
lines.push("");
|
|
652
|
+
lines.push("Related commands:");
|
|
653
|
+
lines.push(" gh-symphony workflow preview");
|
|
654
|
+
lines.push(" gh-symphony doctor");
|
|
655
|
+
lines.push(" gh-symphony repo status");
|
|
656
|
+
lines.push(" gh-symphony repo logs --issue " + report.issue.identifier);
|
|
657
|
+
return lines.join("\n") + "\n";
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// src/commands/run.ts
|
|
661
|
+
function parseRunArgs(args) {
|
|
662
|
+
const parsed = {
|
|
663
|
+
watch: false
|
|
664
|
+
};
|
|
665
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
666
|
+
const arg = args[i];
|
|
667
|
+
if (arg === "--watch" || arg === "-w") {
|
|
668
|
+
parsed.watch = true;
|
|
669
|
+
} else if (arg === "--project" || arg === "--project-id") {
|
|
670
|
+
const value = args[i + 1];
|
|
671
|
+
if (!value || value.startsWith("-")) {
|
|
672
|
+
parsed.error = `Option '${arg}' argument missing`;
|
|
673
|
+
return parsed;
|
|
674
|
+
}
|
|
675
|
+
parsed.projectId = value;
|
|
676
|
+
i += 1;
|
|
677
|
+
} else if (arg === "--log-level") {
|
|
678
|
+
const value = args[i + 1];
|
|
679
|
+
if (!value || value.startsWith("-")) {
|
|
680
|
+
parsed.error = `Option '${arg}' argument missing`;
|
|
681
|
+
return parsed;
|
|
682
|
+
}
|
|
683
|
+
parsed.logLevel = value;
|
|
684
|
+
i += 1;
|
|
685
|
+
} else if (!arg?.startsWith("-")) {
|
|
686
|
+
parsed.issue = arg;
|
|
687
|
+
} else {
|
|
688
|
+
parsed.error = `Unknown option '${arg}'`;
|
|
689
|
+
return parsed;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
return parsed;
|
|
693
|
+
}
|
|
694
|
+
var handler4 = async (args, options) => {
|
|
695
|
+
const parsed = parseRunArgs(args);
|
|
696
|
+
if (parsed.error) {
|
|
697
|
+
process.stderr.write(`${parsed.error}
|
|
698
|
+
`);
|
|
699
|
+
process.exitCode = 2;
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
if (!parsed.issue) {
|
|
703
|
+
process.stderr.write("Usage: gh-symphony repo run <owner/repo#number>\n");
|
|
704
|
+
process.exitCode = 2;
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
const projectConfig = await resolveManagedProjectConfig({
|
|
708
|
+
configDir: options.configDir,
|
|
709
|
+
requestedProjectId: parsed.projectId
|
|
710
|
+
});
|
|
711
|
+
if (!projectConfig) {
|
|
712
|
+
handleMissingManagedProjectConfig();
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
const runtimeRoot = resolveRuntimeRoot(options.configDir);
|
|
716
|
+
const projectId = projectConfig.projectId;
|
|
717
|
+
const [repoSpec] = parsed.issue.split("#");
|
|
718
|
+
const configuredRepos = [projectConfig.repository].filter(
|
|
719
|
+
(repository) => Boolean(repository?.owner && repository.name)
|
|
720
|
+
).map((repository) => `${repository.owner}/${repository.name}`);
|
|
721
|
+
const configuredRepoSet = new Set(configuredRepos);
|
|
722
|
+
if (configuredRepoSet.size === 0) {
|
|
723
|
+
process.stderr.write(
|
|
724
|
+
"No repository is configured in this project. Run 'gh-symphony repo init' from the target repository first.\n"
|
|
725
|
+
);
|
|
726
|
+
process.exitCode = 1;
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
if (repoSpec && !configuredRepoSet.has(repoSpec)) {
|
|
730
|
+
process.stderr.write(
|
|
731
|
+
`Repository "${repoSpec}" is not configured in this project.
|
|
732
|
+
Configured repo: ${configuredRepos.join(", ")}
|
|
733
|
+
`
|
|
734
|
+
);
|
|
735
|
+
process.exitCode = 1;
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
process.stdout.write(`Dispatching issue: ${parsed.issue}
|
|
739
|
+
`);
|
|
740
|
+
await runCli([
|
|
741
|
+
"run-issue",
|
|
742
|
+
"--runtime-root",
|
|
743
|
+
runtimeRoot,
|
|
744
|
+
"--project-id",
|
|
745
|
+
projectId,
|
|
746
|
+
"--issue",
|
|
747
|
+
parsed.issue,
|
|
748
|
+
...parsed.logLevel ? ["--log-level", parsed.logLevel] : []
|
|
749
|
+
]);
|
|
750
|
+
if (parsed.watch) {
|
|
751
|
+
process.stdout.write("\nWatching for status changes...\n");
|
|
752
|
+
await runCli([
|
|
753
|
+
"status",
|
|
754
|
+
"--runtime-root",
|
|
755
|
+
runtimeRoot,
|
|
756
|
+
"--project-id",
|
|
757
|
+
projectId
|
|
758
|
+
]);
|
|
759
|
+
}
|
|
760
|
+
};
|
|
761
|
+
var run_default = handler4;
|
|
762
|
+
|
|
763
|
+
// src/commands/start.ts
|
|
764
|
+
import { writeFile, mkdir, readFile as readFile5, rm } from "fs/promises";
|
|
765
|
+
import { dirname as dirname2, join as join6 } from "path";
|
|
766
|
+
import { spawn } from "child_process";
|
|
767
|
+
import { createServer as createServer3 } from "http";
|
|
768
|
+
|
|
769
|
+
// ../dashboard/src/store.ts
|
|
770
|
+
import { open } from "fs/promises";
|
|
771
|
+
import { join as join4, resolve as resolve3 } from "path";
|
|
772
|
+
var DEFAULT_RECENT_EVENT_LIMIT = 20;
|
|
773
|
+
var RECENT_EVENT_CHUNK_SIZE = 4096;
|
|
774
|
+
var MAX_RECENT_EVENT_SCAN_BYTES = 64 * 1024;
|
|
775
|
+
var RUN_RECORD_LOAD_CONCURRENCY = 8;
|
|
776
|
+
var DashboardFsReader = class {
|
|
777
|
+
constructor(runtimeRoot) {
|
|
778
|
+
this.runtimeRoot = runtimeRoot;
|
|
779
|
+
this.resolvedRuntimeRoot = resolve3(runtimeRoot);
|
|
780
|
+
}
|
|
781
|
+
resolvedRuntimeRoot;
|
|
782
|
+
projectDir() {
|
|
783
|
+
return this.resolvedRuntimeRoot;
|
|
784
|
+
}
|
|
785
|
+
runDir(runId) {
|
|
786
|
+
assertValidDashboardRunId(runId);
|
|
787
|
+
return join4(this.resolvedRuntimeRoot, "runs", runId);
|
|
788
|
+
}
|
|
789
|
+
async loadProjectStatus() {
|
|
790
|
+
const snapshot = await readJsonFile(join4(this.projectDir(), "status.json"));
|
|
791
|
+
if (!snapshot) {
|
|
792
|
+
return null;
|
|
793
|
+
}
|
|
794
|
+
const status = { ...snapshot };
|
|
795
|
+
delete status.projectId;
|
|
796
|
+
delete status.slug;
|
|
797
|
+
if (!isRepositoryRef(status.repository)) {
|
|
798
|
+
return null;
|
|
799
|
+
}
|
|
800
|
+
return status;
|
|
801
|
+
}
|
|
802
|
+
async loadProjectState() {
|
|
803
|
+
const snapshot = await this.loadProjectStatus();
|
|
804
|
+
if (!snapshot) {
|
|
805
|
+
return null;
|
|
806
|
+
}
|
|
807
|
+
const issues = await this.loadProjectIssueOrchestrations();
|
|
808
|
+
return {
|
|
809
|
+
...snapshot,
|
|
810
|
+
completedCount: issues.filter((issue) => issue.completedOnce).length,
|
|
811
|
+
issues
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
async loadProjectIssueOrchestrations() {
|
|
815
|
+
const issues = await readJsonFile(
|
|
816
|
+
join4(this.projectDir(), "issues.json")
|
|
817
|
+
);
|
|
818
|
+
if (issues) {
|
|
819
|
+
return issues.map((issue) => ({
|
|
820
|
+
...issue,
|
|
821
|
+
completedOnce: issue.completedOnce ?? false,
|
|
822
|
+
failureRetryCount: issue.failureRetryCount ?? 0
|
|
823
|
+
}));
|
|
824
|
+
}
|
|
825
|
+
const legacyLeases = await readJsonFile(join4(this.projectDir(), "leases.json")) ?? [];
|
|
826
|
+
return legacyLeases.map((lease) => ({
|
|
827
|
+
issueId: lease.issueId,
|
|
828
|
+
identifier: lease.issueIdentifier,
|
|
829
|
+
workspaceKey: deriveIssueWorkspaceKeyFromIdentifier(
|
|
830
|
+
lease.issueIdentifier
|
|
831
|
+
),
|
|
832
|
+
completedOnce: false,
|
|
833
|
+
failureRetryCount: 0,
|
|
834
|
+
state: lease.status === "active" ? "claimed" : "released",
|
|
835
|
+
currentRunId: lease.status === "active" ? lease.runId : null,
|
|
836
|
+
retryEntry: null,
|
|
837
|
+
updatedAt: lease.updatedAt
|
|
838
|
+
}));
|
|
839
|
+
}
|
|
840
|
+
async loadRun(runId) {
|
|
841
|
+
return readJsonFile(
|
|
842
|
+
join4(this.runDir(runId), "run.json")
|
|
843
|
+
);
|
|
844
|
+
}
|
|
845
|
+
async loadAllRuns() {
|
|
846
|
+
const runIds = await safeReadDir(join4(this.projectDir(), "runs"));
|
|
847
|
+
const runs = await mapWithConcurrency(
|
|
848
|
+
runIds,
|
|
849
|
+
RUN_RECORD_LOAD_CONCURRENCY,
|
|
850
|
+
(runId) => this.loadRun(runId)
|
|
851
|
+
);
|
|
852
|
+
return runs.filter((run) => Boolean(run));
|
|
853
|
+
}
|
|
854
|
+
async loadRunsForIssue(issueId, issueIdentifier) {
|
|
855
|
+
const runIds = await safeReadDir(join4(this.projectDir(), "runs"));
|
|
856
|
+
const runs = await mapWithConcurrency(
|
|
857
|
+
runIds,
|
|
858
|
+
RUN_RECORD_LOAD_CONCURRENCY,
|
|
859
|
+
async (runId) => {
|
|
860
|
+
try {
|
|
861
|
+
const run = await this.loadRun(runId);
|
|
862
|
+
if (!run) {
|
|
863
|
+
return null;
|
|
864
|
+
}
|
|
865
|
+
return run.issueId === issueId || run.issueIdentifier === issueIdentifier ? run : null;
|
|
866
|
+
} catch (error) {
|
|
867
|
+
if (isFileMissing(error)) {
|
|
868
|
+
return null;
|
|
869
|
+
}
|
|
870
|
+
return null;
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
);
|
|
874
|
+
return runs.filter((run) => Boolean(run));
|
|
875
|
+
}
|
|
876
|
+
async loadRecentRunEvents(runId, limit = DEFAULT_RECENT_EVENT_LIMIT) {
|
|
877
|
+
if (limit <= 0) {
|
|
878
|
+
return [];
|
|
879
|
+
}
|
|
880
|
+
const path = join4(this.runDir(runId), "events.ndjson");
|
|
881
|
+
try {
|
|
882
|
+
const handle = await open(path, "r");
|
|
883
|
+
try {
|
|
884
|
+
const stats = await handle.stat();
|
|
885
|
+
let position = stats.size;
|
|
886
|
+
let bytesScanned = 0;
|
|
887
|
+
let newlineCount = 0;
|
|
888
|
+
const chunks = [];
|
|
889
|
+
while (position > 0 && bytesScanned < MAX_RECENT_EVENT_SCAN_BYTES && newlineCount <= limit) {
|
|
890
|
+
const readSize = Math.min(
|
|
891
|
+
position,
|
|
892
|
+
RECENT_EVENT_CHUNK_SIZE,
|
|
893
|
+
MAX_RECENT_EVENT_SCAN_BYTES - bytesScanned
|
|
894
|
+
);
|
|
895
|
+
position -= readSize;
|
|
896
|
+
const chunk = Buffer.allocUnsafe(readSize);
|
|
897
|
+
const { bytesRead } = await handle.read(chunk, 0, readSize, position);
|
|
898
|
+
if (bytesRead === 0) {
|
|
899
|
+
break;
|
|
900
|
+
}
|
|
901
|
+
const populatedChunk = chunk.subarray(0, bytesRead);
|
|
902
|
+
chunks.unshift(populatedChunk);
|
|
903
|
+
bytesScanned += bytesRead;
|
|
904
|
+
newlineCount += countNewlines(populatedChunk);
|
|
905
|
+
}
|
|
906
|
+
return parseRecentEvents(
|
|
907
|
+
Buffer.concat(chunks).toString("utf8"),
|
|
908
|
+
limit,
|
|
909
|
+
{
|
|
910
|
+
allowPartialFirstLine: position > 0
|
|
911
|
+
}
|
|
912
|
+
);
|
|
913
|
+
} finally {
|
|
914
|
+
await handle.close();
|
|
915
|
+
}
|
|
916
|
+
} catch (error) {
|
|
917
|
+
if (isFileMissing(error)) {
|
|
918
|
+
return [];
|
|
919
|
+
}
|
|
920
|
+
throw error;
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
};
|
|
924
|
+
function countNewlines(chunk) {
|
|
925
|
+
let count = 0;
|
|
926
|
+
for (const byte of chunk) {
|
|
927
|
+
if (byte === 10) {
|
|
928
|
+
count += 1;
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
return count;
|
|
932
|
+
}
|
|
933
|
+
async function statusForIssue(reader, issueIdentifier) {
|
|
934
|
+
const issueRecords = await reader.loadProjectIssueOrchestrations();
|
|
935
|
+
const issueRecord = issueRecords.find(
|
|
936
|
+
(record) => record.identifier === issueIdentifier
|
|
937
|
+
);
|
|
938
|
+
if (!issueRecord) {
|
|
939
|
+
return null;
|
|
940
|
+
}
|
|
941
|
+
const currentRunCandidate = issueRecord.currentRunId ? await reader.loadRun(issueRecord.currentRunId) : null;
|
|
942
|
+
const currentRun = isMatchingIssueRun(
|
|
943
|
+
currentRunCandidate,
|
|
944
|
+
issueRecord.issueId,
|
|
945
|
+
issueIdentifier
|
|
946
|
+
) ? currentRunCandidate : null;
|
|
947
|
+
const issueRuns = currentRun === null ? await reader.loadRunsForIssue(issueRecord.issueId, issueIdentifier) : currentRun.tokenUsage ? await reader.loadRunsForIssue(issueRecord.issueId, issueIdentifier) : null;
|
|
948
|
+
const resolvedRun = currentRun ?? findLatestRunForIssue(issueRuns ?? []);
|
|
949
|
+
const recentEvents = resolvedRun === null ? [] : await reader.loadRecentRunEvents(resolvedRun.runId);
|
|
950
|
+
const cumulativeTokens = aggregateIssueTokenUsage(issueRuns ?? []);
|
|
951
|
+
const latestEventMessage = recentEvents[recentEvents.length - 1]?.message ?? null;
|
|
952
|
+
const currentAttempt = resolvedRun?.attempt ?? issueRecord.retryEntry?.attempt ?? 0;
|
|
953
|
+
return {
|
|
954
|
+
issue_identifier: issueRecord.identifier,
|
|
955
|
+
issue_id: issueRecord.issueId,
|
|
956
|
+
status: resolvedRun?.status ?? mapIssueOrchestrationStateToStatus(issueRecord.state),
|
|
957
|
+
workspace: {
|
|
958
|
+
path: resolvedRun?.workingDirectory ?? null
|
|
959
|
+
},
|
|
960
|
+
attempts: {
|
|
961
|
+
restart_count: Math.max(0, currentAttempt - 1),
|
|
962
|
+
current_retry_attempt: currentAttempt
|
|
963
|
+
},
|
|
964
|
+
running: resolvedRun === null ? null : {
|
|
965
|
+
session_id: resolvedRun.runtimeSession?.sessionId ?? null,
|
|
966
|
+
turn_count: resolvedRun.turnCount ?? null,
|
|
967
|
+
state: resolvedRun.issueState ?? null,
|
|
968
|
+
started_at: resolvedRun.startedAt ?? null,
|
|
969
|
+
last_event: resolvedRun.lastEvent ?? null,
|
|
970
|
+
last_message: latestEventMessage,
|
|
971
|
+
last_event_at: resolvedRun.lastEventAt ?? null,
|
|
972
|
+
tokens: resolvedRun.tokenUsage ? {
|
|
973
|
+
input_tokens: resolvedRun.tokenUsage.inputTokens,
|
|
974
|
+
output_tokens: resolvedRun.tokenUsage.outputTokens,
|
|
975
|
+
total_tokens: resolvedRun.tokenUsage.totalTokens,
|
|
976
|
+
cumulative_input_tokens: cumulativeTokens.inputTokens,
|
|
977
|
+
cumulative_output_tokens: cumulativeTokens.outputTokens,
|
|
978
|
+
cumulative_total_tokens: cumulativeTokens.totalTokens
|
|
979
|
+
} : null
|
|
980
|
+
},
|
|
981
|
+
retry: resolvedRun?.nextRetryAt ?? issueRecord.retryEntry?.dueAt ? {
|
|
982
|
+
due_at: resolvedRun?.nextRetryAt ?? issueRecord.retryEntry?.dueAt ?? "",
|
|
983
|
+
kind: resolvedRun?.retryKind ?? null,
|
|
984
|
+
error: resolvedRun?.lastError ?? issueRecord.retryEntry?.error ?? null
|
|
985
|
+
} : null,
|
|
986
|
+
logs: {
|
|
987
|
+
codex_session_logs: resolvedRun === null ? [] : [
|
|
988
|
+
{
|
|
989
|
+
label: "worker",
|
|
990
|
+
path: join4(reader.runDir(resolvedRun.runId), "worker.log"),
|
|
991
|
+
url: null
|
|
992
|
+
}
|
|
993
|
+
]
|
|
994
|
+
},
|
|
995
|
+
recent_events: recentEvents,
|
|
996
|
+
last_error: resolvedRun?.lastError ?? issueRecord.retryEntry?.error ?? null,
|
|
997
|
+
tracked: {
|
|
998
|
+
issue_orchestration_state: issueRecord.state,
|
|
999
|
+
current_run_id: issueRecord.currentRunId,
|
|
1000
|
+
workspace_key: issueRecord.workspaceKey,
|
|
1001
|
+
completed_once: issueRecord.completedOnce,
|
|
1002
|
+
run_phase: resolvedRun?.runPhase ?? null,
|
|
1003
|
+
execution_phase: resolvedRun?.executionPhase ?? null
|
|
1004
|
+
}
|
|
1005
|
+
};
|
|
1006
|
+
}
|
|
1007
|
+
function aggregateIssueTokenUsage(runs) {
|
|
1008
|
+
return runs.reduce(
|
|
1009
|
+
(total, run) => ({
|
|
1010
|
+
inputTokens: total.inputTokens + (run.tokenUsage?.inputTokens ?? 0),
|
|
1011
|
+
outputTokens: total.outputTokens + (run.tokenUsage?.outputTokens ?? 0),
|
|
1012
|
+
totalTokens: total.totalTokens + (run.tokenUsage?.totalTokens ?? 0)
|
|
1013
|
+
}),
|
|
1014
|
+
{
|
|
1015
|
+
inputTokens: 0,
|
|
1016
|
+
outputTokens: 0,
|
|
1017
|
+
totalTokens: 0
|
|
1018
|
+
}
|
|
1019
|
+
);
|
|
1020
|
+
}
|
|
1021
|
+
function findLatestRunForIssue(matchingRuns) {
|
|
1022
|
+
const sortedRuns = [...matchingRuns].sort(
|
|
1023
|
+
(left, right) => new Date(right.updatedAt).getTime() - new Date(left.updatedAt).getTime()
|
|
1024
|
+
);
|
|
1025
|
+
return sortedRuns[0] ?? null;
|
|
1026
|
+
}
|
|
1027
|
+
function assertValidDashboardRunId(runId) {
|
|
1028
|
+
if (runId.length === 0 || runId === "." || runId === ".." || runId.includes("/") || runId.includes("\\")) {
|
|
1029
|
+
throw new Error(
|
|
1030
|
+
`Invalid run ID "${runId}". Run IDs must not contain path separators or traversal segments.`
|
|
1031
|
+
);
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
function isRepositoryRef(value) {
|
|
1035
|
+
if (!value || typeof value !== "object") {
|
|
1036
|
+
return false;
|
|
1037
|
+
}
|
|
1038
|
+
const repository = value;
|
|
1039
|
+
return typeof repository.owner === "string" && repository.owner.length > 0 && typeof repository.name === "string" && repository.name.length > 0 && typeof repository.cloneUrl === "string";
|
|
1040
|
+
}
|
|
1041
|
+
async function mapWithConcurrency(items, concurrency, mapper) {
|
|
1042
|
+
const results = new Array(items.length);
|
|
1043
|
+
let nextIndex = 0;
|
|
1044
|
+
const worker = async () => {
|
|
1045
|
+
while (nextIndex < items.length) {
|
|
1046
|
+
const currentIndex = nextIndex;
|
|
1047
|
+
nextIndex += 1;
|
|
1048
|
+
results[currentIndex] = await mapper(items[currentIndex]);
|
|
1049
|
+
}
|
|
1050
|
+
};
|
|
1051
|
+
const workerCount = Math.min(Math.max(concurrency, 1), items.length);
|
|
1052
|
+
await Promise.all(Array.from({ length: workerCount }, () => worker()));
|
|
1053
|
+
return results;
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
// ../dashboard/src/server.ts
|
|
1057
|
+
import {
|
|
1058
|
+
createServer
|
|
1059
|
+
} from "http";
|
|
1060
|
+
async function resolveDashboardResponse(options) {
|
|
1061
|
+
const method = options.method ?? "GET";
|
|
1062
|
+
if (options.pathname === "/healthz") {
|
|
1063
|
+
return {
|
|
1064
|
+
status: 200,
|
|
1065
|
+
payload: { ok: true }
|
|
1066
|
+
};
|
|
1067
|
+
}
|
|
1068
|
+
if (options.pathname === "/api/v1/state") {
|
|
1069
|
+
if (method !== "GET") {
|
|
1070
|
+
return {
|
|
1071
|
+
status: 405,
|
|
1072
|
+
payload: { error: "Method not allowed" }
|
|
1073
|
+
};
|
|
1074
|
+
}
|
|
1075
|
+
const snapshot = await options.reader.loadProjectState();
|
|
1076
|
+
if (!snapshot) {
|
|
1077
|
+
return {
|
|
1078
|
+
status: 404,
|
|
1079
|
+
payload: { error: "Project status not found." }
|
|
1080
|
+
};
|
|
1081
|
+
}
|
|
1082
|
+
return {
|
|
1083
|
+
status: 200,
|
|
1084
|
+
payload: snapshot
|
|
1085
|
+
};
|
|
1086
|
+
}
|
|
1087
|
+
if (options.pathname.startsWith("/api/v1/")) {
|
|
1088
|
+
if (method !== "GET") {
|
|
1089
|
+
return {
|
|
1090
|
+
status: 405,
|
|
1091
|
+
payload: { error: "Method not allowed" }
|
|
1092
|
+
};
|
|
1093
|
+
}
|
|
1094
|
+
const rawIdentifier = options.pathname.slice("/api/v1/".length);
|
|
1095
|
+
if (!rawIdentifier || rawIdentifier === "state") {
|
|
1096
|
+
return {
|
|
1097
|
+
status: 404,
|
|
1098
|
+
payload: { error: "Not found" }
|
|
1099
|
+
};
|
|
1100
|
+
}
|
|
1101
|
+
let issueIdentifier;
|
|
1102
|
+
try {
|
|
1103
|
+
issueIdentifier = decodeURIComponent(rawIdentifier);
|
|
1104
|
+
} catch {
|
|
1105
|
+
return {
|
|
1106
|
+
status: 400,
|
|
1107
|
+
payload: {
|
|
1108
|
+
error: {
|
|
1109
|
+
code: "invalid_issue_identifier",
|
|
1110
|
+
message: "Issue identifier path segment is not valid URL encoding."
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
};
|
|
1114
|
+
}
|
|
1115
|
+
const issueStatus = await statusForIssue(options.reader, issueIdentifier);
|
|
1116
|
+
if (!issueStatus) {
|
|
1117
|
+
return {
|
|
1118
|
+
status: 404,
|
|
1119
|
+
payload: {
|
|
1120
|
+
error: {
|
|
1121
|
+
code: "issue_not_found",
|
|
1122
|
+
message: `Issue "${issueIdentifier}" is unknown to the current filesystem state.`
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
};
|
|
1126
|
+
}
|
|
1127
|
+
return {
|
|
1128
|
+
status: 200,
|
|
1129
|
+
payload: issueStatus
|
|
1130
|
+
};
|
|
1131
|
+
}
|
|
1132
|
+
return {
|
|
1133
|
+
status: 404,
|
|
1134
|
+
payload: { error: "Not found" }
|
|
1135
|
+
};
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
// ../control-plane/src/server.ts
|
|
1139
|
+
import {
|
|
1140
|
+
createServer as createServer2
|
|
1141
|
+
} from "http";
|
|
1142
|
+
import { readFile as readFile4, stat as stat2 } from "fs/promises";
|
|
1143
|
+
import { dirname, extname, join as join5, resolve as resolve4, sep } from "path";
|
|
1144
|
+
import { fileURLToPath } from "url";
|
|
1145
|
+
var CLIENT_DIST_DIR = join5(
|
|
1146
|
+
dirname(fileURLToPath(import.meta.url)),
|
|
1147
|
+
"../client/dist"
|
|
1148
|
+
);
|
|
1149
|
+
var BUNDLED_CLIENT_DIST_DIR = join5(
|
|
1150
|
+
dirname(fileURLToPath(import.meta.url)),
|
|
1151
|
+
"../../control-plane/client/dist"
|
|
1152
|
+
);
|
|
1153
|
+
var WORKSPACE_CLIENT_DIST_DIR = join5(
|
|
1154
|
+
process.cwd(),
|
|
1155
|
+
"packages/control-plane/client/dist"
|
|
1156
|
+
);
|
|
1157
|
+
var NODE_MODULES_CLIENT_DIST_DIR = join5(
|
|
1158
|
+
process.cwd(),
|
|
1159
|
+
"node_modules/@gh-symphony/control-plane/client/dist"
|
|
1160
|
+
);
|
|
1161
|
+
var CLIENT_DIST_DIR_CANDIDATES = [
|
|
1162
|
+
CLIENT_DIST_DIR,
|
|
1163
|
+
BUNDLED_CLIENT_DIST_DIR,
|
|
1164
|
+
WORKSPACE_CLIENT_DIST_DIR,
|
|
1165
|
+
NODE_MODULES_CLIENT_DIST_DIR
|
|
1166
|
+
];
|
|
1167
|
+
var clientDistDirPromise;
|
|
1168
|
+
var TEXT_CONTENT_TYPES = /* @__PURE__ */ new Set([
|
|
1169
|
+
"application/javascript",
|
|
1170
|
+
"application/json",
|
|
1171
|
+
"image/svg+xml",
|
|
1172
|
+
"text/css",
|
|
1173
|
+
"text/html",
|
|
1174
|
+
"text/plain"
|
|
1175
|
+
]);
|
|
1176
|
+
var CONTENT_TYPES = {
|
|
1177
|
+
".css": "text/css",
|
|
1178
|
+
".gif": "image/gif",
|
|
1179
|
+
".html": "text/html",
|
|
1180
|
+
".ico": "image/x-icon",
|
|
1181
|
+
".jpeg": "image/jpeg",
|
|
1182
|
+
".jpg": "image/jpeg",
|
|
1183
|
+
".js": "application/javascript",
|
|
1184
|
+
".json": "application/json",
|
|
1185
|
+
".map": "application/json",
|
|
1186
|
+
".png": "image/png",
|
|
1187
|
+
".svg": "image/svg+xml",
|
|
1188
|
+
".txt": "text/plain",
|
|
1189
|
+
".webp": "image/webp"
|
|
1190
|
+
};
|
|
1191
|
+
function createControlPlaneHandler(options) {
|
|
1192
|
+
return async (request, response) => {
|
|
1193
|
+
try {
|
|
1194
|
+
const method = request.method ?? "GET";
|
|
1195
|
+
const url = new URL(request.url ?? "/", "http://127.0.0.1");
|
|
1196
|
+
if (url.pathname === "/api/v1/refresh") {
|
|
1197
|
+
await handleRefreshRequest(method, request, response, options);
|
|
1198
|
+
return;
|
|
1199
|
+
}
|
|
1200
|
+
if (isDashboardRequest(url.pathname)) {
|
|
1201
|
+
const resolved = await resolveDashboardResponse({
|
|
1202
|
+
pathname: url.pathname,
|
|
1203
|
+
method,
|
|
1204
|
+
reader: options.reader
|
|
1205
|
+
});
|
|
1206
|
+
respondJson(response, resolved.status, resolved.payload);
|
|
1207
|
+
return;
|
|
1208
|
+
}
|
|
1209
|
+
if (!isStaticRequestMethod(method)) {
|
|
1210
|
+
respondJson(response, 405, { error: "Method not allowed" });
|
|
1211
|
+
return;
|
|
1212
|
+
}
|
|
1213
|
+
const asset = await resolveStaticAsset(url.pathname);
|
|
1214
|
+
if (!asset) {
|
|
1215
|
+
respondJson(response, 404, { error: "Not found" });
|
|
1216
|
+
return;
|
|
1217
|
+
}
|
|
1218
|
+
if (asset.kind === "error") {
|
|
1219
|
+
respondJson(response, asset.status, { error: "Bad request" });
|
|
1220
|
+
return;
|
|
1221
|
+
}
|
|
1222
|
+
await respondFile(response, asset.path, method, asset.fallback);
|
|
1223
|
+
} catch (error) {
|
|
1224
|
+
console.error("Control plane request failed.", error);
|
|
1225
|
+
if (!response.headersSent) {
|
|
1226
|
+
respondJson(response, 500, { error: "Internal server error" });
|
|
1227
|
+
} else {
|
|
1228
|
+
response.end();
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
};
|
|
1232
|
+
}
|
|
1233
|
+
async function startControlPlaneServer(options) {
|
|
1234
|
+
const reader = new DashboardFsReader(options.runtimeRoot);
|
|
1235
|
+
const handler9 = createControlPlaneHandler({
|
|
1236
|
+
reader,
|
|
1237
|
+
onRefreshRequest: options.onRefreshRequest
|
|
1238
|
+
});
|
|
1239
|
+
for (let port = options.port; port <= 65535; port += 1) {
|
|
1240
|
+
const server = createServer2((request, response) => {
|
|
1241
|
+
void handler9(request, response);
|
|
1242
|
+
});
|
|
1243
|
+
try {
|
|
1244
|
+
await new Promise((resolveReady, rejectReady) => {
|
|
1245
|
+
const cleanup = () => {
|
|
1246
|
+
server.off("listening", handleListening);
|
|
1247
|
+
server.off("error", handleError);
|
|
1248
|
+
};
|
|
1249
|
+
const handleListening = () => {
|
|
1250
|
+
cleanup();
|
|
1251
|
+
resolveReady();
|
|
1252
|
+
};
|
|
1253
|
+
const handleError = (error) => {
|
|
1254
|
+
cleanup();
|
|
1255
|
+
rejectReady(error);
|
|
1256
|
+
};
|
|
1257
|
+
server.once("listening", handleListening);
|
|
1258
|
+
server.once("error", handleError);
|
|
1259
|
+
server.listen(port, options.host);
|
|
1260
|
+
});
|
|
1261
|
+
const address = server.address();
|
|
1262
|
+
const boundPort = address && typeof address !== "string" ? address.port : port;
|
|
1263
|
+
return {
|
|
1264
|
+
server,
|
|
1265
|
+
port: boundPort,
|
|
1266
|
+
url: formatBoundUrl(server)
|
|
1267
|
+
};
|
|
1268
|
+
} catch (error) {
|
|
1269
|
+
await closeServer(server).catch(() => {
|
|
1270
|
+
});
|
|
1271
|
+
if (error.code === "EADDRINUSE") {
|
|
1272
|
+
continue;
|
|
1273
|
+
}
|
|
1274
|
+
throw error;
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
throw new Error(
|
|
1278
|
+
`Unable to bind control plane server starting from port ${options.port}`
|
|
1279
|
+
);
|
|
1280
|
+
}
|
|
1281
|
+
async function handleRefreshRequest(method, request, response, options) {
|
|
1282
|
+
if (method !== "POST") {
|
|
1283
|
+
respondJson(response, 405, { error: "Method not allowed" });
|
|
1284
|
+
return;
|
|
1285
|
+
}
|
|
1286
|
+
request.resume();
|
|
1287
|
+
options.onRefreshRequest?.();
|
|
1288
|
+
respondJson(response, 202, { ok: true });
|
|
1289
|
+
}
|
|
1290
|
+
function isDashboardRequest(pathname) {
|
|
1291
|
+
return pathname === "/healthz" || pathname === "/api/v1/state" || pathname.startsWith("/api/v1/");
|
|
1292
|
+
}
|
|
1293
|
+
function isStaticRequestMethod(method) {
|
|
1294
|
+
return method === "GET" || method === "HEAD";
|
|
1295
|
+
}
|
|
1296
|
+
async function resolveStaticAsset(pathname) {
|
|
1297
|
+
const clientDistDir = await resolveClientDistDir();
|
|
1298
|
+
if (!clientDistDir) {
|
|
1299
|
+
return null;
|
|
1300
|
+
}
|
|
1301
|
+
const indexPath = join5(clientDistDir, "index.html");
|
|
1302
|
+
if (pathname === "/") {
|
|
1303
|
+
return await existsAsFile(indexPath) ? { kind: "asset", path: indexPath, fallback: true } : null;
|
|
1304
|
+
}
|
|
1305
|
+
let decodedPathname;
|
|
1306
|
+
try {
|
|
1307
|
+
decodedPathname = decodeURIComponent(pathname);
|
|
1308
|
+
} catch {
|
|
1309
|
+
return { kind: "error", status: 400 };
|
|
1310
|
+
}
|
|
1311
|
+
const resolvedPath = resolve4(clientDistDir, `.${decodedPathname}`);
|
|
1312
|
+
if (!isPathInsideClientDist(clientDistDir, resolvedPath)) {
|
|
1313
|
+
return null;
|
|
1314
|
+
}
|
|
1315
|
+
if (await existsAsFile(resolvedPath)) {
|
|
1316
|
+
return { kind: "asset", path: resolvedPath, fallback: false };
|
|
1317
|
+
}
|
|
1318
|
+
if (hasFileExtension(decodedPathname)) {
|
|
1319
|
+
return null;
|
|
1320
|
+
}
|
|
1321
|
+
return await existsAsFile(indexPath) ? { kind: "asset", path: indexPath, fallback: true } : null;
|
|
1322
|
+
}
|
|
1323
|
+
function isPathInsideClientDist(clientDistDir, path) {
|
|
1324
|
+
return path === clientDistDir || path.startsWith(`${clientDistDir}${sep}`);
|
|
1325
|
+
}
|
|
1326
|
+
function hasFileExtension(pathname) {
|
|
1327
|
+
const lastSegment = pathname.split("/").pop() ?? "";
|
|
1328
|
+
return lastSegment.includes(".");
|
|
1329
|
+
}
|
|
1330
|
+
async function existsAsFile(path) {
|
|
1331
|
+
try {
|
|
1332
|
+
return (await stat2(path)).isFile();
|
|
1333
|
+
} catch {
|
|
1334
|
+
return false;
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
async function resolveClientDistDir() {
|
|
1338
|
+
clientDistDirPromise ??= (async () => {
|
|
1339
|
+
for (const candidate of CLIENT_DIST_DIR_CANDIDATES) {
|
|
1340
|
+
try {
|
|
1341
|
+
if ((await stat2(candidate)).isDirectory()) {
|
|
1342
|
+
return candidate;
|
|
1343
|
+
}
|
|
1344
|
+
} catch {
|
|
1345
|
+
continue;
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
return null;
|
|
1349
|
+
})();
|
|
1350
|
+
return clientDistDirPromise;
|
|
1351
|
+
}
|
|
1352
|
+
async function respondFile(response, path, method, fallback) {
|
|
1353
|
+
const contentType = contentTypeForPath(path);
|
|
1354
|
+
const body = method === "HEAD" ? void 0 : await readFile4(path);
|
|
1355
|
+
const cacheControl = fallback || path.endsWith(`${sep}index.html`) ? "no-cache" : "public, max-age=31536000, immutable";
|
|
1356
|
+
response.writeHead(200, {
|
|
1357
|
+
"cache-control": cacheControl,
|
|
1358
|
+
"content-type": contentType
|
|
1359
|
+
});
|
|
1360
|
+
response.end(body);
|
|
1361
|
+
}
|
|
1362
|
+
function contentTypeForPath(path) {
|
|
1363
|
+
const contentType = CONTENT_TYPES[extname(path).toLowerCase()];
|
|
1364
|
+
if (!contentType) {
|
|
1365
|
+
return "application/octet-stream";
|
|
1366
|
+
}
|
|
1367
|
+
if (TEXT_CONTENT_TYPES.has(contentType)) {
|
|
1368
|
+
return `${contentType}; charset=utf-8`;
|
|
1369
|
+
}
|
|
1370
|
+
return contentType;
|
|
1371
|
+
}
|
|
1372
|
+
function respondJson(response, status, payload) {
|
|
1373
|
+
response.writeHead(status, {
|
|
1374
|
+
"content-type": "application/json; charset=utf-8"
|
|
1375
|
+
});
|
|
1376
|
+
response.end(JSON.stringify(payload));
|
|
1377
|
+
}
|
|
1378
|
+
function formatBoundUrl(server) {
|
|
1379
|
+
const address = server.address();
|
|
1380
|
+
if (!address || typeof address === "string") {
|
|
1381
|
+
return "http://localhost";
|
|
1382
|
+
}
|
|
1383
|
+
const host = address.address === "::" || address.address === "::1" || address.address === "0.0.0.0" || address.address === "127.0.0.1" ? "localhost" : address.address;
|
|
1384
|
+
const urlHost = host.includes(":") ? `[${host}]` : host;
|
|
1385
|
+
return `http://${urlHost}:${address.port}`;
|
|
1386
|
+
}
|
|
1387
|
+
async function closeServer(server) {
|
|
1388
|
+
await new Promise((resolveClose, rejectClose) => {
|
|
1389
|
+
server.close((error) => {
|
|
1390
|
+
if (error) {
|
|
1391
|
+
rejectClose(error);
|
|
1392
|
+
return;
|
|
1393
|
+
}
|
|
1394
|
+
resolveClose();
|
|
1395
|
+
});
|
|
1396
|
+
});
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
// src/removed-project-id.ts
|
|
1400
|
+
var REMOVED_PROJECT_ID_MESSAGE = "--project-id has been removed. gh-symphony now uses the current repository directory; run the command from the target repo or pass --repo-dir where supported.";
|
|
1401
|
+
function rejectRemovedProjectId(args, options = { rejectProjectAlias: true }) {
|
|
1402
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
1403
|
+
const arg = args[i];
|
|
1404
|
+
if (arg === "--project-id" || options.rejectProjectAlias !== false && arg === "--project") {
|
|
1405
|
+
process.stderr.write(`${REMOVED_PROJECT_ID_MESSAGE}
|
|
1406
|
+
`);
|
|
1407
|
+
process.exitCode = 2;
|
|
1408
|
+
return true;
|
|
1409
|
+
}
|
|
1410
|
+
if (arg?.startsWith("--project-id=") || options.rejectProjectAlias !== false && arg?.startsWith("--project=")) {
|
|
1411
|
+
process.stderr.write(`${REMOVED_PROJECT_ID_MESSAGE}
|
|
1412
|
+
`);
|
|
1413
|
+
process.exitCode = 2;
|
|
1414
|
+
return true;
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
return false;
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
// src/format/repository.ts
|
|
1421
|
+
function formatRepositoryDisplay(snapshot, fallback = "repository") {
|
|
1422
|
+
if (snapshot.repository) {
|
|
1423
|
+
return `${snapshot.repository.owner}/${snapshot.repository.name}`;
|
|
1424
|
+
}
|
|
1425
|
+
return snapshot.slug ?? fallback;
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
// src/commands/start.ts
|
|
1429
|
+
function timestamp() {
|
|
1430
|
+
const now = /* @__PURE__ */ new Date();
|
|
1431
|
+
const hh = String(now.getHours()).padStart(2, "0");
|
|
1432
|
+
const mm = String(now.getMinutes()).padStart(2, "0");
|
|
1433
|
+
const ss = String(now.getSeconds()).padStart(2, "0");
|
|
1434
|
+
return dim(`${hh}:${mm}:${ss}`);
|
|
1435
|
+
}
|
|
1436
|
+
function logLine(icon, msg) {
|
|
1437
|
+
process.stdout.write(`${timestamp()} ${icon} ${msg}
|
|
1438
|
+
`);
|
|
1439
|
+
}
|
|
1440
|
+
var DEFAULT_HTTP_PORT = 4680;
|
|
1441
|
+
var HTTP_HOST = "0.0.0.0";
|
|
1442
|
+
function parseStartArgs(args) {
|
|
1443
|
+
const parsed = {
|
|
1444
|
+
daemon: false,
|
|
1445
|
+
once: false
|
|
1446
|
+
};
|
|
1447
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
1448
|
+
const arg = args[i];
|
|
1449
|
+
if (arg === "--daemon" || arg === "-d") {
|
|
1450
|
+
parsed.daemon = true;
|
|
1451
|
+
continue;
|
|
1452
|
+
}
|
|
1453
|
+
if (arg === "--once") {
|
|
1454
|
+
parsed.once = true;
|
|
1455
|
+
continue;
|
|
1456
|
+
}
|
|
1457
|
+
if (arg === "--http") {
|
|
1458
|
+
const value = args[i + 1];
|
|
1459
|
+
if (!value || value.startsWith("-")) {
|
|
1460
|
+
parsed.httpPort = DEFAULT_HTTP_PORT;
|
|
1461
|
+
continue;
|
|
1462
|
+
}
|
|
1463
|
+
parsed.httpPort = parsePort(value, arg);
|
|
1464
|
+
i += 1;
|
|
1465
|
+
continue;
|
|
1466
|
+
}
|
|
1467
|
+
if (arg === "--web") {
|
|
1468
|
+
const value = args[i + 1];
|
|
1469
|
+
if (!value || value.startsWith("-")) {
|
|
1470
|
+
parsed.webPort = DEFAULT_HTTP_PORT;
|
|
1471
|
+
continue;
|
|
1472
|
+
}
|
|
1473
|
+
parsed.webPort = parsePort(value, arg);
|
|
1474
|
+
i += 1;
|
|
1475
|
+
continue;
|
|
1476
|
+
}
|
|
1477
|
+
if (arg === "--log-level") {
|
|
1478
|
+
const value = args[i + 1];
|
|
1479
|
+
if (!value || value.startsWith("-")) {
|
|
1480
|
+
parsed.error = `Option '${arg}' argument missing`;
|
|
1481
|
+
return parsed;
|
|
1482
|
+
}
|
|
1483
|
+
parsed.logLevel = value;
|
|
1484
|
+
i += 1;
|
|
1485
|
+
continue;
|
|
1486
|
+
}
|
|
1487
|
+
if (arg?.startsWith("-")) {
|
|
1488
|
+
parsed.error = `Unknown option '${arg}'`;
|
|
1489
|
+
return parsed;
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
if (parsed.httpPort !== void 0 && parsed.webPort !== void 0) {
|
|
1493
|
+
parsed.error = "Options '--http' and '--web' cannot be used together";
|
|
1494
|
+
}
|
|
1495
|
+
return parsed;
|
|
1496
|
+
}
|
|
1497
|
+
function logTickResult(snapshot, prevSnapshot, isFirst) {
|
|
1498
|
+
if (isFirst) {
|
|
1499
|
+
const healthColor = snapshot.health === "degraded" ? red : snapshot.health === "running" ? green : cyan;
|
|
1500
|
+
logLine(
|
|
1501
|
+
green("\u25CF"),
|
|
1502
|
+
`Repository ${bold(formatRepositoryDisplay(snapshot))} connected ${dim(
|
|
1503
|
+
"("
|
|
1504
|
+
)}${healthColor(snapshot.health)}${dim(")")}`
|
|
1505
|
+
);
|
|
1506
|
+
if (snapshot.summary.activeRuns > 0) {
|
|
1507
|
+
logLine(cyan("\u25B8"), `${snapshot.summary.activeRuns} active run(s)`);
|
|
1508
|
+
}
|
|
1509
|
+
return;
|
|
1510
|
+
}
|
|
1511
|
+
if (prevSnapshot && prevSnapshot.health !== snapshot.health) {
|
|
1512
|
+
const icon = snapshot.health === "degraded" ? red("\u25CF") : green("\u25CF");
|
|
1513
|
+
logLine(
|
|
1514
|
+
icon,
|
|
1515
|
+
`Health changed: ${prevSnapshot.health} \u2192 ${bold(snapshot.health)}`
|
|
1516
|
+
);
|
|
1517
|
+
}
|
|
1518
|
+
if (snapshot.lastError && snapshot.lastError !== prevSnapshot?.lastError) {
|
|
1519
|
+
logLine(red("\u2717"), red(snapshot.lastError));
|
|
1520
|
+
}
|
|
1521
|
+
if (!snapshot.lastError && prevSnapshot?.lastError) {
|
|
1522
|
+
logLine(green("\u2713"), green("Error cleared"));
|
|
1523
|
+
}
|
|
1524
|
+
const prevDispatched = prevSnapshot?.summary.dispatched ?? 0;
|
|
1525
|
+
if (snapshot.summary.dispatched > prevDispatched) {
|
|
1526
|
+
const delta = snapshot.summary.dispatched - prevDispatched;
|
|
1527
|
+
logLine(yellow("\u25B8"), `Dispatched ${bold(String(delta))} new run(s)`);
|
|
1528
|
+
}
|
|
1529
|
+
const prevRunIds = new Set(
|
|
1530
|
+
prevSnapshot?.activeRuns.map((run) => run.runId) ?? []
|
|
1531
|
+
);
|
|
1532
|
+
for (const run of snapshot.activeRuns) {
|
|
1533
|
+
if (!prevRunIds.has(run.runId)) {
|
|
1534
|
+
logLine(
|
|
1535
|
+
cyan("\u25B8"),
|
|
1536
|
+
`Run started: ${bold(run.issueIdentifier)} ${dim("state=")}${run.issueState} ${dim("status=")}${run.status}`
|
|
1537
|
+
);
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
const currentRunIds = new Set(snapshot.activeRuns.map((run) => run.runId));
|
|
1541
|
+
for (const prevRun of prevSnapshot?.activeRuns ?? []) {
|
|
1542
|
+
if (!currentRunIds.has(prevRun.runId)) {
|
|
1543
|
+
logLine(
|
|
1544
|
+
green("\u2713"),
|
|
1545
|
+
`Run finished: ${bold(prevRun.issueIdentifier)} ${dim("(")}${prevRun.status}${dim(")")}`
|
|
1546
|
+
);
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
const prevSuppressed = prevSnapshot?.summary.suppressed ?? 0;
|
|
1550
|
+
if (snapshot.summary.suppressed > prevSuppressed) {
|
|
1551
|
+
const delta = snapshot.summary.suppressed - prevSuppressed;
|
|
1552
|
+
logLine(
|
|
1553
|
+
dim("\u25CB"),
|
|
1554
|
+
dim(`${delta} issue(s) suppressed (already running or at limit)`)
|
|
1555
|
+
);
|
|
1556
|
+
}
|
|
1557
|
+
const prevRecovered = prevSnapshot?.summary.recovered ?? 0;
|
|
1558
|
+
if (snapshot.summary.recovered > prevRecovered) {
|
|
1559
|
+
const delta = snapshot.summary.recovered - prevRecovered;
|
|
1560
|
+
logLine(
|
|
1561
|
+
yellow("\u21BA"),
|
|
1562
|
+
`Recovered ${bold(String(delta))} stalled run(s)`
|
|
1563
|
+
);
|
|
1564
|
+
}
|
|
1565
|
+
const prevRetryCount = prevSnapshot?.retryQueue.length ?? 0;
|
|
1566
|
+
if (snapshot.retryQueue.length > prevRetryCount) {
|
|
1567
|
+
const delta = snapshot.retryQueue.length - prevRetryCount;
|
|
1568
|
+
logLine(yellow("\u25CC"), `${delta} run(s) queued for retry`);
|
|
1569
|
+
}
|
|
1570
|
+
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);
|
|
1571
|
+
if (!changed) {
|
|
1572
|
+
logLine(
|
|
1573
|
+
dim("\xB7"),
|
|
1574
|
+
dim(
|
|
1575
|
+
`tick \u2014 ${snapshot.summary.activeRuns} active, ${snapshot.health}`
|
|
1576
|
+
)
|
|
1577
|
+
);
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
function parsePort(value, optionName) {
|
|
1581
|
+
if (!/^\d+$/.test(value)) {
|
|
1582
|
+
throw new Error(`Option '${optionName}' must be an integer port number`);
|
|
1583
|
+
}
|
|
1584
|
+
const parsed = Number.parseInt(value, 10);
|
|
1585
|
+
if (!Number.isSafeInteger(parsed) || parsed < 0 || parsed > 65535) {
|
|
1586
|
+
throw new Error(
|
|
1587
|
+
`Option '${optionName}' must be a port number between 0 and 65535`
|
|
1588
|
+
);
|
|
1589
|
+
}
|
|
1590
|
+
return parsed;
|
|
1591
|
+
}
|
|
1592
|
+
function respondJson2(response, status, payload) {
|
|
1593
|
+
response.writeHead(status, {
|
|
1594
|
+
"content-type": "application/json"
|
|
1595
|
+
});
|
|
1596
|
+
response.end(JSON.stringify(payload));
|
|
1597
|
+
}
|
|
1598
|
+
function formatBoundUrl2(server) {
|
|
1599
|
+
const address = server.address();
|
|
1600
|
+
if (!address || typeof address === "string") {
|
|
1601
|
+
return `http://${HTTP_HOST}`;
|
|
1602
|
+
}
|
|
1603
|
+
const host = address.address === "::" || address.address === "::1" || address.address === "0.0.0.0" || address.address === "127.0.0.1" ? "localhost" : address.address;
|
|
1604
|
+
const urlHost = host.includes(":") ? `[${host}]` : host;
|
|
1605
|
+
return `http://${urlHost}:${address.port}`;
|
|
1606
|
+
}
|
|
1607
|
+
function logHttpRequestError(error) {
|
|
1608
|
+
const message = error instanceof Error ? error.stack ?? error.message : String(error);
|
|
1609
|
+
process.stderr.write(`[start] HTTP request failed: ${message}
|
|
1610
|
+
`);
|
|
1611
|
+
}
|
|
1612
|
+
async function closeHttpServer(server) {
|
|
1613
|
+
if (!server) {
|
|
1614
|
+
return;
|
|
1615
|
+
}
|
|
1616
|
+
await new Promise((resolveClose, rejectClose) => {
|
|
1617
|
+
server.close((error) => {
|
|
1618
|
+
if (error) {
|
|
1619
|
+
rejectClose(error);
|
|
1620
|
+
return;
|
|
1621
|
+
}
|
|
1622
|
+
resolveClose();
|
|
1623
|
+
});
|
|
1624
|
+
});
|
|
1625
|
+
}
|
|
1626
|
+
async function writeHttpBindingState(configDir, projectId, binding) {
|
|
1627
|
+
await writeJsonFile(httpStatusPath(configDir, projectId), binding);
|
|
1628
|
+
}
|
|
1629
|
+
async function removeHttpBindingState(configDir, projectId) {
|
|
1630
|
+
await rm(httpStatusPath(configDir, projectId), { force: true });
|
|
1631
|
+
}
|
|
1632
|
+
async function startHttpServer(input) {
|
|
1633
|
+
const reader = new DashboardFsReader(input.runtimeRoot);
|
|
1634
|
+
for (let port = input.initialPort; port <= 65535; port += 1) {
|
|
1635
|
+
const server = createServer3((request, response) => {
|
|
1636
|
+
void (async () => {
|
|
1637
|
+
try {
|
|
1638
|
+
const url = new URL(request.url ?? "/", `http://${HTTP_HOST}`);
|
|
1639
|
+
if (request.method === "POST" && url.pathname === "/api/v1/refresh") {
|
|
1640
|
+
request.resume();
|
|
1641
|
+
input.service.requestReconcile();
|
|
1642
|
+
respondJson2(response, 202, { ok: true });
|
|
1643
|
+
return;
|
|
1644
|
+
}
|
|
1645
|
+
const resolved = await resolveDashboardResponse({
|
|
1646
|
+
pathname: url.pathname,
|
|
1647
|
+
method: request.method ?? "GET",
|
|
1648
|
+
reader
|
|
1649
|
+
});
|
|
1650
|
+
respondJson2(response, resolved.status, resolved.payload);
|
|
1651
|
+
} catch (error) {
|
|
1652
|
+
logHttpRequestError(error);
|
|
1653
|
+
if (!response.headersSent) {
|
|
1654
|
+
respondJson2(response, 500, {
|
|
1655
|
+
error: "Internal server error"
|
|
1656
|
+
});
|
|
1657
|
+
} else {
|
|
1658
|
+
response.end();
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
})();
|
|
1662
|
+
});
|
|
1663
|
+
try {
|
|
1664
|
+
await new Promise((resolveReady, rejectReady) => {
|
|
1665
|
+
const handleListening = () => {
|
|
1666
|
+
cleanup();
|
|
1667
|
+
resolveReady();
|
|
1668
|
+
};
|
|
1669
|
+
const handleError = (error) => {
|
|
1670
|
+
cleanup();
|
|
1671
|
+
rejectReady(error);
|
|
1672
|
+
};
|
|
1673
|
+
const cleanup = () => {
|
|
1674
|
+
server.off("listening", handleListening);
|
|
1675
|
+
server.off("error", handleError);
|
|
1676
|
+
};
|
|
1677
|
+
server.once("listening", handleListening);
|
|
1678
|
+
server.once("error", handleError);
|
|
1679
|
+
server.listen(port, HTTP_HOST);
|
|
1680
|
+
});
|
|
1681
|
+
return {
|
|
1682
|
+
server,
|
|
1683
|
+
port,
|
|
1684
|
+
url: formatBoundUrl2(server)
|
|
1685
|
+
};
|
|
1686
|
+
} catch (error) {
|
|
1687
|
+
await closeHttpServer(server).catch(() => {
|
|
1688
|
+
});
|
|
1689
|
+
if (error?.code === "EADDRINUSE") {
|
|
1690
|
+
continue;
|
|
1691
|
+
}
|
|
1692
|
+
throw error;
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
throw new Error(
|
|
1696
|
+
`Unable to bind HTTP server starting from port ${input.initialPort}`
|
|
1697
|
+
);
|
|
1698
|
+
}
|
|
1699
|
+
var handler5 = async (args, options) => {
|
|
1700
|
+
setNoColor(options.noColor);
|
|
1701
|
+
let parsed;
|
|
1702
|
+
try {
|
|
1703
|
+
if (rejectRemovedProjectId(args)) {
|
|
1704
|
+
return;
|
|
1705
|
+
}
|
|
1706
|
+
parsed = parseStartArgs(args);
|
|
1707
|
+
} catch (error) {
|
|
1708
|
+
process.stderr.write(
|
|
1709
|
+
`${error instanceof Error ? error.message : "Invalid arguments"}
|
|
1710
|
+
`
|
|
1711
|
+
);
|
|
1712
|
+
process.exitCode = 2;
|
|
1713
|
+
return;
|
|
1714
|
+
}
|
|
1715
|
+
if (parsed.error) {
|
|
1716
|
+
process.stderr.write(`${parsed.error}
|
|
1717
|
+
`);
|
|
1718
|
+
process.stderr.write(
|
|
1719
|
+
"Usage: gh-symphony repo start [--daemon] [--once] [--http [port]] [--web [port]]\n"
|
|
1720
|
+
);
|
|
1721
|
+
process.exitCode = 2;
|
|
1722
|
+
return;
|
|
1723
|
+
}
|
|
1724
|
+
if (parsed.daemon && parsed.once) {
|
|
1725
|
+
process.stderr.write(
|
|
1726
|
+
"Options '--daemon' and '--once' cannot be used together\n"
|
|
1727
|
+
);
|
|
1728
|
+
process.exitCode = 2;
|
|
1729
|
+
return;
|
|
1730
|
+
}
|
|
1731
|
+
const projectConfig = await resolveManagedProjectConfig({
|
|
1732
|
+
configDir: options.configDir,
|
|
1733
|
+
requestedProjectId: void 0
|
|
1734
|
+
});
|
|
1735
|
+
if (!projectConfig) {
|
|
1736
|
+
handleMissingManagedProjectConfig();
|
|
1737
|
+
return;
|
|
1738
|
+
}
|
|
1739
|
+
if (!hasConfiguredRepository(projectConfig)) {
|
|
1740
|
+
process.stderr.write(
|
|
1741
|
+
"No repository is configured in this project. Run 'gh-symphony repo init' from the target repository first.\n"
|
|
1742
|
+
);
|
|
1743
|
+
process.exitCode = 1;
|
|
1744
|
+
return;
|
|
1745
|
+
}
|
|
1746
|
+
const runtimeRoot = resolveRuntimeRoot(options.configDir);
|
|
1747
|
+
const projectId = projectConfig.projectId;
|
|
1748
|
+
let logLevel;
|
|
1749
|
+
try {
|
|
1750
|
+
logLevel = resolveOrchestratorLogLevel(
|
|
1751
|
+
parsed.logLevel ?? process.env.SYMPHONY_LOG_LEVEL
|
|
1752
|
+
);
|
|
1753
|
+
} catch (error) {
|
|
1754
|
+
process.stderr.write(
|
|
1755
|
+
`${error instanceof Error ? error.message : "Unsupported log level"}
|
|
1756
|
+
`
|
|
1757
|
+
);
|
|
1758
|
+
process.exitCode = 2;
|
|
1759
|
+
return;
|
|
1760
|
+
}
|
|
1761
|
+
if (parsed.daemon) {
|
|
1762
|
+
await startDaemon(
|
|
1763
|
+
options,
|
|
1764
|
+
projectId,
|
|
1765
|
+
parsed.logLevel,
|
|
1766
|
+
parsed.httpPort,
|
|
1767
|
+
parsed.webPort
|
|
1768
|
+
);
|
|
1769
|
+
return;
|
|
1770
|
+
}
|
|
1771
|
+
if (!process.env.GITHUB_GRAPHQL_TOKEN) {
|
|
1772
|
+
try {
|
|
1773
|
+
process.env.GITHUB_GRAPHQL_TOKEN = getGhToken();
|
|
1774
|
+
} catch {
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1777
|
+
let projectLock = null;
|
|
1778
|
+
try {
|
|
1779
|
+
projectLock = await acquireProjectLock({
|
|
1780
|
+
runtimeRoot,
|
|
1781
|
+
projectId
|
|
1782
|
+
});
|
|
1783
|
+
await removeHttpBindingState(options.configDir, projectId);
|
|
1784
|
+
const store = createStore(runtimeRoot);
|
|
1785
|
+
let prevSnapshot = null;
|
|
1786
|
+
let isFirst = true;
|
|
1787
|
+
const service = new OrchestratorService(store, projectConfig, {
|
|
1788
|
+
logLevel,
|
|
1789
|
+
onTick: async (snapshot) => {
|
|
1790
|
+
try {
|
|
1791
|
+
logTickResult(snapshot, prevSnapshot, isFirst);
|
|
1792
|
+
if (!isFirst) {
|
|
1793
|
+
const currentRunIds = new Set(
|
|
1794
|
+
snapshot.activeRuns.map((run) => run.runId)
|
|
1795
|
+
);
|
|
1796
|
+
for (const prevRun of prevSnapshot?.activeRuns ?? []) {
|
|
1797
|
+
if (!currentRunIds.has(prevRun.runId)) {
|
|
1798
|
+
await tailWorkerLog(
|
|
1799
|
+
runtimeRoot,
|
|
1800
|
+
projectId,
|
|
1801
|
+
prevRun.runId,
|
|
1802
|
+
prevRun.issueIdentifier
|
|
1803
|
+
);
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
prevSnapshot = snapshot;
|
|
1808
|
+
isFirst = false;
|
|
1809
|
+
} catch (error) {
|
|
1810
|
+
logLine(
|
|
1811
|
+
red("\u2717"),
|
|
1812
|
+
red(
|
|
1813
|
+
`Tick error: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
1814
|
+
)
|
|
1815
|
+
);
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
});
|
|
1819
|
+
let shuttingDown = false;
|
|
1820
|
+
let shutdownPromise = null;
|
|
1821
|
+
let keepHttpAliveResolve = null;
|
|
1822
|
+
let httpServer = null;
|
|
1823
|
+
const shutdown = async () => {
|
|
1824
|
+
if (shuttingDown) {
|
|
1825
|
+
return shutdownPromise;
|
|
1826
|
+
}
|
|
1827
|
+
shuttingDown = true;
|
|
1828
|
+
keepHttpAliveResolve?.();
|
|
1829
|
+
keepHttpAliveResolve = null;
|
|
1830
|
+
const heldLock = projectLock;
|
|
1831
|
+
projectLock = null;
|
|
1832
|
+
shutdownPromise = shutdownForegroundOrchestrator({
|
|
1833
|
+
configDir: options.configDir,
|
|
1834
|
+
projectId,
|
|
1835
|
+
httpServer: httpServer?.server,
|
|
1836
|
+
projectLock: heldLock,
|
|
1837
|
+
service
|
|
1838
|
+
});
|
|
1839
|
+
return shutdownPromise;
|
|
1840
|
+
};
|
|
1841
|
+
const handleSigint = () => {
|
|
1842
|
+
void shutdown();
|
|
1843
|
+
};
|
|
1844
|
+
const handleSigterm = () => {
|
|
1845
|
+
void shutdown();
|
|
1846
|
+
};
|
|
1847
|
+
process.on("SIGINT", handleSigint);
|
|
1848
|
+
process.on("SIGTERM", handleSigterm);
|
|
1849
|
+
try {
|
|
1850
|
+
httpServer = parsed.webPort !== void 0 ? await startControlPlaneServer({
|
|
1851
|
+
host: HTTP_HOST,
|
|
1852
|
+
port: parsed.webPort,
|
|
1853
|
+
runtimeRoot,
|
|
1854
|
+
onRefreshRequest: () => service.requestReconcile()
|
|
1855
|
+
}) : parsed.httpPort !== void 0 ? await startHttpServer({
|
|
1856
|
+
runtimeRoot,
|
|
1857
|
+
projectId,
|
|
1858
|
+
initialPort: parsed.httpPort,
|
|
1859
|
+
service
|
|
1860
|
+
}) : null;
|
|
1861
|
+
if (httpServer) {
|
|
1862
|
+
try {
|
|
1863
|
+
await writeHttpBindingState(options.configDir, projectId, {
|
|
1864
|
+
host: HTTP_HOST,
|
|
1865
|
+
port: httpServer.port,
|
|
1866
|
+
endpoint: httpServer.url
|
|
1867
|
+
});
|
|
1868
|
+
} catch (error) {
|
|
1869
|
+
logLine(
|
|
1870
|
+
yellow("\u26A0"),
|
|
1871
|
+
yellow(
|
|
1872
|
+
`Failed to persist HTTP binding state (http.json): ${error instanceof Error ? error.message : "Unknown error"}`
|
|
1873
|
+
)
|
|
1874
|
+
);
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
logLine(
|
|
1878
|
+
green("\u25B2"),
|
|
1879
|
+
`Starting orchestrator for project: ${bold(projectId)}`
|
|
1880
|
+
);
|
|
1881
|
+
if (httpServer) {
|
|
1882
|
+
logLine(
|
|
1883
|
+
cyan("\u25A1"),
|
|
1884
|
+
parsed.webPort !== void 0 ? `Web dashboard listening on ${httpServer.url}` : `HTTP dashboard listening on ${httpServer.url}`
|
|
1885
|
+
);
|
|
1886
|
+
}
|
|
1887
|
+
logLine(
|
|
1888
|
+
dim("\xB7"),
|
|
1889
|
+
dim(
|
|
1890
|
+
parsed.once ? "Running one orchestration tick" : "Press Ctrl+C to stop"
|
|
1891
|
+
)
|
|
1892
|
+
);
|
|
1893
|
+
while (!shuttingDown) {
|
|
1894
|
+
try {
|
|
1895
|
+
await service.run({ once: parsed.once });
|
|
1896
|
+
if (shuttingDown) {
|
|
1897
|
+
break;
|
|
1898
|
+
}
|
|
1899
|
+
if (parsed.once) {
|
|
1900
|
+
if (httpServer) {
|
|
1901
|
+
logLine(
|
|
1902
|
+
cyan("\u25A1"),
|
|
1903
|
+
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"
|
|
1904
|
+
);
|
|
1905
|
+
if (shuttingDown) {
|
|
1906
|
+
break;
|
|
1907
|
+
}
|
|
1908
|
+
await new Promise((resolve5) => {
|
|
1909
|
+
keepHttpAliveResolve = resolve5;
|
|
1910
|
+
});
|
|
1911
|
+
} else {
|
|
1912
|
+
await shutdown();
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1915
|
+
break;
|
|
1916
|
+
} catch (error) {
|
|
1917
|
+
if (shuttingDown) {
|
|
1918
|
+
break;
|
|
1919
|
+
}
|
|
1920
|
+
logLine(
|
|
1921
|
+
red("\u2717"),
|
|
1922
|
+
red(
|
|
1923
|
+
`${parsed.once ? "One-shot run failed" : "Run loop error"}: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
1924
|
+
)
|
|
1925
|
+
);
|
|
1926
|
+
if (parsed.once) {
|
|
1927
|
+
process.exitCode = 1;
|
|
1928
|
+
await closeHttpServer(httpServer?.server).catch((closeError) => {
|
|
1929
|
+
logLine(
|
|
1930
|
+
yellow("\u26A0"),
|
|
1931
|
+
`Failed to stop HTTP server: ${closeError instanceof Error ? closeError.message : "Unknown error"}`
|
|
1932
|
+
);
|
|
1933
|
+
});
|
|
1934
|
+
await removeHttpBindingState(options.configDir, projectId).catch(
|
|
1935
|
+
(removeError) => {
|
|
1936
|
+
logLine(
|
|
1937
|
+
yellow("\u26A0"),
|
|
1938
|
+
`Failed to remove HTTP state: ${removeError instanceof Error ? removeError.message : "Unknown error"}`
|
|
1939
|
+
);
|
|
1940
|
+
}
|
|
1941
|
+
);
|
|
1942
|
+
return;
|
|
1943
|
+
}
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1946
|
+
} finally {
|
|
1947
|
+
process.off("SIGINT", handleSigint);
|
|
1948
|
+
process.off("SIGTERM", handleSigterm);
|
|
1949
|
+
if (shutdownPromise) {
|
|
1950
|
+
await shutdownPromise;
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1953
|
+
} finally {
|
|
1954
|
+
await releaseProjectLock(projectLock);
|
|
1955
|
+
}
|
|
1956
|
+
};
|
|
1957
|
+
async function shutdownForegroundOrchestrator(input) {
|
|
1958
|
+
logLine(yellow("\u25BC"), "Shutting down...");
|
|
1959
|
+
if (input.service) {
|
|
1960
|
+
try {
|
|
1961
|
+
await input.service.shutdown();
|
|
1962
|
+
} catch (error) {
|
|
1963
|
+
logLine(
|
|
1964
|
+
red("\u2717"),
|
|
1965
|
+
red(
|
|
1966
|
+
`Failed to shut down workers: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
1967
|
+
)
|
|
1968
|
+
);
|
|
1969
|
+
}
|
|
1970
|
+
}
|
|
1971
|
+
try {
|
|
1972
|
+
await closeHttpServer(input.httpServer);
|
|
1973
|
+
} catch (error) {
|
|
1974
|
+
logLine(
|
|
1975
|
+
yellow("\u26A0"),
|
|
1976
|
+
`Failed to stop HTTP server: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
1977
|
+
);
|
|
1978
|
+
}
|
|
1979
|
+
try {
|
|
1980
|
+
await removeHttpBindingState(input.configDir, input.projectId);
|
|
1981
|
+
} catch (error) {
|
|
1982
|
+
logLine(
|
|
1983
|
+
yellow("\u26A0"),
|
|
1984
|
+
`Failed to remove HTTP state: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
1985
|
+
);
|
|
1986
|
+
}
|
|
1987
|
+
try {
|
|
1988
|
+
await (input.releaseLock ?? releaseProjectLock)(input.projectLock);
|
|
1989
|
+
} catch (error) {
|
|
1990
|
+
logLine(
|
|
1991
|
+
yellow("\u26A0"),
|
|
1992
|
+
`Failed to release project lock: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
1993
|
+
);
|
|
1994
|
+
}
|
|
1995
|
+
return (input.exit ?? process.exit)(0);
|
|
1996
|
+
}
|
|
1997
|
+
function hasConfiguredRepository(config) {
|
|
1998
|
+
return Boolean(config.repository?.owner && config.repository.name);
|
|
1999
|
+
}
|
|
2000
|
+
async function tailWorkerLog(runtimeRoot, projectId, runId, issueIdentifier) {
|
|
2001
|
+
for (const logPath of [
|
|
2002
|
+
join6(runtimeRoot, "runs", runId, "worker.log"),
|
|
2003
|
+
join6(runtimeRoot, "projects", projectId, "runs", runId, "worker.log")
|
|
2004
|
+
]) {
|
|
2005
|
+
try {
|
|
2006
|
+
const content = await readFile5(logPath, "utf8");
|
|
2007
|
+
const lines = content.split("\n").filter((l) => l.trim());
|
|
2008
|
+
if (lines.length === 0) return;
|
|
2009
|
+
const tail = lines.slice(-30);
|
|
2010
|
+
logLine(red("\u2717"), red(`Worker stderr (${issueIdentifier}):`));
|
|
2011
|
+
for (const line of tail) {
|
|
2012
|
+
process.stdout.write(` ${dim(line)}
|
|
2013
|
+
`);
|
|
2014
|
+
}
|
|
2015
|
+
return;
|
|
2016
|
+
} catch {
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
var start_default = handler5;
|
|
2021
|
+
async function startDaemon(options, projectId, logLevel, httpPort, webPort) {
|
|
2022
|
+
const logPath = orchestratorLogPath(options.configDir, projectId);
|
|
2023
|
+
await mkdir(dirname2(logPath), { recursive: true });
|
|
2024
|
+
const { openSync } = await import("fs");
|
|
2025
|
+
const logFd = openSync(logPath, "a");
|
|
2026
|
+
const child = spawn(
|
|
2027
|
+
process.execPath,
|
|
2028
|
+
[
|
|
2029
|
+
process.argv[1],
|
|
2030
|
+
"repo",
|
|
2031
|
+
"start",
|
|
2032
|
+
...httpPort !== void 0 ? ["--http", String(httpPort)] : [],
|
|
2033
|
+
...webPort !== void 0 ? ["--web", String(webPort)] : [],
|
|
2034
|
+
...logLevel ? ["--log-level", logLevel] : []
|
|
2035
|
+
],
|
|
2036
|
+
{
|
|
2037
|
+
cwd: process.cwd(),
|
|
2038
|
+
env: {
|
|
2039
|
+
...process.env,
|
|
2040
|
+
GH_SYMPHONY_CONFIG_DIR: options.configDir
|
|
2041
|
+
},
|
|
2042
|
+
detached: true,
|
|
2043
|
+
stdio: ["ignore", logFd, logFd]
|
|
2044
|
+
}
|
|
2045
|
+
);
|
|
2046
|
+
const pidPath = daemonPidPath(options.configDir, projectId);
|
|
2047
|
+
await mkdir(dirname2(pidPath), { recursive: true });
|
|
2048
|
+
await writeFile(pidPath, String(child.pid), "utf8");
|
|
2049
|
+
child.unref();
|
|
2050
|
+
const { closeSync } = await import("fs");
|
|
2051
|
+
closeSync(logFd);
|
|
2052
|
+
process.stdout.write(
|
|
2053
|
+
`Orchestrator started in background (PID: ${child.pid}).
|
|
2054
|
+
Logs: ${logPath}
|
|
2055
|
+
Stop with: gh-symphony repo stop
|
|
2056
|
+
`
|
|
2057
|
+
);
|
|
2058
|
+
}
|
|
2059
|
+
|
|
2060
|
+
// src/commands/status.ts
|
|
2061
|
+
import { readFile as readFile6 } from "fs/promises";
|
|
2062
|
+
import { join as join7 } from "path";
|
|
2063
|
+
|
|
2064
|
+
// src/dashboard/renderer.ts
|
|
2065
|
+
var COL_ID = 24;
|
|
2066
|
+
var COL_STATUS = 14;
|
|
2067
|
+
var COL_PID = 8;
|
|
2068
|
+
var COL_AGE_TURN = 12;
|
|
2069
|
+
var COL_TOKENS = 17;
|
|
2070
|
+
var COL_SESSION = 14;
|
|
2071
|
+
var COL_ID_HEADER = COL_ID + 2;
|
|
2072
|
+
var identity = (s) => s;
|
|
2073
|
+
function makeColors(noColor) {
|
|
2074
|
+
if (noColor) {
|
|
2075
|
+
return {
|
|
2076
|
+
bold: identity,
|
|
2077
|
+
dim: identity,
|
|
2078
|
+
green: identity,
|
|
2079
|
+
red: identity,
|
|
2080
|
+
yellow: identity,
|
|
2081
|
+
cyan: identity,
|
|
2082
|
+
magenta: identity,
|
|
2083
|
+
blue: identity
|
|
2084
|
+
};
|
|
2085
|
+
}
|
|
2086
|
+
return { bold, dim, green, red, yellow, cyan, magenta, blue };
|
|
2087
|
+
}
|
|
2088
|
+
function pad(s, width, align = "left") {
|
|
2089
|
+
const visible = stripAnsi(s);
|
|
2090
|
+
if (visible.length >= width) return visible.slice(0, width);
|
|
2091
|
+
const padding = " ".repeat(width - visible.length);
|
|
2092
|
+
return align === "right" ? padding + s : s + padding;
|
|
2093
|
+
}
|
|
2094
|
+
function compactSessionId(id) {
|
|
2095
|
+
if (!id) return "\u2014";
|
|
2096
|
+
if (id.length <= 10) return id;
|
|
2097
|
+
return `${id.slice(0, 4)}...${id.slice(-6)}`;
|
|
2098
|
+
}
|
|
2099
|
+
function fmtTokens(n) {
|
|
2100
|
+
return n.toLocaleString("en-US");
|
|
2101
|
+
}
|
|
2102
|
+
function fmtTokenPair(delta, cumulative) {
|
|
2103
|
+
const left = fmtTokens(delta ?? 0);
|
|
2104
|
+
const right = fmtTokens(cumulative ?? delta ?? 0);
|
|
2105
|
+
return `${left} / ${right}`;
|
|
2106
|
+
}
|
|
2107
|
+
function fmtAge(startedAt, now) {
|
|
2108
|
+
if (!startedAt) return "0m";
|
|
2109
|
+
const diffMs = now - new Date(startedAt).getTime();
|
|
2110
|
+
if (diffMs < 0) return "0m";
|
|
2111
|
+
const totalMin = Math.floor(diffMs / 6e4);
|
|
2112
|
+
if (totalMin < 60) return `${totalMin}m`;
|
|
2113
|
+
const h = Math.floor(totalMin / 60);
|
|
2114
|
+
const m = totalMin % 60;
|
|
2115
|
+
return `${h}h ${m}m`;
|
|
2116
|
+
}
|
|
2117
|
+
function fmtRuntime(ms) {
|
|
2118
|
+
if (ms <= 0) return "0h 0m";
|
|
2119
|
+
const totalMin = Math.floor(ms / 6e4);
|
|
2120
|
+
const h = Math.floor(totalMin / 60);
|
|
2121
|
+
const m = totalMin % 60;
|
|
2122
|
+
return `${h}h ${m}m`;
|
|
2123
|
+
}
|
|
2124
|
+
function fmtRetryTime(nextRetryAt, now) {
|
|
2125
|
+
if (!nextRetryAt) return "\u2014";
|
|
2126
|
+
const diffMs = new Date(nextRetryAt).getTime() - now;
|
|
2127
|
+
if (diffMs <= 0) return "now";
|
|
2128
|
+
const totalSec = Math.ceil(diffMs / 1e3);
|
|
2129
|
+
if (totalSec < 60) return `${totalSec}s`;
|
|
2130
|
+
const m = Math.floor(totalSec / 60);
|
|
2131
|
+
const s = totalSec % 60;
|
|
2132
|
+
return s > 0 ? `${m}m ${s}s` : `${m}m`;
|
|
2133
|
+
}
|
|
2134
|
+
var COL_SEPARATORS = 6;
|
|
2135
|
+
function eventColWidth(termWidth) {
|
|
2136
|
+
const fixed = 2 + COL_ID_HEADER + COL_STATUS + COL_PID + COL_AGE_TURN + COL_TOKENS + COL_SESSION + COL_SEPARATORS;
|
|
2137
|
+
return Math.max(5, termWidth - fixed);
|
|
2138
|
+
}
|
|
2139
|
+
function statusDot(run, c) {
|
|
2140
|
+
const event = run.lastEvent;
|
|
2141
|
+
if (event === null || event === void 0 || run.status === "failed")
|
|
2142
|
+
return c.red("\u25CF");
|
|
2143
|
+
if (event === "token_count") return c.yellow("\u25CF");
|
|
2144
|
+
if (event === "task_started") return c.green("\u25CF");
|
|
2145
|
+
if (event === "turn_completed") return c.magenta("\u25CF");
|
|
2146
|
+
return c.blue("\u25CF");
|
|
2147
|
+
}
|
|
2148
|
+
function titleBar(width, c) {
|
|
2149
|
+
const title = " gh-symphony ";
|
|
2150
|
+
const side = Math.max(0, Math.floor((width - title.length) / 2));
|
|
2151
|
+
const right = Math.max(0, width - side - title.length);
|
|
2152
|
+
return c.bold("\u2550".repeat(side) + title + "\u2550".repeat(right));
|
|
2153
|
+
}
|
|
2154
|
+
function sectionDivider(label, width, c) {
|
|
2155
|
+
const prefix = `\u2500\u2500 ${label} `;
|
|
2156
|
+
const fill = "\u2500".repeat(Math.max(0, width - prefix.length));
|
|
2157
|
+
return c.dim(prefix + fill);
|
|
2158
|
+
}
|
|
2159
|
+
function buildSummaryLines(snapshots, options, c) {
|
|
2160
|
+
const now = options.now ?? Date.now();
|
|
2161
|
+
const lines = [];
|
|
2162
|
+
const totalActive = snapshots.reduce(
|
|
2163
|
+
(sum, s) => sum + s.summary.activeRuns,
|
|
2164
|
+
0
|
|
2165
|
+
);
|
|
2166
|
+
const agentStr = options.maxAgents != null ? `${totalActive}/${options.maxAgents}` : `${totalActive}`;
|
|
2167
|
+
const totIn = snapshots.reduce(
|
|
2168
|
+
(sum, s) => sum + (s.codexTotals?.inputTokens ?? 0),
|
|
2169
|
+
0
|
|
2170
|
+
);
|
|
2171
|
+
const totOut = snapshots.reduce(
|
|
2172
|
+
(sum, s) => sum + (s.codexTotals?.outputTokens ?? 0),
|
|
2173
|
+
0
|
|
2174
|
+
);
|
|
2175
|
+
const totAll = snapshots.reduce(
|
|
2176
|
+
(sum, s) => sum + (s.codexTotals?.totalTokens ?? 0),
|
|
2177
|
+
0
|
|
2178
|
+
);
|
|
2179
|
+
const allStarts = snapshots.flatMap((s) => s.activeRuns).map((r) => r.startedAt).filter((t) => t != null).map((t) => new Date(t).getTime());
|
|
2180
|
+
const runtimeMs = allStarts.length > 0 ? now - Math.min(...allStarts) : 0;
|
|
2181
|
+
const runtime = fmtRuntime(runtimeMs);
|
|
2182
|
+
lines.push(
|
|
2183
|
+
` ${c.dim("Agents")} ${c.bold(agentStr)} ${c.dim("Runtime")} ${c.bold(runtime)} ${c.dim("Tokens")} ${fmtTokens(totIn)} in / ${fmtTokens(totOut)} out / ${c.bold(fmtTokens(totAll))} total`
|
|
2184
|
+
);
|
|
2185
|
+
const hasLimits = snapshots.some((s) => s.rateLimits != null);
|
|
2186
|
+
const limitStr = hasLimits ? "active" : "standard";
|
|
2187
|
+
lines.push(` ${c.dim("Rate Limits")} ${limitStr}`);
|
|
2188
|
+
return lines;
|
|
2189
|
+
}
|
|
2190
|
+
function tableHeaderRow(c) {
|
|
2191
|
+
const cols = [
|
|
2192
|
+
pad("ID", COL_ID_HEADER),
|
|
2193
|
+
pad("STATUS", COL_STATUS),
|
|
2194
|
+
pad("PID", COL_PID),
|
|
2195
|
+
pad("AGE/TURN", COL_AGE_TURN),
|
|
2196
|
+
pad("TOKENS", COL_TOKENS),
|
|
2197
|
+
pad("SESSION", COL_SESSION),
|
|
2198
|
+
"EVENT"
|
|
2199
|
+
].join(" ");
|
|
2200
|
+
return ` ${c.dim(cols)}`;
|
|
2201
|
+
}
|
|
2202
|
+
function activeRunRow(run, now, evtWidth, c) {
|
|
2203
|
+
const dot = statusDot(run, c);
|
|
2204
|
+
const id = pad(run.issueIdentifier, COL_ID);
|
|
2205
|
+
const status = pad(
|
|
2206
|
+
run.issueState ?? run.executionPhase ?? "\u2014",
|
|
2207
|
+
COL_STATUS
|
|
2208
|
+
);
|
|
2209
|
+
const pid = pad(
|
|
2210
|
+
run.processId != null ? String(run.processId) : "\u2014",
|
|
2211
|
+
COL_PID
|
|
2212
|
+
);
|
|
2213
|
+
const age = fmtAge(run.startedAt, now);
|
|
2214
|
+
const turn = run.turnCount ?? 0;
|
|
2215
|
+
const ageTurn = pad(`${age}/${turn}`, COL_AGE_TURN);
|
|
2216
|
+
const tokens = pad(
|
|
2217
|
+
fmtTokenPair(
|
|
2218
|
+
run.tokenUsage?.totalTokens,
|
|
2219
|
+
run.tokenUsage?.cumulativeTotalTokens
|
|
2220
|
+
),
|
|
2221
|
+
COL_TOKENS,
|
|
2222
|
+
"right"
|
|
2223
|
+
);
|
|
2224
|
+
const sessionId = run.runtimeSession?.sessionId ?? run.runtimeSession?.threadId ?? null;
|
|
2225
|
+
const session = pad(compactSessionId(sessionId), COL_SESSION);
|
|
2226
|
+
const event = pad(run.lastEvent ?? "\u2014", evtWidth);
|
|
2227
|
+
const columns = [id, status, pid, ageTurn, tokens, session, event].join(" ");
|
|
2228
|
+
return ` ${dot} ${columns}`;
|
|
2229
|
+
}
|
|
2230
|
+
function retryRow(entry, snapshot, now, c) {
|
|
2231
|
+
const id = entry.issueIdentifier;
|
|
2232
|
+
const kind = entry.retryKind;
|
|
2233
|
+
const timeStr = fmtRetryTime(entry.nextRetryAt, now);
|
|
2234
|
+
const matchingRun = snapshot.activeRuns.find((r) => r.runId === entry.runId);
|
|
2235
|
+
const errorHint = matchingRun?.lastEvent ?? "";
|
|
2236
|
+
return ` ${c.yellow("\u21BB")} ${id} ${kind} retrying in ${timeStr}${errorHint ? " " + errorHint : ""}`;
|
|
2237
|
+
}
|
|
2238
|
+
function renderDashboard(snapshots, options) {
|
|
2239
|
+
const width = options.terminalWidth || 115;
|
|
2240
|
+
const now = options.now ?? Date.now();
|
|
2241
|
+
const c = makeColors(options.noColor);
|
|
2242
|
+
const evtWidth = eventColWidth(width);
|
|
2243
|
+
const lines = [];
|
|
2244
|
+
lines.push(titleBar(width, c));
|
|
2245
|
+
lines.push(...buildSummaryLines(snapshots, options, c));
|
|
2246
|
+
lines.push("");
|
|
2247
|
+
for (const snap of snapshots) {
|
|
2248
|
+
const hasActiveRuns = snap.activeRuns.length > 0;
|
|
2249
|
+
const hasRetries = snap.retryQueue.length > 0;
|
|
2250
|
+
if (!hasActiveRuns && !hasRetries) continue;
|
|
2251
|
+
lines.push(
|
|
2252
|
+
sectionDivider(formatRepositoryDisplay(snap), width, c)
|
|
2253
|
+
);
|
|
2254
|
+
if (hasActiveRuns) {
|
|
2255
|
+
lines.push(tableHeaderRow(c));
|
|
2256
|
+
for (const rawRun of snap.activeRuns) {
|
|
2257
|
+
const run = rawRun;
|
|
2258
|
+
lines.push(activeRunRow(run, now, evtWidth, c));
|
|
2259
|
+
}
|
|
2260
|
+
}
|
|
2261
|
+
lines.push("");
|
|
2262
|
+
}
|
|
2263
|
+
const allRetries = [];
|
|
2264
|
+
for (const snap of snapshots) {
|
|
2265
|
+
for (const entry of snap.retryQueue) {
|
|
2266
|
+
allRetries.push({ entry, snapshot: snap });
|
|
2267
|
+
}
|
|
2268
|
+
}
|
|
2269
|
+
if (allRetries.length > 0) {
|
|
2270
|
+
lines.push(sectionDivider("Backoff Queue", width, c));
|
|
2271
|
+
for (const { entry, snapshot } of allRetries) {
|
|
2272
|
+
lines.push(retryRow(entry, snapshot, now, c));
|
|
2273
|
+
}
|
|
2274
|
+
lines.push("");
|
|
2275
|
+
}
|
|
2276
|
+
const result = lines.map((line) => {
|
|
2277
|
+
const visible = stripAnsi(line);
|
|
2278
|
+
if (visible.length <= width) return line;
|
|
2279
|
+
return visible.slice(0, width);
|
|
2280
|
+
});
|
|
2281
|
+
return result.join("\n");
|
|
2282
|
+
}
|
|
2283
|
+
|
|
2284
|
+
// src/commands/status.ts
|
|
2285
|
+
function healthIcon(health) {
|
|
2286
|
+
switch (health) {
|
|
2287
|
+
case "idle":
|
|
2288
|
+
case "running":
|
|
2289
|
+
return green("\u25CF");
|
|
2290
|
+
case "degraded":
|
|
2291
|
+
return red("\u25CF");
|
|
2292
|
+
}
|
|
2293
|
+
}
|
|
2294
|
+
function relativeTime(isoString) {
|
|
2295
|
+
const now = /* @__PURE__ */ new Date();
|
|
2296
|
+
const then = new Date(isoString);
|
|
2297
|
+
const diffMs = now.getTime() - then.getTime();
|
|
2298
|
+
const diffS = Math.floor(diffMs / 1e3);
|
|
2299
|
+
const diffM = Math.floor(diffS / 60);
|
|
2300
|
+
const diffH = Math.floor(diffM / 60);
|
|
2301
|
+
if (diffS < 60) return `${diffS}s ago`;
|
|
2302
|
+
if (diffM < 60) return `${diffM}m ago`;
|
|
2303
|
+
return `${diffH}h ago`;
|
|
2304
|
+
}
|
|
2305
|
+
function truncate(s, len) {
|
|
2306
|
+
if (s.length <= len) return s;
|
|
2307
|
+
return s.slice(0, len - 3) + "...";
|
|
2308
|
+
}
|
|
2309
|
+
function formatTokenPair(delta, cumulative) {
|
|
2310
|
+
return `${delta.toLocaleString("en-US")} / ${cumulative.toLocaleString("en-US")}`;
|
|
2311
|
+
}
|
|
2312
|
+
function resolveProjectTokenDelta(snapshot) {
|
|
2313
|
+
return snapshot.activeRuns.reduce(
|
|
2314
|
+
(sum, run) => sum + (run.tokenUsage?.totalTokens ?? 0),
|
|
2315
|
+
0
|
|
2316
|
+
);
|
|
2317
|
+
}
|
|
2318
|
+
function renderLegacyStatus(snapshot, noColor) {
|
|
2319
|
+
const apply = noColor ? (s) => stripAnsi(s) : (s) => s;
|
|
2320
|
+
const lines = [];
|
|
2321
|
+
const headerTitle = `gh-symphony \u2219 ${formatRepositoryDisplay(snapshot)}`;
|
|
2322
|
+
const headerWidth = 45;
|
|
2323
|
+
const headerPadding = Math.max(
|
|
2324
|
+
0,
|
|
2325
|
+
headerWidth - stripAnsi(headerTitle).length
|
|
2326
|
+
);
|
|
2327
|
+
lines.push("\u256D" + "\u2500".repeat(headerWidth) + "\u256E");
|
|
2328
|
+
lines.push(
|
|
2329
|
+
"\u2502 " + apply(bold(headerTitle)) + " ".repeat(headerPadding) + "\u2502"
|
|
2330
|
+
);
|
|
2331
|
+
lines.push("\u2570" + "\u2500".repeat(headerWidth) + "\u256F");
|
|
2332
|
+
lines.push("");
|
|
2333
|
+
const healthStr = apply(
|
|
2334
|
+
`${healthIcon(snapshot.health)} Health ${snapshot.health}`
|
|
2335
|
+
);
|
|
2336
|
+
const lastTickStr = apply(`Last tick ${relativeTime(snapshot.lastTickAt)}`);
|
|
2337
|
+
lines.push(
|
|
2338
|
+
` ${healthStr}${" ".repeat(Math.max(0, 30 - stripAnsi(healthStr).length))}${lastTickStr}`
|
|
2339
|
+
);
|
|
2340
|
+
lines.push("");
|
|
2341
|
+
const dispatchedStr = apply(`Dispatched ${snapshot.summary.dispatched}`);
|
|
2342
|
+
const activeRunsStr = apply(`Active Runs ${snapshot.summary.activeRuns}`);
|
|
2343
|
+
const suppressedStr = apply(`Suppressed ${snapshot.summary.suppressed}`);
|
|
2344
|
+
const recoveredStr = apply(`Recovered ${snapshot.summary.recovered}`);
|
|
2345
|
+
lines.push(
|
|
2346
|
+
` ${dispatchedStr}${" ".repeat(Math.max(0, 20 - stripAnsi(dispatchedStr).length))}${activeRunsStr}`
|
|
2347
|
+
);
|
|
2348
|
+
lines.push(
|
|
2349
|
+
` ${suppressedStr}${" ".repeat(Math.max(0, 20 - stripAnsi(suppressedStr).length))}${recoveredStr}`
|
|
2350
|
+
);
|
|
2351
|
+
lines.push("");
|
|
2352
|
+
if (snapshot.activeRuns.length > 0) {
|
|
2353
|
+
lines.push(" Active Runs:");
|
|
2354
|
+
for (const run of snapshot.activeRuns) {
|
|
2355
|
+
const runIdDisplay = truncate(run.runId, 12);
|
|
2356
|
+
const stateStr = apply(cyan(run.issueState));
|
|
2357
|
+
const statusColor = run.status === "running" ? green : run.status === "failed" ? red : run.status === "succeeded" ? green : dim;
|
|
2358
|
+
const statusStr = apply(statusColor(run.status));
|
|
2359
|
+
lines.push(
|
|
2360
|
+
` ${runIdDisplay} ${run.issueIdentifier} ${stateStr} ${statusStr}`
|
|
2361
|
+
);
|
|
2362
|
+
}
|
|
2363
|
+
lines.push("");
|
|
2364
|
+
} else {
|
|
2365
|
+
lines.push(" No active runs.");
|
|
2366
|
+
lines.push("");
|
|
2367
|
+
}
|
|
2368
|
+
if (snapshot.retryQueue.length > 0) {
|
|
2369
|
+
lines.push(" Retry Queue:");
|
|
2370
|
+
for (const retry of snapshot.retryQueue) {
|
|
2371
|
+
const runIdDisplay = truncate(retry.runId, 12);
|
|
2372
|
+
const nextRetryDisplay = retry.nextRetryAt ? relativeTime(retry.nextRetryAt) : "pending";
|
|
2373
|
+
lines.push(
|
|
2374
|
+
` ${runIdDisplay} ${retry.issueIdentifier} ${apply(yellow(retry.retryKind))} ${nextRetryDisplay}`
|
|
2375
|
+
);
|
|
2376
|
+
}
|
|
2377
|
+
lines.push("");
|
|
2378
|
+
}
|
|
2379
|
+
if (snapshot.lastError) {
|
|
2380
|
+
lines.push(apply(red(` \u2717 ${snapshot.lastError}`)));
|
|
2381
|
+
lines.push("");
|
|
2382
|
+
}
|
|
2383
|
+
if (snapshot.codexTotals) {
|
|
2384
|
+
const tokenDelta = resolveProjectTokenDelta(snapshot);
|
|
2385
|
+
const tokenStr = apply(
|
|
2386
|
+
`Tokens: ${formatTokenPair(tokenDelta, snapshot.codexTotals.totalTokens)} total`
|
|
2387
|
+
);
|
|
2388
|
+
lines.push(` ${tokenStr}`);
|
|
2389
|
+
} else {
|
|
2390
|
+
lines.push(" Tokens: 0 / 0 total");
|
|
2391
|
+
}
|
|
2392
|
+
return lines.join("\n");
|
|
2393
|
+
}
|
|
2394
|
+
function parseStatusArgs(args) {
|
|
2395
|
+
const parsed = {
|
|
2396
|
+
watch: false
|
|
2397
|
+
};
|
|
2398
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
2399
|
+
const arg = args[i];
|
|
2400
|
+
if (arg === "--watch" || arg === "-w") {
|
|
2401
|
+
parsed.watch = true;
|
|
2402
|
+
continue;
|
|
2403
|
+
}
|
|
2404
|
+
if (arg?.startsWith("-")) {
|
|
2405
|
+
parsed.error = `Unknown option '${arg}'`;
|
|
2406
|
+
return parsed;
|
|
2407
|
+
}
|
|
2408
|
+
}
|
|
2409
|
+
return parsed;
|
|
2410
|
+
}
|
|
2411
|
+
async function readStatusSnapshot(runtimeRoot, projectId) {
|
|
2412
|
+
for (const statusPath of [
|
|
2413
|
+
join7(runtimeRoot, "status.json"),
|
|
2414
|
+
join7(runtimeRoot, "projects", projectId, "status.json")
|
|
2415
|
+
]) {
|
|
2416
|
+
try {
|
|
2417
|
+
const content = await readFile6(statusPath, "utf-8");
|
|
2418
|
+
return JSON.parse(content);
|
|
2419
|
+
} catch {
|
|
2420
|
+
}
|
|
2421
|
+
}
|
|
2422
|
+
return null;
|
|
2423
|
+
}
|
|
2424
|
+
var handler6 = async (args, options) => {
|
|
2425
|
+
if (rejectRemovedProjectId(args)) {
|
|
2426
|
+
return;
|
|
2427
|
+
}
|
|
2428
|
+
const parsed = parseStatusArgs(args);
|
|
2429
|
+
if (parsed.error) {
|
|
2430
|
+
process.stderr.write(`${parsed.error}
|
|
2431
|
+
`);
|
|
2432
|
+
process.stderr.write("Usage: gh-symphony repo status [--watch]\n");
|
|
2433
|
+
process.exitCode = 2;
|
|
2434
|
+
return;
|
|
2435
|
+
}
|
|
2436
|
+
const projectConfig = await resolveManagedProjectConfig({
|
|
2437
|
+
configDir: options.configDir,
|
|
2438
|
+
requestedProjectId: void 0
|
|
2439
|
+
});
|
|
2440
|
+
if (!projectConfig) {
|
|
2441
|
+
handleMissingManagedProjectConfig();
|
|
2442
|
+
return;
|
|
2443
|
+
}
|
|
2444
|
+
const runtimeRoot = resolveRuntimeRoot(options.configDir);
|
|
2445
|
+
const projectId = projectConfig.projectId;
|
|
2446
|
+
if (parsed.watch) {
|
|
2447
|
+
const isTTY = process.stdout.isTTY === true;
|
|
2448
|
+
let terminalWidth = process.stdout.columns ?? 115;
|
|
2449
|
+
let runPromise = null;
|
|
2450
|
+
const run = async () => {
|
|
2451
|
+
const snapshot2 = await readStatusSnapshot(runtimeRoot, projectId);
|
|
2452
|
+
if (options.json || !isTTY) {
|
|
2453
|
+
process.stdout.write(JSON.stringify(snapshot2, null, 2) + "\n");
|
|
2454
|
+
} else {
|
|
2455
|
+
if (!snapshot2) {
|
|
2456
|
+
process.stdout.write(
|
|
2457
|
+
clearScreen() + "Unable to read status snapshot.\n"
|
|
2458
|
+
);
|
|
2459
|
+
return;
|
|
2460
|
+
}
|
|
2461
|
+
process.stdout.write(
|
|
2462
|
+
clearScreen() + renderDashboard([snapshot2], {
|
|
2463
|
+
terminalWidth,
|
|
2464
|
+
noColor: options.noColor
|
|
2465
|
+
}) + "\n"
|
|
2466
|
+
);
|
|
2467
|
+
}
|
|
2468
|
+
};
|
|
2469
|
+
const tick = () => {
|
|
2470
|
+
if (runPromise) {
|
|
2471
|
+
return;
|
|
2472
|
+
}
|
|
2473
|
+
runPromise = run().finally(() => {
|
|
2474
|
+
runPromise = null;
|
|
2475
|
+
});
|
|
2476
|
+
};
|
|
2477
|
+
if (isTTY) {
|
|
2478
|
+
process.stdout.write(hideCursor());
|
|
2479
|
+
}
|
|
2480
|
+
tick();
|
|
2481
|
+
await runPromise;
|
|
2482
|
+
const interval = setInterval(tick, 2e3);
|
|
2483
|
+
process.on("SIGWINCH", () => {
|
|
2484
|
+
terminalWidth = process.stdout.columns ?? terminalWidth;
|
|
2485
|
+
});
|
|
2486
|
+
const shutdown = () => {
|
|
2487
|
+
clearInterval(interval);
|
|
2488
|
+
process.stdout.write(showCursor() + "\n");
|
|
2489
|
+
process.exit(0);
|
|
2490
|
+
};
|
|
2491
|
+
process.on("SIGINT", shutdown);
|
|
2492
|
+
process.on("SIGTERM", shutdown);
|
|
2493
|
+
await new Promise(() => {
|
|
2494
|
+
});
|
|
2495
|
+
}
|
|
2496
|
+
const snapshot = await readStatusSnapshot(runtimeRoot, projectId);
|
|
2497
|
+
if (snapshot) {
|
|
2498
|
+
if (options.json) {
|
|
2499
|
+
process.stdout.write(JSON.stringify(snapshot, null, 2) + "\n");
|
|
2500
|
+
} else {
|
|
2501
|
+
process.stdout.write(
|
|
2502
|
+
renderLegacyStatus(snapshot, options.noColor) + "\n"
|
|
2503
|
+
);
|
|
2504
|
+
}
|
|
2505
|
+
} else {
|
|
2506
|
+
process.stderr.write("Unable to read status snapshot.\n");
|
|
2507
|
+
process.exitCode = 1;
|
|
2508
|
+
}
|
|
2509
|
+
};
|
|
2510
|
+
var status_default = handler6;
|
|
2511
|
+
|
|
2512
|
+
// src/commands/stop.ts
|
|
2513
|
+
import { readFile as readFile7, rm as rm2 } from "fs/promises";
|
|
2514
|
+
function parseStopArgs(args) {
|
|
2515
|
+
const parsed = {
|
|
2516
|
+
force: false
|
|
2517
|
+
};
|
|
2518
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
2519
|
+
const arg = args[i];
|
|
2520
|
+
if (arg === "--force") {
|
|
2521
|
+
parsed.force = true;
|
|
2522
|
+
continue;
|
|
2523
|
+
}
|
|
2524
|
+
if (arg?.startsWith("-")) {
|
|
2525
|
+
parsed.error = `Unknown option '${arg}'`;
|
|
2526
|
+
return parsed;
|
|
2527
|
+
}
|
|
2528
|
+
}
|
|
2529
|
+
return parsed;
|
|
2530
|
+
}
|
|
2531
|
+
var handler7 = async (args, options) => {
|
|
2532
|
+
if (rejectRemovedProjectId(args)) {
|
|
2533
|
+
return;
|
|
2534
|
+
}
|
|
2535
|
+
const parsed = parseStopArgs(args);
|
|
2536
|
+
if (parsed.error) {
|
|
2537
|
+
process.stderr.write(`${parsed.error}
|
|
2538
|
+
`);
|
|
2539
|
+
process.stderr.write("Usage: gh-symphony repo stop [--force]\n");
|
|
2540
|
+
process.exitCode = 2;
|
|
2541
|
+
return;
|
|
2542
|
+
}
|
|
2543
|
+
const resolvedForce = parsed.force;
|
|
2544
|
+
const projectConfig = await resolveManagedProjectConfig({
|
|
2545
|
+
configDir: options.configDir,
|
|
2546
|
+
requestedProjectId: void 0
|
|
2547
|
+
});
|
|
2548
|
+
if (!projectConfig) {
|
|
2549
|
+
handleMissingManagedProjectConfig();
|
|
2550
|
+
return;
|
|
2551
|
+
}
|
|
2552
|
+
const resolvedProjectId = projectConfig.projectId;
|
|
2553
|
+
const pidPath = daemonPidPath(options.configDir, resolvedProjectId);
|
|
2554
|
+
let pidStr;
|
|
2555
|
+
try {
|
|
2556
|
+
pidStr = await readFile7(pidPath, "utf8");
|
|
2557
|
+
} catch {
|
|
2558
|
+
process.stderr.write(
|
|
2559
|
+
`No running daemon found for project "${resolvedProjectId}" (PID file missing).
|
|
2560
|
+
`
|
|
2561
|
+
);
|
|
2562
|
+
process.exitCode = 1;
|
|
2563
|
+
return;
|
|
2564
|
+
}
|
|
2565
|
+
const pid = Number.parseInt(pidStr.trim(), 10);
|
|
2566
|
+
if (!Number.isFinite(pid)) {
|
|
2567
|
+
process.stderr.write(`Invalid PID in ${pidPath}: ${pidStr}
|
|
2568
|
+
`);
|
|
2569
|
+
process.exitCode = 1;
|
|
2570
|
+
return;
|
|
2571
|
+
}
|
|
2572
|
+
try {
|
|
2573
|
+
process.kill(pid, 0);
|
|
2574
|
+
} catch {
|
|
2575
|
+
process.stdout.write(
|
|
2576
|
+
`Daemon for project "${resolvedProjectId}" (PID ${pid}) is not running. Cleaning up PID file.
|
|
2577
|
+
`
|
|
2578
|
+
);
|
|
2579
|
+
await rm2(pidPath, { force: true });
|
|
2580
|
+
return;
|
|
2581
|
+
}
|
|
2582
|
+
const signal = resolvedForce ? "SIGKILL" : "SIGTERM";
|
|
2583
|
+
try {
|
|
2584
|
+
process.kill(pid, signal);
|
|
2585
|
+
process.stdout.write(`Sent ${signal} to orchestrator (PID ${pid}).
|
|
2586
|
+
`);
|
|
2587
|
+
} catch (error) {
|
|
2588
|
+
process.stderr.write(
|
|
2589
|
+
`Failed to stop process ${pid}: ${error instanceof Error ? error.message : "Unknown error"}
|
|
2590
|
+
`
|
|
2591
|
+
);
|
|
2592
|
+
process.exitCode = 1;
|
|
2593
|
+
return;
|
|
2594
|
+
}
|
|
2595
|
+
await rm2(pidPath, { force: true });
|
|
2596
|
+
process.stdout.write("Daemon stopped.\n");
|
|
2597
|
+
};
|
|
2598
|
+
var stop_default = handler7;
|
|
2599
|
+
|
|
2600
|
+
// src/commands/repo.ts
|
|
2601
|
+
var handler8 = async (args, options) => {
|
|
2602
|
+
const [subcommand, ...rest] = args;
|
|
2603
|
+
switch (subcommand) {
|
|
2604
|
+
case "init":
|
|
2605
|
+
await repoInit(rest, options);
|
|
2606
|
+
break;
|
|
2607
|
+
case "start":
|
|
2608
|
+
if (rejectRemovedProjectId(rest)) return;
|
|
2609
|
+
await start_default(rest, repoOptions(options));
|
|
2610
|
+
break;
|
|
2611
|
+
case "run":
|
|
2612
|
+
if (rejectRemovedProjectId(rest)) return;
|
|
2613
|
+
await run_default(rest, repoOptions(options));
|
|
2614
|
+
break;
|
|
2615
|
+
case "recover":
|
|
2616
|
+
if (rejectRemovedProjectId(rest)) return;
|
|
2617
|
+
await recover_default(rest, repoOptions(options));
|
|
2618
|
+
break;
|
|
2619
|
+
case "logs":
|
|
2620
|
+
if (rejectRemovedProjectId(rest)) return;
|
|
2621
|
+
await logs_default(rest, repoOptions(options));
|
|
2622
|
+
break;
|
|
2623
|
+
case "explain":
|
|
2624
|
+
if (rejectRemovedProjectId(rest)) return;
|
|
2625
|
+
await repo_explain_default(rest, repoOptions(options));
|
|
2626
|
+
break;
|
|
2627
|
+
case "status":
|
|
2628
|
+
if (rejectRemovedProjectId(rest)) return;
|
|
2629
|
+
await status_default(rest, repoOptions(options));
|
|
2630
|
+
break;
|
|
2631
|
+
case "stop":
|
|
2632
|
+
if (rejectRemovedProjectId(rest)) return;
|
|
2633
|
+
await stop_default(rest, repoOptions(options));
|
|
2634
|
+
break;
|
|
2635
|
+
default:
|
|
2636
|
+
process.stderr.write(
|
|
2637
|
+
"Usage: gh-symphony repo <init|start|status|stop|run|recover|logs|explain> [repo]\n"
|
|
2638
|
+
);
|
|
2639
|
+
process.exitCode = 2;
|
|
2640
|
+
}
|
|
2641
|
+
};
|
|
2642
|
+
var repo_default = handler8;
|
|
2643
|
+
function repoOptions(options) {
|
|
2644
|
+
return {
|
|
2645
|
+
...options,
|
|
2646
|
+
configDir: resolveRepoRuntimeRoot()
|
|
2647
|
+
};
|
|
2648
|
+
}
|
|
2649
|
+
async function repoInit(args, options) {
|
|
2650
|
+
if (rejectRemovedProjectId(args)) {
|
|
2651
|
+
return;
|
|
2652
|
+
}
|
|
2653
|
+
let flags;
|
|
2654
|
+
try {
|
|
2655
|
+
flags = parseRepoRuntimeFlags(args);
|
|
2656
|
+
} catch (error) {
|
|
2657
|
+
process.stderr.write(
|
|
2658
|
+
`${error instanceof Error ? error.message : "Invalid arguments"}
|
|
2659
|
+
`
|
|
2660
|
+
);
|
|
2661
|
+
process.stderr.write(
|
|
2662
|
+
"Usage: gh-symphony repo init [--repo-dir <path>] [--workflow-file <path>]\n"
|
|
2663
|
+
);
|
|
2664
|
+
process.exitCode = 2;
|
|
2665
|
+
return;
|
|
2666
|
+
}
|
|
2667
|
+
try {
|
|
2668
|
+
const result = await initRepoRuntime(flags);
|
|
2669
|
+
if (options.json) {
|
|
2670
|
+
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
2671
|
+
return;
|
|
2672
|
+
}
|
|
2673
|
+
process.stdout.write(
|
|
2674
|
+
[
|
|
2675
|
+
`Repository initialized: ${formatRepoSpec(result.repository)}`,
|
|
2676
|
+
`Runtime: ${result.configDir}`,
|
|
2677
|
+
`Workflow: ${result.workflowPath}`
|
|
2678
|
+
].join("\n") + "\n"
|
|
2679
|
+
);
|
|
2680
|
+
} catch (error) {
|
|
2681
|
+
process.stderr.write(
|
|
2682
|
+
`${error instanceof Error ? error.message : "Repository initialization failed."}
|
|
2683
|
+
`
|
|
2684
|
+
);
|
|
2685
|
+
process.exitCode = 1;
|
|
2686
|
+
}
|
|
2687
|
+
}
|
|
2688
|
+
function formatRepoSpec(repo) {
|
|
2689
|
+
return `${repo.owner}/${repo.name}`;
|
|
2690
|
+
}
|
|
2691
|
+
export {
|
|
2692
|
+
repo_default as default
|
|
2693
|
+
};
|