@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 +25 -0
- package/dist/{chunk-IWR4UQEJ.js → chunk-5YLETHMR.js} +9 -358
- package/dist/chunk-62L6QQE6.js +362 -0
- package/dist/{chunk-JO3AXHQI.js → chunk-7UBUBSMH.js} +6 -2
- package/dist/chunk-C7G7RJ4G.js +146 -0
- package/dist/{chunk-EFMFGOWM.js → chunk-LZE6YUSB.js} +267 -53
- package/dist/{chunk-TF3QNWNC.js → chunk-OL73UN2X.js} +246 -212
- package/dist/{chunk-6HBZC3BE.js → chunk-XN5ABWZ6.js} +23 -5
- package/dist/{chunk-76QPITKI.js → chunk-Y6TYJMNT.js} +1 -1
- package/dist/{chunk-MHIWAIVD.js → chunk-ZYYY55WB.js} +70 -33
- package/dist/doctor-3QT5CZN4.js +532 -0
- package/dist/index.js +21 -9
- package/dist/{init-EZXQAXZM.js → init-E432UZ32.js} +3 -2
- package/dist/{logs-6LNGT2GF.js → logs-6JKKYDGJ.js} +1 -1
- package/dist/{project-557FE2GD.js → project-O57C32WF.js} +14 -12
- package/dist/{recover-LVBI2TGH.js → recover-UGUTQTWA.js} +3 -3
- package/dist/{run-WITYAYFZ.js → run-5H2R6CHB.js} +3 -3
- package/dist/{start-JUFKNL3N.js → start-5JGGJIMC.js} +5 -5
- package/dist/{status-3WK5BWRZ.js → status-QSCFVGRQ.js} +2 -2
- package/dist/{stop-AA3AP5M6.js → stop-7MFCBQVW.js} +2 -2
- package/dist/{version-YVM2A25J.js → version-N7YXKG6V.js} +1 -1
- package/dist/worker-entry.js +16 -10
- package/package.json +2 -2
- package/dist/chunk-TH5QPO3Y.js +0 -67
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-
|
|
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,
|