@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,1047 +0,0 @@
1
- #!/usr/bin/env node
2
- import {
3
- abortIfCancelled,
4
- generateProjectId,
5
- warnIfProjectDiscoveryPartial,
6
- writeConfig
7
- } from "./chunk-HMLBBZNY.js";
8
- import {
9
- start_default
10
- } from "./chunk-E7HYEEZD.js";
11
- import {
12
- explainIssueDispatch,
13
- isActiveRunRecordStatus,
14
- parseIssueIdentifier,
15
- resolveTrackerAdapter
16
- } from "./chunk-PUDXVBSN.js";
17
- import {
18
- findGithubProjectIssue
19
- } from "./chunk-2TSM3INR.js";
20
- import {
21
- GhAuthError,
22
- GitHubScopeError,
23
- checkRequiredScopes,
24
- createClient,
25
- discoverUserProjects,
26
- getGhToken,
27
- getGhTokenWithSource,
28
- getProjectDetail,
29
- listUserProjects,
30
- resolveGitHubAuth,
31
- validateToken
32
- } from "./chunk-C67H3OUL.js";
33
- import {
34
- WorkflowConfigStore
35
- } from "./chunk-EEQQWTXS.js";
36
- import {
37
- status_default
38
- } from "./chunk-DFLXHNYQ.js";
39
- import {
40
- bold,
41
- green,
42
- red,
43
- stripAnsi,
44
- yellow
45
- } from "./chunk-36KYEDEO.js";
46
- import {
47
- resolveRuntimeRoot
48
- } from "./chunk-IWFX2FMA.js";
49
- import {
50
- stop_default
51
- } from "./chunk-GSX2FV3M.js";
52
- import {
53
- handleMissingManagedProjectConfig,
54
- resolveManagedProjectConfig
55
- } from "./chunk-DDL4BWSL.js";
56
- import {
57
- daemonPidPath,
58
- httpStatusPath,
59
- loadGlobalConfig,
60
- loadProjectConfig,
61
- projectConfigDir,
62
- saveGlobalConfig
63
- } from "./chunk-QIRE2VXS.js";
64
-
65
- // src/commands/project.ts
66
- import * as p from "@clack/prompts";
67
- import { execFile as execFileCallback } from "child_process";
68
- import { promisify } from "util";
69
- import { readdir, readFile } from "fs/promises";
70
- import { join } from "path";
71
- import { resolve } from "path";
72
- var execFile = promisify(execFileCallback);
73
- var KNOWN_REQUIRED_SCOPES = ["repo", "read:org", "project"];
74
- function formatProjectRepoSummary(selectedRepos, totalLinked) {
75
- if (totalLinked === 0) {
76
- return "none linked yet (0 linked)";
77
- }
78
- if (selectedRepos.length === totalLinked) {
79
- return `${selectedRepos.map((repo) => `${repo.owner}/${repo.name}`).join(", ")} (all ${selectedRepos.length} linked)`;
80
- }
81
- if (selectedRepos.length === 0) {
82
- return `none selected (0 of ${totalLinked} linked)`;
83
- }
84
- return `${selectedRepos.map((repo) => `${repo.owner}/${repo.name}`).join(", ")} (${selectedRepos.length} of ${totalLinked} linked)`;
85
- }
86
- function projectCreatedMessage(projectId, repositoryCount) {
87
- const lines = [
88
- `Project "${projectId}" created with ${repositoryCount} repositor${repositoryCount === 1 ? "y" : "ies"}.`,
89
- "Run 'gh-symphony start' to begin orchestration."
90
- ];
91
- if (repositoryCount === 0) {
92
- lines.push(
93
- "Next step: run 'gh-symphony repo add <owner/name>' to register a repository.",
94
- "Or add a repo-linked issue to the GitHub Project and re-run setup later."
95
- );
96
- }
97
- return lines.join("\n");
98
- }
99
- function displayScopeError(error, retryCommand) {
100
- const plural = error.requiredScopes.length === 1 ? "" : "s";
101
- p.log.error(
102
- `Token is missing required scope${plural}: ${error.requiredScopes.join(", ")}`
103
- );
104
- const currentSet = new Set(error.currentScopes.map((s) => s.toLowerCase()));
105
- const scopesToAdd = KNOWN_REQUIRED_SCOPES.filter((s) => !currentSet.has(s));
106
- const scopeArg = scopesToAdd.length > 0 ? scopesToAdd.join(",") : error.requiredScopes.join(",");
107
- p.note(
108
- `gh auth refresh --scopes ${scopeArg}
109
-
110
- Then re-run: ${retryCommand}`,
111
- "Fix missing scope"
112
- );
113
- }
114
- function parseProjectAddFlags(args) {
115
- const flags = { nonInteractive: false };
116
- for (let i = 0; i < args.length; i += 1) {
117
- const arg = args[i];
118
- const next = args[i + 1];
119
- switch (arg) {
120
- case "--non-interactive":
121
- flags.nonInteractive = true;
122
- break;
123
- case "--project":
124
- flags.project = next;
125
- i += 1;
126
- break;
127
- case "--workspace-dir":
128
- flags.workspaceDir = next;
129
- i += 1;
130
- break;
131
- case "--assigned-only":
132
- flags.assignedOnly = true;
133
- break;
134
- }
135
- }
136
- return flags;
137
- }
138
- function parseProjectExplainFlags(args) {
139
- const parsed = {};
140
- for (let i = 0; i < args.length; i += 1) {
141
- const arg = args[i];
142
- if (arg === "--project" || arg === "--project-id") {
143
- const value = args[i + 1];
144
- if (!value || value.startsWith("-")) {
145
- parsed.error = `Option '${arg}' argument missing`;
146
- return parsed;
147
- }
148
- parsed.projectId = value;
149
- i += 1;
150
- continue;
151
- }
152
- if (arg === "--workflow" || arg === "--workflow-path") {
153
- const value = args[i + 1];
154
- if (!value || value.startsWith("-")) {
155
- parsed.error = `Option '${arg}' argument missing`;
156
- return parsed;
157
- }
158
- parsed.workflowPath = value;
159
- i += 1;
160
- continue;
161
- }
162
- if (arg?.startsWith("-")) {
163
- parsed.error = `Unknown option '${arg}'`;
164
- return parsed;
165
- }
166
- if (parsed.identifier) {
167
- parsed.error = "Only one issue identifier can be explained at a time";
168
- return parsed;
169
- }
170
- parsed.identifier = arg;
171
- }
172
- if (!parsed.identifier) {
173
- parsed.error = "Issue identifier argument missing";
174
- } else if (!parseIssueIdentifier(parsed.identifier)) {
175
- parsed.error = "Issue identifier must use the form <owner>/<repo>#<number>";
176
- }
177
- return parsed;
178
- }
179
- var handler = async (args, options) => {
180
- const [subcommand, ...rest] = args;
181
- switch (subcommand) {
182
- case "add":
183
- await projectAdd(rest, options);
184
- return;
185
- case "list":
186
- await projectList(options);
187
- return;
188
- case "remove":
189
- await projectRemove(rest, options);
190
- return;
191
- case "start":
192
- await start_default(rest, options);
193
- return;
194
- case "stop":
195
- await stop_default(rest, options);
196
- return;
197
- case "switch":
198
- await projectSwitch(options);
199
- return;
200
- case "status":
201
- await status_default(rest, options);
202
- return;
203
- case "explain":
204
- await projectExplain(rest, options);
205
- return;
206
- default:
207
- process.stdout.write(
208
- "Usage: gh-symphony project <add|list|remove|start|stop|switch|status|explain>\n"
209
- );
210
- }
211
- };
212
- var project_default = handler;
213
- function relativeTimeFromNow(isoString, now = /* @__PURE__ */ new Date()) {
214
- const then = new Date(isoString);
215
- if (!Number.isFinite(then.getTime())) {
216
- return "-";
217
- }
218
- const diffMs = Math.max(0, now.getTime() - then.getTime());
219
- const diffS = Math.floor(diffMs / 1e3);
220
- const diffM = Math.floor(diffS / 60);
221
- const diffH = Math.floor(diffM / 60);
222
- const diffD = Math.floor(diffH / 24);
223
- if (diffS < 60) return `${diffS}s ago`;
224
- if (diffM < 60) return `${diffM}m ago`;
225
- if (diffH < 24) return `${diffH}h ago`;
226
- return `${diffD}d ago`;
227
- }
228
- function formatDuration(seconds) {
229
- if (seconds < 60) return `${seconds}s`;
230
- const days = Math.floor(seconds / 86400);
231
- const hours = Math.floor(seconds % 86400 / 3600);
232
- const minutes = Math.floor(seconds % 3600 / 60);
233
- if (days > 0) {
234
- return hours > 0 ? `${days}d ${hours}h` : `${days}d`;
235
- }
236
- if (hours > 0) {
237
- return minutes > 0 ? `${hours}h ${minutes}m` : `${hours}h`;
238
- }
239
- return `${minutes}m`;
240
- }
241
- function parsePsElapsedTime(raw) {
242
- const value = raw.trim();
243
- if (value.length === 0) {
244
- return null;
245
- }
246
- const [dayPart, timePart] = value.includes("-") ? value.split("-", 2) : [null, value];
247
- const timeSegments = timePart.split(":").map((segment) => Number.parseInt(segment, 10));
248
- if (timeSegments.some((segment) => !Number.isFinite(segment))) {
249
- return null;
250
- }
251
- let seconds = 0;
252
- if (timeSegments.length === 3) {
253
- seconds += timeSegments[0] * 3600;
254
- seconds += timeSegments[1] * 60;
255
- seconds += timeSegments[2];
256
- } else if (timeSegments.length === 2) {
257
- seconds += timeSegments[0] * 60;
258
- seconds += timeSegments[1];
259
- } else {
260
- return null;
261
- }
262
- if (dayPart !== null) {
263
- const days = Number.parseInt(dayPart, 10);
264
- if (!Number.isFinite(days)) {
265
- return null;
266
- }
267
- seconds += days * 86400;
268
- }
269
- return seconds;
270
- }
271
- async function readPid(configDir, projectId) {
272
- try {
273
- const raw = await readFile(daemonPidPath(configDir, projectId), "utf8");
274
- const pid = Number.parseInt(raw.trim(), 10);
275
- return Number.isInteger(pid) && pid > 0 ? pid : null;
276
- } catch {
277
- return null;
278
- }
279
- }
280
- function isProcessRunning(pid) {
281
- try {
282
- process.kill(pid, 0);
283
- return true;
284
- } catch (error) {
285
- if (error instanceof Error && "code" in error && error.code === "EPERM") {
286
- return true;
287
- }
288
- return false;
289
- }
290
- }
291
- async function readPersistedSnapshot(configDir, projectId) {
292
- try {
293
- const runtimeRoot = resolveRuntimeRoot(configDir);
294
- const content = await readFile(
295
- join(runtimeRoot, "projects", projectId, "status.json"),
296
- "utf8"
297
- );
298
- return JSON.parse(content);
299
- } catch {
300
- return null;
301
- }
302
- }
303
- async function fetchProjectSnapshot(configDir, projectId) {
304
- return readPersistedSnapshot(configDir, projectId);
305
- }
306
- async function projectExplain(args, options) {
307
- const parsed = parseProjectExplainFlags(args);
308
- if (parsed.error) {
309
- process.stderr.write(`${parsed.error}
310
- `);
311
- process.stderr.write(
312
- "Usage: gh-symphony project explain <owner/repo#number> [--project-id <project-id>] [--workflow <path>]\n"
313
- );
314
- process.exitCode = 2;
315
- return;
316
- }
317
- const projectConfig = await resolveManagedProjectConfig({
318
- configDir: options.configDir,
319
- requestedProjectId: parsed.projectId
320
- });
321
- if (!projectConfig) {
322
- handleMissingManagedProjectConfig();
323
- return;
324
- }
325
- const identifier = parsed.identifier;
326
- const parsedIdentifier = parseIssueIdentifier(identifier);
327
- const fallbackRepository = {
328
- owner: parsedIdentifier.owner,
329
- name: parsedIdentifier.name,
330
- cloneUrl: `https://github.com/${parsedIdentifier.owner}/${parsedIdentifier.name}.git`
331
- };
332
- const workflowRepository = projectConfig.repository ?? fallbackRepository;
333
- let token;
334
- try {
335
- token = getGhToken();
336
- } catch (error) {
337
- if (error instanceof GhAuthError) {
338
- process.stderr.write(
339
- `Error: GitHub authentication is required for project explain. ${error.message}
340
- `
341
- );
342
- process.stderr.write(
343
- "Run 'gh auth login --scopes repo,read:org,project' or set GITHUB_GRAPHQL_TOKEN, then re-run this command.\n"
344
- );
345
- process.exitCode = 2;
346
- return;
347
- }
348
- throw error;
349
- }
350
- const trackerAdapter = resolveTrackerAdapter(projectConfig.tracker);
351
- const orchestratorProject = {
352
- ...projectConfig,
353
- repository: workflowRepository
354
- };
355
- const trackerDependencies = {
356
- token,
357
- projectItemsCache: createProjectItemsCache()
358
- };
359
- const runtimeRoot = join(
360
- resolveRuntimeRoot(options.configDir),
361
- "projects",
362
- projectConfig.projectId
363
- );
364
- const issuesPromise = trackerAdapter.listIssues(
365
- orchestratorProject,
366
- trackerDependencies
367
- );
368
- const issuePromise = projectConfig.tracker.adapter === "github-project" ? findGithubProjectIssue(
369
- orchestratorProject,
370
- identifier,
371
- trackerDependencies
372
- ) : issuesPromise.then(
373
- (issues2) => issues2.find(
374
- (candidate) => candidate.identifier.trim().toLowerCase() === identifier.trim().toLowerCase()
375
- ) ?? null
376
- );
377
- const [issues, issue, issueRecords, runs, snapshot] = await Promise.all([
378
- issuesPromise,
379
- issuePromise,
380
- readJsonFile(join(runtimeRoot, "issues.json")),
381
- readRuns(runtimeRoot, projectConfig.projectId),
382
- readPersistedSnapshot(options.configDir, projectConfig.projectId)
383
- ]);
384
- let workflow;
385
- try {
386
- workflow = await loadExplainWorkflow({
387
- explicitWorkflowPath: parsed.workflowPath,
388
- repository: workflowRepository,
389
- runs
390
- });
391
- } catch (error) {
392
- if (error instanceof ProjectExplainWorkflowError) {
393
- process.stderr.write(`Error: ${error.message}
394
- `);
395
- process.stderr.write(
396
- "Hint: pass --workflow <path-to-WORKFLOW.md> or run 'gh-symphony workflow preview --file <path>' to verify the workflow file.\n"
397
- );
398
- process.exitCode = 2;
399
- return;
400
- }
401
- throw error;
402
- }
403
- const activeRunCount = runs.filter(
404
- (run) => isActiveRunRecordStatus(run.status)
405
- ).length;
406
- const report = explainIssueDispatch({
407
- identifier,
408
- issue,
409
- projectRepository: projectConfig.repository ?? null,
410
- allIssues: issues,
411
- lifecycle: workflow.lifecycle,
412
- issueRecords: issueRecords ?? [],
413
- runs,
414
- activeRunCount,
415
- maxConcurrentAgents: workflow.maxConcurrentAgents,
416
- maxConcurrentAgentsByState: workflow.maxConcurrentAgentsByState
417
- });
418
- const enrichedReport = {
419
- ...report,
420
- project: {
421
- id: projectConfig.projectId,
422
- slug: projectConfig.slug,
423
- tracker: projectConfig.tracker,
424
- lastTickAt: snapshot?.lastTickAt ?? null,
425
- health: snapshot?.health ?? null
426
- }
427
- };
428
- if (options.json) {
429
- process.stdout.write(JSON.stringify(enrichedReport, null, 2) + "\n");
430
- return;
431
- }
432
- process.stdout.write(renderProjectExplainReport(report, options.noColor));
433
- }
434
- var ProjectExplainWorkflowError = class extends Error {
435
- constructor(message) {
436
- super(message);
437
- this.name = "ProjectExplainWorkflowError";
438
- }
439
- };
440
- async function loadExplainWorkflow(input) {
441
- const workflowPaths = resolveExplainWorkflowCandidates(input);
442
- if (workflowPaths.length === 0) {
443
- throw new ProjectExplainWorkflowError(
444
- "No WORKFLOW.md path could be resolved from --workflow, the configured repository path, or previous run records."
445
- );
446
- }
447
- const failures = [];
448
- for (const workflowPath of workflowPaths) {
449
- try {
450
- const resolution = await new WorkflowConfigStore().load(workflowPath);
451
- return {
452
- lifecycle: resolution.lifecycle,
453
- maxConcurrentAgents: resolution.workflow.agent.maxConcurrentAgents,
454
- maxConcurrentAgentsByState: resolution.workflow.agent.maxConcurrentAgentsByState
455
- };
456
- } catch (error) {
457
- const message = error instanceof Error ? error.message : String(error);
458
- failures.push(`${workflowPath}: ${message}`);
459
- }
460
- }
461
- throw new ProjectExplainWorkflowError(
462
- `Unable to load WORKFLOW.md for project explain. Checked: ${failures.join("; ")}`
463
- );
464
- }
465
- function resolveExplainWorkflowCandidates(input) {
466
- const paths = [];
467
- if (input.explicitWorkflowPath) {
468
- paths.push(resolve(input.explicitWorkflowPath));
469
- }
470
- if (input.repository.path) {
471
- paths.push(join(resolve(input.repository.path), "WORKFLOW.md"));
472
- }
473
- const newestRuns = [...input.runs].sort(
474
- (left, right) => (Date.parse(right.updatedAt) || 0) - (Date.parse(left.updatedAt) || 0)
475
- );
476
- for (const run of newestRuns) {
477
- if (run.workflowPath) {
478
- paths.push(resolve(run.workflowPath));
479
- }
480
- if (run.workingDirectory) {
481
- paths.push(join(resolve(run.workingDirectory), "WORKFLOW.md"));
482
- }
483
- }
484
- return [...new Set(paths)];
485
- }
486
- function createProjectItemsCache() {
487
- const entries = /* @__PURE__ */ new Map();
488
- return {
489
- getOrLoad(key, load) {
490
- const cached = entries.get(key);
491
- if (cached) {
492
- return cached;
493
- }
494
- const pending = load().catch((error) => {
495
- entries.delete(key);
496
- throw error;
497
- });
498
- entries.set(key, pending);
499
- return pending;
500
- }
501
- };
502
- }
503
- async function readRuns(runtimeRoot, projectId) {
504
- let runIds;
505
- try {
506
- runIds = await readdir(join(runtimeRoot, "runs"));
507
- } catch {
508
- return [];
509
- }
510
- const runs = await Promise.all(
511
- runIds.map(
512
- (runId) => readJsonFile(
513
- join(runtimeRoot, "runs", runId, "run.json")
514
- )
515
- )
516
- );
517
- return runs.filter(
518
- (run) => run !== null && run.projectId === projectId
519
- );
520
- }
521
- async function readJsonFile(path) {
522
- try {
523
- return JSON.parse(await readFile(path, "utf8"));
524
- } catch {
525
- return null;
526
- }
527
- }
528
- function renderProjectExplainReport(report, noColor) {
529
- const apply = noColor ? (value) => stripAnsi(value) : (value) => value;
530
- const lines = [
531
- apply(bold(`Issue dispatch explanation: ${report.issue.identifier}`)),
532
- report.summary,
533
- "",
534
- `State: ${report.issue.state ?? "unknown"}`,
535
- `Repository: ${report.issue.repository}`,
536
- "",
537
- "Checks:"
538
- ];
539
- for (const check of report.checks) {
540
- const marker = check.status === "pass" ? green("\u2713") : check.status === "warn" ? yellow("!") : red("\u2717");
541
- lines.push(` ${apply(marker)} ${check.message}`);
542
- if (check.hint) {
543
- lines.push(` Hint: ${check.hint}`);
544
- }
545
- }
546
- lines.push("");
547
- lines.push("Related commands:");
548
- lines.push(" gh-symphony workflow preview");
549
- lines.push(" gh-symphony doctor");
550
- lines.push(" gh-symphony project status");
551
- lines.push(" gh-symphony logs --issue " + report.issue.identifier);
552
- return lines.join("\n") + "\n";
553
- }
554
- async function readHttpEndpoint(configDir, projectId) {
555
- try {
556
- const content = await readFile(
557
- httpStatusPath(configDir, projectId),
558
- "utf8"
559
- );
560
- const state = JSON.parse(content);
561
- return typeof state.endpoint === "string" && state.endpoint.length > 0 ? state.endpoint : null;
562
- } catch {
563
- return null;
564
- }
565
- }
566
- async function readProcessUptime(pid) {
567
- if (process.platform === "win32") {
568
- return "-";
569
- }
570
- try {
571
- const { stdout } = await execFile("ps", [
572
- "-o",
573
- "etime=",
574
- "-p",
575
- String(pid)
576
- ]);
577
- const seconds = parsePsElapsedTime(stdout);
578
- return seconds === null ? "-" : formatDuration(seconds);
579
- } catch {
580
- return "-";
581
- }
582
- }
583
- function defaultProjectName(config, projectId) {
584
- return config?.displayName ?? config?.slug ?? projectId;
585
- }
586
- function isCombiningCodePoint(codePoint) {
587
- return codePoint >= 768 && codePoint <= 879 || codePoint >= 6832 && codePoint <= 6911 || codePoint >= 7616 && codePoint <= 7679 || codePoint >= 8400 && codePoint <= 8447 || codePoint >= 65056 && codePoint <= 65071;
588
- }
589
- function isWideCodePoint(codePoint) {
590
- return codePoint >= 4352 && (codePoint <= 4447 || codePoint === 9001 || codePoint === 9002 || codePoint >= 11904 && codePoint <= 42191 && codePoint !== 12351 || codePoint >= 44032 && codePoint <= 55203 || codePoint >= 63744 && codePoint <= 64255 || codePoint >= 65040 && codePoint <= 65049 || codePoint >= 65072 && codePoint <= 65135 || codePoint >= 65280 && codePoint <= 65376 || codePoint >= 65504 && codePoint <= 65510 || codePoint >= 127744 && codePoint <= 128591 || codePoint >= 129280 && codePoint <= 129535 || codePoint >= 131072 && codePoint <= 262141);
591
- }
592
- function stringDisplayWidth(value) {
593
- const visible = stripAnsi(value);
594
- let width = 0;
595
- for (const char of visible) {
596
- const codePoint = char.codePointAt(0);
597
- if (codePoint === void 0) {
598
- continue;
599
- }
600
- if (codePoint === 0 || codePoint < 32 || codePoint >= 127 && codePoint < 160 || isCombiningCodePoint(codePoint)) {
601
- continue;
602
- }
603
- width += isWideCodePoint(codePoint) ? 2 : 1;
604
- }
605
- return width;
606
- }
607
- async function collectProjectListRows(configDir, global) {
608
- return Promise.all(
609
- global.projects.map(async (projectId) => {
610
- const config = await loadProjectConfig(configDir, projectId);
611
- const pid = await readPid(configDir, projectId);
612
- const running = pid !== null && isProcessRunning(pid);
613
- const snapshot = running ? await fetchProjectSnapshot(configDir, projectId) : null;
614
- return {
615
- id: projectId,
616
- name: defaultProjectName(config, projectId),
617
- status: running ? "running" : "stopped",
618
- health: snapshot?.health ?? "-",
619
- activeRuns: snapshot?.summary.activeRuns ?? null,
620
- lastTick: snapshot?.lastTickAt ? relativeTimeFromNow(snapshot.lastTickAt) : "-",
621
- uptime: pid !== null && running ? await readProcessUptime(pid) : "-",
622
- endpoint: running ? await readHttpEndpoint(configDir, projectId) : null,
623
- active: global.activeProject === projectId
624
- };
625
- })
626
- );
627
- }
628
- function renderTable(headers, rows) {
629
- const widths = headers.map(
630
- (header, index) => Math.max(
631
- stringDisplayWidth(header),
632
- ...rows.map((row) => stringDisplayWidth(row[index] ?? ""))
633
- )
634
- );
635
- const formatRow = (left, sep, right, values) => left + values.map((value, index) => {
636
- const width = widths[index];
637
- const displayWidth = stringDisplayWidth(value);
638
- return ` ${value}${" ".repeat(width - displayWidth)} `;
639
- }).join(sep) + right;
640
- const border = (left, middle, right) => left + widths.map((width) => "\u2500".repeat(width + 2)).join(middle) + right;
641
- return [
642
- border("\u250C", "\u252C", "\u2510"),
643
- formatRow("\u2502", "\u2502", "\u2502", headers),
644
- border("\u251C", "\u253C", "\u2524"),
645
- ...rows.map((row) => formatRow("\u2502", "\u2502", "\u2502", row)),
646
- border("\u2514", "\u2534", "\u2518")
647
- ].join("\n");
648
- }
649
- async function projectAdd(args, options) {
650
- const flags = parseProjectAddFlags(args);
651
- if (flags.nonInteractive) {
652
- await projectAddNonInteractive(flags, options);
653
- return;
654
- }
655
- await projectAddInteractive(flags, options);
656
- }
657
- async function projectAddNonInteractive(flags, options) {
658
- let token;
659
- try {
660
- token = getGhTokenWithSource().token;
661
- } catch {
662
- process.stderr.write(
663
- "Error: GitHub token not found. Run 'gh auth login --scopes repo,read:org,project' or set GITHUB_GRAPHQL_TOKEN.\n"
664
- );
665
- process.exitCode = 1;
666
- return;
667
- }
668
- const client = createClient(token);
669
- let viewer;
670
- try {
671
- viewer = await validateToken(client);
672
- } catch {
673
- process.stderr.write("Error: Invalid GitHub token.\n");
674
- process.exitCode = 1;
675
- return;
676
- }
677
- const scopeCheck = checkRequiredScopes(viewer.scopes);
678
- if (!scopeCheck.valid) {
679
- process.stderr.write(
680
- `Error: Missing required PAT scopes: ${scopeCheck.missing.join(", ")}
681
- `
682
- );
683
- process.exitCode = 1;
684
- return;
685
- }
686
- const projects = await listUserProjects(client);
687
- let project;
688
- if (flags.project) {
689
- const match = projects.find(
690
- (entry) => entry.id === flags.project || entry.url === flags.project
691
- );
692
- if (!match) {
693
- process.stderr.write(`Error: Project not found: ${flags.project}
694
- `);
695
- process.exitCode = 1;
696
- return;
697
- }
698
- project = await getProjectDetail(client, match.id);
699
- } else if (projects.length === 1) {
700
- project = await getProjectDetail(client, projects[0].id);
701
- } else {
702
- process.stderr.write(
703
- "Error: --project is required when multiple projects exist.\n"
704
- );
705
- process.exitCode = 1;
706
- return;
707
- }
708
- const projectId = generateProjectId(project.title, project.id);
709
- const workspaceDir = flags.workspaceDir ?? join(options.configDir, "workspaces");
710
- await writeConfig(options.configDir, {
711
- projectId,
712
- project,
713
- repos: project.linkedRepositories,
714
- workspaceDir,
715
- assignedOnly: flags.assignedOnly
716
- });
717
- if (options.json) {
718
- process.stdout.write(
719
- JSON.stringify({ projectId, status: "created" }) + "\n"
720
- );
721
- } else {
722
- process.stdout.write(
723
- projectCreatedMessage(projectId, project.linkedRepositories.length) + "\n"
724
- );
725
- }
726
- }
727
- async function projectAddInteractive(flags, options) {
728
- p.intro("gh-symphony - Project Setup");
729
- const defaultWorkspaceDir = join(options.configDir, "workspaces");
730
- const existingConfig = await loadGlobalConfig(options.configDir);
731
- if (existingConfig) {
732
- const action = await abortIfCancelled(
733
- p.select({
734
- message: "Existing configuration detected. What would you like to do?",
735
- options: [
736
- { value: "add", label: "Add a new project" },
737
- { value: "overwrite", label: "Start fresh (overwrite)" }
738
- ]
739
- })
740
- );
741
- if (action === "overwrite") {
742
- }
743
- }
744
- const s1 = p.spinner();
745
- s1.start("Checking GitHub authentication...");
746
- let login;
747
- let client;
748
- try {
749
- const auth = await resolveGitHubAuth();
750
- const sourceLabel = auth.source === "env" ? "GITHUB_GRAPHQL_TOKEN" : "gh CLI";
751
- login = auth.login;
752
- client = createClient(auth.token);
753
- s1.stop(`Authenticated via ${sourceLabel} as ${login}`);
754
- } catch (error) {
755
- s1.stop("Authentication failed.");
756
- if (error instanceof GhAuthError) {
757
- p.log.error(error.message);
758
- } else {
759
- p.log.error(error instanceof Error ? error.message : "Unknown error");
760
- }
761
- process.exitCode = 1;
762
- return;
763
- }
764
- const s2 = p.spinner();
765
- s2.start("Loading GitHub Project boards...");
766
- let projects;
767
- try {
768
- const discovery = await discoverUserProjects(client);
769
- projects = discovery.projects;
770
- s2.stop(
771
- `Found ${projects.length} project${projects.length === 1 ? "" : "s"}`
772
- );
773
- warnIfProjectDiscoveryPartial(discovery);
774
- } catch (error) {
775
- s2.stop("Failed to load projects.");
776
- if (error instanceof GitHubScopeError) {
777
- displayScopeError(error, "gh-symphony project add");
778
- } else {
779
- p.log.error(error instanceof Error ? error.message : "Unknown error");
780
- }
781
- process.exitCode = 1;
782
- return;
783
- }
784
- if (projects.length === 0) {
785
- p.log.error(
786
- "No GitHub Projects found. Create a project at https://github.com/orgs/YOUR_ORG/projects and re-run."
787
- );
788
- process.exitCode = 1;
789
- return;
790
- }
791
- const selectedProjectId = await abortIfCancelled(
792
- p.select({
793
- message: "Step 1/2 - Select a GitHub Project board:",
794
- options: projects.map((project) => ({
795
- value: project.id,
796
- label: `${project.owner.login}/${project.title}`,
797
- hint: `${project.openItemCount} items`
798
- })),
799
- maxItems: 15
800
- })
801
- );
802
- const s2d = p.spinner();
803
- s2d.start("Loading project details...");
804
- let projectDetail;
805
- try {
806
- projectDetail = await getProjectDetail(client, selectedProjectId);
807
- s2d.stop(`Loaded: ${projectDetail.title}`);
808
- } catch (error) {
809
- s2d.stop("Failed to load project details.");
810
- p.log.error(error instanceof Error ? error.message : "Unknown error");
811
- process.exitCode = 1;
812
- return;
813
- }
814
- if (projectDetail.linkedRepositories.length === 0) {
815
- p.log.warn(
816
- "No linked repositories found in this project. Add issues from repositories to the project, or run 'gh-symphony repo add owner/name' to validate and save a repository before your first orchestration run."
817
- );
818
- }
819
- const {
820
- assignedOnly: promptAssignedOnly,
821
- selectedRepos,
822
- workspaceDir
823
- } = await promptProjectRegistrationOptions({
824
- projectDetail,
825
- defaultWorkspaceDir,
826
- assignedOnlyMessage: "Step 2/2 - Only process issues assigned to the authenticated GitHub user?",
827
- assignedOnlyInitialValue: flags.assignedOnly
828
- });
829
- const assignedOnly = flags.assignedOnly || promptAssignedOnly;
830
- const repoSummary = formatProjectRepoSummary(
831
- selectedRepos,
832
- projectDetail.linkedRepositories.length
833
- );
834
- p.note(
835
- renderProjectRegistrationSummary({
836
- login,
837
- projectTitle: projectDetail.title,
838
- repoSummary,
839
- assignedOnly,
840
- workspaceDir
841
- }),
842
- "Configuration Summary"
843
- );
844
- const confirmed = await abortIfCancelled(
845
- p.confirm({ message: "Apply this configuration?" })
846
- );
847
- if (!confirmed) {
848
- p.cancel("Setup cancelled.");
849
- process.exitCode = 130;
850
- return;
851
- }
852
- const projectId = generateProjectId(projectDetail.title, projectDetail.id);
853
- const s6 = p.spinner();
854
- s6.start("Writing configuration...");
855
- try {
856
- await writeConfig(options.configDir, {
857
- projectId,
858
- project: projectDetail,
859
- repos: selectedRepos,
860
- workspaceDir,
861
- assignedOnly
862
- });
863
- s6.stop("Configuration saved.");
864
- } catch (error) {
865
- s6.stop("Failed to write configuration.");
866
- p.log.error(error instanceof Error ? error.message : "Unknown error");
867
- process.exitCode = 1;
868
- return;
869
- }
870
- p.outro(projectCreatedMessage(projectId, selectedRepos.length));
871
- }
872
- async function promptProjectRegistrationOptions(input) {
873
- const assignedOnly = await abortIfCancelled(
874
- p.confirm({
875
- message: input.assignedOnlyMessage ?? "Only process issues assigned to the authenticated GitHub user?",
876
- initialValue: input.assignedOnlyInitialValue ?? false
877
- })
878
- );
879
- const customizeAdvancedOptions = await abortIfCancelled(
880
- p.confirm({
881
- message: "Customize advanced options? (default: No)",
882
- initialValue: false
883
- })
884
- );
885
- let selectedRepos = input.projectDetail.linkedRepositories;
886
- let workspaceDir = input.defaultWorkspaceDir;
887
- if (customizeAdvancedOptions) {
888
- if (input.projectDetail.linkedRepositories.length > 0) {
889
- const filterRepositories = await abortIfCancelled(
890
- p.confirm({
891
- message: "Filter specific repositories? (default: No)",
892
- initialValue: false
893
- })
894
- );
895
- if (filterRepositories) {
896
- selectedRepos = await abortIfCancelled(
897
- p.multiselect({
898
- message: "Select repositories to orchestrate:",
899
- options: input.projectDetail.linkedRepositories.map((repo) => ({
900
- value: repo,
901
- label: `${repo.owner}/${repo.name}`
902
- })),
903
- required: true
904
- })
905
- );
906
- }
907
- }
908
- workspaceDir = await abortIfCancelled(
909
- p.text({
910
- message: "Workspace root directory:",
911
- placeholder: input.defaultWorkspaceDir,
912
- defaultValue: input.defaultWorkspaceDir,
913
- validate(value) {
914
- return value.trim().length > 0 ? void 0 : "Workspace directory is required.";
915
- }
916
- })
917
- );
918
- }
919
- return {
920
- assignedOnly,
921
- selectedRepos,
922
- workspaceDir
923
- };
924
- }
925
- function renderProjectRegistrationSummary(input) {
926
- return [
927
- `User: ${input.login}`,
928
- `Project: ${input.projectTitle}`,
929
- `Repos: ${input.repoSummary}`,
930
- `Assigned: ${input.assignedOnly ? `Only issues assigned to ${input.login}` : "All project issues"}`,
931
- `Workspace: ${input.workspaceDir}`
932
- ].join("\n");
933
- }
934
- async function projectList(options) {
935
- const global = await loadGlobalConfig(options.configDir);
936
- if (!global?.projects?.length) {
937
- process.stdout.write("No projects configured.\n");
938
- return;
939
- }
940
- const rows = await collectProjectListRows(options.configDir, global);
941
- if (options.json) {
942
- process.stdout.write(JSON.stringify(rows, null, 2) + "\n");
943
- return;
944
- }
945
- const showEndpointColumn = rows.some((row) => row.endpoint !== null);
946
- const headers = [
947
- "ID",
948
- "Name",
949
- "Status",
950
- "Health",
951
- "Active Runs",
952
- "Last Tick",
953
- "Uptime",
954
- ...showEndpointColumn ? ["Endpoint"] : []
955
- ];
956
- const table = renderTable(
957
- headers,
958
- rows.map((row) => [
959
- row.id,
960
- row.name,
961
- row.status,
962
- row.health,
963
- row.activeRuns === null ? "-" : String(row.activeRuns),
964
- row.lastTick,
965
- row.uptime,
966
- ...showEndpointColumn ? [row.endpoint ?? "-"] : []
967
- ])
968
- );
969
- process.stdout.write(`${table}
970
- `);
971
- }
972
- async function projectRemove(args, options) {
973
- const projectId = args[0];
974
- if (!projectId) {
975
- process.stderr.write("Usage: gh-symphony project remove <project-id>\n");
976
- process.exitCode = 1;
977
- return;
978
- }
979
- const global = await loadGlobalConfig(options.configDir);
980
- if (!global) {
981
- process.stderr.write("No configuration found.\n");
982
- process.exitCode = 1;
983
- return;
984
- }
985
- const updatedProjects = global.projects.filter(
986
- (entry) => entry !== projectId
987
- );
988
- if (updatedProjects.length === global.projects.length) {
989
- process.stderr.write(`Project "${projectId}" not found.
990
- `);
991
- process.exitCode = 1;
992
- return;
993
- }
994
- const updatedConfig = {
995
- ...global,
996
- projects: updatedProjects,
997
- activeProject: global.activeProject === projectId ? null : global.activeProject
998
- };
999
- await saveGlobalConfig(options.configDir, updatedConfig);
1000
- const { rm } = await import("fs/promises");
1001
- try {
1002
- await rm(projectConfigDir(options.configDir, projectId), {
1003
- recursive: true,
1004
- force: true
1005
- });
1006
- } catch {
1007
- }
1008
- process.stdout.write(`Project "${projectId}" removed.
1009
- `);
1010
- }
1011
- async function projectSwitch(options) {
1012
- const global = await loadGlobalConfig(options.configDir);
1013
- if (!global || global.projects.length === 0) {
1014
- process.stderr.write(
1015
- "No projects configured. Run 'gh-symphony workflow init'.\n"
1016
- );
1017
- process.exitCode = 1;
1018
- return;
1019
- }
1020
- if (global.projects.length === 1) {
1021
- process.stdout.write(`Only one project exists: ${global.projects[0]}
1022
- `);
1023
- return;
1024
- }
1025
- const selected = await p.select({
1026
- message: "Select project to activate:",
1027
- options: global.projects.map((projectId) => ({
1028
- value: projectId,
1029
- label: projectId,
1030
- hint: projectId === global.activeProject ? "current" : void 0
1031
- }))
1032
- });
1033
- if (p.isCancel(selected)) {
1034
- p.cancel("Cancelled.");
1035
- return;
1036
- }
1037
- global.activeProject = selected;
1038
- await saveGlobalConfig(options.configDir, global);
1039
- process.stdout.write(`Switched to project: ${selected}
1040
- `);
1041
- }
1042
-
1043
- export {
1044
- project_default,
1045
- promptProjectRegistrationOptions,
1046
- renderProjectRegistrationSummary
1047
- };