@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.
@@ -1,532 +0,0 @@
1
- #!/usr/bin/env node
2
- import {
3
- GitHubApiError,
4
- createClient,
5
- getProjectDetail
6
- } from "./chunk-62L6QQE6.js";
7
- import {
8
- REQUIRED_GH_SCOPES,
9
- checkGhAuthenticated,
10
- checkGhInstalled,
11
- checkGhScopes,
12
- getGhToken
13
- } from "./chunk-7UBUBSMH.js";
14
- import {
15
- parseWorkflowMarkdown
16
- } from "./chunk-OL73UN2X.js";
17
- import {
18
- resolveRuntimeRoot
19
- } from "./chunk-5NV3LSAJ.js";
20
- import {
21
- inspectManagedProjectSelection
22
- } from "./chunk-C7G7RJ4G.js";
23
- import "./chunk-ROGRTUFI.js";
24
-
25
- // src/commands/doctor.ts
26
- import { constants } from "fs";
27
- import {
28
- access,
29
- mkdir,
30
- readFile,
31
- stat
32
- } from "fs/promises";
33
- import { delimiter, isAbsolute, join, resolve } from "path";
34
- var DEFAULT_DEPENDENCIES = {
35
- checkGhInstalled,
36
- checkGhAuthenticated,
37
- checkGhScopes,
38
- getGhToken,
39
- inspectManagedProjectSelection,
40
- createClient,
41
- getProjectDetail,
42
- readFile,
43
- access,
44
- mkdir,
45
- stat,
46
- parseWorkflowMarkdown,
47
- pathEnv: process.env.PATH,
48
- pathExtEnv: process.env.PATHEXT,
49
- platform: process.platform
50
- };
51
- function parseDoctorArgs(args) {
52
- const parsed = {};
53
- for (let i = 0; i < args.length; i += 1) {
54
- const arg = args[i];
55
- if (arg === "--project" || arg === "--project-id") {
56
- const value = args[i + 1];
57
- if (!value || value.startsWith("-")) {
58
- parsed.error = `Option '${arg}' argument missing`;
59
- return parsed;
60
- }
61
- parsed.projectId = value;
62
- i += 1;
63
- continue;
64
- }
65
- if (arg?.startsWith("-")) {
66
- parsed.error = `Unknown option '${arg}'`;
67
- return parsed;
68
- }
69
- }
70
- return parsed;
71
- }
72
- function passCheck(id, title, summary, details) {
73
- return { id, title, status: "pass", required: true, summary, details };
74
- }
75
- function failCheck(id, title, summary, remediation, details) {
76
- return {
77
- id,
78
- title,
79
- status: "fail",
80
- required: true,
81
- summary,
82
- remediation,
83
- details
84
- };
85
- }
86
- async function checkWritablePath(targetPath, deps) {
87
- try {
88
- await deps.access(targetPath, constants.W_OK);
89
- const target = await deps.stat(targetPath);
90
- return target.isDirectory();
91
- } catch {
92
- try {
93
- await deps.mkdir(targetPath, { recursive: true });
94
- await deps.access(targetPath, constants.W_OK);
95
- const target = await deps.stat(targetPath);
96
- return target.isDirectory();
97
- } catch {
98
- return false;
99
- }
100
- }
101
- }
102
- function getCommandCandidates(binary, deps) {
103
- if (deps.platform !== "win32") {
104
- return [binary];
105
- }
106
- const pathExts = (deps.pathExtEnv ?? ".COM;.EXE;.BAT;.CMD").split(";").map((ext) => ext.trim()).filter(Boolean);
107
- const normalizedBinary = binary.toLowerCase();
108
- if (pathExts.some((ext) => normalizedBinary.endsWith(ext.toLowerCase()))) {
109
- return [binary];
110
- }
111
- return [binary, ...pathExts.map((ext) => `${binary}${ext}`)];
112
- }
113
- async function commandExistsOnPath(binary, deps) {
114
- if (!binary) {
115
- return false;
116
- }
117
- const candidates = getCommandCandidates(binary, deps);
118
- if (isAbsolute(binary) || binary.includes("/") || binary.includes("\\")) {
119
- for (const candidate of candidates) {
120
- try {
121
- await deps.access(resolve(candidate), constants.X_OK);
122
- return true;
123
- } catch {
124
- continue;
125
- }
126
- }
127
- return false;
128
- }
129
- for (const segment of (deps.pathEnv ?? "").split(delimiter)) {
130
- if (!segment) {
131
- continue;
132
- }
133
- for (const command of candidates) {
134
- const candidate = join(segment, command);
135
- try {
136
- await deps.access(candidate, constants.X_OK);
137
- return true;
138
- } catch {
139
- continue;
140
- }
141
- }
142
- }
143
- return false;
144
- }
145
- function extractCommandBinary(command) {
146
- const trimmed = command.trim();
147
- if (!trimmed) {
148
- return null;
149
- }
150
- const tokens = trimmed.match(/"[^"]*"|'[^']*'|\S+/g) ?? [];
151
- if (tokens.length === 0) {
152
- return null;
153
- }
154
- const shell = stripQuotes(tokens[0]);
155
- if ((shell === "bash" || shell === "sh" || shell === "zsh" || shell === "fish") && tokens.length >= 3) {
156
- const flagIndex = tokens.findIndex((token) => {
157
- const value = stripQuotes(token);
158
- return value === "-c" || value === "-lc";
159
- });
160
- if (flagIndex >= 0 && flagIndex + 1 < tokens.length) {
161
- const nested = stripQuotes(tokens[flagIndex + 1]);
162
- const nestedTokens = nested.match(/"[^"]*"|'[^']*'|\S+/g) ?? [];
163
- return nestedTokens.length > 0 ? stripQuotes(nestedTokens[0]) : shell;
164
- }
165
- }
166
- return shell;
167
- }
168
- function stripQuotes(value) {
169
- return value.replace(/^['"]|['"]$/g, "");
170
- }
171
- async function checkWorkflow(repoRoot, deps) {
172
- const workflowPath = join(repoRoot, "WORKFLOW.md");
173
- let markdown;
174
- try {
175
- markdown = await deps.readFile(workflowPath, "utf8");
176
- } catch {
177
- return {
178
- status: "fail",
179
- workflowPath,
180
- summary: "WORKFLOW.md was not found in the repository root.",
181
- remediation: "Run 'gh-symphony init' in this repository or add a valid WORKFLOW.md at the repo root."
182
- };
183
- }
184
- try {
185
- const parsed = deps.parseWorkflowMarkdown(markdown, process.env);
186
- return {
187
- status: "pass",
188
- command: parsed.agentCommand,
189
- workflowPath,
190
- format: parsed.format
191
- };
192
- } catch (error) {
193
- return {
194
- status: "fail",
195
- workflowPath,
196
- summary: "WORKFLOW.md could not be parsed.",
197
- remediation: "Fix the WORKFLOW.md front matter or re-run 'gh-symphony init' to regenerate it.",
198
- error: error instanceof Error ? error.message : "Unknown workflow parse error."
199
- };
200
- }
201
- }
202
- async function runDoctorDiagnostics(options, args, dependencies = {}) {
203
- const deps = { ...DEFAULT_DEPENDENCIES, ...dependencies };
204
- const parsedArgs = parseDoctorArgs(args);
205
- if (parsedArgs.error) {
206
- throw new Error(
207
- `${parsedArgs.error}
208
- Usage: gh-symphony doctor [--project-id <project-id>]`
209
- );
210
- }
211
- const checks = [];
212
- let token = null;
213
- let tokenError = null;
214
- let resolvedProjectId = null;
215
- let resolvedProjectConfig = null;
216
- const ghInstalled = deps.checkGhInstalled();
217
- if (ghInstalled) {
218
- checks.push(
219
- passCheck("gh_installation", "gh CLI installation", "gh CLI is installed.")
220
- );
221
- } else {
222
- checks.push(
223
- failCheck(
224
- "gh_installation",
225
- "gh CLI installation",
226
- "gh CLI is not installed.",
227
- "Install GitHub CLI from https://cli.github.com and re-run 'gh-symphony doctor'."
228
- )
229
- );
230
- }
231
- const ghAuth = ghInstalled ? deps.checkGhAuthenticated() : { authenticated: false };
232
- if (ghInstalled && ghAuth.authenticated) {
233
- checks.push(
234
- passCheck(
235
- "gh_authentication",
236
- "gh authentication",
237
- `gh auth status succeeded${ghAuth.login ? ` as ${ghAuth.login}` : ""}.`,
238
- ghAuth.login ? { login: ghAuth.login } : void 0
239
- )
240
- );
241
- } else {
242
- checks.push(
243
- failCheck(
244
- "gh_authentication",
245
- "gh authentication",
246
- "gh auth status failed or no GitHub login is configured.",
247
- `Run 'gh auth login --scopes ${REQUIRED_GH_SCOPES.join(",")}' and re-run the doctor command.`
248
- )
249
- );
250
- }
251
- const ghScopes = ghInstalled && ghAuth.authenticated ? deps.checkGhScopes() : { valid: false, missing: [...REQUIRED_GH_SCOPES], scopes: [] };
252
- if (ghInstalled && ghAuth.authenticated && ghScopes.valid) {
253
- checks.push(
254
- passCheck(
255
- "gh_scopes",
256
- "GitHub token scopes",
257
- `Required scopes are present: ${REQUIRED_GH_SCOPES.join(", ")}.`,
258
- { scopes: ghScopes.scopes }
259
- )
260
- );
261
- } else {
262
- const missingScopes = ghInstalled && ghAuth.authenticated ? ghScopes.missing : [...REQUIRED_GH_SCOPES];
263
- checks.push(
264
- failCheck(
265
- "gh_scopes",
266
- "GitHub token scopes",
267
- `Missing required scopes: ${missingScopes.join(", ")}.`,
268
- `Run 'gh auth refresh --scopes ${REQUIRED_GH_SCOPES.join(",")}' and confirm 'gh auth status' shows the updated scopes.`,
269
- { missing: missingScopes, scopes: ghScopes.scopes }
270
- )
271
- );
272
- }
273
- if (ghInstalled && ghAuth.authenticated) {
274
- try {
275
- token = deps.getGhToken();
276
- } catch (error) {
277
- tokenError = error instanceof Error ? error.message : "Unknown token retrieval error.";
278
- token = null;
279
- }
280
- }
281
- resolvedProjectConfig = await deps.inspectManagedProjectSelection({
282
- configDir: options.configDir,
283
- requestedProjectId: parsedArgs.projectId
284
- });
285
- if (resolvedProjectConfig.kind === "resolved") {
286
- resolvedProjectId = resolvedProjectConfig.projectId;
287
- checks.push(
288
- passCheck(
289
- "managed_project",
290
- "Managed project selection",
291
- `Resolved managed project "${resolvedProjectConfig.projectId}".`,
292
- {
293
- projectId: resolvedProjectConfig.projectId,
294
- workspaceDir: resolvedProjectConfig.projectConfig.workspaceDir
295
- }
296
- )
297
- );
298
- } else {
299
- checks.push(
300
- failCheck(
301
- "managed_project",
302
- "Managed project selection",
303
- resolvedProjectConfig.message,
304
- "Run 'gh-symphony project add' to register a project, or select one with 'gh-symphony project switch' / '--project-id'.",
305
- resolvedProjectConfig.projectId ? { projectId: resolvedProjectConfig.projectId } : void 0
306
- )
307
- );
308
- }
309
- if (resolvedProjectConfig.kind === "resolved" && !token) {
310
- checks.push(
311
- failCheck(
312
- "github_project_resolution",
313
- "GitHub project resolution",
314
- tokenError ? "GitHub project resolution could not run because the GitHub token could not be retrieved." : "GitHub project resolution could not run because authentication failed.",
315
- tokenError ? "Check the local keychain or environment used by 'gh auth token', then re-run 'gh-symphony doctor'." : "Fix the gh authentication check first, then re-run 'gh-symphony doctor'.",
316
- tokenError ? { error: tokenError } : void 0
317
- )
318
- );
319
- } else if (resolvedProjectConfig.kind === "resolved" && !resolvedProjectConfig.projectConfig.tracker.bindingId) {
320
- checks.push(
321
- failCheck(
322
- "github_project_resolution",
323
- "GitHub project resolution",
324
- `Managed project "${resolvedProjectConfig.projectId}" is not bound to a GitHub Project.`,
325
- "Re-run 'gh-symphony project add' and select a valid GitHub Project binding, then run the doctor command again.",
326
- { projectId: resolvedProjectConfig.projectId }
327
- )
328
- );
329
- } else if (token && resolvedProjectConfig.kind === "resolved" && resolvedProjectConfig.projectConfig.tracker.bindingId) {
330
- try {
331
- const client = deps.createClient(token);
332
- const detail = await deps.getProjectDetail(
333
- client,
334
- resolvedProjectConfig.projectConfig.tracker.bindingId
335
- );
336
- checks.push(
337
- passCheck(
338
- "github_project_resolution",
339
- "GitHub project resolution",
340
- `Resolved GitHub Project "${detail.title}".`,
341
- {
342
- bindingId: resolvedProjectConfig.projectConfig.tracker.bindingId,
343
- url: detail.url
344
- }
345
- )
346
- );
347
- } catch (error) {
348
- const message = error instanceof GitHubApiError ? error.message : error instanceof Error ? error.message : "Unknown GitHub API error.";
349
- checks.push(
350
- failCheck(
351
- "github_project_resolution",
352
- "GitHub project resolution",
353
- `Failed to resolve configured project binding '${resolvedProjectConfig.projectConfig.tracker.bindingId}'.`,
354
- "Re-run 'gh-symphony project add' and select a valid GitHub Project, then run the doctor command again.",
355
- {
356
- bindingId: resolvedProjectConfig.projectConfig.tracker.bindingId,
357
- error: message
358
- }
359
- )
360
- );
361
- }
362
- } else {
363
- checks.push(
364
- failCheck(
365
- "github_project_resolution",
366
- "GitHub project resolution",
367
- "GitHub project resolution could not run because managed project selection failed.",
368
- "Fix the managed project selection check first, then re-run 'gh-symphony doctor'."
369
- )
370
- );
371
- }
372
- const configDirWritable = await checkWritablePath(options.configDir, deps);
373
- checks.push(
374
- configDirWritable ? passCheck(
375
- "config_directory",
376
- "Config directory",
377
- `Config directory is writable: ${options.configDir}.`,
378
- { path: options.configDir }
379
- ) : failCheck(
380
- "config_directory",
381
- "Config directory",
382
- `Config directory is not writable: ${options.configDir}.`,
383
- `Create the directory and ensure your user can write to it: 'mkdir -p ${options.configDir}' and fix ownership/permissions as needed.`,
384
- { path: options.configDir }
385
- )
386
- );
387
- const runtimeRoot = resolveRuntimeRoot(options.configDir);
388
- const runtimeRootWritable = await checkWritablePath(runtimeRoot, deps);
389
- checks.push(
390
- runtimeRootWritable ? passCheck(
391
- "runtime_root",
392
- "Runtime root",
393
- `Runtime root is writable: ${runtimeRoot}.`,
394
- { path: runtimeRoot }
395
- ) : failCheck(
396
- "runtime_root",
397
- "Runtime root",
398
- `Runtime root is not writable: ${runtimeRoot}.`,
399
- `Create the runtime root and ensure your user can write to it: 'mkdir -p ${runtimeRoot}'.`,
400
- { path: runtimeRoot }
401
- )
402
- );
403
- if (resolvedProjectConfig.kind === "resolved") {
404
- const workspaceDir = resolvedProjectConfig.projectConfig.workspaceDir;
405
- const workspaceWritable = await checkWritablePath(workspaceDir, deps);
406
- checks.push(
407
- workspaceWritable ? passCheck(
408
- "workspace_root",
409
- "Workspace root",
410
- `Workspace root is writable: ${workspaceDir}.`,
411
- { path: workspaceDir }
412
- ) : failCheck(
413
- "workspace_root",
414
- "Workspace root",
415
- `Workspace root is not writable: ${workspaceDir}.`,
416
- "Update the managed project workspaceDir to a writable path or fix the filesystem permissions.",
417
- { path: workspaceDir }
418
- )
419
- );
420
- } else {
421
- checks.push(
422
- failCheck(
423
- "workspace_root",
424
- "Workspace root",
425
- "Workspace root could not be checked because no managed project was resolved.",
426
- "Fix the managed project selection check first, then re-run 'gh-symphony doctor'."
427
- )
428
- );
429
- }
430
- const workflow = await checkWorkflow(process.cwd(), deps);
431
- if (workflow.status === "pass") {
432
- checks.push(
433
- passCheck(
434
- "workflow_file",
435
- "Repository WORKFLOW.md",
436
- `WORKFLOW.md parsed successfully (${workflow.format}).`,
437
- { path: workflow.workflowPath, format: workflow.format }
438
- )
439
- );
440
- } else {
441
- checks.push(
442
- failCheck(
443
- "workflow_file",
444
- "Repository WORKFLOW.md",
445
- workflow.summary,
446
- workflow.remediation,
447
- {
448
- path: workflow.workflowPath,
449
- ...workflow.error ? { error: workflow.error } : {}
450
- }
451
- )
452
- );
453
- }
454
- if (workflow.status === "pass") {
455
- const binary = extractCommandBinary(workflow.command);
456
- if (binary && await commandExistsOnPath(binary, deps)) {
457
- checks.push(
458
- passCheck(
459
- "runtime_command",
460
- "Runtime command detection",
461
- `Configured runtime command is available: ${binary}.`,
462
- { command: workflow.command, binary }
463
- )
464
- );
465
- } else {
466
- checks.push(
467
- failCheck(
468
- "runtime_command",
469
- "Runtime command detection",
470
- `Configured runtime command could not be found on PATH: ${workflow.command}.`,
471
- "Install the configured runtime binary or update the workflow/context configuration to a command that exists on this machine.",
472
- { command: workflow.command, binary }
473
- )
474
- );
475
- }
476
- } else {
477
- checks.push(
478
- failCheck(
479
- "runtime_command",
480
- "Runtime command detection",
481
- "Runtime command detection could not run because WORKFLOW.md is missing or invalid.",
482
- "Fix the WORKFLOW.md check first so the configured runtime command can be validated."
483
- )
484
- );
485
- }
486
- return {
487
- ok: checks.every((check) => check.status === "pass"),
488
- checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
489
- configDir: options.configDir,
490
- projectId: resolvedProjectId,
491
- checks
492
- };
493
- }
494
- function renderTextReport(report) {
495
- const lines = [`gh-symphony doctor`, ""];
496
- for (const check of report.checks) {
497
- lines.push(`${check.status === "pass" ? "PASS" : "FAIL"} ${check.title}`);
498
- lines.push(` ${check.summary}`);
499
- if (check.remediation) {
500
- lines.push(` Fix: ${check.remediation}`);
501
- }
502
- }
503
- lines.push("");
504
- lines.push(
505
- report.ok ? "Doctor completed successfully." : "Doctor found required checks that need attention."
506
- );
507
- return lines.join("\n");
508
- }
509
- async function runDoctorCommand(args, options, dependencies = {}) {
510
- try {
511
- const report = await runDoctorDiagnostics(options, args, dependencies);
512
- if (options.json) {
513
- process.stdout.write(JSON.stringify(report, null, 2) + "\n");
514
- } else {
515
- process.stdout.write(renderTextReport(report) + "\n");
516
- }
517
- process.exitCode = report.ok ? 0 : 1;
518
- } catch (error) {
519
- process.stderr.write(
520
- `${error instanceof Error ? error.message : "Unknown error"}
521
- `
522
- );
523
- process.exitCode = 2;
524
- }
525
- }
526
- var handler = async (args, options) => runDoctorCommand(args, options);
527
- var doctor_default = handler;
528
- export {
529
- doctor_default as default,
530
- runDoctorCommand,
531
- runDoctorDiagnostics
532
- };
@@ -1,18 +0,0 @@
1
- #!/usr/bin/env node
2
- import {
3
- abortIfCancelled,
4
- generateProjectId,
5
- init_default,
6
- writeConfig,
7
- writeEcosystem
8
- } from "./chunk-5YLETHMR.js";
9
- import "./chunk-62L6QQE6.js";
10
- import "./chunk-7UBUBSMH.js";
11
- import "./chunk-ROGRTUFI.js";
12
- export {
13
- abortIfCancelled,
14
- init_default as default,
15
- generateProjectId,
16
- writeConfig,
17
- writeEcosystem
18
- };
@@ -1,121 +0,0 @@
1
- #!/usr/bin/env node
2
- import {
3
- loadActiveProjectConfig,
4
- loadGlobalConfig,
5
- saveProjectConfig
6
- } from "./chunk-ROGRTUFI.js";
7
-
8
- // src/commands/repo.ts
9
- var handler = async (args, options) => {
10
- const [subcommand, ...rest] = args;
11
- switch (subcommand) {
12
- case "list":
13
- await repoList(options);
14
- break;
15
- case "add":
16
- await repoAdd(rest, options);
17
- break;
18
- case "remove":
19
- await repoRemove(rest, options);
20
- break;
21
- default:
22
- process.stderr.write(
23
- "Usage: gh-symphony repo <list|add|remove> [repo]\n"
24
- );
25
- process.exitCode = 2;
26
- }
27
- };
28
- var repo_default = handler;
29
- async function repoList(options) {
30
- const ws = await loadActiveProjectConfig(options.configDir);
31
- if (!ws) {
32
- process.stderr.write("No project configured.\n");
33
- process.exitCode = 1;
34
- return;
35
- }
36
- if (options.json) {
37
- process.stdout.write(JSON.stringify(ws.repositories, null, 2) + "\n");
38
- return;
39
- }
40
- process.stdout.write("Repositories:\n");
41
- for (const repo of ws.repositories) {
42
- process.stdout.write(` ${repo.owner}/${repo.name}
43
- `);
44
- }
45
- }
46
- async function repoAdd(args, options) {
47
- const [repoSpec] = args;
48
- if (!repoSpec || !repoSpec.includes("/")) {
49
- process.stderr.write("Usage: gh-symphony repo add <owner/name>\n");
50
- process.exitCode = 2;
51
- return;
52
- }
53
- const global = await loadGlobalConfig(options.configDir);
54
- if (!global?.activeProject) {
55
- process.stderr.write("No active project.\n");
56
- process.exitCode = 1;
57
- return;
58
- }
59
- const ws = await loadActiveProjectConfig(options.configDir);
60
- if (!ws) {
61
- process.stderr.write("Project config missing.\n");
62
- process.exitCode = 1;
63
- return;
64
- }
65
- const [owner, name] = repoSpec.split("/");
66
- if (!owner || !name) {
67
- process.stderr.write("Invalid repo format. Use: owner/name\n");
68
- process.exitCode = 2;
69
- return;
70
- }
71
- if (ws.repositories.some((r) => r.owner === owner && r.name === name)) {
72
- process.stdout.write(`Repository ${repoSpec} is already configured.
73
- `);
74
- return;
75
- }
76
- ws.repositories.push({
77
- owner,
78
- name,
79
- cloneUrl: `https://github.com/${owner}/${name}.git`
80
- });
81
- await saveProjectConfig(options.configDir, global.activeProject, ws);
82
- process.stdout.write(`Added repository: ${repoSpec}
83
- `);
84
- }
85
- async function repoRemove(args, options) {
86
- const [repoSpec] = args;
87
- if (!repoSpec || !repoSpec.includes("/")) {
88
- process.stderr.write("Usage: gh-symphony repo remove <owner/name>\n");
89
- process.exitCode = 2;
90
- return;
91
- }
92
- const global = await loadGlobalConfig(options.configDir);
93
- if (!global?.activeProject) {
94
- process.stderr.write("No active project.\n");
95
- process.exitCode = 1;
96
- return;
97
- }
98
- const ws = await loadActiveProjectConfig(options.configDir);
99
- if (!ws) {
100
- process.stderr.write("Project config missing.\n");
101
- process.exitCode = 1;
102
- return;
103
- }
104
- const [owner, name] = repoSpec.split("/");
105
- const idx = ws.repositories.findIndex(
106
- (r) => r.owner === owner && r.name === name
107
- );
108
- if (idx === -1) {
109
- process.stderr.write(`Repository ${repoSpec} is not configured.
110
- `);
111
- process.exitCode = 1;
112
- return;
113
- }
114
- ws.repositories.splice(idx, 1);
115
- await saveProjectConfig(options.configDir, global.activeProject, ws);
116
- process.stdout.write(`Removed repository: ${repoSpec}
117
- `);
118
- }
119
- export {
120
- repo_default as default
121
- };