@gh-symphony/cli 0.0.18 → 0.0.20
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 +85 -14
- package/dist/{project-O57C32WF.js → chunk-3AWF54PI.js} +104 -90
- package/dist/{chunk-ZYYY55WB.js → chunk-EKKT5USP.js} +74 -23
- package/dist/{chunk-LZE6YUSB.js → chunk-HZVDTAPS.js} +32 -72
- package/dist/{chunk-5YLETHMR.js → chunk-RN2PACNV.js} +345 -169
- package/dist/{chunk-62L6QQE6.js → chunk-TILHWBP6.js} +277 -1
- package/dist/{config-cmd-AZ7POMAA.js → config-cmd-DNXNL26Z.js} +3 -1
- package/dist/doctor-IYHCFXOZ.js +1126 -0
- package/dist/index.js +144 -18
- package/dist/init-KZT6YNOH.js +33 -0
- package/dist/project-UUVHS3ZR.js +22 -0
- package/dist/{recover-UGUTQTWA.js → recover-5KQI7WH5.js} +2 -2
- package/dist/repo-HDDE7OUI.js +321 -0
- package/dist/{run-5H2R6CHB.js → run-ETC5UTRA.js} +2 -2
- package/dist/setup-VWB7RZUQ.js +431 -0
- package/dist/{start-5JGGJIMC.js → start-ENFLZUI6.js} +4 -4
- package/dist/upgrade-3YNF3VKY.js +165 -0
- package/dist/{version-N7YXKG6V.js → version-NUBTTOG7.js} +1 -1
- package/dist/worker-entry.js +71 -193
- package/dist/workflow-TBIFY5MO.js +497 -0
- package/package.json +2 -2
- package/dist/chunk-7UBUBSMH.js +0 -134
- package/dist/doctor-3QT5CZN4.js +0 -532
- package/dist/init-E432UZ32.js +0 -18
- package/dist/repo-R3XBIVAX.js +0 -121
- package/dist/{chunk-OL73UN2X.js → chunk-M3IFVLQS.js} +77 -77
|
@@ -0,0 +1,1126 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
parseWorkflowMarkdown
|
|
4
|
+
} from "./chunk-M3IFVLQS.js";
|
|
5
|
+
import {
|
|
6
|
+
GitHubApiError,
|
|
7
|
+
REQUIRED_GH_SCOPES,
|
|
8
|
+
checkGhAuthenticated,
|
|
9
|
+
checkGhInstalled,
|
|
10
|
+
checkGhScopes,
|
|
11
|
+
createClient,
|
|
12
|
+
getEnvGitHubToken,
|
|
13
|
+
getGhToken,
|
|
14
|
+
getProjectDetail,
|
|
15
|
+
runGhAuthLogin,
|
|
16
|
+
runGhAuthRefresh,
|
|
17
|
+
validateGitHubToken
|
|
18
|
+
} from "./chunk-TILHWBP6.js";
|
|
19
|
+
import {
|
|
20
|
+
resolveRuntimeRoot
|
|
21
|
+
} from "./chunk-5NV3LSAJ.js";
|
|
22
|
+
import {
|
|
23
|
+
inspectManagedProjectSelection
|
|
24
|
+
} from "./chunk-C7G7RJ4G.js";
|
|
25
|
+
import "./chunk-ROGRTUFI.js";
|
|
26
|
+
|
|
27
|
+
// src/commands/doctor.ts
|
|
28
|
+
import { constants } from "fs";
|
|
29
|
+
import { execFileSync, spawnSync } from "child_process";
|
|
30
|
+
import { access, mkdir, readFile, stat } from "fs/promises";
|
|
31
|
+
import { delimiter, isAbsolute, join, resolve } from "path";
|
|
32
|
+
var DEFAULT_DEPENDENCIES = {
|
|
33
|
+
checkGhInstalled,
|
|
34
|
+
checkGhAuthenticated,
|
|
35
|
+
checkGhScopes,
|
|
36
|
+
getEnvGitHubToken,
|
|
37
|
+
getGhToken,
|
|
38
|
+
validateGitHubToken,
|
|
39
|
+
inspectManagedProjectSelection,
|
|
40
|
+
createClient,
|
|
41
|
+
getProjectDetail,
|
|
42
|
+
readFile,
|
|
43
|
+
access,
|
|
44
|
+
mkdir,
|
|
45
|
+
stat,
|
|
46
|
+
parseWorkflowMarkdown,
|
|
47
|
+
execFileSync,
|
|
48
|
+
runGhAuthLogin,
|
|
49
|
+
runGhAuthRefresh,
|
|
50
|
+
spawnSync,
|
|
51
|
+
pathEnv: process.env.PATH,
|
|
52
|
+
pathExtEnv: process.env.PATHEXT,
|
|
53
|
+
platform: process.platform,
|
|
54
|
+
processVersion: process.version,
|
|
55
|
+
stdinIsTTY: process.stdin.isTTY === true,
|
|
56
|
+
stdoutIsTTY: process.stdout.isTTY === true,
|
|
57
|
+
execPath: process.execPath,
|
|
58
|
+
cliArgv: [...process.argv]
|
|
59
|
+
};
|
|
60
|
+
var MINIMUM_NODE_MAJOR = 24;
|
|
61
|
+
var MINIMUM_NODE_VERSION = `v${MINIMUM_NODE_MAJOR}.0.0`;
|
|
62
|
+
function parseDoctorArgs(args) {
|
|
63
|
+
const parsed = { fix: false };
|
|
64
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
65
|
+
const arg = args[i];
|
|
66
|
+
if (arg === "--project" || arg === "--project-id") {
|
|
67
|
+
const value = args[i + 1];
|
|
68
|
+
if (!value || value.startsWith("-")) {
|
|
69
|
+
parsed.error = `Option '${arg}' argument missing`;
|
|
70
|
+
return parsed;
|
|
71
|
+
}
|
|
72
|
+
parsed.projectId = value;
|
|
73
|
+
i += 1;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (arg === "--fix") {
|
|
77
|
+
parsed.fix = true;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (arg?.startsWith("-")) {
|
|
81
|
+
parsed.error = `Unknown option '${arg}'`;
|
|
82
|
+
return parsed;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return parsed;
|
|
86
|
+
}
|
|
87
|
+
function passCheck(id, title, summary, details) {
|
|
88
|
+
return { id, title, status: "pass", required: true, summary, details };
|
|
89
|
+
}
|
|
90
|
+
function failCheck(id, title, summary, remediation, details) {
|
|
91
|
+
return {
|
|
92
|
+
id,
|
|
93
|
+
title,
|
|
94
|
+
status: "fail",
|
|
95
|
+
required: true,
|
|
96
|
+
summary,
|
|
97
|
+
remediation,
|
|
98
|
+
details
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
function formatAuthSource(source) {
|
|
102
|
+
return source === "env" ? "GITHUB_GRAPHQL_TOKEN" : "gh CLI";
|
|
103
|
+
}
|
|
104
|
+
function remediationStep(id, checkId, title, status, summary, command, details) {
|
|
105
|
+
return { id, checkId, title, status, summary, command, details };
|
|
106
|
+
}
|
|
107
|
+
async function inspectPathState(targetPath, deps) {
|
|
108
|
+
try {
|
|
109
|
+
const target = await deps.stat(targetPath);
|
|
110
|
+
if (!target.isDirectory()) {
|
|
111
|
+
return {
|
|
112
|
+
exists: true,
|
|
113
|
+
isDirectory: false,
|
|
114
|
+
writable: false
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
try {
|
|
118
|
+
await deps.access(targetPath, constants.W_OK);
|
|
119
|
+
return {
|
|
120
|
+
exists: true,
|
|
121
|
+
isDirectory: true,
|
|
122
|
+
writable: true
|
|
123
|
+
};
|
|
124
|
+
} catch (error) {
|
|
125
|
+
const err = error;
|
|
126
|
+
return {
|
|
127
|
+
exists: true,
|
|
128
|
+
isDirectory: true,
|
|
129
|
+
writable: false,
|
|
130
|
+
code: err.code
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
} catch (error) {
|
|
134
|
+
const err = error;
|
|
135
|
+
if (err.code === "ENOENT" || err.code === "ENOTDIR") {
|
|
136
|
+
return {
|
|
137
|
+
exists: false,
|
|
138
|
+
isDirectory: false,
|
|
139
|
+
writable: false,
|
|
140
|
+
code: err.code
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
throw error;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
function buildPathCheck(id, title, targetPath, state, createCommand, fallbackRemediation) {
|
|
147
|
+
if (state.exists && state.isDirectory && state.writable) {
|
|
148
|
+
return passCheck(id, title, `${title} is writable: ${targetPath}.`, {
|
|
149
|
+
path: targetPath
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
let summary = `${title} is not writable: ${targetPath}.`;
|
|
153
|
+
let reason = "not_writable";
|
|
154
|
+
let remediation = fallbackRemediation;
|
|
155
|
+
if (!state.exists) {
|
|
156
|
+
summary = `${title} does not exist: ${targetPath}.`;
|
|
157
|
+
reason = "missing";
|
|
158
|
+
remediation = `Create the directory before re-running doctor with: ${createCommand}.`;
|
|
159
|
+
} else if (!state.isDirectory) {
|
|
160
|
+
summary = `${title} is not a directory: ${targetPath}.`;
|
|
161
|
+
reason = "not_directory";
|
|
162
|
+
remediation = `Move or remove the conflicting file at '${targetPath}', then create the directory with: ${createCommand}.`;
|
|
163
|
+
}
|
|
164
|
+
return failCheck(id, title, summary, remediation, {
|
|
165
|
+
path: targetPath,
|
|
166
|
+
reason
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
function quotePosixShellArg(value) {
|
|
170
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
171
|
+
}
|
|
172
|
+
function quotePowerShellArg(value) {
|
|
173
|
+
return `'${value.replace(/'/g, "''")}'`;
|
|
174
|
+
}
|
|
175
|
+
function quoteCommandArg(value, platform) {
|
|
176
|
+
if (/^[A-Za-z0-9_./:=+-]+$/.test(value)) {
|
|
177
|
+
return value;
|
|
178
|
+
}
|
|
179
|
+
return platform === "win32" ? quotePowerShellArg(value) : quotePosixShellArg(value);
|
|
180
|
+
}
|
|
181
|
+
function formatEnsureDirectoryCommand(pathValue, platform) {
|
|
182
|
+
if (platform === "win32") {
|
|
183
|
+
return `powershell -NoProfile -Command "New-Item -ItemType Directory -Force -Path ${quotePowerShellArg(pathValue)} | Out-Null"`;
|
|
184
|
+
}
|
|
185
|
+
return `mkdir -p -- ${quotePosixShellArg(pathValue)}`;
|
|
186
|
+
}
|
|
187
|
+
function formatGhSymphonyCommand(args, deps, options) {
|
|
188
|
+
const commandArgs = ["--config", options.configDir, ...args].map(
|
|
189
|
+
(arg) => quoteCommandArg(arg, deps.platform)
|
|
190
|
+
);
|
|
191
|
+
return `gh-symphony ${commandArgs.join(" ")}`;
|
|
192
|
+
}
|
|
193
|
+
function getCommandCandidates(binary, deps) {
|
|
194
|
+
if (deps.platform !== "win32") {
|
|
195
|
+
return [binary];
|
|
196
|
+
}
|
|
197
|
+
const pathExts = (deps.pathExtEnv ?? ".COM;.EXE;.BAT;.CMD").split(";").map((ext) => ext.trim()).filter(Boolean);
|
|
198
|
+
const normalizedBinary = binary.toLowerCase();
|
|
199
|
+
if (pathExts.some((ext) => normalizedBinary.endsWith(ext.toLowerCase()))) {
|
|
200
|
+
return [binary];
|
|
201
|
+
}
|
|
202
|
+
return [binary, ...pathExts.map((ext) => `${binary}${ext}`)];
|
|
203
|
+
}
|
|
204
|
+
async function commandExistsOnPath(binary, deps) {
|
|
205
|
+
if (!binary) {
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
const candidates = getCommandCandidates(binary, deps);
|
|
209
|
+
if (isAbsolute(binary) || binary.includes("/") || binary.includes("\\")) {
|
|
210
|
+
for (const candidate of candidates) {
|
|
211
|
+
try {
|
|
212
|
+
await deps.access(resolve(candidate), constants.X_OK);
|
|
213
|
+
return true;
|
|
214
|
+
} catch {
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
for (const segment of (deps.pathEnv ?? "").split(delimiter)) {
|
|
221
|
+
if (!segment) {
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
for (const command of candidates) {
|
|
225
|
+
const candidate = join(segment, command);
|
|
226
|
+
try {
|
|
227
|
+
await deps.access(candidate, constants.X_OK);
|
|
228
|
+
return true;
|
|
229
|
+
} catch {
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
function extractCommandBinary(command) {
|
|
237
|
+
const trimmed = command.trim();
|
|
238
|
+
if (!trimmed) {
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
const tokens = trimmed.match(/"[^"]*"|'[^']*'|\S+/g) ?? [];
|
|
242
|
+
if (tokens.length === 0) {
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
const shell = stripQuotes(tokens[0]);
|
|
246
|
+
if ((shell === "bash" || shell === "sh" || shell === "zsh" || shell === "fish") && tokens.length >= 3) {
|
|
247
|
+
const flagIndex = tokens.findIndex((token) => {
|
|
248
|
+
const value = stripQuotes(token);
|
|
249
|
+
return value === "-c" || value === "-lc";
|
|
250
|
+
});
|
|
251
|
+
if (flagIndex >= 0 && flagIndex + 1 < tokens.length) {
|
|
252
|
+
const nested = stripQuotes(tokens[flagIndex + 1]);
|
|
253
|
+
const nestedTokens = nested.match(/"[^"]*"|'[^']*'|\S+/g) ?? [];
|
|
254
|
+
return nestedTokens.length > 0 ? stripQuotes(nestedTokens[0]) : shell;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return shell;
|
|
258
|
+
}
|
|
259
|
+
function stripQuotes(value) {
|
|
260
|
+
return value.replace(/^['"]|['"]$/g, "");
|
|
261
|
+
}
|
|
262
|
+
function parseMajorNodeVersion(version) {
|
|
263
|
+
const matched = version.match(/^v?(\d+)(?:\.\d+)?(?:\.\d+)?$/);
|
|
264
|
+
if (!matched) {
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
return Number.parseInt(matched[1], 10);
|
|
268
|
+
}
|
|
269
|
+
async function checkGitInstallation(deps) {
|
|
270
|
+
const installed = await commandExistsOnPath("git", deps);
|
|
271
|
+
if (!installed) {
|
|
272
|
+
return { installed: false };
|
|
273
|
+
}
|
|
274
|
+
try {
|
|
275
|
+
const version = deps.execFileSync("git", ["--version"], {
|
|
276
|
+
encoding: "utf8",
|
|
277
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
278
|
+
}).toString().trim();
|
|
279
|
+
return version ? { installed: true, version } : { installed: false, error: "git --version returned an empty response." };
|
|
280
|
+
} catch (error) {
|
|
281
|
+
return {
|
|
282
|
+
installed: false,
|
|
283
|
+
error: error instanceof Error ? error.message : "Unknown Git execution error."
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
async function checkWorkflow(repoRoot, deps) {
|
|
288
|
+
const workflowPath = join(repoRoot, "WORKFLOW.md");
|
|
289
|
+
let markdown;
|
|
290
|
+
try {
|
|
291
|
+
markdown = await deps.readFile(workflowPath, "utf8");
|
|
292
|
+
} catch {
|
|
293
|
+
return {
|
|
294
|
+
status: "fail",
|
|
295
|
+
reason: "missing",
|
|
296
|
+
workflowPath,
|
|
297
|
+
summary: "WORKFLOW.md was not found in the repository root.",
|
|
298
|
+
remediation: "Run 'gh-symphony init' in this repository or add a valid WORKFLOW.md at the repo root."
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
try {
|
|
302
|
+
const parsed = deps.parseWorkflowMarkdown(markdown, process.env);
|
|
303
|
+
return {
|
|
304
|
+
status: "pass",
|
|
305
|
+
command: parsed.agentCommand,
|
|
306
|
+
workflowPath,
|
|
307
|
+
format: parsed.format
|
|
308
|
+
};
|
|
309
|
+
} catch (error) {
|
|
310
|
+
return {
|
|
311
|
+
status: "fail",
|
|
312
|
+
reason: "invalid",
|
|
313
|
+
workflowPath,
|
|
314
|
+
summary: "WORKFLOW.md could not be parsed.",
|
|
315
|
+
remediation: "Fix the WORKFLOW.md front matter or re-run 'gh-symphony init' to regenerate it.",
|
|
316
|
+
error: error instanceof Error ? error.message : "Unknown workflow parse error."
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
async function runDoctorDiagnostics(options, args, dependencies = {}) {
|
|
321
|
+
const deps = { ...DEFAULT_DEPENDENCIES, ...dependencies };
|
|
322
|
+
const parsedArgs = parseDoctorArgs(args);
|
|
323
|
+
if (parsedArgs.error) {
|
|
324
|
+
throw new Error(
|
|
325
|
+
`${parsedArgs.error}
|
|
326
|
+
Usage: gh-symphony doctor [--project-id <project-id>] [--fix]`
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
const checks = [];
|
|
330
|
+
let auth = null;
|
|
331
|
+
let tokenError = null;
|
|
332
|
+
let authSource = null;
|
|
333
|
+
let authLogin = null;
|
|
334
|
+
let envTokenError = null;
|
|
335
|
+
let resolvedProjectId = null;
|
|
336
|
+
let resolvedProjectConfig = null;
|
|
337
|
+
const envToken = deps.getEnvGitHubToken();
|
|
338
|
+
const currentNodeVersion = deps.processVersion;
|
|
339
|
+
const currentNodeMajor = parseMajorNodeVersion(currentNodeVersion);
|
|
340
|
+
if (currentNodeMajor !== null && currentNodeMajor >= MINIMUM_NODE_MAJOR) {
|
|
341
|
+
checks.push(
|
|
342
|
+
passCheck(
|
|
343
|
+
"node_runtime",
|
|
344
|
+
"Node.js runtime",
|
|
345
|
+
`Node.js ${currentNodeVersion} satisfies the minimum supported version ${MINIMUM_NODE_VERSION}.`,
|
|
346
|
+
{
|
|
347
|
+
currentVersion: currentNodeVersion,
|
|
348
|
+
minimumVersion: MINIMUM_NODE_VERSION
|
|
349
|
+
}
|
|
350
|
+
)
|
|
351
|
+
);
|
|
352
|
+
} else {
|
|
353
|
+
checks.push(
|
|
354
|
+
failCheck(
|
|
355
|
+
"node_runtime",
|
|
356
|
+
"Node.js runtime",
|
|
357
|
+
`Node.js ${currentNodeVersion} does not satisfy the minimum supported version ${MINIMUM_NODE_VERSION}.`,
|
|
358
|
+
`Install Node.js ${MINIMUM_NODE_VERSION} or newer and re-run 'gh-symphony doctor'.`,
|
|
359
|
+
{
|
|
360
|
+
currentVersion: currentNodeVersion,
|
|
361
|
+
minimumVersion: MINIMUM_NODE_VERSION
|
|
362
|
+
}
|
|
363
|
+
)
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
const gitInstallation = await checkGitInstallation(deps);
|
|
367
|
+
if (gitInstallation.installed) {
|
|
368
|
+
checks.push(
|
|
369
|
+
passCheck(
|
|
370
|
+
"git_installation",
|
|
371
|
+
"Git installation",
|
|
372
|
+
`Git is installed: ${gitInstallation.version}.`,
|
|
373
|
+
{ version: gitInstallation.version }
|
|
374
|
+
)
|
|
375
|
+
);
|
|
376
|
+
} else {
|
|
377
|
+
checks.push(
|
|
378
|
+
failCheck(
|
|
379
|
+
"git_installation",
|
|
380
|
+
"Git installation",
|
|
381
|
+
gitInstallation.error ? `Git could not be executed successfully from PATH: ${gitInstallation.error}.` : "Git could not be found on PATH.",
|
|
382
|
+
"Install Git, confirm 'git --version' works in this shell, and re-run 'gh-symphony doctor'.",
|
|
383
|
+
gitInstallation.error ? { error: gitInstallation.error } : void 0
|
|
384
|
+
)
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
const ghInstalled = deps.checkGhInstalled();
|
|
388
|
+
if (ghInstalled) {
|
|
389
|
+
checks.push(
|
|
390
|
+
passCheck("gh_installation", "gh CLI installation", "gh CLI is installed.")
|
|
391
|
+
);
|
|
392
|
+
} else if (envToken) {
|
|
393
|
+
checks.push(
|
|
394
|
+
passCheck(
|
|
395
|
+
"gh_installation",
|
|
396
|
+
"gh CLI installation",
|
|
397
|
+
"gh CLI is not installed, but GITHUB_GRAPHQL_TOKEN is configured so gh is optional.",
|
|
398
|
+
{ authSource: "env" }
|
|
399
|
+
)
|
|
400
|
+
);
|
|
401
|
+
} else {
|
|
402
|
+
checks.push(
|
|
403
|
+
failCheck(
|
|
404
|
+
"gh_installation",
|
|
405
|
+
"gh CLI installation",
|
|
406
|
+
"gh CLI is not installed.",
|
|
407
|
+
"Install GitHub CLI from https://cli.github.com and re-run 'gh-symphony doctor'."
|
|
408
|
+
)
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
const ghAuth = ghInstalled ? deps.checkGhAuthenticated() : { authenticated: false };
|
|
412
|
+
const ghScopes = ghInstalled && ghAuth.authenticated ? deps.checkGhScopes() : { valid: false, missing: [...REQUIRED_GH_SCOPES], scopes: [] };
|
|
413
|
+
if (envToken) {
|
|
414
|
+
try {
|
|
415
|
+
auth = await deps.validateGitHubToken(envToken, "env");
|
|
416
|
+
} catch (error) {
|
|
417
|
+
envTokenError = error instanceof Error ? error.message : "Unknown token validation error.";
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
if (!auth && ghInstalled && ghAuth.authenticated && ghScopes.valid) {
|
|
421
|
+
try {
|
|
422
|
+
const ghToken = deps.getGhToken({ allowEnv: false });
|
|
423
|
+
auth = await deps.validateGitHubToken(ghToken, "gh");
|
|
424
|
+
} catch (error) {
|
|
425
|
+
tokenError = error instanceof Error ? error.message : "Unknown token retrieval error.";
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
if (auth) {
|
|
429
|
+
authSource = auth.source;
|
|
430
|
+
authLogin = auth.login;
|
|
431
|
+
checks.push(
|
|
432
|
+
passCheck(
|
|
433
|
+
"gh_authentication",
|
|
434
|
+
"GitHub authentication",
|
|
435
|
+
`Using ${formatAuthSource(auth.source)} as ${auth.login}.`,
|
|
436
|
+
{ authSource: auth.source, login: auth.login }
|
|
437
|
+
)
|
|
438
|
+
);
|
|
439
|
+
} else if (envTokenError) {
|
|
440
|
+
checks.push(
|
|
441
|
+
failCheck(
|
|
442
|
+
"gh_authentication",
|
|
443
|
+
"GitHub authentication",
|
|
444
|
+
"Configured GITHUB_GRAPHQL_TOKEN could not be used.",
|
|
445
|
+
`${envTokenError} Fix GITHUB_GRAPHQL_TOKEN or configure gh auth, then re-run the doctor command.`,
|
|
446
|
+
{ authSource: "env", error: envTokenError }
|
|
447
|
+
)
|
|
448
|
+
);
|
|
449
|
+
} else {
|
|
450
|
+
checks.push(
|
|
451
|
+
failCheck(
|
|
452
|
+
"gh_authentication",
|
|
453
|
+
"GitHub authentication",
|
|
454
|
+
"gh auth status failed or no GitHub login is configured.",
|
|
455
|
+
`Run 'gh auth login --scopes ${REQUIRED_GH_SCOPES.join(",")}' and re-run the doctor command.`
|
|
456
|
+
)
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
if (auth) {
|
|
460
|
+
checks.push(
|
|
461
|
+
passCheck(
|
|
462
|
+
"gh_scopes",
|
|
463
|
+
"GitHub token scopes",
|
|
464
|
+
`Required scopes are present via ${formatAuthSource(auth.source)}: ${REQUIRED_GH_SCOPES.join(", ")}.`,
|
|
465
|
+
{ authSource: auth.source, scopes: auth.scopes }
|
|
466
|
+
)
|
|
467
|
+
);
|
|
468
|
+
} else if (envTokenError) {
|
|
469
|
+
checks.push(
|
|
470
|
+
failCheck(
|
|
471
|
+
"gh_scopes",
|
|
472
|
+
"GitHub token scopes",
|
|
473
|
+
envTokenError,
|
|
474
|
+
"Update GITHUB_GRAPHQL_TOKEN to include repo, read:org, and project, or configure gh auth with the same scopes.",
|
|
475
|
+
{ authSource: "env" }
|
|
476
|
+
)
|
|
477
|
+
);
|
|
478
|
+
} else {
|
|
479
|
+
const missingScopes = ghInstalled && ghAuth.authenticated ? ghScopes.missing : [...REQUIRED_GH_SCOPES];
|
|
480
|
+
checks.push(
|
|
481
|
+
failCheck(
|
|
482
|
+
"gh_scopes",
|
|
483
|
+
"GitHub token scopes",
|
|
484
|
+
`Missing required scopes: ${missingScopes.join(", ")}.`,
|
|
485
|
+
`Run 'gh auth refresh --scopes ${REQUIRED_GH_SCOPES.join(",")}' and confirm 'gh auth status' shows the updated scopes.`,
|
|
486
|
+
{ missing: missingScopes, scopes: ghScopes.scopes }
|
|
487
|
+
)
|
|
488
|
+
);
|
|
489
|
+
}
|
|
490
|
+
resolvedProjectConfig = await deps.inspectManagedProjectSelection({
|
|
491
|
+
configDir: options.configDir,
|
|
492
|
+
requestedProjectId: parsedArgs.projectId
|
|
493
|
+
});
|
|
494
|
+
if (resolvedProjectConfig.kind === "resolved") {
|
|
495
|
+
resolvedProjectId = resolvedProjectConfig.projectId;
|
|
496
|
+
checks.push(
|
|
497
|
+
passCheck(
|
|
498
|
+
"managed_project",
|
|
499
|
+
"Managed project selection",
|
|
500
|
+
`Resolved managed project "${resolvedProjectConfig.projectId}".`,
|
|
501
|
+
{
|
|
502
|
+
projectId: resolvedProjectConfig.projectId,
|
|
503
|
+
workspaceDir: resolvedProjectConfig.projectConfig.workspaceDir
|
|
504
|
+
}
|
|
505
|
+
)
|
|
506
|
+
);
|
|
507
|
+
} else {
|
|
508
|
+
checks.push(
|
|
509
|
+
failCheck(
|
|
510
|
+
"managed_project",
|
|
511
|
+
"Managed project selection",
|
|
512
|
+
resolvedProjectConfig.message,
|
|
513
|
+
"Run 'gh-symphony project add' to register a project, or select one with 'gh-symphony project switch' / '--project-id'.",
|
|
514
|
+
{
|
|
515
|
+
reason: resolvedProjectConfig.kind,
|
|
516
|
+
...resolvedProjectConfig.projectId ? { projectId: resolvedProjectConfig.projectId } : {}
|
|
517
|
+
}
|
|
518
|
+
)
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
if (resolvedProjectConfig.kind === "resolved" && !auth) {
|
|
522
|
+
checks.push(
|
|
523
|
+
failCheck(
|
|
524
|
+
"github_project_resolution",
|
|
525
|
+
"GitHub project resolution",
|
|
526
|
+
tokenError ? "GitHub project resolution could not run because the GitHub token could not be retrieved." : envTokenError ? "GitHub project resolution could not run because the configured GITHUB_GRAPHQL_TOKEN could not be used." : "GitHub project resolution could not run because authentication failed.",
|
|
527
|
+
tokenError ? "Check the local keychain or environment used by 'gh auth token', then re-run 'gh-symphony doctor'." : envTokenError ? "Fix GITHUB_GRAPHQL_TOKEN or gh authentication first, then re-run 'gh-symphony doctor'." : "Fix the GitHub authentication check first, then re-run 'gh-symphony doctor'.",
|
|
528
|
+
tokenError || envTokenError ? {
|
|
529
|
+
error: tokenError ?? envTokenError,
|
|
530
|
+
...authSource ? { authSource } : {}
|
|
531
|
+
} : void 0
|
|
532
|
+
)
|
|
533
|
+
);
|
|
534
|
+
} else if (resolvedProjectConfig.kind === "resolved" && !resolvedProjectConfig.projectConfig.tracker.bindingId) {
|
|
535
|
+
checks.push(
|
|
536
|
+
failCheck(
|
|
537
|
+
"github_project_resolution",
|
|
538
|
+
"GitHub project resolution",
|
|
539
|
+
`Managed project "${resolvedProjectConfig.projectId}" is not bound to a GitHub Project.`,
|
|
540
|
+
"Re-run 'gh-symphony project add' and select a valid GitHub Project binding, then run the doctor command again.",
|
|
541
|
+
{
|
|
542
|
+
reason: "missing_binding",
|
|
543
|
+
projectId: resolvedProjectConfig.projectId
|
|
544
|
+
}
|
|
545
|
+
)
|
|
546
|
+
);
|
|
547
|
+
} else if (auth && resolvedProjectConfig.kind === "resolved" && resolvedProjectConfig.projectConfig.tracker.bindingId) {
|
|
548
|
+
try {
|
|
549
|
+
const client = deps.createClient(auth.token);
|
|
550
|
+
const detail = await deps.getProjectDetail(
|
|
551
|
+
client,
|
|
552
|
+
resolvedProjectConfig.projectConfig.tracker.bindingId
|
|
553
|
+
);
|
|
554
|
+
checks.push(
|
|
555
|
+
passCheck(
|
|
556
|
+
"github_project_resolution",
|
|
557
|
+
"GitHub project resolution",
|
|
558
|
+
`Resolved GitHub Project "${detail.title}".`,
|
|
559
|
+
{
|
|
560
|
+
bindingId: resolvedProjectConfig.projectConfig.tracker.bindingId,
|
|
561
|
+
url: detail.url
|
|
562
|
+
}
|
|
563
|
+
)
|
|
564
|
+
);
|
|
565
|
+
} catch (error) {
|
|
566
|
+
const message = error instanceof GitHubApiError ? error.message : error instanceof Error ? error.message : "Unknown GitHub API error.";
|
|
567
|
+
checks.push(
|
|
568
|
+
failCheck(
|
|
569
|
+
"github_project_resolution",
|
|
570
|
+
"GitHub project resolution",
|
|
571
|
+
`Failed to resolve configured project binding '${resolvedProjectConfig.projectConfig.tracker.bindingId}'.`,
|
|
572
|
+
"Re-run 'gh-symphony project add' and select a valid GitHub Project, then run the doctor command again.",
|
|
573
|
+
{
|
|
574
|
+
reason: "api_error",
|
|
575
|
+
bindingId: resolvedProjectConfig.projectConfig.tracker.bindingId,
|
|
576
|
+
error: message
|
|
577
|
+
}
|
|
578
|
+
)
|
|
579
|
+
);
|
|
580
|
+
}
|
|
581
|
+
} else {
|
|
582
|
+
checks.push(
|
|
583
|
+
failCheck(
|
|
584
|
+
"github_project_resolution",
|
|
585
|
+
"GitHub project resolution",
|
|
586
|
+
"GitHub project resolution could not run because managed project selection failed.",
|
|
587
|
+
"Fix the managed project selection check first, then re-run 'gh-symphony doctor'.",
|
|
588
|
+
{
|
|
589
|
+
reason: "selection_failed"
|
|
590
|
+
}
|
|
591
|
+
)
|
|
592
|
+
);
|
|
593
|
+
}
|
|
594
|
+
const configDirState = await inspectPathState(options.configDir, deps);
|
|
595
|
+
checks.push(
|
|
596
|
+
buildPathCheck(
|
|
597
|
+
"config_directory",
|
|
598
|
+
"Config directory",
|
|
599
|
+
options.configDir,
|
|
600
|
+
configDirState,
|
|
601
|
+
formatEnsureDirectoryCommand(options.configDir, deps.platform),
|
|
602
|
+
`Ensure your user can write to '${options.configDir}'.`
|
|
603
|
+
)
|
|
604
|
+
);
|
|
605
|
+
const runtimeRoot = resolveRuntimeRoot(options.configDir);
|
|
606
|
+
const runtimeRootState = await inspectPathState(runtimeRoot, deps);
|
|
607
|
+
checks.push(
|
|
608
|
+
buildPathCheck(
|
|
609
|
+
"runtime_root",
|
|
610
|
+
"Runtime root",
|
|
611
|
+
runtimeRoot,
|
|
612
|
+
runtimeRootState,
|
|
613
|
+
formatEnsureDirectoryCommand(runtimeRoot, deps.platform),
|
|
614
|
+
`Ensure your user can write to '${runtimeRoot}'.`
|
|
615
|
+
)
|
|
616
|
+
);
|
|
617
|
+
if (resolvedProjectConfig.kind === "resolved") {
|
|
618
|
+
const workspaceDir = resolvedProjectConfig.projectConfig.workspaceDir;
|
|
619
|
+
const workspaceState = await inspectPathState(workspaceDir, deps);
|
|
620
|
+
checks.push(
|
|
621
|
+
buildPathCheck(
|
|
622
|
+
"workspace_root",
|
|
623
|
+
"Workspace root",
|
|
624
|
+
workspaceDir,
|
|
625
|
+
workspaceState,
|
|
626
|
+
formatEnsureDirectoryCommand(workspaceDir, deps.platform),
|
|
627
|
+
"Update the managed project workspaceDir to a writable path or fix the filesystem permissions."
|
|
628
|
+
)
|
|
629
|
+
);
|
|
630
|
+
} else {
|
|
631
|
+
checks.push(
|
|
632
|
+
failCheck(
|
|
633
|
+
"workspace_root",
|
|
634
|
+
"Workspace root",
|
|
635
|
+
"Workspace root could not be checked because no managed project was resolved.",
|
|
636
|
+
"Fix the managed project selection check first, then re-run 'gh-symphony doctor'.",
|
|
637
|
+
{ blockedBy: "managed_project" }
|
|
638
|
+
)
|
|
639
|
+
);
|
|
640
|
+
}
|
|
641
|
+
const workflow = await checkWorkflow(process.cwd(), deps);
|
|
642
|
+
if (workflow.status === "pass") {
|
|
643
|
+
checks.push(
|
|
644
|
+
passCheck(
|
|
645
|
+
"workflow_file",
|
|
646
|
+
"Repository WORKFLOW.md",
|
|
647
|
+
`WORKFLOW.md parsed successfully (${workflow.format}).`,
|
|
648
|
+
{ path: workflow.workflowPath, format: workflow.format }
|
|
649
|
+
)
|
|
650
|
+
);
|
|
651
|
+
} else {
|
|
652
|
+
checks.push(
|
|
653
|
+
failCheck(
|
|
654
|
+
"workflow_file",
|
|
655
|
+
"Repository WORKFLOW.md",
|
|
656
|
+
workflow.summary,
|
|
657
|
+
workflow.remediation,
|
|
658
|
+
{
|
|
659
|
+
path: workflow.workflowPath,
|
|
660
|
+
reason: workflow.reason,
|
|
661
|
+
...workflow.error ? { error: workflow.error } : {}
|
|
662
|
+
}
|
|
663
|
+
)
|
|
664
|
+
);
|
|
665
|
+
}
|
|
666
|
+
if (workflow.status === "pass") {
|
|
667
|
+
const binary = extractCommandBinary(workflow.command);
|
|
668
|
+
if (binary && await commandExistsOnPath(binary, deps)) {
|
|
669
|
+
checks.push(
|
|
670
|
+
passCheck(
|
|
671
|
+
"runtime_command",
|
|
672
|
+
"Runtime command detection",
|
|
673
|
+
`Configured runtime command is available: ${binary}.`,
|
|
674
|
+
{ command: workflow.command, binary }
|
|
675
|
+
)
|
|
676
|
+
);
|
|
677
|
+
} else {
|
|
678
|
+
checks.push(
|
|
679
|
+
failCheck(
|
|
680
|
+
"runtime_command",
|
|
681
|
+
"Runtime command detection",
|
|
682
|
+
`Configured runtime command could not be found on PATH: ${workflow.command}.`,
|
|
683
|
+
buildRuntimeInstallGuidance(binary, deps.platform),
|
|
684
|
+
{ command: workflow.command, binary }
|
|
685
|
+
)
|
|
686
|
+
);
|
|
687
|
+
}
|
|
688
|
+
} else {
|
|
689
|
+
checks.push(
|
|
690
|
+
failCheck(
|
|
691
|
+
"runtime_command",
|
|
692
|
+
"Runtime command detection",
|
|
693
|
+
"Runtime command detection could not run because WORKFLOW.md is missing or invalid.",
|
|
694
|
+
"Fix the WORKFLOW.md check first so the configured runtime command can be validated.",
|
|
695
|
+
{ blockedBy: "workflow_file" }
|
|
696
|
+
)
|
|
697
|
+
);
|
|
698
|
+
}
|
|
699
|
+
return {
|
|
700
|
+
ok: checks.every((check) => check.status === "pass"),
|
|
701
|
+
checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
702
|
+
configDir: options.configDir,
|
|
703
|
+
projectId: resolvedProjectId,
|
|
704
|
+
authSource,
|
|
705
|
+
authLogin,
|
|
706
|
+
checks
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
function buildRuntimeInstallGuidance(binary, platform) {
|
|
710
|
+
if (binary === "codex") {
|
|
711
|
+
return "Install Codex CLI using its official installation instructions and ensure 'codex' is on PATH.";
|
|
712
|
+
}
|
|
713
|
+
if (binary === "claude" || binary === "claude-code") {
|
|
714
|
+
return "Install Claude Code using its official installation instructions and ensure the runtime binary is on PATH.";
|
|
715
|
+
}
|
|
716
|
+
if (platform === "win32" && binary) {
|
|
717
|
+
return `Install '${binary}' using its official installation instructions and ensure the directory containing '${binary}.exe' is on PATH.`;
|
|
718
|
+
}
|
|
719
|
+
if (binary) {
|
|
720
|
+
return `Install '${binary}' using its official installation instructions and ensure it is available on PATH.`;
|
|
721
|
+
}
|
|
722
|
+
return "Install the configured runtime command using its official installation instructions and ensure it is available on PATH.";
|
|
723
|
+
}
|
|
724
|
+
function isInteractiveTerminal(deps) {
|
|
725
|
+
return deps.stdinIsTTY && deps.stdoutIsTTY;
|
|
726
|
+
}
|
|
727
|
+
function runCliRemediation(title, checkId, args, deps, options, interactive, details) {
|
|
728
|
+
const command = formatGhSymphonyCommand(args, deps, options);
|
|
729
|
+
if (!interactive) {
|
|
730
|
+
return remediationStep(
|
|
731
|
+
`remediate_${checkId}`,
|
|
732
|
+
checkId,
|
|
733
|
+
title,
|
|
734
|
+
"manual",
|
|
735
|
+
`Interactive terminal not available. Run this command manually: ${command}.`,
|
|
736
|
+
command,
|
|
737
|
+
details
|
|
738
|
+
);
|
|
739
|
+
}
|
|
740
|
+
const cliEntry = deps.cliArgv[1];
|
|
741
|
+
if (!cliEntry) {
|
|
742
|
+
return remediationStep(
|
|
743
|
+
`remediate_${checkId}`,
|
|
744
|
+
checkId,
|
|
745
|
+
title,
|
|
746
|
+
"manual",
|
|
747
|
+
`Could not determine the current CLI entrypoint. Run this command manually: ${command}.`,
|
|
748
|
+
command,
|
|
749
|
+
details
|
|
750
|
+
);
|
|
751
|
+
}
|
|
752
|
+
const result = deps.spawnSync(deps.execPath, [cliEntry, "--config", options.configDir, ...args], {
|
|
753
|
+
stdio: "inherit"
|
|
754
|
+
});
|
|
755
|
+
if ((result.status ?? 1) === 0) {
|
|
756
|
+
return remediationStep(
|
|
757
|
+
`remediate_${checkId}`,
|
|
758
|
+
checkId,
|
|
759
|
+
title,
|
|
760
|
+
"applied",
|
|
761
|
+
`Executed: ${command}.`,
|
|
762
|
+
command,
|
|
763
|
+
details
|
|
764
|
+
);
|
|
765
|
+
}
|
|
766
|
+
return remediationStep(
|
|
767
|
+
`remediate_${checkId}`,
|
|
768
|
+
checkId,
|
|
769
|
+
title,
|
|
770
|
+
"manual",
|
|
771
|
+
`Failed to complete this command automatically. Re-run it manually: ${command}.`,
|
|
772
|
+
command,
|
|
773
|
+
details
|
|
774
|
+
);
|
|
775
|
+
}
|
|
776
|
+
async function ensureDirectoryRemediation(check, deps) {
|
|
777
|
+
const pathValue = typeof check.details?.path === "string" ? check.details.path : null;
|
|
778
|
+
const reason = typeof check.details?.reason === "string" ? check.details.reason : null;
|
|
779
|
+
const command = pathValue === null ? void 0 : formatEnsureDirectoryCommand(pathValue, deps.platform);
|
|
780
|
+
if (!pathValue) {
|
|
781
|
+
return remediationStep(
|
|
782
|
+
`remediate_${check.id}`,
|
|
783
|
+
check.id,
|
|
784
|
+
check.title,
|
|
785
|
+
"manual",
|
|
786
|
+
"No filesystem path was recorded for this failing check.",
|
|
787
|
+
command
|
|
788
|
+
);
|
|
789
|
+
}
|
|
790
|
+
if (reason === "not_directory") {
|
|
791
|
+
return remediationStep(
|
|
792
|
+
`remediate_${check.id}`,
|
|
793
|
+
check.id,
|
|
794
|
+
check.title,
|
|
795
|
+
"manual",
|
|
796
|
+
`A file already exists at '${pathValue}'. Remove or move it before creating the directory.`,
|
|
797
|
+
command,
|
|
798
|
+
{ path: pathValue }
|
|
799
|
+
);
|
|
800
|
+
}
|
|
801
|
+
try {
|
|
802
|
+
await deps.mkdir(pathValue, { recursive: true });
|
|
803
|
+
await deps.access(pathValue, constants.W_OK);
|
|
804
|
+
const target = await deps.stat(pathValue);
|
|
805
|
+
if (!target.isDirectory()) {
|
|
806
|
+
return remediationStep(
|
|
807
|
+
`remediate_${check.id}`,
|
|
808
|
+
check.id,
|
|
809
|
+
check.title,
|
|
810
|
+
"manual",
|
|
811
|
+
`Created path '${pathValue}', but it is not a directory.`,
|
|
812
|
+
command,
|
|
813
|
+
{ path: pathValue }
|
|
814
|
+
);
|
|
815
|
+
}
|
|
816
|
+
return remediationStep(
|
|
817
|
+
`remediate_${check.id}`,
|
|
818
|
+
check.id,
|
|
819
|
+
check.title,
|
|
820
|
+
"applied",
|
|
821
|
+
`Ensured writable directory '${pathValue}'.`,
|
|
822
|
+
command,
|
|
823
|
+
{ path: pathValue }
|
|
824
|
+
);
|
|
825
|
+
} catch (error) {
|
|
826
|
+
const errorMessage = error instanceof Error ? error.message : "unknown error";
|
|
827
|
+
const summary = reason === "not_writable" ? `Directory '${pathValue}' exists but is not writable. Update its permissions or choose a writable location: ${errorMessage}.` : `Failed to create '${pathValue}' automatically: ${errorMessage}.`;
|
|
828
|
+
return remediationStep(
|
|
829
|
+
`remediate_${check.id}`,
|
|
830
|
+
check.id,
|
|
831
|
+
check.title,
|
|
832
|
+
"manual",
|
|
833
|
+
summary,
|
|
834
|
+
command,
|
|
835
|
+
{ path: pathValue }
|
|
836
|
+
);
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
async function runDoctorFixes(report, deps, options) {
|
|
840
|
+
const steps = [];
|
|
841
|
+
const interactive = !options.json && isInteractiveTerminal(deps);
|
|
842
|
+
const failed = new Map(
|
|
843
|
+
report.checks.filter((check) => check.status === "fail").map((check) => [check.id, check])
|
|
844
|
+
);
|
|
845
|
+
for (const check of report.checks) {
|
|
846
|
+
if (check.status !== "fail") {
|
|
847
|
+
continue;
|
|
848
|
+
}
|
|
849
|
+
switch (check.id) {
|
|
850
|
+
case "gh_installation":
|
|
851
|
+
steps.push(
|
|
852
|
+
remediationStep(
|
|
853
|
+
"remediate_gh_installation",
|
|
854
|
+
check.id,
|
|
855
|
+
check.title,
|
|
856
|
+
"manual",
|
|
857
|
+
"Install GitHub CLI first. Automatic installation is not attempted by doctor.",
|
|
858
|
+
"https://cli.github.com"
|
|
859
|
+
)
|
|
860
|
+
);
|
|
861
|
+
break;
|
|
862
|
+
case "gh_authentication": {
|
|
863
|
+
if (failed.has("gh_installation")) {
|
|
864
|
+
steps.push(
|
|
865
|
+
remediationStep(
|
|
866
|
+
"remediate_gh_authentication",
|
|
867
|
+
check.id,
|
|
868
|
+
check.title,
|
|
869
|
+
"skipped",
|
|
870
|
+
"Skipped because gh CLI is not installed."
|
|
871
|
+
)
|
|
872
|
+
);
|
|
873
|
+
break;
|
|
874
|
+
}
|
|
875
|
+
const result = deps.runGhAuthLogin({
|
|
876
|
+
spawnImpl: deps.spawnSync,
|
|
877
|
+
interactive
|
|
878
|
+
});
|
|
879
|
+
steps.push(
|
|
880
|
+
remediationStep(
|
|
881
|
+
"remediate_gh_authentication",
|
|
882
|
+
check.id,
|
|
883
|
+
check.title,
|
|
884
|
+
result.status,
|
|
885
|
+
result.summary,
|
|
886
|
+
result.command
|
|
887
|
+
)
|
|
888
|
+
);
|
|
889
|
+
break;
|
|
890
|
+
}
|
|
891
|
+
case "gh_scopes": {
|
|
892
|
+
if (failed.has("gh_installation") || failed.has("gh_authentication")) {
|
|
893
|
+
steps.push(
|
|
894
|
+
remediationStep(
|
|
895
|
+
"remediate_gh_scopes",
|
|
896
|
+
check.id,
|
|
897
|
+
check.title,
|
|
898
|
+
"skipped",
|
|
899
|
+
"Skipped because gh installation/authentication must be fixed first."
|
|
900
|
+
)
|
|
901
|
+
);
|
|
902
|
+
break;
|
|
903
|
+
}
|
|
904
|
+
const result = deps.runGhAuthRefresh({
|
|
905
|
+
spawnImpl: deps.spawnSync,
|
|
906
|
+
interactive
|
|
907
|
+
});
|
|
908
|
+
steps.push(
|
|
909
|
+
remediationStep(
|
|
910
|
+
"remediate_gh_scopes",
|
|
911
|
+
check.id,
|
|
912
|
+
check.title,
|
|
913
|
+
result.status,
|
|
914
|
+
result.summary,
|
|
915
|
+
result.command,
|
|
916
|
+
check.details
|
|
917
|
+
)
|
|
918
|
+
);
|
|
919
|
+
break;
|
|
920
|
+
}
|
|
921
|
+
case "managed_project":
|
|
922
|
+
if (check.details?.reason === "multiple_projects_require_selection") {
|
|
923
|
+
steps.push(
|
|
924
|
+
runCliRemediation(
|
|
925
|
+
"Managed project selection",
|
|
926
|
+
check.id,
|
|
927
|
+
["project", "switch"],
|
|
928
|
+
deps,
|
|
929
|
+
options,
|
|
930
|
+
interactive,
|
|
931
|
+
check.details
|
|
932
|
+
)
|
|
933
|
+
);
|
|
934
|
+
break;
|
|
935
|
+
}
|
|
936
|
+
steps.push(
|
|
937
|
+
runCliRemediation(
|
|
938
|
+
"Managed project setup",
|
|
939
|
+
check.id,
|
|
940
|
+
["project", "add"],
|
|
941
|
+
deps,
|
|
942
|
+
options,
|
|
943
|
+
interactive,
|
|
944
|
+
check.details
|
|
945
|
+
)
|
|
946
|
+
);
|
|
947
|
+
break;
|
|
948
|
+
case "github_project_resolution": {
|
|
949
|
+
if (failed.has("managed_project")) {
|
|
950
|
+
steps.push(
|
|
951
|
+
remediationStep(
|
|
952
|
+
"remediate_github_project_resolution",
|
|
953
|
+
check.id,
|
|
954
|
+
check.title,
|
|
955
|
+
"skipped",
|
|
956
|
+
"Skipped because managed project selection must be fixed first."
|
|
957
|
+
)
|
|
958
|
+
);
|
|
959
|
+
break;
|
|
960
|
+
}
|
|
961
|
+
const reason = typeof check.details?.reason === "string" ? check.details.reason : null;
|
|
962
|
+
if (reason === "missing_binding" || reason === "api_error") {
|
|
963
|
+
steps.push(
|
|
964
|
+
runCliRemediation(
|
|
965
|
+
"GitHub project binding setup",
|
|
966
|
+
check.id,
|
|
967
|
+
["project", "add"],
|
|
968
|
+
deps,
|
|
969
|
+
options,
|
|
970
|
+
interactive,
|
|
971
|
+
check.details
|
|
972
|
+
)
|
|
973
|
+
);
|
|
974
|
+
break;
|
|
975
|
+
}
|
|
976
|
+
steps.push(
|
|
977
|
+
remediationStep(
|
|
978
|
+
"remediate_github_project_resolution",
|
|
979
|
+
check.id,
|
|
980
|
+
check.title,
|
|
981
|
+
"manual",
|
|
982
|
+
check.remediation ?? "Resolve the GitHub Project binding manually.",
|
|
983
|
+
formatGhSymphonyCommand(["project", "add"], deps, options),
|
|
984
|
+
check.details
|
|
985
|
+
)
|
|
986
|
+
);
|
|
987
|
+
break;
|
|
988
|
+
}
|
|
989
|
+
case "config_directory":
|
|
990
|
+
case "runtime_root":
|
|
991
|
+
case "workspace_root":
|
|
992
|
+
if (check.details?.blockedBy === "managed_project") {
|
|
993
|
+
steps.push(
|
|
994
|
+
remediationStep(
|
|
995
|
+
`remediate_${check.id}`,
|
|
996
|
+
check.id,
|
|
997
|
+
check.title,
|
|
998
|
+
"skipped",
|
|
999
|
+
"Skipped because managed project selection must be fixed first."
|
|
1000
|
+
)
|
|
1001
|
+
);
|
|
1002
|
+
break;
|
|
1003
|
+
}
|
|
1004
|
+
steps.push(await ensureDirectoryRemediation(check, deps));
|
|
1005
|
+
break;
|
|
1006
|
+
case "workflow_file": {
|
|
1007
|
+
const reason = typeof check.details?.reason === "string" ? check.details.reason : null;
|
|
1008
|
+
const title = reason === "missing" ? "Repository workflow initialization" : "Repository workflow regeneration";
|
|
1009
|
+
steps.push(
|
|
1010
|
+
runCliRemediation(title, check.id, ["init"], deps, options, interactive, check.details)
|
|
1011
|
+
);
|
|
1012
|
+
break;
|
|
1013
|
+
}
|
|
1014
|
+
case "runtime_command": {
|
|
1015
|
+
if (failed.has("workflow_file")) {
|
|
1016
|
+
steps.push(
|
|
1017
|
+
remediationStep(
|
|
1018
|
+
"remediate_runtime_command",
|
|
1019
|
+
check.id,
|
|
1020
|
+
check.title,
|
|
1021
|
+
"skipped",
|
|
1022
|
+
"Skipped because WORKFLOW.md must be fixed first."
|
|
1023
|
+
)
|
|
1024
|
+
);
|
|
1025
|
+
break;
|
|
1026
|
+
}
|
|
1027
|
+
steps.push(
|
|
1028
|
+
remediationStep(
|
|
1029
|
+
"remediate_runtime_command",
|
|
1030
|
+
check.id,
|
|
1031
|
+
check.title,
|
|
1032
|
+
"manual",
|
|
1033
|
+
check.remediation ?? "Install the configured runtime command manually.",
|
|
1034
|
+
typeof check.details?.binary === "string" ? String(check.details.binary) : void 0,
|
|
1035
|
+
check.details
|
|
1036
|
+
)
|
|
1037
|
+
);
|
|
1038
|
+
break;
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
return steps;
|
|
1043
|
+
}
|
|
1044
|
+
function renderTextReport(report) {
|
|
1045
|
+
const lines = [
|
|
1046
|
+
`gh-symphony doctor`,
|
|
1047
|
+
`Auth source: ${report.authSource ?? "unavailable"}`,
|
|
1048
|
+
...report.authLogin ? [`Authenticated GitHub user: ${report.authLogin}`] : [],
|
|
1049
|
+
""
|
|
1050
|
+
];
|
|
1051
|
+
if (report.remediation) {
|
|
1052
|
+
lines.push("Remediation");
|
|
1053
|
+
for (const step of report.remediation.steps) {
|
|
1054
|
+
lines.push(`${step.status.toUpperCase()} ${step.title}`);
|
|
1055
|
+
lines.push(` ${step.summary}`);
|
|
1056
|
+
if (step.command) {
|
|
1057
|
+
lines.push(` Command: ${step.command}`);
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
lines.push("");
|
|
1061
|
+
}
|
|
1062
|
+
for (const check of report.checks) {
|
|
1063
|
+
lines.push(`${check.status === "pass" ? "PASS" : "FAIL"} ${check.title}`);
|
|
1064
|
+
lines.push(` ${check.summary}`);
|
|
1065
|
+
if (check.remediation) {
|
|
1066
|
+
lines.push(` Fix: ${check.remediation}`);
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
lines.push("");
|
|
1070
|
+
lines.push(
|
|
1071
|
+
report.ok ? "Doctor completed successfully." : "Doctor found required checks that need attention."
|
|
1072
|
+
);
|
|
1073
|
+
return lines.join("\n");
|
|
1074
|
+
}
|
|
1075
|
+
async function runDoctorCommand(args, options, dependencies = {}) {
|
|
1076
|
+
try {
|
|
1077
|
+
const deps = { ...DEFAULT_DEPENDENCIES, ...dependencies };
|
|
1078
|
+
const parsedArgs = parseDoctorArgs(args);
|
|
1079
|
+
if (parsedArgs.error) {
|
|
1080
|
+
throw new Error(
|
|
1081
|
+
`${parsedArgs.error}
|
|
1082
|
+
Usage: gh-symphony doctor [--project-id <project-id>] [--fix]`
|
|
1083
|
+
);
|
|
1084
|
+
}
|
|
1085
|
+
const initialReport = await runDoctorDiagnostics(options, args, deps);
|
|
1086
|
+
if (parsedArgs.fix) {
|
|
1087
|
+
const remediation = {
|
|
1088
|
+
attempted: true,
|
|
1089
|
+
steps: await runDoctorFixes(initialReport, deps, options)
|
|
1090
|
+
};
|
|
1091
|
+
const report2 = await runDoctorDiagnostics(
|
|
1092
|
+
options,
|
|
1093
|
+
args.filter((arg) => arg !== "--fix"),
|
|
1094
|
+
deps
|
|
1095
|
+
);
|
|
1096
|
+
report2.remediation = remediation;
|
|
1097
|
+
if (options.json) {
|
|
1098
|
+
process.stdout.write(JSON.stringify(report2, null, 2) + "\n");
|
|
1099
|
+
} else {
|
|
1100
|
+
process.stdout.write(renderTextReport(report2) + "\n");
|
|
1101
|
+
}
|
|
1102
|
+
process.exitCode = report2.ok ? 0 : 1;
|
|
1103
|
+
return;
|
|
1104
|
+
}
|
|
1105
|
+
const report = initialReport;
|
|
1106
|
+
if (options.json) {
|
|
1107
|
+
process.stdout.write(JSON.stringify(report, null, 2) + "\n");
|
|
1108
|
+
} else {
|
|
1109
|
+
process.stdout.write(renderTextReport(report) + "\n");
|
|
1110
|
+
}
|
|
1111
|
+
process.exitCode = report.ok ? 0 : 1;
|
|
1112
|
+
} catch (error) {
|
|
1113
|
+
process.stderr.write(
|
|
1114
|
+
`${error instanceof Error ? error.message : "Unknown error"}
|
|
1115
|
+
`
|
|
1116
|
+
);
|
|
1117
|
+
process.exitCode = 2;
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
var handler = async (args, options) => runDoctorCommand(args, options);
|
|
1121
|
+
var doctor_default = handler;
|
|
1122
|
+
export {
|
|
1123
|
+
doctor_default as default,
|
|
1124
|
+
runDoctorCommand,
|
|
1125
|
+
runDoctorDiagnostics
|
|
1126
|
+
};
|