@gh-symphony/cli 0.2.0 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +68 -0
- package/dist/{chunk-Q3UEPUE3.js → chunk-3SKN5L3I.js} +256 -18
- package/dist/{chunk-WOVNN5NW.js → chunk-4ICDSQCJ.js} +1 -0
- package/dist/{chunk-JU3WSGMZ.js → chunk-AA3T5AAJ.js} +201 -7
- package/dist/{chunk-Z3NZOPLZ.js → chunk-BOM2BYZQ.js} +43 -0
- package/dist/{chunk-C44DYDNU.js → chunk-DLZ2XHWY.js} +6 -5
- package/dist/{chunk-B6OHDUSH.js → chunk-FAU72YC2.js} +1 -1
- package/dist/{chunk-F46FTZJE.js → chunk-PLBG7TZA.js} +290 -30
- package/dist/{chunk-6I753NYO.js → chunk-RZ3WO7OV.js} +1 -1
- package/dist/{chunk-CTTFIZYG.js → chunk-ZGNAAHLD.js} +140 -12
- package/dist/{config-cmd-2ADPUYWA.js → config-cmd-AOZVS6GU.js} +1 -1
- package/dist/{doctor-JPNA7OCD.js → doctor-GIJAH7MA.js} +602 -27
- package/dist/index.js +21 -14
- package/dist/{repo-OJLSMOR3.js → repo-LNO3Q3O7.js} +17 -10
- package/dist/{setup-PD27LSPP.js → setup-KZ3U53PY.js} +37 -47
- package/dist/{upgrade-HRI3KEO7.js → upgrade-K2PNQNWE.js} +2 -2
- package/dist/{version-JSBTKS6Q.js → version-E45DDQPQ.js} +1 -1
- package/dist/worker-entry.js +77 -9
- package/dist/{workflow-KB3TX5Z4.js → workflow-ZPERNZJT.js} +7 -7
- package/package.json +5 -5
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
+
buildPriorityConfigDiagnostics,
|
|
4
|
+
buildPriorityDriftDiagnostics,
|
|
3
5
|
parseIssueReference,
|
|
4
6
|
readGitHubProjectBinding,
|
|
5
7
|
renderIssueWorkflowPreview
|
|
6
|
-
} from "./chunk-
|
|
7
|
-
import "./chunk-
|
|
8
|
+
} from "./chunk-AA3T5AAJ.js";
|
|
9
|
+
import "./chunk-PLBG7TZA.js";
|
|
8
10
|
import {
|
|
9
11
|
fetchGithubProjectIssueByRepositoryAndNumber,
|
|
10
12
|
fetchGithubProjectIssues,
|
|
11
13
|
inspectManagedProjectSelection
|
|
12
|
-
} from "./chunk-
|
|
13
|
-
import "./chunk-
|
|
14
|
+
} from "./chunk-ZGNAAHLD.js";
|
|
15
|
+
import "./chunk-FAU72YC2.js";
|
|
14
16
|
import {
|
|
15
17
|
resolveRuntimeRoot
|
|
16
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-RZ3WO7OV.js";
|
|
17
19
|
import {
|
|
18
20
|
GitHubApiError,
|
|
19
21
|
REQUIRED_GH_SCOPES,
|
|
@@ -25,24 +27,397 @@ import {
|
|
|
25
27
|
getEnvGitHubToken,
|
|
26
28
|
getGhToken,
|
|
27
29
|
getProjectDetail,
|
|
30
|
+
listRepositoryLabels,
|
|
28
31
|
runGhAuthLogin,
|
|
29
32
|
runGhAuthRefresh,
|
|
30
33
|
validateGitHubToken
|
|
31
|
-
} from "./chunk-
|
|
34
|
+
} from "./chunk-BOM2BYZQ.js";
|
|
32
35
|
import {
|
|
33
36
|
isClaudeRuntimeCommand,
|
|
34
37
|
parseWorkflowMarkdown,
|
|
38
|
+
redactObservabilityDiagnosticsWithStats,
|
|
39
|
+
redactObservabilityTextWithStats,
|
|
35
40
|
resolveClaudeCommandBinary,
|
|
36
41
|
resolveRuntimeCommandBinary,
|
|
37
42
|
runClaudePreflight
|
|
38
|
-
} from "./chunk-
|
|
39
|
-
import
|
|
43
|
+
} from "./chunk-3SKN5L3I.js";
|
|
44
|
+
import {
|
|
45
|
+
configFilePath,
|
|
46
|
+
orchestratorLogPath,
|
|
47
|
+
projectConfigPath
|
|
48
|
+
} from "./chunk-4ICDSQCJ.js";
|
|
40
49
|
|
|
41
50
|
// src/commands/doctor.ts
|
|
42
|
-
import { constants } from "fs";
|
|
51
|
+
import { constants as constants2 } from "fs";
|
|
43
52
|
import { execFileSync, spawnSync } from "child_process";
|
|
44
|
-
import { access, mkdir, readFile, stat } from "fs/promises";
|
|
45
|
-
import { delimiter, isAbsolute, join, resolve } from "path";
|
|
53
|
+
import { access as access2, mkdir as mkdir2, readFile as readFile2, stat as stat2 } from "fs/promises";
|
|
54
|
+
import { delimiter, isAbsolute, join as join2, resolve as resolve2 } from "path";
|
|
55
|
+
|
|
56
|
+
// src/support/bundle.ts
|
|
57
|
+
import { constants } from "fs";
|
|
58
|
+
import { open } from "fs/promises";
|
|
59
|
+
import {
|
|
60
|
+
access,
|
|
61
|
+
mkdir,
|
|
62
|
+
readdir,
|
|
63
|
+
readFile,
|
|
64
|
+
stat,
|
|
65
|
+
writeFile
|
|
66
|
+
} from "fs/promises";
|
|
67
|
+
import { dirname, join, relative, resolve, sep } from "path";
|
|
68
|
+
var SUPPORT_BUNDLE_LIMITS = {
|
|
69
|
+
maxRuns: 3,
|
|
70
|
+
maxLogBytes: 64 * 1024,
|
|
71
|
+
maxLogLines: 500,
|
|
72
|
+
maxBundleBytes: 5 * 1024 * 1024
|
|
73
|
+
};
|
|
74
|
+
async function createSupportBundle(input) {
|
|
75
|
+
const createdAt = (input.now ?? /* @__PURE__ */ new Date()).toISOString();
|
|
76
|
+
const root = resolveBundleRoot(input.outputPath, input.repoRoot, createdAt);
|
|
77
|
+
await ensureWritableBundleRoot(root);
|
|
78
|
+
const state = {
|
|
79
|
+
root,
|
|
80
|
+
writtenBytes: 0,
|
|
81
|
+
manifest: {
|
|
82
|
+
version: 1,
|
|
83
|
+
createdAt,
|
|
84
|
+
projectId: input.projectId,
|
|
85
|
+
configDir: resolve(input.configDir),
|
|
86
|
+
included: [],
|
|
87
|
+
missing: [],
|
|
88
|
+
redactions: [],
|
|
89
|
+
truncations: [],
|
|
90
|
+
limits: SUPPORT_BUNDLE_LIMITS,
|
|
91
|
+
bundleBytes: {
|
|
92
|
+
written: 0,
|
|
93
|
+
softMax: SUPPORT_BUNDLE_LIMITS.maxBundleBytes,
|
|
94
|
+
exceeded: false
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
await writeJsonArtifact(state, "doctor.json", input.doctorReport);
|
|
99
|
+
await copyJsonArtifact(
|
|
100
|
+
state,
|
|
101
|
+
configFilePath(input.configDir),
|
|
102
|
+
"config/config.json"
|
|
103
|
+
);
|
|
104
|
+
await copyJsonArtifact(
|
|
105
|
+
state,
|
|
106
|
+
projectConfigPath(input.configDir, input.projectId),
|
|
107
|
+
"config/project.json"
|
|
108
|
+
);
|
|
109
|
+
await copyTextArtifact(
|
|
110
|
+
state,
|
|
111
|
+
join(input.repoRoot, "WORKFLOW.md"),
|
|
112
|
+
"repo/WORKFLOW.md",
|
|
113
|
+
{ bounded: false }
|
|
114
|
+
);
|
|
115
|
+
const runtimeRoot = await resolveRuntimeArtifactRoot(
|
|
116
|
+
input.configDir,
|
|
117
|
+
input.projectId
|
|
118
|
+
);
|
|
119
|
+
await copyJsonArtifact(
|
|
120
|
+
state,
|
|
121
|
+
join(runtimeRoot, "status.json"),
|
|
122
|
+
"runtime/status.json"
|
|
123
|
+
);
|
|
124
|
+
await copyJsonArtifact(
|
|
125
|
+
state,
|
|
126
|
+
join(runtimeRoot, "issues.json"),
|
|
127
|
+
"runtime/issues.json"
|
|
128
|
+
);
|
|
129
|
+
await copyTextArtifact(
|
|
130
|
+
state,
|
|
131
|
+
orchestratorLogPath(input.configDir, input.projectId),
|
|
132
|
+
"runtime/orchestrator.log.tail",
|
|
133
|
+
{ bounded: true }
|
|
134
|
+
);
|
|
135
|
+
const recentRuns = await listRecentRuns(input.configDir, input.projectId);
|
|
136
|
+
if (recentRuns.length === 0) {
|
|
137
|
+
state.manifest.missing.push({
|
|
138
|
+
path: "runs",
|
|
139
|
+
reason: "No run records were found for the selected project."
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
for (const recentRun of recentRuns) {
|
|
143
|
+
const destinationDir = `runs/${sanitizePathSegment(recentRun.runId)}`;
|
|
144
|
+
if (recentRun.run) {
|
|
145
|
+
await writeJsonArtifact(
|
|
146
|
+
state,
|
|
147
|
+
`${destinationDir}/run.json`,
|
|
148
|
+
recentRun.run
|
|
149
|
+
);
|
|
150
|
+
} else {
|
|
151
|
+
state.manifest.missing.push({
|
|
152
|
+
path: `${destinationDir}/run.json`,
|
|
153
|
+
reason: "Run metadata is missing or unreadable."
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
await copyTextArtifact(
|
|
157
|
+
state,
|
|
158
|
+
join(recentRun.runDir, "events.ndjson"),
|
|
159
|
+
`${destinationDir}/events.ndjson.tail`,
|
|
160
|
+
{ bounded: true }
|
|
161
|
+
);
|
|
162
|
+
await copyTextArtifact(
|
|
163
|
+
state,
|
|
164
|
+
join(recentRun.runDir, "worker.log"),
|
|
165
|
+
`${destinationDir}/worker.log.tail`,
|
|
166
|
+
{ bounded: true }
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
await writeManifest(state);
|
|
170
|
+
return buildSummary(state);
|
|
171
|
+
}
|
|
172
|
+
function resolveBundleRoot(outputPath, repoRoot, createdAt) {
|
|
173
|
+
if (outputPath) {
|
|
174
|
+
return resolve(repoRoot, outputPath);
|
|
175
|
+
}
|
|
176
|
+
const timestamp = createdAt.replace(/[:.]/g, "").replace("T", "-");
|
|
177
|
+
return resolve(repoRoot, `gh-symphony-support-bundle-${timestamp}`);
|
|
178
|
+
}
|
|
179
|
+
async function ensureWritableBundleRoot(root) {
|
|
180
|
+
await mkdir(root, { recursive: true });
|
|
181
|
+
const target = await stat(root);
|
|
182
|
+
if (!target.isDirectory()) {
|
|
183
|
+
throw new Error(`Bundle output path is not a directory: ${root}`);
|
|
184
|
+
}
|
|
185
|
+
await access(root, constants.W_OK);
|
|
186
|
+
}
|
|
187
|
+
async function writeJsonArtifact(state, relativePath, value) {
|
|
188
|
+
const redacted = redactObservabilityDiagnosticsWithStats(value);
|
|
189
|
+
addRedactions(state, redacted.redactions);
|
|
190
|
+
await writeBundleFile(
|
|
191
|
+
state,
|
|
192
|
+
relativePath,
|
|
193
|
+
JSON.stringify(redacted.value, null, 2) + "\n"
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
async function copyJsonArtifact(state, sourcePath, destinationPath) {
|
|
197
|
+
let raw;
|
|
198
|
+
try {
|
|
199
|
+
raw = await readFile(sourcePath, "utf8");
|
|
200
|
+
} catch (error) {
|
|
201
|
+
recordMissing(state, destinationPath, sourcePath, error);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
try {
|
|
205
|
+
const parsed = JSON.parse(raw);
|
|
206
|
+
await writeJsonArtifact(state, destinationPath, parsed);
|
|
207
|
+
} catch (error) {
|
|
208
|
+
if (error instanceof SyntaxError) {
|
|
209
|
+
recordMissing(state, destinationPath, sourcePath, error);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
throw new Error(
|
|
213
|
+
`Failed to redact/write JSON artifact ${sourcePath}: ${formatError(error)}`
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
async function copyTextArtifact(state, sourcePath, destinationPath, options) {
|
|
218
|
+
let captured;
|
|
219
|
+
try {
|
|
220
|
+
captured = options.bounded ? await readBoundedTail(sourcePath) : { text: await readFile(sourcePath, "utf8"), truncated: false };
|
|
221
|
+
} catch (error) {
|
|
222
|
+
recordMissing(state, destinationPath, sourcePath, error);
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
const redacted = redactObservabilityTextWithStats(captured.text);
|
|
226
|
+
addRedactions(state, redacted.redactions);
|
|
227
|
+
await writeBundleFile(state, destinationPath, redacted.value);
|
|
228
|
+
if (captured.truncated) {
|
|
229
|
+
state.manifest.truncations.push({
|
|
230
|
+
path: destinationPath,
|
|
231
|
+
originalBytes: captured.originalBytes,
|
|
232
|
+
writtenBytes: Buffer.byteLength(redacted.value, "utf8"),
|
|
233
|
+
maxBytes: SUPPORT_BUNDLE_LIMITS.maxLogBytes,
|
|
234
|
+
maxLines: SUPPORT_BUNDLE_LIMITS.maxLogLines,
|
|
235
|
+
reason: captured.reason ?? "bounded_tail"
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
async function readBoundedTail(sourcePath) {
|
|
240
|
+
const handle = await open(sourcePath, "r");
|
|
241
|
+
try {
|
|
242
|
+
const stats = await handle.stat();
|
|
243
|
+
const start = Math.max(0, stats.size - SUPPORT_BUNDLE_LIMITS.maxLogBytes);
|
|
244
|
+
const length = stats.size - start;
|
|
245
|
+
const buffer = Buffer.alloc(length);
|
|
246
|
+
if (length > 0) {
|
|
247
|
+
await handle.read(buffer, 0, length, start);
|
|
248
|
+
}
|
|
249
|
+
let text = buffer.toString("utf8");
|
|
250
|
+
let truncated = start > 0;
|
|
251
|
+
const reasons = [];
|
|
252
|
+
if (start > 0) {
|
|
253
|
+
reasons.push("maxLogBytes");
|
|
254
|
+
const firstNewline = text.indexOf("\n");
|
|
255
|
+
if (firstNewline >= 0) {
|
|
256
|
+
text = text.slice(firstNewline + 1);
|
|
257
|
+
} else {
|
|
258
|
+
reasons.push("partialLine");
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
const lines = text.split(/\r?\n/);
|
|
262
|
+
if (lines.length > SUPPORT_BUNDLE_LIMITS.maxLogLines) {
|
|
263
|
+
text = lines.slice(-SUPPORT_BUNDLE_LIMITS.maxLogLines).join("\n");
|
|
264
|
+
truncated = true;
|
|
265
|
+
reasons.push("maxLogLines");
|
|
266
|
+
}
|
|
267
|
+
return {
|
|
268
|
+
text,
|
|
269
|
+
truncated,
|
|
270
|
+
originalBytes: stats.size,
|
|
271
|
+
reason: reasons.join(",") || void 0
|
|
272
|
+
};
|
|
273
|
+
} finally {
|
|
274
|
+
await handle.close();
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
async function writeBundleFile(state, relativePath, content) {
|
|
278
|
+
const target = resolveBundlePath(state.root, relativePath);
|
|
279
|
+
await mkdir(dirname(target), { recursive: true });
|
|
280
|
+
await writeFile(target, content, "utf8");
|
|
281
|
+
state.writtenBytes += Buffer.byteLength(content, "utf8");
|
|
282
|
+
state.manifest.included.push(relativePath);
|
|
283
|
+
}
|
|
284
|
+
async function writeManifest(state) {
|
|
285
|
+
if (!state.manifest.included.includes("manifest.json")) {
|
|
286
|
+
state.manifest.included.push("manifest.json");
|
|
287
|
+
}
|
|
288
|
+
state.manifest.bundleBytes = {
|
|
289
|
+
written: state.writtenBytes,
|
|
290
|
+
softMax: SUPPORT_BUNDLE_LIMITS.maxBundleBytes,
|
|
291
|
+
exceeded: state.writtenBytes > SUPPORT_BUNDLE_LIMITS.maxBundleBytes
|
|
292
|
+
};
|
|
293
|
+
const target = resolveBundlePath(state.root, "manifest.json");
|
|
294
|
+
await writeFile(
|
|
295
|
+
target,
|
|
296
|
+
JSON.stringify(state.manifest, null, 2) + "\n",
|
|
297
|
+
"utf8"
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
function resolveBundlePath(root, relativePath) {
|
|
301
|
+
const target = resolve(root, relativePath);
|
|
302
|
+
const relativeTarget = relative(root, target);
|
|
303
|
+
if (relativeTarget === "" || relativeTarget.startsWith("..") || relativeTarget.includes(`..${sep}`)) {
|
|
304
|
+
throw new Error(`Refusing to write outside bundle root: ${relativePath}`);
|
|
305
|
+
}
|
|
306
|
+
return target;
|
|
307
|
+
}
|
|
308
|
+
function recordMissing(state, destinationPath, sourcePath, error) {
|
|
309
|
+
state.manifest.missing.push({
|
|
310
|
+
path: destinationPath,
|
|
311
|
+
reason: `${sourcePath}: ${formatError(error)}`
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
function addRedactions(state, redactions) {
|
|
315
|
+
const existing = new Map(
|
|
316
|
+
state.manifest.redactions.map((entry) => [entry.class, entry.count])
|
|
317
|
+
);
|
|
318
|
+
for (const redaction of redactions) {
|
|
319
|
+
existing.set(
|
|
320
|
+
redaction.class,
|
|
321
|
+
(existing.get(redaction.class) ?? 0) + redaction.count
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
state.manifest.redactions = Array.from(existing.entries()).map(([redactionClass, count]) => ({ class: redactionClass, count })).sort((left, right) => left.class.localeCompare(right.class));
|
|
325
|
+
}
|
|
326
|
+
async function resolveRuntimeArtifactRoot(configDir, projectId) {
|
|
327
|
+
const candidates = [
|
|
328
|
+
resolve(configDir),
|
|
329
|
+
resolve(configDir, "projects", projectId)
|
|
330
|
+
];
|
|
331
|
+
for (const candidate of candidates) {
|
|
332
|
+
try {
|
|
333
|
+
await access(join(candidate, "status.json"), constants.R_OK);
|
|
334
|
+
return candidate;
|
|
335
|
+
} catch {
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return candidates[0];
|
|
340
|
+
}
|
|
341
|
+
async function listRecentRuns(configDir, projectId) {
|
|
342
|
+
const runsDirs = [
|
|
343
|
+
resolve(configDir, "runs"),
|
|
344
|
+
resolve(configDir, "projects", projectId, "runs")
|
|
345
|
+
];
|
|
346
|
+
const seen = /* @__PURE__ */ new Set();
|
|
347
|
+
const runs = [];
|
|
348
|
+
for (const runsDir of runsDirs) {
|
|
349
|
+
let entries;
|
|
350
|
+
try {
|
|
351
|
+
entries = await readdir(runsDir);
|
|
352
|
+
} catch {
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
for (const entry of entries) {
|
|
356
|
+
const runId = sanitizePathSegment(entry);
|
|
357
|
+
if (seen.has(runId)) {
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
const runDir = join(runsDir, entry);
|
|
361
|
+
const runJsonPath = join(runDir, "run.json");
|
|
362
|
+
let run = null;
|
|
363
|
+
let updatedAt = "";
|
|
364
|
+
let active = false;
|
|
365
|
+
try {
|
|
366
|
+
const raw = await readFile(runJsonPath, "utf8");
|
|
367
|
+
run = JSON.parse(raw);
|
|
368
|
+
updatedAt = stringField(run, "updatedAt") ?? stringField(run, "endedAt") ?? stringField(run, "startedAt") ?? "";
|
|
369
|
+
const statusValue = stringField(run, "status");
|
|
370
|
+
active = statusValue === "running" || statusValue === "retrying";
|
|
371
|
+
} catch {
|
|
372
|
+
try {
|
|
373
|
+
const metadata = await stat(runDir);
|
|
374
|
+
updatedAt = metadata.mtime.toISOString();
|
|
375
|
+
} catch {
|
|
376
|
+
updatedAt = "";
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
seen.add(runId);
|
|
380
|
+
runs.push({ runId, runDir, run, updatedAt, active });
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
return runs.sort((left, right) => {
|
|
384
|
+
if (left.active !== right.active) {
|
|
385
|
+
return left.active ? -1 : 1;
|
|
386
|
+
}
|
|
387
|
+
return right.updatedAt.localeCompare(left.updatedAt);
|
|
388
|
+
}).slice(0, SUPPORT_BUNDLE_LIMITS.maxRuns);
|
|
389
|
+
}
|
|
390
|
+
function stringField(value, key) {
|
|
391
|
+
const field = value[key];
|
|
392
|
+
return typeof field === "string" ? field : null;
|
|
393
|
+
}
|
|
394
|
+
function sanitizePathSegment(value) {
|
|
395
|
+
return value.replace(/[^A-Za-z0-9._-]/g, "_");
|
|
396
|
+
}
|
|
397
|
+
function buildSummary(state) {
|
|
398
|
+
const redactionCount = state.manifest.redactions.reduce(
|
|
399
|
+
(sum, entry) => sum + entry.count,
|
|
400
|
+
0
|
|
401
|
+
);
|
|
402
|
+
return {
|
|
403
|
+
outputPath: state.root,
|
|
404
|
+
projectId: state.manifest.projectId,
|
|
405
|
+
includedCount: state.manifest.included.length,
|
|
406
|
+
missingCount: state.manifest.missing.length,
|
|
407
|
+
redactionCount,
|
|
408
|
+
redactionClasses: state.manifest.redactions,
|
|
409
|
+
truncationCount: state.manifest.truncations.length,
|
|
410
|
+
manifestPath: join(state.root, "manifest.json")
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
function formatError(error) {
|
|
414
|
+
if (error && typeof error === "object" && "code" in error && typeof error.code === "string") {
|
|
415
|
+
return error.code;
|
|
416
|
+
}
|
|
417
|
+
return error instanceof Error ? error.message : String(error);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// src/commands/doctor.ts
|
|
46
421
|
var DEFAULT_DEPENDENCIES = {
|
|
47
422
|
checkGhInstalled,
|
|
48
423
|
checkGhAuthenticated,
|
|
@@ -53,12 +428,13 @@ var DEFAULT_DEPENDENCIES = {
|
|
|
53
428
|
inspectManagedProjectSelection,
|
|
54
429
|
createClient,
|
|
55
430
|
getProjectDetail,
|
|
431
|
+
listRepositoryLabels,
|
|
56
432
|
fetchProjectIssues: fetchGithubProjectIssues,
|
|
57
433
|
fetchProjectIssue: fetchGithubProjectIssueByRepositoryAndNumber,
|
|
58
|
-
readFile,
|
|
59
|
-
access,
|
|
60
|
-
mkdir,
|
|
61
|
-
stat,
|
|
434
|
+
readFile: readFile2,
|
|
435
|
+
access: access2,
|
|
436
|
+
mkdir: mkdir2,
|
|
437
|
+
stat: stat2,
|
|
62
438
|
parseWorkflowMarkdown,
|
|
63
439
|
execFileSync,
|
|
64
440
|
runGhAuthLogin,
|
|
@@ -76,9 +452,9 @@ var DEFAULT_DEPENDENCIES = {
|
|
|
76
452
|
};
|
|
77
453
|
var MINIMUM_NODE_MAJOR = 24;
|
|
78
454
|
var MINIMUM_NODE_VERSION = `v${MINIMUM_NODE_MAJOR}.0.0`;
|
|
79
|
-
var DOCTOR_USAGE = "Usage: gh-symphony doctor [--project-id <project-id>] [--fix] [--smoke] [--issue <owner/repo#number>]";
|
|
455
|
+
var DOCTOR_USAGE = "Usage: gh-symphony doctor [--project-id <project-id>] [--fix] [--smoke] [--issue <owner/repo#number>] [--bundle [path]]";
|
|
80
456
|
function parseDoctorArgs(args) {
|
|
81
|
-
const parsed = { fix: false, smoke: false };
|
|
457
|
+
const parsed = { fix: false, smoke: false, bundle: false };
|
|
82
458
|
for (let i = 0; i < args.length; i += 1) {
|
|
83
459
|
const arg = args[i];
|
|
84
460
|
if (arg === "--project" || arg === "--project-id") {
|
|
@@ -99,6 +475,15 @@ function parseDoctorArgs(args) {
|
|
|
99
475
|
parsed.smoke = true;
|
|
100
476
|
continue;
|
|
101
477
|
}
|
|
478
|
+
if (arg === "--bundle") {
|
|
479
|
+
parsed.bundle = true;
|
|
480
|
+
const value = args[i + 1];
|
|
481
|
+
if (value && !value.startsWith("-")) {
|
|
482
|
+
parsed.bundlePath = value;
|
|
483
|
+
i += 1;
|
|
484
|
+
}
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
102
487
|
if (arg === "--issue") {
|
|
103
488
|
const value = args[i + 1];
|
|
104
489
|
if (!value || value.startsWith("-")) {
|
|
@@ -113,10 +498,15 @@ function parseDoctorArgs(args) {
|
|
|
113
498
|
parsed.error = `Unknown option '${arg}'`;
|
|
114
499
|
return parsed;
|
|
115
500
|
}
|
|
501
|
+
parsed.error = `Unexpected argument '${arg}'`;
|
|
502
|
+
return parsed;
|
|
116
503
|
}
|
|
117
504
|
if (parsed.issue && !parsed.smoke) {
|
|
118
505
|
parsed.error = "Option '--issue' requires '--smoke'";
|
|
119
506
|
}
|
|
507
|
+
if (parsed.bundle && parsed.fix) {
|
|
508
|
+
parsed.error = "Option '--fix' cannot be used with '--bundle'";
|
|
509
|
+
}
|
|
120
510
|
return parsed;
|
|
121
511
|
}
|
|
122
512
|
function passCheck(id, title, summary, details) {
|
|
@@ -161,7 +551,7 @@ async function inspectPathState(targetPath, deps) {
|
|
|
161
551
|
};
|
|
162
552
|
}
|
|
163
553
|
try {
|
|
164
|
-
await deps.access(targetPath,
|
|
554
|
+
await deps.access(targetPath, constants2.W_OK);
|
|
165
555
|
return {
|
|
166
556
|
exists: true,
|
|
167
557
|
isDirectory: true,
|
|
@@ -255,7 +645,7 @@ async function commandExistsOnPath(binary, deps) {
|
|
|
255
645
|
if (isAbsolute(binary) || binary.includes("/") || binary.includes("\\")) {
|
|
256
646
|
for (const candidate of candidates) {
|
|
257
647
|
try {
|
|
258
|
-
await deps.access(
|
|
648
|
+
await deps.access(resolve2(candidate), constants2.X_OK);
|
|
259
649
|
return true;
|
|
260
650
|
} catch {
|
|
261
651
|
continue;
|
|
@@ -268,9 +658,9 @@ async function commandExistsOnPath(binary, deps) {
|
|
|
268
658
|
continue;
|
|
269
659
|
}
|
|
270
660
|
for (const command of candidates) {
|
|
271
|
-
const candidate =
|
|
661
|
+
const candidate = join2(segment, command);
|
|
272
662
|
try {
|
|
273
|
-
await deps.access(candidate,
|
|
663
|
+
await deps.access(candidate, constants2.X_OK);
|
|
274
664
|
return true;
|
|
275
665
|
} catch {
|
|
276
666
|
continue;
|
|
@@ -330,7 +720,7 @@ async function checkGitInstallation(deps) {
|
|
|
330
720
|
}
|
|
331
721
|
}
|
|
332
722
|
async function checkWorkflow(repoRoot, deps) {
|
|
333
|
-
const workflowPath =
|
|
723
|
+
const workflowPath = join2(repoRoot, "WORKFLOW.md");
|
|
334
724
|
let markdown;
|
|
335
725
|
try {
|
|
336
726
|
markdown = await deps.readFile(workflowPath, "utf8");
|
|
@@ -371,10 +761,140 @@ function buildGithubTrackerConfig(input) {
|
|
|
371
761
|
apiUrl: input.projectConfig.projectConfig.tracker.apiUrl,
|
|
372
762
|
lifecycle: input.workflow.lifecycle,
|
|
373
763
|
assignedOnly: settings?.assignedOnly === true,
|
|
764
|
+
priority: input.workflow.tracker.priority,
|
|
374
765
|
priorityFieldName: typeof settings?.priorityFieldName === "string" ? settings.priorityFieldName : void 0,
|
|
375
766
|
timeoutMs: typeof settings?.timeoutMs === "number" ? settings.timeoutMs : void 0
|
|
376
767
|
};
|
|
377
768
|
}
|
|
769
|
+
async function buildPriorityMappingChecks(input) {
|
|
770
|
+
if (input.workflow.status !== "pass") {
|
|
771
|
+
return [];
|
|
772
|
+
}
|
|
773
|
+
const configDiagnostics = buildPriorityConfigDiagnostics(
|
|
774
|
+
input.workflow.workflow
|
|
775
|
+
);
|
|
776
|
+
const checks = configDiagnostics.map(
|
|
777
|
+
(diagnostic) => warnCheck(
|
|
778
|
+
"priority_mapping",
|
|
779
|
+
diagnostic.title,
|
|
780
|
+
diagnostic.summary,
|
|
781
|
+
diagnostic.remediation,
|
|
782
|
+
diagnostic.details
|
|
783
|
+
)
|
|
784
|
+
);
|
|
785
|
+
const priority = input.workflow.workflow.tracker.priority;
|
|
786
|
+
const parsedWorkflow = input.workflow.workflow;
|
|
787
|
+
if (input.workflow.workflow.tracker.kind !== "github-project" || !priority || priority.source === "disabled") {
|
|
788
|
+
if (checks.length === 0) {
|
|
789
|
+
checks.push(
|
|
790
|
+
passCheck(
|
|
791
|
+
"priority_mapping",
|
|
792
|
+
"Priority mapping",
|
|
793
|
+
priority?.source === "disabled" ? "Explicit priority mapping is disabled; dispatch priority resolves to null." : "No explicit priority mapping drift checks are required.",
|
|
794
|
+
{ source: priority?.source ?? null }
|
|
795
|
+
)
|
|
796
|
+
);
|
|
797
|
+
}
|
|
798
|
+
return checks;
|
|
799
|
+
}
|
|
800
|
+
if (!input.auth || !input.selection || input.selection.kind !== "resolved" || !input.projectDetail || !input.projectBindingId) {
|
|
801
|
+
checks.push(
|
|
802
|
+
warnCheck(
|
|
803
|
+
"priority_mapping",
|
|
804
|
+
"Priority mapping drift",
|
|
805
|
+
"Live priority mapping drift checks could not run because GitHub authentication, managed project selection, or project resolution is unavailable.",
|
|
806
|
+
"Fix the prerequisite doctor checks, then re-run 'gh-symphony doctor'.",
|
|
807
|
+
{
|
|
808
|
+
blockedBy: [
|
|
809
|
+
...!input.auth ? ["gh_authentication"] : [],
|
|
810
|
+
...!input.selection || input.selection.kind !== "resolved" ? ["managed_project"] : [],
|
|
811
|
+
...!input.projectDetail || !input.projectBindingId ? ["github_project_resolution"] : []
|
|
812
|
+
]
|
|
813
|
+
}
|
|
814
|
+
)
|
|
815
|
+
);
|
|
816
|
+
return checks;
|
|
817
|
+
}
|
|
818
|
+
const client = input.deps.createClient(input.auth.token, {
|
|
819
|
+
apiUrl: input.selection.projectConfig.tracker.apiUrl
|
|
820
|
+
});
|
|
821
|
+
let repositoryLabels = priority.source === "labels" ? [] : null;
|
|
822
|
+
if (priority.source === "labels") {
|
|
823
|
+
try {
|
|
824
|
+
repositoryLabels = await Promise.all(
|
|
825
|
+
input.projectDetail.linkedRepositories.map(async (repository) => ({
|
|
826
|
+
repository: `${repository.owner}/${repository.name}`,
|
|
827
|
+
labels: (await input.deps.listRepositoryLabels(
|
|
828
|
+
client,
|
|
829
|
+
repository.owner,
|
|
830
|
+
repository.name
|
|
831
|
+
)).map((label) => label.name)
|
|
832
|
+
}))
|
|
833
|
+
);
|
|
834
|
+
} catch (error) {
|
|
835
|
+
checks.push(
|
|
836
|
+
warnCheck(
|
|
837
|
+
"priority_mapping",
|
|
838
|
+
"Priority label drift",
|
|
839
|
+
"Live repository labels could not be read for priority mapping drift checks.",
|
|
840
|
+
"Confirm GitHub token repository access and re-run 'gh-symphony doctor'.",
|
|
841
|
+
{ error: formatSmokeError(error) }
|
|
842
|
+
)
|
|
843
|
+
);
|
|
844
|
+
repositoryLabels = null;
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
let activeIssues = [];
|
|
848
|
+
try {
|
|
849
|
+
const trackerConfig = buildGithubTrackerConfig({
|
|
850
|
+
projectConfig: input.selection,
|
|
851
|
+
bindingId: input.projectBindingId,
|
|
852
|
+
token: input.auth.token,
|
|
853
|
+
workflow: input.workflow.workflow
|
|
854
|
+
});
|
|
855
|
+
activeIssues = (await input.deps.fetchProjectIssues(trackerConfig)).filter(
|
|
856
|
+
(issue) => isActiveSmokeIssue(issue, parsedWorkflow)
|
|
857
|
+
);
|
|
858
|
+
} catch (error) {
|
|
859
|
+
checks.push(
|
|
860
|
+
warnCheck(
|
|
861
|
+
"priority_mapping",
|
|
862
|
+
"Active priority drift",
|
|
863
|
+
"Active issues could not be read for priority mapping drift checks.",
|
|
864
|
+
"Confirm GitHub token scopes, project visibility, and network access, then re-run 'gh-symphony doctor'.",
|
|
865
|
+
{ error: formatSmokeError(error) }
|
|
866
|
+
)
|
|
867
|
+
);
|
|
868
|
+
}
|
|
869
|
+
const driftDiagnostics = buildPriorityDriftDiagnostics({
|
|
870
|
+
workflow: parsedWorkflow,
|
|
871
|
+
projectDetail: input.projectDetail,
|
|
872
|
+
repositoryLabels,
|
|
873
|
+
activeIssues
|
|
874
|
+
});
|
|
875
|
+
checks.push(
|
|
876
|
+
...driftDiagnostics.map(
|
|
877
|
+
(diagnostic) => warnCheck(
|
|
878
|
+
"priority_mapping",
|
|
879
|
+
diagnostic.title,
|
|
880
|
+
diagnostic.summary,
|
|
881
|
+
diagnostic.remediation,
|
|
882
|
+
diagnostic.details
|
|
883
|
+
)
|
|
884
|
+
)
|
|
885
|
+
);
|
|
886
|
+
if (checks.length === 0) {
|
|
887
|
+
checks.push(
|
|
888
|
+
passCheck(
|
|
889
|
+
"priority_mapping",
|
|
890
|
+
"Priority mapping",
|
|
891
|
+
"Explicit priority mapping matches the live Project/repository state inspected by doctor.",
|
|
892
|
+
{ source: priority.source }
|
|
893
|
+
)
|
|
894
|
+
);
|
|
895
|
+
}
|
|
896
|
+
return checks;
|
|
897
|
+
}
|
|
378
898
|
function isActiveSmokeIssue(issue, workflow) {
|
|
379
899
|
const normalized = issue.state.trim().toLowerCase();
|
|
380
900
|
return workflow.lifecycle.activeStates.some(
|
|
@@ -432,9 +952,9 @@ async function buildHookChecks(repoRoot, workflow, deps) {
|
|
|
432
952
|
inline += 1;
|
|
433
953
|
continue;
|
|
434
954
|
}
|
|
435
|
-
const path = isAbsolute(command) ? command :
|
|
955
|
+
const path = isAbsolute(command) ? command : resolve2(repoRoot, command);
|
|
436
956
|
try {
|
|
437
|
-
await deps.access(path,
|
|
957
|
+
await deps.access(path, constants2.F_OK);
|
|
438
958
|
checked.push({ hook, command, path });
|
|
439
959
|
} catch {
|
|
440
960
|
unresolved.push({ hook, command, path });
|
|
@@ -447,7 +967,13 @@ async function buildHookChecks(repoRoot, workflow, deps) {
|
|
|
447
967
|
"Workflow hook paths",
|
|
448
968
|
`Unresolved WORKFLOW.md hook path${unresolved.length === 1 ? "" : "s"}: ${unresolved.map((entry) => `${entry.hook}=${entry.command}`).join(", ")}.`,
|
|
449
969
|
"Create the referenced hook script(s), fix the hook path(s), or replace them with inline commands.",
|
|
450
|
-
{
|
|
970
|
+
{
|
|
971
|
+
configured: configured.length,
|
|
972
|
+
pathsChecked: checked.length,
|
|
973
|
+
inline,
|
|
974
|
+
unresolved,
|
|
975
|
+
checked
|
|
976
|
+
}
|
|
451
977
|
)
|
|
452
978
|
];
|
|
453
979
|
}
|
|
@@ -458,7 +984,12 @@ async function buildHookChecks(repoRoot, workflow, deps) {
|
|
|
458
984
|
"workflow_hooks",
|
|
459
985
|
"Workflow hook paths",
|
|
460
986
|
`${pathSummary}${inlineSummary}`,
|
|
461
|
-
{
|
|
987
|
+
{
|
|
988
|
+
configured: configured.length,
|
|
989
|
+
pathsChecked: checked.length,
|
|
990
|
+
inline,
|
|
991
|
+
checked
|
|
992
|
+
}
|
|
462
993
|
)
|
|
463
994
|
];
|
|
464
995
|
}
|
|
@@ -1145,6 +1676,16 @@ ${DOCTOR_USAGE}`);
|
|
|
1145
1676
|
)
|
|
1146
1677
|
);
|
|
1147
1678
|
}
|
|
1679
|
+
checks.push(
|
|
1680
|
+
...await buildPriorityMappingChecks({
|
|
1681
|
+
auth,
|
|
1682
|
+
selection: resolvedProjectConfig,
|
|
1683
|
+
workflow,
|
|
1684
|
+
projectDetail: resolvedGithubProjectDetail,
|
|
1685
|
+
projectBindingId: resolvedGithubProjectBindingId,
|
|
1686
|
+
deps
|
|
1687
|
+
})
|
|
1688
|
+
);
|
|
1148
1689
|
if (parsedArgs.smoke) {
|
|
1149
1690
|
checks.push(
|
|
1150
1691
|
...await buildDoctorSmokeChecks({
|
|
@@ -1268,7 +1809,7 @@ async function ensureDirectoryRemediation(check, deps) {
|
|
|
1268
1809
|
}
|
|
1269
1810
|
try {
|
|
1270
1811
|
await deps.mkdir(pathValue, { recursive: true });
|
|
1271
|
-
await deps.access(pathValue,
|
|
1812
|
+
await deps.access(pathValue, constants2.W_OK);
|
|
1272
1813
|
const target = await deps.stat(pathValue);
|
|
1273
1814
|
if (!target.isDirectory()) {
|
|
1274
1815
|
return remediationStep(
|
|
@@ -1564,6 +2105,19 @@ function renderTextReport(report) {
|
|
|
1564
2105
|
);
|
|
1565
2106
|
return lines.join("\n");
|
|
1566
2107
|
}
|
|
2108
|
+
function renderBundleSummary(summary) {
|
|
2109
|
+
const redactionClasses = summary.redactionClasses.length === 0 ? "none" : summary.redactionClasses.map((entry) => `${entry.class}:${entry.count}`).join(", ");
|
|
2110
|
+
return [
|
|
2111
|
+
"gh-symphony doctor support bundle",
|
|
2112
|
+
`Output path: ${summary.outputPath}`,
|
|
2113
|
+
`Project: ${summary.projectId}`,
|
|
2114
|
+
`Included artifacts: ${summary.includedCount}`,
|
|
2115
|
+
`Missing artifacts: ${summary.missingCount}`,
|
|
2116
|
+
`Redactions: ${summary.redactionCount} (${redactionClasses})`,
|
|
2117
|
+
`Truncations: ${summary.truncationCount}`,
|
|
2118
|
+
`Manifest: ${summary.manifestPath}`
|
|
2119
|
+
].join("\n");
|
|
2120
|
+
}
|
|
1567
2121
|
async function runDoctorCommand(args, options, dependencies = {}) {
|
|
1568
2122
|
try {
|
|
1569
2123
|
const deps = { ...DEFAULT_DEPENDENCIES, ...dependencies };
|
|
@@ -1573,6 +2127,27 @@ async function runDoctorCommand(args, options, dependencies = {}) {
|
|
|
1573
2127
|
${DOCTOR_USAGE}`);
|
|
1574
2128
|
}
|
|
1575
2129
|
const initialReport = await runDoctorDiagnostics(options, args, deps);
|
|
2130
|
+
if (parsedArgs.bundle) {
|
|
2131
|
+
if (!initialReport.projectId) {
|
|
2132
|
+
throw new Error(
|
|
2133
|
+
"Cannot create a support bundle because no managed project was resolved."
|
|
2134
|
+
);
|
|
2135
|
+
}
|
|
2136
|
+
const summary = await createSupportBundle({
|
|
2137
|
+
configDir: options.configDir,
|
|
2138
|
+
projectId: initialReport.projectId,
|
|
2139
|
+
repoRoot: process.cwd(),
|
|
2140
|
+
outputPath: parsedArgs.bundlePath,
|
|
2141
|
+
doctorReport: initialReport
|
|
2142
|
+
});
|
|
2143
|
+
if (options.json) {
|
|
2144
|
+
process.stdout.write(JSON.stringify(summary, null, 2) + "\n");
|
|
2145
|
+
} else {
|
|
2146
|
+
process.stdout.write(renderBundleSummary(summary) + "\n");
|
|
2147
|
+
}
|
|
2148
|
+
process.exitCode = initialReport.ok ? 0 : 1;
|
|
2149
|
+
return;
|
|
2150
|
+
}
|
|
1576
2151
|
if (parsedArgs.fix) {
|
|
1577
2152
|
const remediation = {
|
|
1578
2153
|
attempted: true,
|