@gh-symphony/cli 0.0.22 → 0.1.3

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