@gh-symphony/cli 0.0.17 → 0.0.18

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 CHANGED
@@ -25,6 +25,13 @@ Verify the installation:
25
25
  gh-symphony --version
26
26
  ```
27
27
 
28
+ Validate the machine and repo prerequisites before first use:
29
+
30
+ ```bash
31
+ gh-symphony doctor
32
+ gh-symphony doctor --json
33
+ ```
34
+
28
35
  Enable shell completion:
29
36
 
30
37
  ```bash
@@ -113,6 +120,7 @@ The interactive wizard will:
113
120
  ### Project Management
114
121
 
115
122
  ```bash
123
+ gh-symphony doctor # Validate auth, config, WORKFLOW.md, and runtime command
116
124
  gh-symphony project list # List all configured projects
117
125
  gh-symphony project remove <id> # Remove a project
118
126
  ```
@@ -154,11 +162,28 @@ gh-symphony recover # Recover stalled runs
154
162
  gh-symphony recover --dry-run # Preview what would be recovered
155
163
  ```
156
164
 
165
+ ## Diagnostics
166
+
167
+ `gh-symphony doctor` validates the most common first-run prerequisites in one pass:
168
+
169
+ - `gh` installation, auth, and required scopes
170
+ - managed project selection plus GitHub Project binding resolution
171
+ - config/runtime/workspace path writability
172
+ - repository `WORKFLOW.md` presence and parse validity
173
+ - runtime command availability on `PATH`
174
+
175
+ Use JSON output for scripts and CI smoke checks:
176
+
177
+ ```bash
178
+ gh-symphony doctor --json
179
+ ```
180
+
157
181
  ## Command Reference
158
182
 
159
183
  ```
160
184
  Setup:
161
185
  init Interactive repository setup wizard
186
+ doctor Run first-run diagnostics
162
187
  config show Show current configuration
163
188
  config set Set a configuration value
164
189
  config edit Open config in $EDITOR
@@ -1,9 +1,17 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ GitHubScopeError,
4
+ checkRequiredScopes,
5
+ createClient,
6
+ getProjectDetail,
7
+ listUserProjects,
8
+ validateToken
9
+ } from "./chunk-62L6QQE6.js";
2
10
  import {
3
11
  GhAuthError,
4
12
  ensureGhAuth,
5
13
  getGhToken
6
- } from "./chunk-JO3AXHQI.js";
14
+ } from "./chunk-7UBUBSMH.js";
7
15
  import {
8
16
  loadGlobalConfig,
9
17
  saveGlobalConfig,
@@ -16,357 +24,6 @@ import { createHash } from "crypto";
16
24
  import { mkdir as mkdir3, rename, writeFile as writeFile3 } from "fs/promises";
17
25
  import { basename, dirname as dirname2, join as join3, relative, resolve } from "path";
18
26
 
19
- // src/github/client.ts
20
- var DEFAULT_API_URL = "https://api.github.com/graphql";
21
- var REST_API_URL = "https://api.github.com";
22
- var GitHubApiError = class extends Error {
23
- constructor(message, status) {
24
- super(message);
25
- this.status = status;
26
- this.name = "GitHubApiError";
27
- }
28
- };
29
- var GitHubScopeError = class extends GitHubApiError {
30
- constructor(message, requiredScopes, currentScopes) {
31
- super(message);
32
- this.requiredScopes = requiredScopes;
33
- this.currentScopes = currentScopes;
34
- this.name = "GitHubScopeError";
35
- }
36
- };
37
- function createClient(token, options) {
38
- return {
39
- token,
40
- apiUrl: options?.apiUrl ?? DEFAULT_API_URL,
41
- fetchImpl: options?.fetchImpl ?? fetch
42
- };
43
- }
44
- async function validateToken(client) {
45
- const restUrl = client.apiUrl.replace("/graphql", "");
46
- const baseUrl = restUrl === client.apiUrl ? REST_API_URL : restUrl;
47
- const response = await client.fetchImpl(`${baseUrl}/user`, {
48
- headers: {
49
- authorization: `Bearer ${client.token}`,
50
- accept: "application/vnd.github+json"
51
- }
52
- });
53
- if (!response.ok) {
54
- if (response.status === 401) {
55
- throw new GitHubApiError("Invalid token: authentication failed.", 401);
56
- }
57
- throw new GitHubApiError(
58
- `GitHub API error: ${response.status} ${response.statusText}`,
59
- response.status
60
- );
61
- }
62
- const scopes = response.headers.get("x-oauth-scopes")?.split(",").map((s) => s.trim()).filter(Boolean) ?? [];
63
- const user = await response.json();
64
- return {
65
- login: user.login,
66
- name: user.name,
67
- scopes
68
- };
69
- }
70
- function checkRequiredScopes(scopes) {
71
- const required = ["repo", "read:org", "project"];
72
- const normalizedScopes = scopes.map((s) => s.toLowerCase());
73
- const missing = required.filter((r) => !normalizedScopes.includes(r));
74
- return { valid: missing.length === 0, missing };
75
- }
76
- async function listUserProjects(client) {
77
- const data = await graphql(
78
- client,
79
- VIEWER_PROJECTS_QUERY
80
- );
81
- const projects = [];
82
- for (const node of data.viewer.projectsV2?.nodes ?? []) {
83
- if (!node) continue;
84
- projects.push(
85
- normalizeProjectSummary(node, {
86
- login: data.viewer.login,
87
- type: "User"
88
- })
89
- );
90
- }
91
- for (const orgNode of data.viewer.organizations?.nodes ?? []) {
92
- if (!orgNode) continue;
93
- for (const projNode of orgNode.projectsV2?.nodes ?? []) {
94
- if (!projNode) continue;
95
- projects.push(
96
- normalizeProjectSummary(projNode, {
97
- login: orgNode.login,
98
- type: "Organization"
99
- })
100
- );
101
- }
102
- }
103
- return projects;
104
- }
105
- function normalizeProjectSummary(node, owner) {
106
- return {
107
- id: node.id,
108
- title: node.title,
109
- shortDescription: node.shortDescription ?? "",
110
- url: node.url,
111
- openItemCount: node.items?.totalCount ?? 0,
112
- owner
113
- };
114
- }
115
- async function getProjectDetail(client, projectId) {
116
- const data = await graphql(
117
- client,
118
- PROJECT_DETAIL_QUERY,
119
- { projectId }
120
- );
121
- const project = data.node;
122
- if (!project || project.__typename !== "ProjectV2") {
123
- throw new GitHubApiError(`Project not found: ${projectId}`);
124
- }
125
- const statusFields = [];
126
- const textFields = [];
127
- for (const field of project.fields?.nodes ?? []) {
128
- if (!field) continue;
129
- if (field.__typename === "ProjectV2SingleSelectField") {
130
- statusFields.push({
131
- id: field.id,
132
- name: field.name,
133
- options: (field.options ?? []).map((opt) => ({
134
- id: opt.id,
135
- name: opt.name,
136
- description: opt.description ?? null,
137
- color: opt.color ?? null
138
- }))
139
- });
140
- } else if (field.__typename === "ProjectV2Field" && field.dataType) {
141
- textFields.push({
142
- id: field.id,
143
- name: field.name,
144
- dataType: field.dataType
145
- });
146
- }
147
- }
148
- const repoMap = /* @__PURE__ */ new Map();
149
- let cursor = null;
150
- let hasMore = true;
151
- for (const item of project.items?.nodes ?? []) {
152
- const repo = item?.content?.repository;
153
- if (!repo) continue;
154
- const key = `${repo.owner.login}/${repo.name}`;
155
- if (!repoMap.has(key)) {
156
- repoMap.set(key, {
157
- owner: repo.owner.login,
158
- name: repo.name,
159
- url: repo.url,
160
- cloneUrl: repo.url.endsWith(".git") ? repo.url : `${repo.url}.git`
161
- });
162
- }
163
- }
164
- hasMore = project.items?.pageInfo?.hasNextPage ?? false;
165
- cursor = project.items?.pageInfo?.endCursor ?? null;
166
- while (hasMore && cursor) {
167
- const pageData = await graphql(
168
- client,
169
- PROJECT_ITEMS_PAGE_QUERY,
170
- { projectId, cursor }
171
- );
172
- const items = pageData.node?.items;
173
- if (!items) break;
174
- for (const item of items.nodes ?? []) {
175
- const repo = item?.content?.repository;
176
- if (!repo) continue;
177
- const key = `${repo.owner.login}/${repo.name}`;
178
- if (!repoMap.has(key)) {
179
- repoMap.set(key, {
180
- owner: repo.owner.login,
181
- name: repo.name,
182
- url: repo.url,
183
- cloneUrl: repo.url.endsWith(".git") ? repo.url : `${repo.url}.git`
184
- });
185
- }
186
- }
187
- hasMore = items.pageInfo?.hasNextPage ?? false;
188
- cursor = items.pageInfo?.endCursor ?? null;
189
- }
190
- return {
191
- id: project.id,
192
- title: project.title,
193
- url: project.url,
194
- statusFields,
195
- textFields,
196
- linkedRepositories: [...repoMap.values()]
197
- };
198
- }
199
- async function graphql(client, query, variables) {
200
- const response = await client.fetchImpl(client.apiUrl, {
201
- method: "POST",
202
- headers: {
203
- "content-type": "application/json",
204
- authorization: `Bearer ${client.token}`
205
- },
206
- body: JSON.stringify({ query, variables })
207
- });
208
- if (!response.ok) {
209
- const text = await response.text().catch(() => "");
210
- throw new GitHubApiError(
211
- `GitHub GraphQL request failed: ${response.status} ${response.statusText}. ${text}`,
212
- response.status
213
- );
214
- }
215
- const payload = await response.json();
216
- if (payload.errors?.length) {
217
- const scopeMessages = payload.errors.map((e) => e.message).filter((m) => m.includes("has not been granted the required scopes"));
218
- if (scopeMessages.length > 0) {
219
- const requiredScopes = /* @__PURE__ */ new Set();
220
- let currentScopes = [];
221
- for (const msg of scopeMessages) {
222
- for (const match of msg.matchAll(
223
- /requires one of the following scopes: \['([^']+)'\]/g
224
- )) {
225
- requiredScopes.add(match[1]);
226
- }
227
- if (currentScopes.length === 0) {
228
- const currMatch = /has only been granted the: \[([^\]]+)\]/.exec(msg);
229
- if (currMatch) {
230
- currentScopes = currMatch[1].split(",").map((s) => s.trim().replace(/'/g, "")).filter(Boolean);
231
- }
232
- }
233
- }
234
- throw new GitHubScopeError(
235
- "Token is missing required GitHub scopes.",
236
- [...requiredScopes],
237
- currentScopes
238
- );
239
- }
240
- throw new GitHubApiError(
241
- `GraphQL errors: ${payload.errors.map((e) => e.message).join("; ")}`
242
- );
243
- }
244
- if (!payload.data) {
245
- throw new GitHubApiError("GraphQL response missing data.");
246
- }
247
- return payload.data;
248
- }
249
- var VIEWER_PROJECTS_QUERY = `
250
- query ViewerProjects {
251
- viewer {
252
- login
253
- projectsV2(first: 50) {
254
- nodes {
255
- id
256
- title
257
- shortDescription
258
- url
259
- items { totalCount }
260
- }
261
- }
262
- organizations(first: 20) {
263
- nodes {
264
- login
265
- projectsV2(first: 50) {
266
- nodes {
267
- id
268
- title
269
- shortDescription
270
- url
271
- items { totalCount }
272
- }
273
- }
274
- }
275
- }
276
- }
277
- }
278
- `;
279
- var PROJECT_DETAIL_QUERY = `
280
- query ProjectDetail($projectId: ID!) {
281
- node(id: $projectId) {
282
- __typename
283
- ... on ProjectV2 {
284
- id
285
- title
286
- url
287
- fields(first: 50) {
288
- nodes {
289
- __typename
290
- ... on ProjectV2SingleSelectField {
291
- id
292
- name
293
- options {
294
- id
295
- name
296
- description
297
- color
298
- }
299
- }
300
- ... on ProjectV2Field {
301
- id
302
- name
303
- dataType
304
- }
305
- }
306
- }
307
- items(first: 100) {
308
- nodes {
309
- content {
310
- __typename
311
- ... on Issue {
312
- repository {
313
- name
314
- url
315
- owner { login }
316
- }
317
- }
318
- ... on PullRequest {
319
- repository {
320
- name
321
- url
322
- owner { login }
323
- }
324
- }
325
- }
326
- }
327
- pageInfo {
328
- endCursor
329
- hasNextPage
330
- }
331
- }
332
- }
333
- }
334
- }
335
- `;
336
- var PROJECT_ITEMS_PAGE_QUERY = `
337
- query ProjectItemsPage($projectId: ID!, $cursor: String) {
338
- node(id: $projectId) {
339
- ... on ProjectV2 {
340
- items(first: 100, after: $cursor) {
341
- nodes {
342
- content {
343
- __typename
344
- ... on Issue {
345
- repository {
346
- name
347
- url
348
- owner { login }
349
- }
350
- }
351
- ... on PullRequest {
352
- repository {
353
- name
354
- url
355
- owner { login }
356
- }
357
- }
358
- }
359
- }
360
- pageInfo {
361
- endCursor
362
- hasNextPage
363
- }
364
- }
365
- }
366
- }
367
- }
368
- `;
369
-
370
27
  // src/mapping/smart-defaults.ts
371
28
  var ROLE_PATTERNS = [
372
29
  {
@@ -2236,12 +1893,6 @@ function generateProjectId(githubProjectTitle, uniqueKey) {
2236
1893
  }
2237
1894
 
2238
1895
  export {
2239
- GitHubScopeError,
2240
- createClient,
2241
- validateToken,
2242
- checkRequiredScopes,
2243
- listUserProjects,
2244
- getProjectDetail,
2245
1896
  abortIfCancelled,
2246
1897
  init_default,
2247
1898
  writeEcosystem,