@gh-symphony/cli 0.0.20 → 0.0.22

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 (40) hide show
  1. package/README.md +66 -2
  2. package/dist/chunk-2TSM3INR.js +1085 -0
  3. package/dist/chunk-2UW7NQLX.js +684 -0
  4. package/dist/{chunk-MVRF7BES.js → chunk-36KYEDEO.js} +10 -1
  5. package/dist/{chunk-TILHWBP6.js → chunk-C67H3OUL.js} +239 -36
  6. package/dist/{chunk-C7G7RJ4G.js → chunk-DDL4BWSL.js} +1 -1
  7. package/dist/{chunk-XN5ABWZ6.js → chunk-DFLXHNYQ.js} +26 -30
  8. package/dist/{chunk-EKKT5USP.js → chunk-E7HYEEZD.js} +487 -133
  9. package/dist/chunk-EEQQWTXS.js +3257 -0
  10. package/dist/chunk-GDE6FYN4.js +26 -0
  11. package/dist/{chunk-Y6TYJMNT.js → chunk-GSX2FV3M.js} +10 -16
  12. package/dist/{chunk-RN2PACNV.js → chunk-HMLBBZNY.js} +731 -75
  13. package/dist/{chunk-5NV3LSAJ.js → chunk-IWFX2FMA.js} +5 -1
  14. package/dist/{chunk-HZVDTAPS.js → chunk-PUDXVBSN.js} +1549 -1458
  15. package/dist/{chunk-ROGRTUFI.js → chunk-QIRE2VXS.js} +14 -3
  16. package/dist/{chunk-3AWF54PI.js → chunk-ZHOKYUO3.js} +394 -42
  17. package/dist/{config-cmd-DNXNL26Z.js → config-cmd-Z3A7V6NC.js} +1 -1
  18. package/dist/{doctor-IYHCFXOZ.js → doctor-EJUMPBMW.js} +105 -40
  19. package/dist/index.js +112 -24
  20. package/dist/{init-KZT6YNOH.js → init-54HMKNYI.js} +8 -3
  21. package/dist/{logs-6JKKYDGJ.js → logs-GTZ4U5JE.js} +2 -2
  22. package/dist/project-RMYMZSFV.js +25 -0
  23. package/dist/{recover-5KQI7WH5.js → recover-LTLKMTRX.js} +7 -5
  24. package/dist/repo-WI7GF6XQ.js +749 -0
  25. package/dist/{run-ETC5UTRA.js → run-IHN3ZL35.js} +21 -7
  26. package/dist/{setup-VWB7RZUQ.js → setup-TZJSM3QV.js} +53 -14
  27. package/dist/start-RTAHQMR2.js +19 -0
  28. package/dist/status-F4D52OVK.js +12 -0
  29. package/dist/stop-MDKMJPVR.js +10 -0
  30. package/dist/{upgrade-3YNF3VKY.js → upgrade-O33S2SJK.js} +2 -2
  31. package/dist/{version-NUBTTOG7.js → version-CW54Q7BK.js} +1 -1
  32. package/dist/worker-entry.js +848 -693
  33. package/dist/{workflow-TBIFY5MO.js → workflow-L3KT6HB7.js} +177 -11
  34. package/package.json +4 -2
  35. package/dist/chunk-M3IFVLQS.js +0 -1155
  36. package/dist/project-UUVHS3ZR.js +0 -22
  37. package/dist/repo-HDDE7OUI.js +0 -321
  38. package/dist/start-ENFLZUI6.js +0 -16
  39. package/dist/status-QSCFVGRQ.js +0 -11
  40. package/dist/stop-7MFCBQVW.js +0 -9
@@ -5,7 +5,10 @@ import {
5
5
  createStore,
6
6
  releaseProjectLock,
7
7
  resolveOrchestratorLogLevel
8
- } from "./chunk-HZVDTAPS.js";
8
+ } from "./chunk-PUDXVBSN.js";
9
+ import {
10
+ getGhToken
11
+ } from "./chunk-C67H3OUL.js";
9
12
  import {
10
13
  deriveIssueWorkspaceKeyFromIdentifier,
11
14
  isFileMissing,
@@ -14,40 +17,41 @@ 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-EEQQWTXS.js";
21
21
  import {
22
22
  bold,
23
23
  cyan,
24
24
  dim,
25
+ formatRepositoryDisplay,
25
26
  green,
26
27
  red,
27
28
  setNoColor,
28
29
  yellow
29
- } from "./chunk-MVRF7BES.js";
30
+ } from "./chunk-36KYEDEO.js";
30
31
  import {
31
32
  resolveRuntimeRoot
32
- } from "./chunk-5NV3LSAJ.js";
33
+ } from "./chunk-IWFX2FMA.js";
34
+ import {
35
+ rejectRemovedProjectId
36
+ } from "./chunk-GDE6FYN4.js";
33
37
  import {
34
38
  handleMissingManagedProjectConfig,
35
39
  resolveManagedProjectConfig
36
- } from "./chunk-C7G7RJ4G.js";
40
+ } from "./chunk-DDL4BWSL.js";
37
41
  import {
38
42
  daemonPidPath,
39
43
  httpStatusPath,
40
44
  orchestratorLogPath,
41
45
  writeJsonFile
42
- } from "./chunk-ROGRTUFI.js";
46
+ } from "./chunk-QIRE2VXS.js";
43
47
 
44
48
  // src/commands/start.ts
45
- import { writeFile, mkdir, readFile, rm } from "fs/promises";
46
- import { dirname, join as join2 } from "path";
49
+ import { writeFile, mkdir, readFile as readFile2, rm } from "fs/promises";
50
+ import { dirname as dirname2, join as join3 } from "path";
47
51
  import { spawn } from "child_process";
48
- import { createServer as createServer2 } from "http";
52
+ import { createServer as createServer3 } from "http";
49
53
 
50
- // ../dashboard/dist/store.js
54
+ // ../dashboard/src/store.ts
51
55
  import { open } from "fs/promises";
52
56
  import { join, resolve } from "path";
53
57
  var DEFAULT_RECENT_EVENT_LIMIT = 20;
@@ -55,24 +59,30 @@ var RECENT_EVENT_CHUNK_SIZE = 4096;
55
59
  var MAX_RECENT_EVENT_SCAN_BYTES = 64 * 1024;
56
60
  var RUN_RECORD_LOAD_CONCURRENCY = 8;
57
61
  var DashboardFsReader = class {
58
- runtimeRoot;
59
- projectId;
60
- resolvedRuntimeRoot;
61
- constructor(runtimeRoot, projectId) {
62
+ constructor(runtimeRoot) {
62
63
  this.runtimeRoot = runtimeRoot;
63
- this.projectId = projectId;
64
- assertValidDashboardProjectId(projectId);
65
64
  this.resolvedRuntimeRoot = resolve(runtimeRoot);
66
65
  }
66
+ resolvedRuntimeRoot;
67
67
  projectDir() {
68
- return join(this.resolvedRuntimeRoot, "projects", this.projectId);
68
+ return this.resolvedRuntimeRoot;
69
69
  }
70
70
  runDir(runId) {
71
71
  assertValidDashboardRunId(runId);
72
- return join(this.projectDir(), "runs", runId);
72
+ return join(this.resolvedRuntimeRoot, "runs", runId);
73
73
  }
74
74
  async loadProjectStatus() {
75
- return readJsonFile(join(this.projectDir(), "status.json"));
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;
76
86
  }
77
87
  async loadProjectState() {
78
88
  const snapshot = await this.loadProjectStatus();
@@ -87,7 +97,9 @@ var DashboardFsReader = class {
87
97
  };
88
98
  }
89
99
  async loadProjectIssueOrchestrations() {
90
- const issues = await readJsonFile(join(this.projectDir(), "issues.json"));
100
+ const issues = await readJsonFile(
101
+ join(this.projectDir(), "issues.json")
102
+ );
91
103
  if (issues) {
92
104
  return issues.map((issue) => ({
93
105
  ...issue,
@@ -99,7 +111,9 @@ var DashboardFsReader = class {
99
111
  return legacyLeases.map((lease) => ({
100
112
  issueId: lease.issueId,
101
113
  identifier: lease.issueIdentifier,
102
- workspaceKey: deriveIssueWorkspaceKeyFromIdentifier(lease.issueIdentifier),
114
+ workspaceKey: deriveIssueWorkspaceKeyFromIdentifier(
115
+ lease.issueIdentifier
116
+ ),
103
117
  completedOnce: false,
104
118
  failureRetryCount: 0,
105
119
  state: lease.status === "active" ? "claimed" : "released",
@@ -109,29 +123,39 @@ var DashboardFsReader = class {
109
123
  }));
110
124
  }
111
125
  async loadRun(runId) {
112
- return readJsonFile(join(this.runDir(runId), "run.json"));
126
+ return readJsonFile(
127
+ join(this.runDir(runId), "run.json")
128
+ );
113
129
  }
114
130
  async loadAllRuns() {
115
131
  const runIds = await safeReadDir(join(this.projectDir(), "runs"));
116
- const runs = await mapWithConcurrency(runIds, RUN_RECORD_LOAD_CONCURRENCY, (runId) => this.loadRun(runId));
132
+ const runs = await mapWithConcurrency(
133
+ runIds,
134
+ RUN_RECORD_LOAD_CONCURRENCY,
135
+ (runId) => this.loadRun(runId)
136
+ );
117
137
  return runs.filter((run) => Boolean(run));
118
138
  }
119
139
  async loadRunsForIssue(issueId, issueIdentifier) {
120
140
  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)) {
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
+ }
130
155
  return null;
131
156
  }
132
- return null;
133
157
  }
134
- });
158
+ );
135
159
  return runs.filter((run) => Boolean(run));
136
160
  }
137
161
  async loadRecentRunEvents(runId, limit = DEFAULT_RECENT_EVENT_LIMIT) {
@@ -148,7 +172,11 @@ var DashboardFsReader = class {
148
172
  let newlineCount = 0;
149
173
  const chunks = [];
150
174
  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);
175
+ const readSize = Math.min(
176
+ position,
177
+ RECENT_EVENT_CHUNK_SIZE,
178
+ MAX_RECENT_EVENT_SCAN_BYTES - bytesScanned
179
+ );
152
180
  position -= readSize;
153
181
  const chunk = Buffer.allocUnsafe(readSize);
154
182
  const { bytesRead } = await handle.read(chunk, 0, readSize, position);
@@ -160,9 +188,13 @@ var DashboardFsReader = class {
160
188
  bytesScanned += bytesRead;
161
189
  newlineCount += countNewlines(populatedChunk);
162
190
  }
163
- return parseRecentEvents(Buffer.concat(chunks).toString("utf8"), limit, {
164
- allowPartialFirstLine: position > 0
165
- });
191
+ return parseRecentEvents(
192
+ Buffer.concat(chunks).toString("utf8"),
193
+ limit,
194
+ {
195
+ allowPartialFirstLine: position > 0
196
+ }
197
+ );
166
198
  } finally {
167
199
  await handle.close();
168
200
  }
@@ -185,12 +217,18 @@ function countNewlines(chunk) {
185
217
  }
186
218
  async function statusForIssue(reader, issueIdentifier) {
187
219
  const issueRecords = await reader.loadProjectIssueOrchestrations();
188
- const issueRecord = issueRecords.find((record) => record.identifier === issueIdentifier);
220
+ const issueRecord = issueRecords.find(
221
+ (record) => record.identifier === issueIdentifier
222
+ );
189
223
  if (!issueRecord) {
190
224
  return null;
191
225
  }
192
226
  const currentRunCandidate = issueRecord.currentRunId ? await reader.loadRun(issueRecord.currentRunId) : null;
193
- const currentRun = isMatchingIssueRun(currentRunCandidate, reader.projectId, issueRecord.issueId, issueIdentifier) ? currentRunCandidate : null;
227
+ const currentRun = isMatchingIssueRun(
228
+ currentRunCandidate,
229
+ issueRecord.issueId,
230
+ issueIdentifier
231
+ ) ? currentRunCandidate : null;
194
232
  const issueRuns = currentRun === null ? await reader.loadRunsForIssue(issueRecord.issueId, issueIdentifier) : currentRun.tokenUsage ? await reader.loadRunsForIssue(issueRecord.issueId, issueIdentifier) : null;
195
233
  const resolvedRun = currentRun ?? findLatestRunForIssue(issueRuns ?? []);
196
234
  const recentEvents = resolvedRun === null ? [] : await reader.loadRecentRunEvents(resolvedRun.runId);
@@ -252,30 +290,39 @@ async function statusForIssue(reader, issueIdentifier) {
252
290
  };
253
291
  }
254
292
  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
- });
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
+ );
264
305
  }
265
306
  function findLatestRunForIssue(matchingRuns) {
266
- const sortedRuns = [...matchingRuns].sort((left, right) => new Date(right.updatedAt).getTime() - new Date(left.updatedAt).getTime());
307
+ const sortedRuns = [...matchingRuns].sort(
308
+ (left, right) => new Date(right.updatedAt).getTime() - new Date(left.updatedAt).getTime()
309
+ );
267
310
  return sortedRuns[0] ?? null;
268
311
  }
269
- function assertValidDashboardProjectId(projectId) {
270
- 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.`);
272
- }
273
- }
274
312
  function assertValidDashboardRunId(runId) {
275
313
  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.`);
314
+ throw new Error(
315
+ `Invalid run ID "${runId}". Run IDs must not contain path separators or traversal segments.`
316
+ );
277
317
  }
278
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
+ }
279
326
  async function mapWithConcurrency(items, concurrency, mapper) {
280
327
  const results = new Array(items.length);
281
328
  let nextIndex = 0;
@@ -291,8 +338,10 @@ async function mapWithConcurrency(items, concurrency, mapper) {
291
338
  return results;
292
339
  }
293
340
 
294
- // ../dashboard/dist/server.js
295
- import { createServer } from "http";
341
+ // ../dashboard/src/server.ts
342
+ import {
343
+ createServer
344
+ } from "http";
296
345
  async function resolveDashboardResponse(options) {
297
346
  const method = options.method ?? "GET";
298
347
  if (options.pathname === "/healthz") {
@@ -371,6 +420,267 @@ async function resolveDashboardResponse(options) {
371
420
  };
372
421
  }
373
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
+
374
684
  // src/commands/start.ts
375
685
  function timestamp() {
376
686
  const now = /* @__PURE__ */ new Date();
@@ -410,13 +720,13 @@ function parseStartArgs(args) {
410
720
  i += 1;
411
721
  continue;
412
722
  }
413
- if (arg === "--project" || arg === "--project-id") {
723
+ if (arg === "--web") {
414
724
  const value = args[i + 1];
415
725
  if (!value || value.startsWith("-")) {
416
- parsed.error = `Option '${arg}' argument missing`;
417
- return parsed;
726
+ parsed.webPort = DEFAULT_HTTP_PORT;
727
+ continue;
418
728
  }
419
- parsed.projectId = value;
729
+ parsed.webPort = parsePort(value, arg);
420
730
  i += 1;
421
731
  continue;
422
732
  }
@@ -435,6 +745,9 @@ function parseStartArgs(args) {
435
745
  return parsed;
436
746
  }
437
747
  }
748
+ if (parsed.httpPort !== void 0 && parsed.webPort !== void 0) {
749
+ parsed.error = "Options '--http' and '--web' cannot be used together";
750
+ }
438
751
  return parsed;
439
752
  }
440
753
  function logTickResult(snapshot, prevSnapshot, isFirst) {
@@ -442,7 +755,9 @@ function logTickResult(snapshot, prevSnapshot, isFirst) {
442
755
  const healthColor = snapshot.health === "degraded" ? red : snapshot.health === "running" ? green : cyan;
443
756
  logLine(
444
757
  green("\u25CF"),
445
- `Project ${bold(snapshot.slug)} connected ${dim("(")}${healthColor(snapshot.health)}${dim(")")}`
758
+ `Repository ${bold(formatRepositoryDisplay(snapshot))} connected ${dim(
759
+ "("
760
+ )}${healthColor(snapshot.health)}${dim(")")}`
446
761
  );
447
762
  if (snapshot.summary.activeRuns > 0) {
448
763
  logLine(cyan("\u25B8"), `${snapshot.summary.activeRuns} active run(s)`);
@@ -530,13 +845,13 @@ function parsePort(value, optionName) {
530
845
  }
531
846
  return parsed;
532
847
  }
533
- function respondJson(response, status, payload) {
848
+ function respondJson2(response, status, payload) {
534
849
  response.writeHead(status, {
535
850
  "content-type": "application/json"
536
851
  });
537
852
  response.end(JSON.stringify(payload));
538
853
  }
539
- function formatBoundUrl(server) {
854
+ function formatBoundUrl2(server) {
540
855
  const address = server.address();
541
856
  if (!address || typeof address === "string") {
542
857
  return `http://${HTTP_HOST}`;
@@ -571,16 +886,16 @@ async function removeHttpBindingState(configDir, projectId) {
571
886
  await rm(httpStatusPath(configDir, projectId), { force: true });
572
887
  }
573
888
  async function startHttpServer(input) {
574
- const reader = new DashboardFsReader(input.runtimeRoot, input.projectId);
889
+ const reader = new DashboardFsReader(input.runtimeRoot);
575
890
  for (let port = input.initialPort; port <= 65535; port += 1) {
576
- const server = createServer2((request, response) => {
891
+ const server = createServer3((request, response) => {
577
892
  void (async () => {
578
893
  try {
579
894
  const url = new URL(request.url ?? "/", `http://${HTTP_HOST}`);
580
895
  if (request.method === "POST" && url.pathname === "/api/v1/refresh") {
581
896
  request.resume();
582
897
  input.service.requestReconcile();
583
- respondJson(response, 202, { ok: true });
898
+ respondJson2(response, 202, { ok: true });
584
899
  return;
585
900
  }
586
901
  const resolved = await resolveDashboardResponse({
@@ -588,11 +903,11 @@ async function startHttpServer(input) {
588
903
  method: request.method ?? "GET",
589
904
  reader
590
905
  });
591
- respondJson(response, resolved.status, resolved.payload);
906
+ respondJson2(response, resolved.status, resolved.payload);
592
907
  } catch (error) {
593
908
  logHttpRequestError(error);
594
909
  if (!response.headersSent) {
595
- respondJson(response, 500, {
910
+ respondJson2(response, 500, {
596
911
  error: "Internal server error"
597
912
  });
598
913
  } else {
@@ -622,7 +937,7 @@ async function startHttpServer(input) {
622
937
  return {
623
938
  server,
624
939
  port,
625
- url: formatBoundUrl(server)
940
+ url: formatBoundUrl2(server)
626
941
  };
627
942
  } catch (error) {
628
943
  await closeHttpServer(server).catch(() => {
@@ -641,6 +956,9 @@ var handler = async (args, options) => {
641
956
  setNoColor(options.noColor);
642
957
  let parsed;
643
958
  try {
959
+ if (rejectRemovedProjectId(args)) {
960
+ return;
961
+ }
644
962
  parsed = parseStartArgs(args);
645
963
  } catch (error) {
646
964
  process.stderr.write(
@@ -654,24 +972,33 @@ var handler = async (args, options) => {
654
972
  process.stderr.write(`${parsed.error}
655
973
  `);
656
974
  process.stderr.write(
657
- "Usage: gh-symphony start --project-id <project-id> [--daemon] [--once] [--http [port]]\n"
975
+ "Usage: gh-symphony start [--daemon] [--once] [--http [port]] [--web [port]]\n"
658
976
  );
659
977
  process.exitCode = 2;
660
978
  return;
661
979
  }
662
980
  if (parsed.daemon && parsed.once) {
663
- process.stderr.write("Options '--daemon' and '--once' cannot be used together\n");
981
+ process.stderr.write(
982
+ "Options '--daemon' and '--once' cannot be used together\n"
983
+ );
664
984
  process.exitCode = 2;
665
985
  return;
666
986
  }
667
987
  const projectConfig = await resolveManagedProjectConfig({
668
988
  configDir: options.configDir,
669
- requestedProjectId: parsed.projectId
989
+ requestedProjectId: void 0
670
990
  });
671
991
  if (!projectConfig) {
672
992
  handleMissingManagedProjectConfig();
673
993
  return;
674
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
+ }
675
1002
  const runtimeRoot = resolveRuntimeRoot(options.configDir);
676
1003
  const projectId = projectConfig.projectId;
677
1004
  let logLevel;
@@ -688,7 +1015,13 @@ var handler = async (args, options) => {
688
1015
  return;
689
1016
  }
690
1017
  if (parsed.daemon) {
691
- await startDaemon(options, projectId, parsed.logLevel, parsed.httpPort);
1018
+ await startDaemon(
1019
+ options,
1020
+ projectId,
1021
+ parsed.logLevel,
1022
+ parsed.httpPort,
1023
+ parsed.webPort
1024
+ );
692
1025
  return;
693
1026
  }
694
1027
  if (!process.env.GITHUB_GRAPHQL_TOKEN) {
@@ -739,45 +1072,10 @@ var handler = async (args, options) => {
739
1072
  }
740
1073
  }
741
1074
  });
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
1075
  let shuttingDown = false;
779
1076
  let shutdownPromise = null;
780
1077
  let keepHttpAliveResolve = null;
1078
+ let httpServer = null;
781
1079
  const shutdown = async () => {
782
1080
  if (shuttingDown) {
783
1081
  return shutdownPromise;
@@ -805,17 +1103,66 @@ var handler = async (args, options) => {
805
1103
  process.on("SIGINT", handleSigint);
806
1104
  process.on("SIGTERM", handleSigterm);
807
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
+ );
808
1149
  while (!shuttingDown) {
809
1150
  try {
810
1151
  await service.run({ once: parsed.once });
1152
+ if (shuttingDown) {
1153
+ break;
1154
+ }
811
1155
  if (parsed.once) {
812
1156
  if (httpServer) {
813
1157
  logLine(
814
1158
  cyan("\u25A1"),
815
- "One-shot tick completed; HTTP dashboard remains available until Ctrl+C"
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"
816
1160
  );
817
- await new Promise((resolve2) => {
818
- keepHttpAliveResolve = resolve2;
1161
+ if (shuttingDown) {
1162
+ break;
1163
+ }
1164
+ await new Promise((resolve3) => {
1165
+ keepHttpAliveResolve = resolve3;
819
1166
  });
820
1167
  } else {
821
1168
  await shutdown();
@@ -903,25 +1250,33 @@ async function shutdownForegroundOrchestrator(input) {
903
1250
  }
904
1251
  return (input.exit ?? process.exit)(0);
905
1252
  }
1253
+ function hasConfiguredRepository(config) {
1254
+ return Boolean(config.repository?.owner && config.repository.name);
1255
+ }
906
1256
  async function tailWorkerLog(runtimeRoot, projectId, runId, issueIdentifier) {
907
- try {
908
- const logPath = join2(runtimeRoot, "projects", projectId, "runs", runId, "worker.log");
909
- const content = await readFile(logPath, "utf8");
910
- const lines = content.split("\n").filter((l) => l.trim());
911
- if (lines.length === 0) return;
912
- const tail = lines.slice(-30);
913
- logLine(red("\u2717"), red(`Worker stderr (${issueIdentifier}):`));
914
- for (const line of tail) {
915
- process.stdout.write(` ${dim(line)}
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)}
916
1269
  `);
1270
+ }
1271
+ return;
1272
+ } catch {
917
1273
  }
918
- } catch {
919
1274
  }
920
1275
  }
921
1276
  var start_default = handler;
922
- async function startDaemon(options, projectId, logLevel, httpPort) {
1277
+ async function startDaemon(options, projectId, logLevel, httpPort, webPort) {
923
1278
  const logPath = orchestratorLogPath(options.configDir, projectId);
924
- await mkdir(dirname(logPath), { recursive: true });
1279
+ await mkdir(dirname2(logPath), { recursive: true });
925
1280
  const { openSync } = await import("fs");
926
1281
  const logFd = openSync(logPath, "a");
927
1282
  const child = spawn(
@@ -929,9 +1284,8 @@ async function startDaemon(options, projectId, logLevel, httpPort) {
929
1284
  [
930
1285
  process.argv[1],
931
1286
  "start",
932
- "--project",
933
- projectId,
934
1287
  ...httpPort !== void 0 ? ["--http", String(httpPort)] : [],
1288
+ ...webPort !== void 0 ? ["--web", String(webPort)] : [],
935
1289
  ...logLevel ? ["--log-level", logLevel] : []
936
1290
  ],
937
1291
  {
@@ -945,7 +1299,7 @@ async function startDaemon(options, projectId, logLevel, httpPort) {
945
1299
  }
946
1300
  );
947
1301
  const pidPath = daemonPidPath(options.configDir, projectId);
948
- await mkdir(dirname(pidPath), { recursive: true });
1302
+ await mkdir(dirname2(pidPath), { recursive: true });
949
1303
  await writeFile(pidPath, String(child.pid), "utf8");
950
1304
  child.unref();
951
1305
  const { closeSync } = await import("fs");
@@ -953,7 +1307,7 @@ async function startDaemon(options, projectId, logLevel, httpPort) {
953
1307
  process.stdout.write(
954
1308
  `Orchestrator started in background (PID: ${child.pid}).
955
1309
  Logs: ${logPath}
956
- Stop with: gh-symphony project stop --project-id ${projectId}
1310
+ Stop with: gh-symphony repo stop
957
1311
  `
958
1312
  );
959
1313
  }