@gh-symphony/cli 0.1.4 → 0.2.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 +87 -0
- package/dist/{chunk-HT3FAJAO.js → chunk-27UZ6KX2.js} +273 -10
- package/dist/{chunk-EWTMSDCE.js → chunk-3SKN5L3I.js} +260 -18
- package/dist/{chunk-WOVNN5NW.js → chunk-4ICDSQCJ.js} +1 -0
- package/dist/{chunk-DW63WPRE.js → chunk-6PFFGP7S.js} +18 -3
- package/dist/{chunk-Z3NZOPLZ.js → chunk-BOM2BYZQ.js} +43 -0
- package/dist/{chunk-E7OCBNB2.js → chunk-FAU72YC2.js} +1 -1
- package/dist/{chunk-RHLUIMBN.js → chunk-PLBG7TZA.js} +306 -30
- package/dist/{chunk-6I753NYO.js → chunk-RZ3WO7OV.js} +1 -1
- package/dist/{repo-IH6UWE4H.js → chunk-X4QSP3AX.js} +5443 -6207
- package/dist/{config-cmd-2ADPUYWA.js → config-cmd-AOZVS6GU.js} +1 -1
- package/dist/{doctor-I32MANQ4.js → doctor-GDZSGJIT.js} +602 -26
- package/dist/index.js +12 -8
- package/dist/repo-SWEUWY4H.js +2693 -0
- package/dist/{setup-UJC2WYHQ.js → setup-XNOSJ3RX.js} +35 -32
- package/dist/{upgrade-XYHCUGHT.js → upgrade-ZWUAJLHK.js} +2 -2
- package/dist/{version-B2AYYGLM.js → version-PLQK6X2P.js} +1 -1
- package/dist/worker-entry.js +77 -9
- package/dist/{workflow-WSXHMO5B.js → workflow-2ERPNGRB.js} +7 -6
- package/package.json +3 -3
- package/dist/chunk-YIARPBOR.js +0 -1648
|
@@ -1,18 +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-27UZ6KX2.js";
|
|
9
|
+
import "./chunk-PLBG7TZA.js";
|
|
8
10
|
import {
|
|
9
11
|
fetchGithubProjectIssueByRepositoryAndNumber,
|
|
10
12
|
fetchGithubProjectIssues,
|
|
11
13
|
inspectManagedProjectSelection
|
|
12
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-X4QSP3AX.js";
|
|
15
|
+
import "./chunk-FAU72YC2.js";
|
|
13
16
|
import {
|
|
14
17
|
resolveRuntimeRoot
|
|
15
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-RZ3WO7OV.js";
|
|
16
19
|
import {
|
|
17
20
|
GitHubApiError,
|
|
18
21
|
REQUIRED_GH_SCOPES,
|
|
@@ -24,24 +27,397 @@ import {
|
|
|
24
27
|
getEnvGitHubToken,
|
|
25
28
|
getGhToken,
|
|
26
29
|
getProjectDetail,
|
|
30
|
+
listRepositoryLabels,
|
|
27
31
|
runGhAuthLogin,
|
|
28
32
|
runGhAuthRefresh,
|
|
29
33
|
validateGitHubToken
|
|
30
|
-
} from "./chunk-
|
|
34
|
+
} from "./chunk-BOM2BYZQ.js";
|
|
31
35
|
import {
|
|
32
36
|
isClaudeRuntimeCommand,
|
|
33
37
|
parseWorkflowMarkdown,
|
|
38
|
+
redactObservabilityDiagnosticsWithStats,
|
|
39
|
+
redactObservabilityTextWithStats,
|
|
34
40
|
resolveClaudeCommandBinary,
|
|
35
41
|
resolveRuntimeCommandBinary,
|
|
36
42
|
runClaudePreflight
|
|
37
|
-
} from "./chunk-
|
|
38
|
-
import
|
|
43
|
+
} from "./chunk-3SKN5L3I.js";
|
|
44
|
+
import {
|
|
45
|
+
configFilePath,
|
|
46
|
+
orchestratorLogPath,
|
|
47
|
+
projectConfigPath
|
|
48
|
+
} from "./chunk-4ICDSQCJ.js";
|
|
39
49
|
|
|
40
50
|
// src/commands/doctor.ts
|
|
41
|
-
import { constants } from "fs";
|
|
51
|
+
import { constants as constants2 } from "fs";
|
|
42
52
|
import { execFileSync, spawnSync } from "child_process";
|
|
43
|
-
import { access, mkdir, readFile, stat } from "fs/promises";
|
|
44
|
-
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
|
|
45
421
|
var DEFAULT_DEPENDENCIES = {
|
|
46
422
|
checkGhInstalled,
|
|
47
423
|
checkGhAuthenticated,
|
|
@@ -52,12 +428,13 @@ var DEFAULT_DEPENDENCIES = {
|
|
|
52
428
|
inspectManagedProjectSelection,
|
|
53
429
|
createClient,
|
|
54
430
|
getProjectDetail,
|
|
431
|
+
listRepositoryLabels,
|
|
55
432
|
fetchProjectIssues: fetchGithubProjectIssues,
|
|
56
433
|
fetchProjectIssue: fetchGithubProjectIssueByRepositoryAndNumber,
|
|
57
|
-
readFile,
|
|
58
|
-
access,
|
|
59
|
-
mkdir,
|
|
60
|
-
stat,
|
|
434
|
+
readFile: readFile2,
|
|
435
|
+
access: access2,
|
|
436
|
+
mkdir: mkdir2,
|
|
437
|
+
stat: stat2,
|
|
61
438
|
parseWorkflowMarkdown,
|
|
62
439
|
execFileSync,
|
|
63
440
|
runGhAuthLogin,
|
|
@@ -75,9 +452,9 @@ var DEFAULT_DEPENDENCIES = {
|
|
|
75
452
|
};
|
|
76
453
|
var MINIMUM_NODE_MAJOR = 24;
|
|
77
454
|
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>]";
|
|
455
|
+
var DOCTOR_USAGE = "Usage: gh-symphony doctor [--project-id <project-id>] [--fix] [--smoke] [--issue <owner/repo#number>] [--bundle [path]]";
|
|
79
456
|
function parseDoctorArgs(args) {
|
|
80
|
-
const parsed = { fix: false, smoke: false };
|
|
457
|
+
const parsed = { fix: false, smoke: false, bundle: false };
|
|
81
458
|
for (let i = 0; i < args.length; i += 1) {
|
|
82
459
|
const arg = args[i];
|
|
83
460
|
if (arg === "--project" || arg === "--project-id") {
|
|
@@ -98,6 +475,15 @@ function parseDoctorArgs(args) {
|
|
|
98
475
|
parsed.smoke = true;
|
|
99
476
|
continue;
|
|
100
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
|
+
}
|
|
101
487
|
if (arg === "--issue") {
|
|
102
488
|
const value = args[i + 1];
|
|
103
489
|
if (!value || value.startsWith("-")) {
|
|
@@ -112,10 +498,15 @@ function parseDoctorArgs(args) {
|
|
|
112
498
|
parsed.error = `Unknown option '${arg}'`;
|
|
113
499
|
return parsed;
|
|
114
500
|
}
|
|
501
|
+
parsed.error = `Unexpected argument '${arg}'`;
|
|
502
|
+
return parsed;
|
|
115
503
|
}
|
|
116
504
|
if (parsed.issue && !parsed.smoke) {
|
|
117
505
|
parsed.error = "Option '--issue' requires '--smoke'";
|
|
118
506
|
}
|
|
507
|
+
if (parsed.bundle && parsed.fix) {
|
|
508
|
+
parsed.error = "Option '--fix' cannot be used with '--bundle'";
|
|
509
|
+
}
|
|
119
510
|
return parsed;
|
|
120
511
|
}
|
|
121
512
|
function passCheck(id, title, summary, details) {
|
|
@@ -160,7 +551,7 @@ async function inspectPathState(targetPath, deps) {
|
|
|
160
551
|
};
|
|
161
552
|
}
|
|
162
553
|
try {
|
|
163
|
-
await deps.access(targetPath,
|
|
554
|
+
await deps.access(targetPath, constants2.W_OK);
|
|
164
555
|
return {
|
|
165
556
|
exists: true,
|
|
166
557
|
isDirectory: true,
|
|
@@ -254,7 +645,7 @@ async function commandExistsOnPath(binary, deps) {
|
|
|
254
645
|
if (isAbsolute(binary) || binary.includes("/") || binary.includes("\\")) {
|
|
255
646
|
for (const candidate of candidates) {
|
|
256
647
|
try {
|
|
257
|
-
await deps.access(
|
|
648
|
+
await deps.access(resolve2(candidate), constants2.X_OK);
|
|
258
649
|
return true;
|
|
259
650
|
} catch {
|
|
260
651
|
continue;
|
|
@@ -267,9 +658,9 @@ async function commandExistsOnPath(binary, deps) {
|
|
|
267
658
|
continue;
|
|
268
659
|
}
|
|
269
660
|
for (const command of candidates) {
|
|
270
|
-
const candidate =
|
|
661
|
+
const candidate = join2(segment, command);
|
|
271
662
|
try {
|
|
272
|
-
await deps.access(candidate,
|
|
663
|
+
await deps.access(candidate, constants2.X_OK);
|
|
273
664
|
return true;
|
|
274
665
|
} catch {
|
|
275
666
|
continue;
|
|
@@ -329,7 +720,7 @@ async function checkGitInstallation(deps) {
|
|
|
329
720
|
}
|
|
330
721
|
}
|
|
331
722
|
async function checkWorkflow(repoRoot, deps) {
|
|
332
|
-
const workflowPath =
|
|
723
|
+
const workflowPath = join2(repoRoot, "WORKFLOW.md");
|
|
333
724
|
let markdown;
|
|
334
725
|
try {
|
|
335
726
|
markdown = await deps.readFile(workflowPath, "utf8");
|
|
@@ -370,10 +761,140 @@ function buildGithubTrackerConfig(input) {
|
|
|
370
761
|
apiUrl: input.projectConfig.projectConfig.tracker.apiUrl,
|
|
371
762
|
lifecycle: input.workflow.lifecycle,
|
|
372
763
|
assignedOnly: settings?.assignedOnly === true,
|
|
764
|
+
priority: input.workflow.tracker.priority,
|
|
373
765
|
priorityFieldName: typeof settings?.priorityFieldName === "string" ? settings.priorityFieldName : void 0,
|
|
374
766
|
timeoutMs: typeof settings?.timeoutMs === "number" ? settings.timeoutMs : void 0
|
|
375
767
|
};
|
|
376
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
|
+
}
|
|
377
898
|
function isActiveSmokeIssue(issue, workflow) {
|
|
378
899
|
const normalized = issue.state.trim().toLowerCase();
|
|
379
900
|
return workflow.lifecycle.activeStates.some(
|
|
@@ -431,9 +952,9 @@ async function buildHookChecks(repoRoot, workflow, deps) {
|
|
|
431
952
|
inline += 1;
|
|
432
953
|
continue;
|
|
433
954
|
}
|
|
434
|
-
const path = isAbsolute(command) ? command :
|
|
955
|
+
const path = isAbsolute(command) ? command : resolve2(repoRoot, command);
|
|
435
956
|
try {
|
|
436
|
-
await deps.access(path,
|
|
957
|
+
await deps.access(path, constants2.F_OK);
|
|
437
958
|
checked.push({ hook, command, path });
|
|
438
959
|
} catch {
|
|
439
960
|
unresolved.push({ hook, command, path });
|
|
@@ -446,7 +967,13 @@ async function buildHookChecks(repoRoot, workflow, deps) {
|
|
|
446
967
|
"Workflow hook paths",
|
|
447
968
|
`Unresolved WORKFLOW.md hook path${unresolved.length === 1 ? "" : "s"}: ${unresolved.map((entry) => `${entry.hook}=${entry.command}`).join(", ")}.`,
|
|
448
969
|
"Create the referenced hook script(s), fix the hook path(s), or replace them with inline commands.",
|
|
449
|
-
{
|
|
970
|
+
{
|
|
971
|
+
configured: configured.length,
|
|
972
|
+
pathsChecked: checked.length,
|
|
973
|
+
inline,
|
|
974
|
+
unresolved,
|
|
975
|
+
checked
|
|
976
|
+
}
|
|
450
977
|
)
|
|
451
978
|
];
|
|
452
979
|
}
|
|
@@ -457,7 +984,12 @@ async function buildHookChecks(repoRoot, workflow, deps) {
|
|
|
457
984
|
"workflow_hooks",
|
|
458
985
|
"Workflow hook paths",
|
|
459
986
|
`${pathSummary}${inlineSummary}`,
|
|
460
|
-
{
|
|
987
|
+
{
|
|
988
|
+
configured: configured.length,
|
|
989
|
+
pathsChecked: checked.length,
|
|
990
|
+
inline,
|
|
991
|
+
checked
|
|
992
|
+
}
|
|
461
993
|
)
|
|
462
994
|
];
|
|
463
995
|
}
|
|
@@ -1144,6 +1676,16 @@ ${DOCTOR_USAGE}`);
|
|
|
1144
1676
|
)
|
|
1145
1677
|
);
|
|
1146
1678
|
}
|
|
1679
|
+
checks.push(
|
|
1680
|
+
...await buildPriorityMappingChecks({
|
|
1681
|
+
auth,
|
|
1682
|
+
selection: resolvedProjectConfig,
|
|
1683
|
+
workflow,
|
|
1684
|
+
projectDetail: resolvedGithubProjectDetail,
|
|
1685
|
+
projectBindingId: resolvedGithubProjectBindingId,
|
|
1686
|
+
deps
|
|
1687
|
+
})
|
|
1688
|
+
);
|
|
1147
1689
|
if (parsedArgs.smoke) {
|
|
1148
1690
|
checks.push(
|
|
1149
1691
|
...await buildDoctorSmokeChecks({
|
|
@@ -1267,7 +1809,7 @@ async function ensureDirectoryRemediation(check, deps) {
|
|
|
1267
1809
|
}
|
|
1268
1810
|
try {
|
|
1269
1811
|
await deps.mkdir(pathValue, { recursive: true });
|
|
1270
|
-
await deps.access(pathValue,
|
|
1812
|
+
await deps.access(pathValue, constants2.W_OK);
|
|
1271
1813
|
const target = await deps.stat(pathValue);
|
|
1272
1814
|
if (!target.isDirectory()) {
|
|
1273
1815
|
return remediationStep(
|
|
@@ -1563,6 +2105,19 @@ function renderTextReport(report) {
|
|
|
1563
2105
|
);
|
|
1564
2106
|
return lines.join("\n");
|
|
1565
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
|
+
}
|
|
1566
2121
|
async function runDoctorCommand(args, options, dependencies = {}) {
|
|
1567
2122
|
try {
|
|
1568
2123
|
const deps = { ...DEFAULT_DEPENDENCIES, ...dependencies };
|
|
@@ -1572,6 +2127,27 @@ async function runDoctorCommand(args, options, dependencies = {}) {
|
|
|
1572
2127
|
${DOCTOR_USAGE}`);
|
|
1573
2128
|
}
|
|
1574
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
|
+
}
|
|
1575
2151
|
if (parsedArgs.fix) {
|
|
1576
2152
|
const remediation = {
|
|
1577
2153
|
attempted: true,
|