@gh-symphony/cli 0.0.22 → 0.1.2
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 +72 -77
- package/dist/{chunk-IWFX2FMA.js → chunk-6I753NYO.js} +4 -1
- package/dist/{workflow-L3KT6HB7.js → chunk-B4ZJMAZL.js} +27 -19
- package/dist/{chunk-2TSM3INR.js → chunk-DLZAJXZL.js} +575 -12
- package/dist/chunk-GHVDABFO.js +235 -0
- package/dist/{chunk-EEQQWTXS.js → chunk-GPRCOJDJ.js} +158 -75
- package/dist/{chunk-36KYEDEO.js → chunk-MVRF7BES.js} +1 -10
- package/dist/{chunk-2UW7NQLX.js → chunk-VFHMHHZW.js} +1 -1
- package/dist/{chunk-HMLBBZNY.js → chunk-WM2B6BJ7.js} +16 -71
- package/dist/{chunk-QIRE2VXS.js → chunk-WOVNN5NW.js} +16 -17
- package/dist/{chunk-C67H3OUL.js → chunk-Z3NZOPLZ.js} +0 -81
- package/dist/{config-cmd-Z3A7V6NC.js → config-cmd-2ADPUYWA.js} +1 -1
- package/dist/{doctor-EJUMPBMW.js → doctor-EEPNFCGF.js} +464 -40
- package/dist/index.js +340 -294
- package/dist/{chunk-PUDXVBSN.js → repo-RX4OK7XH.js} +5944 -3001
- package/dist/{setup-TZJSM3QV.js → setup-XNHHRBGU.js} +57 -92
- package/dist/{upgrade-O33S2SJK.js → upgrade-NS53EO2B.js} +2 -2
- package/dist/{version-CW54Q7BK.js → version-2RHFZ5CI.js} +1 -1
- package/dist/worker-entry.js +10 -5
- package/dist/workflow-26QNZZWH.js +22 -0
- package/package.json +4 -4
- package/dist/chunk-DDL4BWSL.js +0 -146
- package/dist/chunk-DFLXHNYQ.js +0 -482
- package/dist/chunk-E7HYEEZD.js +0 -1318
- package/dist/chunk-GDE6FYN4.js +0 -26
- package/dist/chunk-GSX2FV3M.js +0 -103
- package/dist/chunk-ZHOKYUO3.js +0 -1047
- package/dist/init-54HMKNYI.js +0 -38
- package/dist/logs-GTZ4U5JE.js +0 -188
- package/dist/project-RMYMZSFV.js +0 -25
- package/dist/recover-LTLKMTRX.js +0 -133
- package/dist/repo-WI7GF6XQ.js +0 -749
- package/dist/run-IHN3ZL35.js +0 -122
- package/dist/start-RTAHQMR2.js +0 -19
- package/dist/status-F4D52OVK.js +0 -12
- package/dist/stop-MDKMJPVR.js +0 -10
|
@@ -1,4 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
parseIssueReference,
|
|
4
|
+
readGitHubProjectBinding,
|
|
5
|
+
renderIssueWorkflowPreview
|
|
6
|
+
} from "./chunk-B4ZJMAZL.js";
|
|
7
|
+
import "./chunk-WM2B6BJ7.js";
|
|
8
|
+
import {
|
|
9
|
+
fetchGithubProjectIssueByRepositoryAndNumber,
|
|
10
|
+
fetchGithubProjectIssues,
|
|
11
|
+
inspectManagedProjectSelection
|
|
12
|
+
} from "./chunk-DLZAJXZL.js";
|
|
13
|
+
import {
|
|
14
|
+
resolveRuntimeRoot
|
|
15
|
+
} from "./chunk-6I753NYO.js";
|
|
2
16
|
import {
|
|
3
17
|
GitHubApiError,
|
|
4
18
|
REQUIRED_GH_SCOPES,
|
|
@@ -6,27 +20,22 @@ import {
|
|
|
6
20
|
checkGhInstalled,
|
|
7
21
|
checkGhScopes,
|
|
8
22
|
createClient,
|
|
23
|
+
findLinkedRepository,
|
|
9
24
|
getEnvGitHubToken,
|
|
10
25
|
getGhToken,
|
|
11
26
|
getProjectDetail,
|
|
12
27
|
runGhAuthLogin,
|
|
13
28
|
runGhAuthRefresh,
|
|
14
29
|
validateGitHubToken
|
|
15
|
-
} from "./chunk-
|
|
30
|
+
} from "./chunk-Z3NZOPLZ.js";
|
|
16
31
|
import {
|
|
17
32
|
isClaudeRuntimeCommand,
|
|
18
33
|
parseWorkflowMarkdown,
|
|
19
34
|
resolveClaudeCommandBinary,
|
|
20
35
|
resolveRuntimeCommandBinary,
|
|
21
36
|
runClaudePreflight
|
|
22
|
-
} from "./chunk-
|
|
23
|
-
import
|
|
24
|
-
resolveRuntimeRoot
|
|
25
|
-
} from "./chunk-IWFX2FMA.js";
|
|
26
|
-
import {
|
|
27
|
-
inspectManagedProjectSelection
|
|
28
|
-
} from "./chunk-DDL4BWSL.js";
|
|
29
|
-
import "./chunk-QIRE2VXS.js";
|
|
37
|
+
} from "./chunk-GPRCOJDJ.js";
|
|
38
|
+
import "./chunk-WOVNN5NW.js";
|
|
30
39
|
|
|
31
40
|
// src/commands/doctor.ts
|
|
32
41
|
import { constants } from "fs";
|
|
@@ -43,6 +52,8 @@ var DEFAULT_DEPENDENCIES = {
|
|
|
43
52
|
inspectManagedProjectSelection,
|
|
44
53
|
createClient,
|
|
45
54
|
getProjectDetail,
|
|
55
|
+
fetchProjectIssues: fetchGithubProjectIssues,
|
|
56
|
+
fetchProjectIssue: fetchGithubProjectIssueByRepositoryAndNumber,
|
|
46
57
|
readFile,
|
|
47
58
|
access,
|
|
48
59
|
mkdir,
|
|
@@ -64,8 +75,9 @@ var DEFAULT_DEPENDENCIES = {
|
|
|
64
75
|
};
|
|
65
76
|
var MINIMUM_NODE_MAJOR = 24;
|
|
66
77
|
var MINIMUM_NODE_VERSION = `v${MINIMUM_NODE_MAJOR}.0.0`;
|
|
78
|
+
var DOCTOR_USAGE = "Usage: gh-symphony doctor [--project-id <project-id>] [--fix] [--smoke] [--issue <owner/repo#number>]";
|
|
67
79
|
function parseDoctorArgs(args) {
|
|
68
|
-
const parsed = { fix: false };
|
|
80
|
+
const parsed = { fix: false, smoke: false };
|
|
69
81
|
for (let i = 0; i < args.length; i += 1) {
|
|
70
82
|
const arg = args[i];
|
|
71
83
|
if (arg === "--project" || arg === "--project-id") {
|
|
@@ -82,11 +94,28 @@ function parseDoctorArgs(args) {
|
|
|
82
94
|
parsed.fix = true;
|
|
83
95
|
continue;
|
|
84
96
|
}
|
|
97
|
+
if (arg === "--smoke") {
|
|
98
|
+
parsed.smoke = true;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (arg === "--issue") {
|
|
102
|
+
const value = args[i + 1];
|
|
103
|
+
if (!value || value.startsWith("-")) {
|
|
104
|
+
parsed.error = "Option '--issue' argument missing";
|
|
105
|
+
return parsed;
|
|
106
|
+
}
|
|
107
|
+
parsed.issue = value;
|
|
108
|
+
i += 1;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
85
111
|
if (arg?.startsWith("-")) {
|
|
86
112
|
parsed.error = `Unknown option '${arg}'`;
|
|
87
113
|
return parsed;
|
|
88
114
|
}
|
|
89
115
|
}
|
|
116
|
+
if (parsed.issue && !parsed.smoke) {
|
|
117
|
+
parsed.error = "Option '--issue' requires '--smoke'";
|
|
118
|
+
}
|
|
90
119
|
return parsed;
|
|
91
120
|
}
|
|
92
121
|
function passCheck(id, title, summary, details) {
|
|
@@ -310,7 +339,7 @@ async function checkWorkflow(repoRoot, deps) {
|
|
|
310
339
|
reason: "missing",
|
|
311
340
|
workflowPath,
|
|
312
341
|
summary: "WORKFLOW.md was not found in the repository root.",
|
|
313
|
-
remediation: "Run 'gh-symphony init' in this repository or add a valid WORKFLOW.md at the repo root."
|
|
342
|
+
remediation: "Run 'gh-symphony workflow init' in this repository or add a valid WORKFLOW.md at the repo root."
|
|
314
343
|
};
|
|
315
344
|
}
|
|
316
345
|
try {
|
|
@@ -319,7 +348,8 @@ async function checkWorkflow(repoRoot, deps) {
|
|
|
319
348
|
status: "pass",
|
|
320
349
|
command: parsed.agentCommand,
|
|
321
350
|
workflowPath,
|
|
322
|
-
format: parsed.format
|
|
351
|
+
format: parsed.format,
|
|
352
|
+
workflow: parsed
|
|
323
353
|
};
|
|
324
354
|
} catch (error) {
|
|
325
355
|
return {
|
|
@@ -327,20 +357,394 @@ async function checkWorkflow(repoRoot, deps) {
|
|
|
327
357
|
reason: "invalid",
|
|
328
358
|
workflowPath,
|
|
329
359
|
summary: "WORKFLOW.md could not be parsed.",
|
|
330
|
-
remediation: "Fix the WORKFLOW.md front matter or re-run 'gh-symphony init' to regenerate it.",
|
|
360
|
+
remediation: "Fix the WORKFLOW.md front matter or re-run 'gh-symphony workflow init' to regenerate it.",
|
|
331
361
|
error: error instanceof Error ? error.message : "Unknown workflow parse error."
|
|
332
362
|
};
|
|
333
363
|
}
|
|
334
364
|
}
|
|
365
|
+
function buildGithubTrackerConfig(input) {
|
|
366
|
+
const settings = input.projectConfig.projectConfig.tracker.settings;
|
|
367
|
+
return {
|
|
368
|
+
projectId: input.bindingId,
|
|
369
|
+
token: input.token,
|
|
370
|
+
apiUrl: input.projectConfig.projectConfig.tracker.apiUrl,
|
|
371
|
+
lifecycle: input.workflow.lifecycle,
|
|
372
|
+
assignedOnly: settings?.assignedOnly === true,
|
|
373
|
+
priorityFieldName: typeof settings?.priorityFieldName === "string" ? settings.priorityFieldName : void 0,
|
|
374
|
+
timeoutMs: typeof settings?.timeoutMs === "number" ? settings.timeoutMs : void 0
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
function isActiveSmokeIssue(issue, workflow) {
|
|
378
|
+
const normalized = issue.state.trim().toLowerCase();
|
|
379
|
+
return workflow.lifecycle.activeStates.some(
|
|
380
|
+
(state) => state.trim().toLowerCase() === normalized
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
function selectRepresentativeIssue(issues, workflow) {
|
|
384
|
+
const activeIssues = issues.filter(
|
|
385
|
+
(issue) => isActiveSmokeIssue(issue, workflow)
|
|
386
|
+
);
|
|
387
|
+
activeIssues.sort((a, b) => {
|
|
388
|
+
const left = a.updatedAt ?? a.createdAt ?? "";
|
|
389
|
+
const right = b.updatedAt ?? b.createdAt ?? "";
|
|
390
|
+
return right.localeCompare(left);
|
|
391
|
+
});
|
|
392
|
+
return activeIssues[0] ?? null;
|
|
393
|
+
}
|
|
394
|
+
function isRepositoryConfigured(projectConfig, owner, name) {
|
|
395
|
+
const repositories = [projectConfig.projectConfig.repository].filter(
|
|
396
|
+
(repository) => Boolean(repository?.owner && repository.name)
|
|
397
|
+
);
|
|
398
|
+
const normalizedOwner = owner.trim().toLowerCase();
|
|
399
|
+
const normalizedName = name.trim().toLowerCase();
|
|
400
|
+
return repositories.some(
|
|
401
|
+
(repo) => repo.owner.trim().toLowerCase() === normalizedOwner && repo.name.trim().toLowerCase() === normalizedName
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
function isHookPathLike(command) {
|
|
405
|
+
const trimmed = command.trim();
|
|
406
|
+
return trimmed.length > 0 && !trimmed.includes("\n") && !/\s/.test(trimmed) && (trimmed.startsWith("/") || trimmed.startsWith("./") || trimmed.startsWith("../") || trimmed.includes("/") || trimmed.includes("\\"));
|
|
407
|
+
}
|
|
408
|
+
async function buildHookChecks(repoRoot, workflow, deps) {
|
|
409
|
+
const hooks = [
|
|
410
|
+
["after_create", workflow.hooks.afterCreate],
|
|
411
|
+
["before_run", workflow.hooks.beforeRun],
|
|
412
|
+
["after_run", workflow.hooks.afterRun],
|
|
413
|
+
["before_remove", workflow.hooks.beforeRemove]
|
|
414
|
+
];
|
|
415
|
+
const configured = hooks.filter(([, command]) => command);
|
|
416
|
+
if (configured.length === 0) {
|
|
417
|
+
return [
|
|
418
|
+
passCheck(
|
|
419
|
+
"workflow_hooks",
|
|
420
|
+
"Workflow hook paths",
|
|
421
|
+
"No WORKFLOW.md hooks are configured.",
|
|
422
|
+
{ configured: 0 }
|
|
423
|
+
)
|
|
424
|
+
];
|
|
425
|
+
}
|
|
426
|
+
const unresolved = [];
|
|
427
|
+
const checked = [];
|
|
428
|
+
let inline = 0;
|
|
429
|
+
for (const [hook, command] of configured) {
|
|
430
|
+
if (!command || !isHookPathLike(command)) {
|
|
431
|
+
inline += 1;
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
const path = isAbsolute(command) ? command : resolve(repoRoot, command);
|
|
435
|
+
try {
|
|
436
|
+
await deps.access(path, constants.F_OK);
|
|
437
|
+
checked.push({ hook, command, path });
|
|
438
|
+
} catch {
|
|
439
|
+
unresolved.push({ hook, command, path });
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
if (unresolved.length > 0) {
|
|
443
|
+
return [
|
|
444
|
+
failCheck(
|
|
445
|
+
"workflow_hooks",
|
|
446
|
+
"Workflow hook paths",
|
|
447
|
+
`Unresolved WORKFLOW.md hook path${unresolved.length === 1 ? "" : "s"}: ${unresolved.map((entry) => `${entry.hook}=${entry.command}`).join(", ")}.`,
|
|
448
|
+
"Create the referenced hook script(s), fix the hook path(s), or replace them with inline commands.",
|
|
449
|
+
{ configured: configured.length, pathsChecked: checked.length, inline, unresolved, checked }
|
|
450
|
+
)
|
|
451
|
+
];
|
|
452
|
+
}
|
|
453
|
+
const pathSummary = checked.length === 0 ? "No hook paths required filesystem validation." : `Resolved ${checked.length} WORKFLOW.md hook path${checked.length === 1 ? "" : "s"}.`;
|
|
454
|
+
const inlineSummary = inline === 0 ? "" : ` Treated ${inline} hook${inline === 1 ? "" : "s"} as inline command${inline === 1 ? "" : "s"}.`;
|
|
455
|
+
return [
|
|
456
|
+
passCheck(
|
|
457
|
+
"workflow_hooks",
|
|
458
|
+
"Workflow hook paths",
|
|
459
|
+
`${pathSummary}${inlineSummary}`,
|
|
460
|
+
{ configured: configured.length, pathsChecked: checked.length, inline, checked }
|
|
461
|
+
)
|
|
462
|
+
];
|
|
463
|
+
}
|
|
464
|
+
function formatSmokeError(error) {
|
|
465
|
+
return error instanceof Error ? error.message : String(error);
|
|
466
|
+
}
|
|
467
|
+
async function buildDoctorSmokeChecks(input) {
|
|
468
|
+
const checks = [];
|
|
469
|
+
if (input.selection.kind !== "resolved") {
|
|
470
|
+
checks.push(
|
|
471
|
+
failCheck(
|
|
472
|
+
"smoke_issue",
|
|
473
|
+
"Smoke target issue",
|
|
474
|
+
"Smoke check could not choose an issue because managed project selection failed.",
|
|
475
|
+
"Fix the managed project selection check first, then re-run 'gh-symphony doctor --smoke'.",
|
|
476
|
+
{ blockedBy: "managed_project" }
|
|
477
|
+
)
|
|
478
|
+
);
|
|
479
|
+
return checks;
|
|
480
|
+
}
|
|
481
|
+
if (!input.auth) {
|
|
482
|
+
checks.push(
|
|
483
|
+
failCheck(
|
|
484
|
+
"smoke_issue",
|
|
485
|
+
"Smoke target issue",
|
|
486
|
+
"Smoke check could not read live issues because GitHub authentication failed.",
|
|
487
|
+
"Fix GitHub authentication first, then re-run 'gh-symphony doctor --smoke'.",
|
|
488
|
+
{ blockedBy: "gh_authentication" }
|
|
489
|
+
)
|
|
490
|
+
);
|
|
491
|
+
return checks;
|
|
492
|
+
}
|
|
493
|
+
if (input.workflow.status !== "pass") {
|
|
494
|
+
checks.push(
|
|
495
|
+
failCheck(
|
|
496
|
+
"smoke_issue",
|
|
497
|
+
"Smoke target issue",
|
|
498
|
+
"Smoke check could not read a live issue because WORKFLOW.md is missing or invalid.",
|
|
499
|
+
"Fix WORKFLOW.md first, then re-run 'gh-symphony doctor --smoke'.",
|
|
500
|
+
{ blockedBy: "workflow_file" }
|
|
501
|
+
)
|
|
502
|
+
);
|
|
503
|
+
return checks;
|
|
504
|
+
}
|
|
505
|
+
if (!input.projectBindingId || !input.projectDetail) {
|
|
506
|
+
checks.push(
|
|
507
|
+
failCheck(
|
|
508
|
+
"smoke_issue",
|
|
509
|
+
"Smoke target issue",
|
|
510
|
+
"Smoke check could not read live issues because the GitHub Project binding did not resolve.",
|
|
511
|
+
"Fix the GitHub project resolution check first, then re-run 'gh-symphony doctor --smoke'.",
|
|
512
|
+
{ blockedBy: "github_project_resolution" }
|
|
513
|
+
)
|
|
514
|
+
);
|
|
515
|
+
return checks;
|
|
516
|
+
}
|
|
517
|
+
const trackerConfig = buildGithubTrackerConfig({
|
|
518
|
+
projectConfig: input.selection,
|
|
519
|
+
bindingId: input.projectBindingId,
|
|
520
|
+
token: input.auth.token,
|
|
521
|
+
workflow: input.workflow.workflow
|
|
522
|
+
});
|
|
523
|
+
let issue = null;
|
|
524
|
+
if (input.parsedArgs.issue) {
|
|
525
|
+
let issueRef;
|
|
526
|
+
try {
|
|
527
|
+
issueRef = parseIssueReference(input.parsedArgs.issue);
|
|
528
|
+
} catch (error) {
|
|
529
|
+
checks.push(
|
|
530
|
+
failCheck(
|
|
531
|
+
"smoke_issue",
|
|
532
|
+
"Smoke target issue",
|
|
533
|
+
`Smoke check issue reference is invalid: ${input.parsedArgs.issue}.`,
|
|
534
|
+
"Use the expected '--issue owner/repo#number' format and re-run the smoke check.",
|
|
535
|
+
{
|
|
536
|
+
issue: input.parsedArgs.issue,
|
|
537
|
+
expectedFormat: "owner/repo#number",
|
|
538
|
+
error: formatSmokeError(error)
|
|
539
|
+
}
|
|
540
|
+
)
|
|
541
|
+
);
|
|
542
|
+
return checks;
|
|
543
|
+
}
|
|
544
|
+
if (!findLinkedRepository(input.projectDetail, issueRef.owner, issueRef.name)) {
|
|
545
|
+
checks.push(
|
|
546
|
+
failCheck(
|
|
547
|
+
"project_repository_link",
|
|
548
|
+
"Project repository link",
|
|
549
|
+
`Repository ${issueRef.owner}/${issueRef.name} is not linked to GitHub Project "${input.projectDetail.title}".`,
|
|
550
|
+
"Run 'gh-symphony setup' from inside the target repository, or run 'gh-symphony workflow init' followed by 'gh-symphony repo init'.",
|
|
551
|
+
{
|
|
552
|
+
repository: `${issueRef.owner}/${issueRef.name}`,
|
|
553
|
+
projectTitle: input.projectDetail.title
|
|
554
|
+
}
|
|
555
|
+
)
|
|
556
|
+
);
|
|
557
|
+
} else if (!isRepositoryConfigured(input.selection, issueRef.owner, issueRef.name)) {
|
|
558
|
+
checks.push(
|
|
559
|
+
failCheck(
|
|
560
|
+
"project_repository_link",
|
|
561
|
+
"Project repository link",
|
|
562
|
+
`Repository ${issueRef.owner}/${issueRef.name} is linked to the GitHub Project but is not configured locally.`,
|
|
563
|
+
"Run 'gh-symphony repo init' from the target repository before running start.",
|
|
564
|
+
{ repository: `${issueRef.owner}/${issueRef.name}` }
|
|
565
|
+
)
|
|
566
|
+
);
|
|
567
|
+
} else {
|
|
568
|
+
checks.push(
|
|
569
|
+
passCheck(
|
|
570
|
+
"project_repository_link",
|
|
571
|
+
"Project repository link",
|
|
572
|
+
`Repository ${issueRef.owner}/${issueRef.name} is linked to the GitHub Project and configured locally.`,
|
|
573
|
+
{ repository: `${issueRef.owner}/${issueRef.name}` }
|
|
574
|
+
)
|
|
575
|
+
);
|
|
576
|
+
}
|
|
577
|
+
try {
|
|
578
|
+
issue = await input.deps.fetchProjectIssue(
|
|
579
|
+
trackerConfig,
|
|
580
|
+
{ owner: issueRef.owner, name: issueRef.name },
|
|
581
|
+
issueRef.number
|
|
582
|
+
);
|
|
583
|
+
} catch (error) {
|
|
584
|
+
checks.push(
|
|
585
|
+
failCheck(
|
|
586
|
+
"smoke_issue",
|
|
587
|
+
"Smoke target issue",
|
|
588
|
+
`Smoke check could not read issue ${issueRef.identifier}.`,
|
|
589
|
+
"Confirm GitHub token scopes, project visibility, and network access, then re-run the smoke check.",
|
|
590
|
+
{ issue: issueRef.identifier, error: formatSmokeError(error) }
|
|
591
|
+
)
|
|
592
|
+
);
|
|
593
|
+
return checks;
|
|
594
|
+
}
|
|
595
|
+
if (!issue) {
|
|
596
|
+
checks.push(
|
|
597
|
+
failCheck(
|
|
598
|
+
"smoke_issue",
|
|
599
|
+
"Smoke target issue",
|
|
600
|
+
`Issue ${issueRef.identifier} is not in the configured GitHub Project or is not readable.`,
|
|
601
|
+
"Add the issue to the project, confirm the token can read it, and re-run the smoke check.",
|
|
602
|
+
{ issue: issueRef.identifier }
|
|
603
|
+
)
|
|
604
|
+
);
|
|
605
|
+
return checks;
|
|
606
|
+
}
|
|
607
|
+
} else {
|
|
608
|
+
let issues;
|
|
609
|
+
try {
|
|
610
|
+
issues = await input.deps.fetchProjectIssues(trackerConfig);
|
|
611
|
+
} catch (error) {
|
|
612
|
+
checks.push(
|
|
613
|
+
failCheck(
|
|
614
|
+
"smoke_issue",
|
|
615
|
+
"Smoke target issue",
|
|
616
|
+
`Smoke check could not read live issues from GitHub Project "${input.projectDetail.title}".`,
|
|
617
|
+
"Confirm GitHub token scopes, project visibility, and network access, then re-run the smoke check.",
|
|
618
|
+
{
|
|
619
|
+
projectTitle: input.projectDetail.title,
|
|
620
|
+
error: formatSmokeError(error)
|
|
621
|
+
}
|
|
622
|
+
)
|
|
623
|
+
);
|
|
624
|
+
return checks;
|
|
625
|
+
}
|
|
626
|
+
issue = selectRepresentativeIssue(issues, input.workflow.workflow);
|
|
627
|
+
if (!issue) {
|
|
628
|
+
checks.push(
|
|
629
|
+
failCheck(
|
|
630
|
+
"smoke_issue",
|
|
631
|
+
"Smoke target issue",
|
|
632
|
+
`No active live issue was found in GitHub Project "${input.projectDetail.title}".`,
|
|
633
|
+
`Move one issue into an active state (${input.workflow.workflow.lifecycle.activeStates.join(", ")}) or re-run with '--issue owner/repo#number'.`,
|
|
634
|
+
{
|
|
635
|
+
projectTitle: input.projectDetail.title,
|
|
636
|
+
activeStates: input.workflow.workflow.lifecycle.activeStates
|
|
637
|
+
}
|
|
638
|
+
)
|
|
639
|
+
);
|
|
640
|
+
return checks;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
const repositoryName = `${issue.repository.owner}/${issue.repository.name}`;
|
|
644
|
+
if (!checks.some((check) => check.id === "project_repository_link")) {
|
|
645
|
+
if (!findLinkedRepository(
|
|
646
|
+
input.projectDetail,
|
|
647
|
+
issue.repository.owner,
|
|
648
|
+
issue.repository.name
|
|
649
|
+
)) {
|
|
650
|
+
checks.push(
|
|
651
|
+
failCheck(
|
|
652
|
+
"project_repository_link",
|
|
653
|
+
"Project repository link",
|
|
654
|
+
`Repository ${repositoryName} is not linked to GitHub Project "${input.projectDetail.title}".`,
|
|
655
|
+
"Run 'gh-symphony setup' from inside the target repository, or run 'gh-symphony workflow init' followed by 'gh-symphony repo init'.",
|
|
656
|
+
{
|
|
657
|
+
repository: repositoryName,
|
|
658
|
+
projectTitle: input.projectDetail.title
|
|
659
|
+
}
|
|
660
|
+
)
|
|
661
|
+
);
|
|
662
|
+
} else if (!isRepositoryConfigured(
|
|
663
|
+
input.selection,
|
|
664
|
+
issue.repository.owner,
|
|
665
|
+
issue.repository.name
|
|
666
|
+
)) {
|
|
667
|
+
checks.push(
|
|
668
|
+
failCheck(
|
|
669
|
+
"project_repository_link",
|
|
670
|
+
"Project repository link",
|
|
671
|
+
`Repository ${repositoryName} is linked to the GitHub Project but is not configured locally.`,
|
|
672
|
+
"Run 'gh-symphony repo init' from the target repository before running start.",
|
|
673
|
+
{ repository: repositoryName }
|
|
674
|
+
)
|
|
675
|
+
);
|
|
676
|
+
} else {
|
|
677
|
+
checks.push(
|
|
678
|
+
passCheck(
|
|
679
|
+
"project_repository_link",
|
|
680
|
+
"Project repository link",
|
|
681
|
+
`Repository ${repositoryName} is linked to the GitHub Project and configured locally.`,
|
|
682
|
+
{ repository: repositoryName }
|
|
683
|
+
)
|
|
684
|
+
);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
checks.push(
|
|
688
|
+
passCheck(
|
|
689
|
+
"smoke_issue",
|
|
690
|
+
"Smoke target issue",
|
|
691
|
+
`Using live issue ${issue.identifier} (${issue.state}).`,
|
|
692
|
+
{
|
|
693
|
+
issue: issue.identifier,
|
|
694
|
+
state: issue.state,
|
|
695
|
+
source: input.parsedArgs.issue ? "explicit" : "auto"
|
|
696
|
+
}
|
|
697
|
+
)
|
|
698
|
+
);
|
|
699
|
+
try {
|
|
700
|
+
const renderedPrompt = renderIssueWorkflowPreview({
|
|
701
|
+
workflow: input.workflow.workflow,
|
|
702
|
+
issue,
|
|
703
|
+
attempt: null
|
|
704
|
+
});
|
|
705
|
+
checks.push(
|
|
706
|
+
passCheck(
|
|
707
|
+
"workflow_prompt_render",
|
|
708
|
+
"Workflow prompt render",
|
|
709
|
+
`WORKFLOW.md rendered successfully for ${issue.identifier}.`,
|
|
710
|
+
{
|
|
711
|
+
issue: issue.identifier,
|
|
712
|
+
promptLength: renderedPrompt.length,
|
|
713
|
+
workflowPath: input.workflow.workflowPath
|
|
714
|
+
}
|
|
715
|
+
)
|
|
716
|
+
);
|
|
717
|
+
} catch (error) {
|
|
718
|
+
checks.push(
|
|
719
|
+
failCheck(
|
|
720
|
+
"workflow_prompt_render",
|
|
721
|
+
"Workflow prompt render",
|
|
722
|
+
`WORKFLOW.md failed to render for ${issue.identifier}.`,
|
|
723
|
+
"Fix the prompt template variables or run 'gh-symphony workflow preview --issue ...' for a detailed preview error.",
|
|
724
|
+
{
|
|
725
|
+
issue: issue.identifier,
|
|
726
|
+
error: error instanceof Error ? error.message : String(error)
|
|
727
|
+
}
|
|
728
|
+
)
|
|
729
|
+
);
|
|
730
|
+
}
|
|
731
|
+
checks.push(
|
|
732
|
+
...await buildHookChecks(
|
|
733
|
+
input.repoRoot,
|
|
734
|
+
input.workflow.workflow,
|
|
735
|
+
input.deps
|
|
736
|
+
)
|
|
737
|
+
);
|
|
738
|
+
return checks;
|
|
739
|
+
}
|
|
335
740
|
async function runDoctorDiagnostics(options, args, dependencies = {}) {
|
|
336
741
|
const deps = { ...DEFAULT_DEPENDENCIES, ...dependencies };
|
|
337
742
|
const parsedArgs = parseDoctorArgs(args);
|
|
338
743
|
if (parsedArgs.error) {
|
|
339
|
-
throw new Error(
|
|
340
|
-
|
|
341
|
-
Usage: gh-symphony doctor [--project-id <project-id>] [--fix]`
|
|
342
|
-
);
|
|
744
|
+
throw new Error(`${parsedArgs.error}
|
|
745
|
+
${DOCTOR_USAGE}`);
|
|
343
746
|
}
|
|
747
|
+
const repoRoot = process.cwd();
|
|
344
748
|
const checks = [];
|
|
345
749
|
let auth = null;
|
|
346
750
|
let tokenError = null;
|
|
@@ -349,6 +753,8 @@ Usage: gh-symphony doctor [--project-id <project-id>] [--fix]`
|
|
|
349
753
|
let envTokenError = null;
|
|
350
754
|
let resolvedProjectId = null;
|
|
351
755
|
let resolvedProjectConfig = null;
|
|
756
|
+
let resolvedGithubProjectDetail = null;
|
|
757
|
+
let resolvedGithubProjectBindingId = null;
|
|
352
758
|
const envToken = deps.getEnvGitHubToken();
|
|
353
759
|
const currentNodeVersion = deps.processVersion;
|
|
354
760
|
const currentNodeMajor = parseMajorNodeVersion(currentNodeVersion);
|
|
@@ -529,7 +935,7 @@ Usage: gh-symphony doctor [--project-id <project-id>] [--fix]`
|
|
|
529
935
|
"managed_project",
|
|
530
936
|
"Managed project selection",
|
|
531
937
|
resolvedProjectConfig.message,
|
|
532
|
-
"Run 'gh-symphony
|
|
938
|
+
"Run 'gh-symphony repo init' from the target repository.",
|
|
533
939
|
{
|
|
534
940
|
reason: resolvedProjectConfig.kind,
|
|
535
941
|
...resolvedProjectConfig.projectId ? { projectId: resolvedProjectConfig.projectId } : {}
|
|
@@ -550,33 +956,38 @@ Usage: gh-symphony doctor [--project-id <project-id>] [--fix]`
|
|
|
550
956
|
} : void 0
|
|
551
957
|
)
|
|
552
958
|
);
|
|
553
|
-
} else if (resolvedProjectConfig.kind === "resolved" && !resolvedProjectConfig.projectConfig
|
|
959
|
+
} else if (resolvedProjectConfig.kind === "resolved" && !readGitHubProjectBinding(resolvedProjectConfig.projectConfig)) {
|
|
554
960
|
checks.push(
|
|
555
961
|
failCheck(
|
|
556
962
|
"github_project_resolution",
|
|
557
963
|
"GitHub project resolution",
|
|
558
964
|
`Managed project "${resolvedProjectConfig.projectId}" is not bound to a GitHub Project.`,
|
|
559
|
-
"
|
|
965
|
+
"Run 'gh-symphony workflow init' to select a valid GitHub Project binding, then run 'gh-symphony repo init' again.",
|
|
560
966
|
{
|
|
561
967
|
reason: "missing_binding",
|
|
562
968
|
projectId: resolvedProjectConfig.projectId
|
|
563
969
|
}
|
|
564
970
|
)
|
|
565
971
|
);
|
|
566
|
-
} else if (auth && resolvedProjectConfig.kind === "resolved" && resolvedProjectConfig.projectConfig
|
|
972
|
+
} else if (auth && resolvedProjectConfig.kind === "resolved" && readGitHubProjectBinding(resolvedProjectConfig.projectConfig)) {
|
|
567
973
|
try {
|
|
568
|
-
const
|
|
569
|
-
|
|
570
|
-
client,
|
|
571
|
-
resolvedProjectConfig.projectConfig.tracker.bindingId
|
|
974
|
+
const bindingId = readGitHubProjectBinding(
|
|
975
|
+
resolvedProjectConfig.projectConfig
|
|
572
976
|
);
|
|
977
|
+
if (!bindingId) {
|
|
978
|
+
throw new Error("Managed project is not bound to a GitHub Project.");
|
|
979
|
+
}
|
|
980
|
+
resolvedGithubProjectBindingId = bindingId;
|
|
981
|
+
const client = deps.createClient(auth.token);
|
|
982
|
+
const detail = await deps.getProjectDetail(client, bindingId);
|
|
983
|
+
resolvedGithubProjectDetail = detail;
|
|
573
984
|
checks.push(
|
|
574
985
|
passCheck(
|
|
575
986
|
"github_project_resolution",
|
|
576
987
|
"GitHub project resolution",
|
|
577
988
|
`Resolved GitHub Project "${detail.title}".`,
|
|
578
989
|
{
|
|
579
|
-
bindingId:
|
|
990
|
+
bindingId: resolvedGithubProjectBindingId,
|
|
580
991
|
url: detail.url
|
|
581
992
|
}
|
|
582
993
|
)
|
|
@@ -587,11 +998,11 @@ Usage: gh-symphony doctor [--project-id <project-id>] [--fix]`
|
|
|
587
998
|
failCheck(
|
|
588
999
|
"github_project_resolution",
|
|
589
1000
|
"GitHub project resolution",
|
|
590
|
-
`Failed to resolve configured project binding '${
|
|
591
|
-
"
|
|
1001
|
+
`Failed to resolve configured project binding '${resolvedGithubProjectBindingId}'.`,
|
|
1002
|
+
"Run 'gh-symphony workflow init' to select a valid GitHub Project, then run 'gh-symphony repo init' again.",
|
|
592
1003
|
{
|
|
593
1004
|
reason: "api_error",
|
|
594
|
-
bindingId:
|
|
1005
|
+
bindingId: resolvedGithubProjectBindingId,
|
|
595
1006
|
error: message
|
|
596
1007
|
}
|
|
597
1008
|
)
|
|
@@ -733,6 +1144,21 @@ Usage: gh-symphony doctor [--project-id <project-id>] [--fix]`
|
|
|
733
1144
|
)
|
|
734
1145
|
);
|
|
735
1146
|
}
|
|
1147
|
+
if (parsedArgs.smoke) {
|
|
1148
|
+
checks.push(
|
|
1149
|
+
...await buildDoctorSmokeChecks({
|
|
1150
|
+
auth,
|
|
1151
|
+
selection: resolvedProjectConfig,
|
|
1152
|
+
workflow,
|
|
1153
|
+
projectDetail: resolvedGithubProjectDetail,
|
|
1154
|
+
projectBindingId: resolvedGithubProjectBindingId,
|
|
1155
|
+
repoRoot,
|
|
1156
|
+
options,
|
|
1157
|
+
parsedArgs,
|
|
1158
|
+
deps
|
|
1159
|
+
})
|
|
1160
|
+
);
|
|
1161
|
+
}
|
|
736
1162
|
return {
|
|
737
1163
|
ok: checks.every((check) => check.status !== "fail"),
|
|
738
1164
|
checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -963,9 +1389,9 @@ async function runDoctorFixes(report, deps, options) {
|
|
|
963
1389
|
if (check.details?.reason === "multiple_projects_require_selection") {
|
|
964
1390
|
steps.push(
|
|
965
1391
|
runCliRemediation(
|
|
966
|
-
"
|
|
1392
|
+
"Repository runtime setup",
|
|
967
1393
|
check.id,
|
|
968
|
-
["
|
|
1394
|
+
["repo", "init"],
|
|
969
1395
|
deps,
|
|
970
1396
|
options,
|
|
971
1397
|
interactive,
|
|
@@ -976,9 +1402,9 @@ async function runDoctorFixes(report, deps, options) {
|
|
|
976
1402
|
}
|
|
977
1403
|
steps.push(
|
|
978
1404
|
runCliRemediation(
|
|
979
|
-
"
|
|
1405
|
+
"Repository runtime setup",
|
|
980
1406
|
check.id,
|
|
981
|
-
["
|
|
1407
|
+
["repo", "init"],
|
|
982
1408
|
deps,
|
|
983
1409
|
options,
|
|
984
1410
|
interactive,
|
|
@@ -1005,7 +1431,7 @@ async function runDoctorFixes(report, deps, options) {
|
|
|
1005
1431
|
runCliRemediation(
|
|
1006
1432
|
"GitHub project binding setup",
|
|
1007
1433
|
check.id,
|
|
1008
|
-
["
|
|
1434
|
+
["setup"],
|
|
1009
1435
|
deps,
|
|
1010
1436
|
options,
|
|
1011
1437
|
interactive,
|
|
@@ -1021,7 +1447,7 @@ async function runDoctorFixes(report, deps, options) {
|
|
|
1021
1447
|
check.title,
|
|
1022
1448
|
"manual",
|
|
1023
1449
|
check.remediation ?? "Resolve the GitHub Project binding manually.",
|
|
1024
|
-
formatGhSymphonyCommand(["
|
|
1450
|
+
formatGhSymphonyCommand(["setup"], deps, options),
|
|
1025
1451
|
check.details
|
|
1026
1452
|
)
|
|
1027
1453
|
);
|
|
@@ -1051,7 +1477,7 @@ async function runDoctorFixes(report, deps, options) {
|
|
|
1051
1477
|
runCliRemediation(
|
|
1052
1478
|
title,
|
|
1053
1479
|
check.id,
|
|
1054
|
-
["init"],
|
|
1480
|
+
["workflow", "init"],
|
|
1055
1481
|
deps,
|
|
1056
1482
|
options,
|
|
1057
1483
|
interactive,
|
|
@@ -1142,10 +1568,8 @@ async function runDoctorCommand(args, options, dependencies = {}) {
|
|
|
1142
1568
|
const deps = { ...DEFAULT_DEPENDENCIES, ...dependencies };
|
|
1143
1569
|
const parsedArgs = parseDoctorArgs(args);
|
|
1144
1570
|
if (parsedArgs.error) {
|
|
1145
|
-
throw new Error(
|
|
1146
|
-
|
|
1147
|
-
Usage: gh-symphony doctor [--project-id <project-id>] [--fix]`
|
|
1148
|
-
);
|
|
1571
|
+
throw new Error(`${parsedArgs.error}
|
|
1572
|
+
${DOCTOR_USAGE}`);
|
|
1149
1573
|
}
|
|
1150
1574
|
const initialReport = await runDoctorDiagnostics(options, args, deps);
|
|
1151
1575
|
if (parsedArgs.fix) {
|