@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.
- package/README.md +66 -2
- package/dist/chunk-2TSM3INR.js +1085 -0
- package/dist/chunk-2UW7NQLX.js +684 -0
- package/dist/{chunk-MVRF7BES.js → chunk-36KYEDEO.js} +10 -1
- package/dist/{chunk-TILHWBP6.js → chunk-C67H3OUL.js} +239 -36
- package/dist/{chunk-C7G7RJ4G.js → chunk-DDL4BWSL.js} +1 -1
- package/dist/{chunk-XN5ABWZ6.js → chunk-DFLXHNYQ.js} +26 -30
- package/dist/{chunk-EKKT5USP.js → chunk-E7HYEEZD.js} +487 -133
- package/dist/chunk-EEQQWTXS.js +3257 -0
- package/dist/chunk-GDE6FYN4.js +26 -0
- package/dist/{chunk-Y6TYJMNT.js → chunk-GSX2FV3M.js} +10 -16
- package/dist/{chunk-RN2PACNV.js → chunk-HMLBBZNY.js} +731 -75
- package/dist/{chunk-5NV3LSAJ.js → chunk-IWFX2FMA.js} +5 -1
- package/dist/{chunk-HZVDTAPS.js → chunk-PUDXVBSN.js} +1549 -1458
- package/dist/{chunk-ROGRTUFI.js → chunk-QIRE2VXS.js} +14 -3
- package/dist/{chunk-3AWF54PI.js → chunk-ZHOKYUO3.js} +394 -42
- package/dist/{config-cmd-DNXNL26Z.js → config-cmd-Z3A7V6NC.js} +1 -1
- package/dist/{doctor-IYHCFXOZ.js → doctor-EJUMPBMW.js} +105 -40
- package/dist/index.js +112 -24
- package/dist/{init-KZT6YNOH.js → init-54HMKNYI.js} +8 -3
- package/dist/{logs-6JKKYDGJ.js → logs-GTZ4U5JE.js} +2 -2
- package/dist/project-RMYMZSFV.js +25 -0
- package/dist/{recover-5KQI7WH5.js → recover-LTLKMTRX.js} +7 -5
- package/dist/repo-WI7GF6XQ.js +749 -0
- package/dist/{run-ETC5UTRA.js → run-IHN3ZL35.js} +21 -7
- package/dist/{setup-VWB7RZUQ.js → setup-TZJSM3QV.js} +53 -14
- package/dist/start-RTAHQMR2.js +19 -0
- package/dist/status-F4D52OVK.js +12 -0
- package/dist/stop-MDKMJPVR.js +10 -0
- package/dist/{upgrade-3YNF3VKY.js → upgrade-O33S2SJK.js} +2 -2
- package/dist/{version-NUBTTOG7.js → version-CW54Q7BK.js} +1 -1
- package/dist/worker-entry.js +848 -693
- package/dist/{workflow-TBIFY5MO.js → workflow-L3KT6HB7.js} +177 -11
- package/package.json +4 -2
- package/dist/chunk-M3IFVLQS.js +0 -1155
- package/dist/project-UUVHS3ZR.js +0 -22
- package/dist/repo-HDDE7OUI.js +0 -321
- package/dist/start-ENFLZUI6.js +0 -16
- package/dist/status-QSCFVGRQ.js +0 -11
- package/dist/stop-7MFCBQVW.js +0 -9
|
@@ -54,9 +54,15 @@ async function saveGlobalConfig(configDir, config) {
|
|
|
54
54
|
await writeJsonFile(configFilePath(configDir), config);
|
|
55
55
|
}
|
|
56
56
|
async function loadProjectConfig(configDir, projectId) {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
57
|
+
const config = await readJsonFile(projectConfigPath(configDir, projectId));
|
|
58
|
+
if (!config) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
const repository = config.repository ?? firstConfiguredRepository(config);
|
|
62
|
+
return {
|
|
63
|
+
...config,
|
|
64
|
+
...repository ? { repository } : {}
|
|
65
|
+
};
|
|
60
66
|
}
|
|
61
67
|
async function saveProjectConfig(configDir, projectId, config) {
|
|
62
68
|
await writeJsonFile(projectConfigPath(configDir, projectId), config);
|
|
@@ -91,6 +97,11 @@ function isFileMissing(error) {
|
|
|
91
97
|
error && typeof error === "object" && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR")
|
|
92
98
|
);
|
|
93
99
|
}
|
|
100
|
+
function firstConfiguredRepository(config) {
|
|
101
|
+
return config.repositories?.find(
|
|
102
|
+
(repository) => typeof repository.owner === "string" && repository.owner.length > 0 && typeof repository.name === "string" && repository.name.length > 0
|
|
103
|
+
);
|
|
104
|
+
}
|
|
94
105
|
|
|
95
106
|
export {
|
|
96
107
|
resolveConfigDir,
|
|
@@ -2,34 +2,57 @@
|
|
|
2
2
|
import {
|
|
3
3
|
abortIfCancelled,
|
|
4
4
|
generateProjectId,
|
|
5
|
+
warnIfProjectDiscoveryPartial,
|
|
5
6
|
writeConfig
|
|
6
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-HMLBBZNY.js";
|
|
7
8
|
import {
|
|
8
9
|
start_default
|
|
9
|
-
} from "./chunk-
|
|
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";
|
|
10
20
|
import {
|
|
11
21
|
GhAuthError,
|
|
12
22
|
GitHubScopeError,
|
|
13
23
|
checkRequiredScopes,
|
|
14
24
|
createClient,
|
|
25
|
+
discoverUserProjects,
|
|
26
|
+
getGhToken,
|
|
15
27
|
getGhTokenWithSource,
|
|
16
28
|
getProjectDetail,
|
|
17
29
|
listUserProjects,
|
|
18
30
|
resolveGitHubAuth,
|
|
19
31
|
validateToken
|
|
20
|
-
} from "./chunk-
|
|
32
|
+
} from "./chunk-C67H3OUL.js";
|
|
33
|
+
import {
|
|
34
|
+
WorkflowConfigStore
|
|
35
|
+
} from "./chunk-EEQQWTXS.js";
|
|
21
36
|
import {
|
|
22
37
|
status_default
|
|
23
|
-
} from "./chunk-
|
|
38
|
+
} from "./chunk-DFLXHNYQ.js";
|
|
24
39
|
import {
|
|
25
|
-
|
|
26
|
-
|
|
40
|
+
bold,
|
|
41
|
+
green,
|
|
42
|
+
red,
|
|
43
|
+
stripAnsi,
|
|
44
|
+
yellow
|
|
45
|
+
} from "./chunk-36KYEDEO.js";
|
|
27
46
|
import {
|
|
28
47
|
resolveRuntimeRoot
|
|
29
|
-
} from "./chunk-
|
|
48
|
+
} from "./chunk-IWFX2FMA.js";
|
|
30
49
|
import {
|
|
31
50
|
stop_default
|
|
32
|
-
} from "./chunk-
|
|
51
|
+
} from "./chunk-GSX2FV3M.js";
|
|
52
|
+
import {
|
|
53
|
+
handleMissingManagedProjectConfig,
|
|
54
|
+
resolveManagedProjectConfig
|
|
55
|
+
} from "./chunk-DDL4BWSL.js";
|
|
33
56
|
import {
|
|
34
57
|
daemonPidPath,
|
|
35
58
|
httpStatusPath,
|
|
@@ -37,16 +60,42 @@ import {
|
|
|
37
60
|
loadProjectConfig,
|
|
38
61
|
projectConfigDir,
|
|
39
62
|
saveGlobalConfig
|
|
40
|
-
} from "./chunk-
|
|
63
|
+
} from "./chunk-QIRE2VXS.js";
|
|
41
64
|
|
|
42
65
|
// src/commands/project.ts
|
|
43
66
|
import * as p from "@clack/prompts";
|
|
44
67
|
import { execFile as execFileCallback } from "child_process";
|
|
45
68
|
import { promisify } from "util";
|
|
46
|
-
import { readFile } from "fs/promises";
|
|
69
|
+
import { readdir, readFile } from "fs/promises";
|
|
47
70
|
import { join } from "path";
|
|
71
|
+
import { resolve } from "path";
|
|
48
72
|
var execFile = promisify(execFileCallback);
|
|
49
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
|
+
}
|
|
50
99
|
function displayScopeError(error, retryCommand) {
|
|
51
100
|
const plural = error.requiredScopes.length === 1 ? "" : "s";
|
|
52
101
|
p.log.error(
|
|
@@ -86,6 +135,47 @@ function parseProjectAddFlags(args) {
|
|
|
86
135
|
}
|
|
87
136
|
return flags;
|
|
88
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
|
+
}
|
|
89
179
|
var handler = async (args, options) => {
|
|
90
180
|
const [subcommand, ...rest] = args;
|
|
91
181
|
switch (subcommand) {
|
|
@@ -110,9 +200,12 @@ var handler = async (args, options) => {
|
|
|
110
200
|
case "status":
|
|
111
201
|
await status_default(rest, options);
|
|
112
202
|
return;
|
|
203
|
+
case "explain":
|
|
204
|
+
await projectExplain(rest, options);
|
|
205
|
+
return;
|
|
113
206
|
default:
|
|
114
207
|
process.stdout.write(
|
|
115
|
-
"Usage: gh-symphony project <add|list|remove|start|stop|switch|status>\n"
|
|
208
|
+
"Usage: gh-symphony project <add|list|remove|start|stop|switch|status|explain>\n"
|
|
116
209
|
);
|
|
117
210
|
}
|
|
118
211
|
};
|
|
@@ -210,9 +303,260 @@ async function readPersistedSnapshot(configDir, projectId) {
|
|
|
210
303
|
async function fetchProjectSnapshot(configDir, projectId) {
|
|
211
304
|
return readPersistedSnapshot(configDir, projectId);
|
|
212
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
|
+
}
|
|
213
554
|
async function readHttpEndpoint(configDir, projectId) {
|
|
214
555
|
try {
|
|
215
|
-
const content = await readFile(
|
|
556
|
+
const content = await readFile(
|
|
557
|
+
httpStatusPath(configDir, projectId),
|
|
558
|
+
"utf8"
|
|
559
|
+
);
|
|
216
560
|
const state = JSON.parse(content);
|
|
217
561
|
return typeof state.endpoint === "string" && state.endpoint.length > 0 ? state.endpoint : null;
|
|
218
562
|
} catch {
|
|
@@ -224,7 +568,12 @@ async function readProcessUptime(pid) {
|
|
|
224
568
|
return "-";
|
|
225
569
|
}
|
|
226
570
|
try {
|
|
227
|
-
const { stdout } = await execFile("ps", [
|
|
571
|
+
const { stdout } = await execFile("ps", [
|
|
572
|
+
"-o",
|
|
573
|
+
"etime=",
|
|
574
|
+
"-p",
|
|
575
|
+
String(pid)
|
|
576
|
+
]);
|
|
228
577
|
const seconds = parsePsElapsedTime(stdout);
|
|
229
578
|
return seconds === null ? "-" : formatDuration(seconds);
|
|
230
579
|
} catch {
|
|
@@ -370,10 +719,9 @@ async function projectAddNonInteractive(flags, options) {
|
|
|
370
719
|
JSON.stringify({ projectId, status: "created" }) + "\n"
|
|
371
720
|
);
|
|
372
721
|
} else {
|
|
373
|
-
process.stdout.write(
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
`);
|
|
722
|
+
process.stdout.write(
|
|
723
|
+
projectCreatedMessage(projectId, project.linkedRepositories.length) + "\n"
|
|
724
|
+
);
|
|
377
725
|
}
|
|
378
726
|
}
|
|
379
727
|
async function projectAddInteractive(flags, options) {
|
|
@@ -417,10 +765,12 @@ async function projectAddInteractive(flags, options) {
|
|
|
417
765
|
s2.start("Loading GitHub Project boards...");
|
|
418
766
|
let projects;
|
|
419
767
|
try {
|
|
420
|
-
|
|
768
|
+
const discovery = await discoverUserProjects(client);
|
|
769
|
+
projects = discovery.projects;
|
|
421
770
|
s2.stop(
|
|
422
771
|
`Found ${projects.length} project${projects.length === 1 ? "" : "s"}`
|
|
423
772
|
);
|
|
773
|
+
warnIfProjectDiscoveryPartial(discovery);
|
|
424
774
|
} catch (error) {
|
|
425
775
|
s2.stop("Failed to load projects.");
|
|
426
776
|
if (error instanceof GitHubScopeError) {
|
|
@@ -463,10 +813,8 @@ async function projectAddInteractive(flags, options) {
|
|
|
463
813
|
}
|
|
464
814
|
if (projectDetail.linkedRepositories.length === 0) {
|
|
465
815
|
p.log.warn(
|
|
466
|
-
"No linked repositories found in this project. Add issues from repositories to the project first."
|
|
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."
|
|
467
817
|
);
|
|
468
|
-
process.exitCode = 1;
|
|
469
|
-
return;
|
|
470
818
|
}
|
|
471
819
|
const {
|
|
472
820
|
assignedOnly: promptAssignedOnly,
|
|
@@ -479,7 +827,10 @@ async function projectAddInteractive(flags, options) {
|
|
|
479
827
|
assignedOnlyInitialValue: flags.assignedOnly
|
|
480
828
|
});
|
|
481
829
|
const assignedOnly = flags.assignedOnly || promptAssignedOnly;
|
|
482
|
-
const repoSummary =
|
|
830
|
+
const repoSummary = formatProjectRepoSummary(
|
|
831
|
+
selectedRepos,
|
|
832
|
+
projectDetail.linkedRepositories.length
|
|
833
|
+
);
|
|
483
834
|
p.note(
|
|
484
835
|
renderProjectRegistrationSummary({
|
|
485
836
|
login,
|
|
@@ -516,10 +867,7 @@ async function projectAddInteractive(flags, options) {
|
|
|
516
867
|
process.exitCode = 1;
|
|
517
868
|
return;
|
|
518
869
|
}
|
|
519
|
-
p.outro(
|
|
520
|
-
`Project "${projectId}" created.
|
|
521
|
-
Run 'gh-symphony start' to begin orchestration.`
|
|
522
|
-
);
|
|
870
|
+
p.outro(projectCreatedMessage(projectId, selectedRepos.length));
|
|
523
871
|
}
|
|
524
872
|
async function promptProjectRegistrationOptions(input) {
|
|
525
873
|
const assignedOnly = await abortIfCancelled(
|
|
@@ -537,23 +885,25 @@ async function promptProjectRegistrationOptions(input) {
|
|
|
537
885
|
let selectedRepos = input.projectDetail.linkedRepositories;
|
|
538
886
|
let workspaceDir = input.defaultWorkspaceDir;
|
|
539
887
|
if (customizeAdvancedOptions) {
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
);
|
|
546
|
-
if (filterRepositories) {
|
|
547
|
-
selectedRepos = await abortIfCancelled(
|
|
548
|
-
p.multiselect({
|
|
549
|
-
message: "Select repositories to orchestrate:",
|
|
550
|
-
options: input.projectDetail.linkedRepositories.map((repo) => ({
|
|
551
|
-
value: repo,
|
|
552
|
-
label: `${repo.owner}/${repo.name}`
|
|
553
|
-
})),
|
|
554
|
-
required: true
|
|
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
|
|
555
893
|
})
|
|
556
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
|
+
}
|
|
557
907
|
}
|
|
558
908
|
workspaceDir = await abortIfCancelled(
|
|
559
909
|
p.text({
|
|
@@ -632,7 +982,9 @@ async function projectRemove(args, options) {
|
|
|
632
982
|
process.exitCode = 1;
|
|
633
983
|
return;
|
|
634
984
|
}
|
|
635
|
-
const updatedProjects = global.projects.filter(
|
|
985
|
+
const updatedProjects = global.projects.filter(
|
|
986
|
+
(entry) => entry !== projectId
|
|
987
|
+
);
|
|
636
988
|
if (updatedProjects.length === global.projects.length) {
|
|
637
989
|
process.stderr.write(`Project "${projectId}" not found.
|
|
638
990
|
`);
|