@gh-symphony/cli 0.0.19 → 0.0.21

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