@gh-symphony/cli 0.0.17 → 0.0.19
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 +105 -9
- package/dist/{chunk-EFMFGOWM.js → chunk-6CI3UUMH.js} +282 -57
- package/dist/chunk-C7G7RJ4G.js +146 -0
- package/dist/{chunk-MHIWAIVD.js → chunk-GKENCODJ.js} +141 -53
- package/dist/{project-557FE2GD.js → chunk-H2YXSYOZ.js} +108 -92
- package/dist/{chunk-TF3QNWNC.js → chunk-M3IFVLQS.js} +246 -212
- package/dist/{chunk-IWR4UQEJ.js → chunk-RN2PACNV.js} +350 -523
- package/dist/chunk-TILHWBP6.js +638 -0
- package/dist/{chunk-6HBZC3BE.js → chunk-XN5ABWZ6.js} +23 -5
- package/dist/{chunk-76QPITKI.js → chunk-Y6TYJMNT.js} +1 -1
- package/dist/{config-cmd-AZ7POMAA.js → config-cmd-DNXNL26Z.js} +3 -1
- package/dist/doctor-IYHCFXOZ.js +1126 -0
- package/dist/index.js +157 -19
- package/dist/init-KZT6YNOH.js +33 -0
- package/dist/{logs-6LNGT2GF.js → logs-6JKKYDGJ.js} +1 -1
- package/dist/project-DNALEWO3.js +22 -0
- package/dist/{recover-LVBI2TGH.js → recover-C3V2QAUB.js} +3 -3
- package/dist/repo-HDDE7OUI.js +321 -0
- package/dist/{run-WITYAYFZ.js → run-XI2S5Y4V.js} +3 -3
- package/dist/setup-K4CYYJBF.js +431 -0
- package/dist/{start-JUFKNL3N.js → start-M6IQGRFO.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/upgrade-F4VE4XBS.js +165 -0
- package/dist/{version-YVM2A25J.js → version-Y5RYNWMF.js} +1 -1
- package/dist/worker-entry.js +39 -11
- package/dist/workflow-TBIFY5MO.js +497 -0
- package/package.json +4 -4
- package/dist/chunk-JO3AXHQI.js +0 -130
- package/dist/chunk-TH5QPO3Y.js +0 -67
- package/dist/init-EZXQAXZM.js +0 -17
- package/dist/repo-R3XBIVAX.js +0 -121
|
@@ -0,0 +1,638 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/github/client.ts
|
|
4
|
+
var DEFAULT_API_URL = "https://api.github.com/graphql";
|
|
5
|
+
var REST_API_URL = "https://api.github.com";
|
|
6
|
+
var GitHubApiError = class extends Error {
|
|
7
|
+
constructor(message, status) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.status = status;
|
|
10
|
+
this.name = "GitHubApiError";
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
var GitHubScopeError = class extends GitHubApiError {
|
|
14
|
+
constructor(message, requiredScopes, currentScopes) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.requiredScopes = requiredScopes;
|
|
17
|
+
this.currentScopes = currentScopes;
|
|
18
|
+
this.name = "GitHubScopeError";
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
function createClient(token, options) {
|
|
22
|
+
return {
|
|
23
|
+
token,
|
|
24
|
+
apiUrl: options?.apiUrl ?? DEFAULT_API_URL,
|
|
25
|
+
fetchImpl: options?.fetchImpl ?? fetch
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
async function validateToken(client) {
|
|
29
|
+
const restUrl = client.apiUrl.replace("/graphql", "");
|
|
30
|
+
const baseUrl = restUrl === client.apiUrl ? REST_API_URL : restUrl;
|
|
31
|
+
const response = await client.fetchImpl(`${baseUrl}/user`, {
|
|
32
|
+
headers: {
|
|
33
|
+
authorization: `Bearer ${client.token}`,
|
|
34
|
+
accept: "application/vnd.github+json"
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
if (!response.ok) {
|
|
38
|
+
if (response.status === 401) {
|
|
39
|
+
throw new GitHubApiError("Invalid token: authentication failed.", 401);
|
|
40
|
+
}
|
|
41
|
+
throw new GitHubApiError(
|
|
42
|
+
`GitHub API error: ${response.status} ${response.statusText}`,
|
|
43
|
+
response.status
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
const scopes = response.headers.get("x-oauth-scopes")?.split(",").map((s) => s.trim()).filter(Boolean) ?? [];
|
|
47
|
+
const user = await response.json();
|
|
48
|
+
return {
|
|
49
|
+
login: user.login,
|
|
50
|
+
name: user.name,
|
|
51
|
+
scopes
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function checkRequiredScopes(scopes) {
|
|
55
|
+
const required = ["repo", "read:org", "project"];
|
|
56
|
+
const normalizedScopes = scopes.map((s) => s.toLowerCase());
|
|
57
|
+
const missing = required.filter((r) => !normalizedScopes.includes(r));
|
|
58
|
+
return { valid: missing.length === 0, missing };
|
|
59
|
+
}
|
|
60
|
+
async function listUserProjects(client) {
|
|
61
|
+
const data = await graphql(
|
|
62
|
+
client,
|
|
63
|
+
VIEWER_PROJECTS_QUERY
|
|
64
|
+
);
|
|
65
|
+
const projects = [];
|
|
66
|
+
for (const node of data.viewer.projectsV2?.nodes ?? []) {
|
|
67
|
+
if (!node) continue;
|
|
68
|
+
projects.push(
|
|
69
|
+
normalizeProjectSummary(node, {
|
|
70
|
+
login: data.viewer.login,
|
|
71
|
+
type: "User"
|
|
72
|
+
})
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
for (const orgNode of data.viewer.organizations?.nodes ?? []) {
|
|
76
|
+
if (!orgNode) continue;
|
|
77
|
+
for (const projNode of orgNode.projectsV2?.nodes ?? []) {
|
|
78
|
+
if (!projNode) continue;
|
|
79
|
+
projects.push(
|
|
80
|
+
normalizeProjectSummary(projNode, {
|
|
81
|
+
login: orgNode.login,
|
|
82
|
+
type: "Organization"
|
|
83
|
+
})
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return projects;
|
|
88
|
+
}
|
|
89
|
+
function normalizeProjectSummary(node, owner) {
|
|
90
|
+
return {
|
|
91
|
+
id: node.id,
|
|
92
|
+
title: node.title,
|
|
93
|
+
shortDescription: node.shortDescription ?? "",
|
|
94
|
+
url: node.url,
|
|
95
|
+
openItemCount: node.items?.totalCount ?? 0,
|
|
96
|
+
owner
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
async function getProjectDetail(client, projectId) {
|
|
100
|
+
const data = await graphql(
|
|
101
|
+
client,
|
|
102
|
+
PROJECT_DETAIL_QUERY,
|
|
103
|
+
{ projectId }
|
|
104
|
+
);
|
|
105
|
+
const project = data.node;
|
|
106
|
+
if (!project || project.__typename !== "ProjectV2") {
|
|
107
|
+
throw new GitHubApiError(`Project not found: ${projectId}`);
|
|
108
|
+
}
|
|
109
|
+
const statusFields = [];
|
|
110
|
+
const textFields = [];
|
|
111
|
+
for (const field of project.fields?.nodes ?? []) {
|
|
112
|
+
if (!field) continue;
|
|
113
|
+
if (field.__typename === "ProjectV2SingleSelectField") {
|
|
114
|
+
statusFields.push({
|
|
115
|
+
id: field.id,
|
|
116
|
+
name: field.name,
|
|
117
|
+
options: (field.options ?? []).map((opt) => ({
|
|
118
|
+
id: opt.id,
|
|
119
|
+
name: opt.name,
|
|
120
|
+
description: opt.description ?? null,
|
|
121
|
+
color: opt.color ?? null
|
|
122
|
+
}))
|
|
123
|
+
});
|
|
124
|
+
} else if (field.__typename === "ProjectV2Field" && field.dataType) {
|
|
125
|
+
textFields.push({
|
|
126
|
+
id: field.id,
|
|
127
|
+
name: field.name,
|
|
128
|
+
dataType: field.dataType
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
const repoMap = /* @__PURE__ */ new Map();
|
|
133
|
+
let cursor = null;
|
|
134
|
+
let hasMore = true;
|
|
135
|
+
for (const item of project.items?.nodes ?? []) {
|
|
136
|
+
const repo = item?.content?.repository;
|
|
137
|
+
if (!repo) continue;
|
|
138
|
+
const key = `${repo.owner.login}/${repo.name}`;
|
|
139
|
+
if (!repoMap.has(key)) {
|
|
140
|
+
repoMap.set(key, {
|
|
141
|
+
owner: repo.owner.login,
|
|
142
|
+
name: repo.name,
|
|
143
|
+
url: repo.url,
|
|
144
|
+
cloneUrl: repo.url.endsWith(".git") ? repo.url : `${repo.url}.git`
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
hasMore = project.items?.pageInfo?.hasNextPage ?? false;
|
|
149
|
+
cursor = project.items?.pageInfo?.endCursor ?? null;
|
|
150
|
+
while (hasMore && cursor) {
|
|
151
|
+
const pageData = await graphql(
|
|
152
|
+
client,
|
|
153
|
+
PROJECT_ITEMS_PAGE_QUERY,
|
|
154
|
+
{ projectId, cursor }
|
|
155
|
+
);
|
|
156
|
+
const items = pageData.node?.items;
|
|
157
|
+
if (!items) break;
|
|
158
|
+
for (const item of items.nodes ?? []) {
|
|
159
|
+
const repo = item?.content?.repository;
|
|
160
|
+
if (!repo) continue;
|
|
161
|
+
const key = `${repo.owner.login}/${repo.name}`;
|
|
162
|
+
if (!repoMap.has(key)) {
|
|
163
|
+
repoMap.set(key, {
|
|
164
|
+
owner: repo.owner.login,
|
|
165
|
+
name: repo.name,
|
|
166
|
+
url: repo.url,
|
|
167
|
+
cloneUrl: repo.url.endsWith(".git") ? repo.url : `${repo.url}.git`
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
hasMore = items.pageInfo?.hasNextPage ?? false;
|
|
172
|
+
cursor = items.pageInfo?.endCursor ?? null;
|
|
173
|
+
}
|
|
174
|
+
return {
|
|
175
|
+
id: project.id,
|
|
176
|
+
title: project.title,
|
|
177
|
+
url: project.url,
|
|
178
|
+
statusFields,
|
|
179
|
+
textFields,
|
|
180
|
+
linkedRepositories: [...repoMap.values()]
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
async function graphql(client, query, variables) {
|
|
184
|
+
const response = await client.fetchImpl(client.apiUrl, {
|
|
185
|
+
method: "POST",
|
|
186
|
+
headers: {
|
|
187
|
+
"content-type": "application/json",
|
|
188
|
+
authorization: `Bearer ${client.token}`
|
|
189
|
+
},
|
|
190
|
+
body: JSON.stringify({ query, variables })
|
|
191
|
+
});
|
|
192
|
+
if (!response.ok) {
|
|
193
|
+
const text = await response.text().catch(() => "");
|
|
194
|
+
throw new GitHubApiError(
|
|
195
|
+
`GitHub GraphQL request failed: ${response.status} ${response.statusText}. ${text}`,
|
|
196
|
+
response.status
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
const payload = await response.json();
|
|
200
|
+
if (payload.errors?.length) {
|
|
201
|
+
const scopeMessages = payload.errors.map((e) => e.message).filter((m) => m.includes("has not been granted the required scopes"));
|
|
202
|
+
if (scopeMessages.length > 0) {
|
|
203
|
+
const requiredScopes = /* @__PURE__ */ new Set();
|
|
204
|
+
let currentScopes = [];
|
|
205
|
+
for (const msg of scopeMessages) {
|
|
206
|
+
for (const match of msg.matchAll(
|
|
207
|
+
/requires one of the following scopes: \['([^']+)'\]/g
|
|
208
|
+
)) {
|
|
209
|
+
requiredScopes.add(match[1]);
|
|
210
|
+
}
|
|
211
|
+
if (currentScopes.length === 0) {
|
|
212
|
+
const currMatch = /has only been granted the: \[([^\]]+)\]/.exec(msg);
|
|
213
|
+
if (currMatch) {
|
|
214
|
+
currentScopes = currMatch[1].split(",").map((s) => s.trim().replace(/'/g, "")).filter(Boolean);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
throw new GitHubScopeError(
|
|
219
|
+
"Token is missing required GitHub scopes.",
|
|
220
|
+
[...requiredScopes],
|
|
221
|
+
currentScopes
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
throw new GitHubApiError(
|
|
225
|
+
`GraphQL errors: ${payload.errors.map((e) => e.message).join("; ")}`
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
if (!payload.data) {
|
|
229
|
+
throw new GitHubApiError("GraphQL response missing data.");
|
|
230
|
+
}
|
|
231
|
+
return payload.data;
|
|
232
|
+
}
|
|
233
|
+
var VIEWER_PROJECTS_QUERY = `
|
|
234
|
+
query ViewerProjects {
|
|
235
|
+
viewer {
|
|
236
|
+
login
|
|
237
|
+
projectsV2(first: 50) {
|
|
238
|
+
nodes {
|
|
239
|
+
id
|
|
240
|
+
title
|
|
241
|
+
shortDescription
|
|
242
|
+
url
|
|
243
|
+
items { totalCount }
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
organizations(first: 20) {
|
|
247
|
+
nodes {
|
|
248
|
+
login
|
|
249
|
+
projectsV2(first: 50) {
|
|
250
|
+
nodes {
|
|
251
|
+
id
|
|
252
|
+
title
|
|
253
|
+
shortDescription
|
|
254
|
+
url
|
|
255
|
+
items { totalCount }
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
`;
|
|
263
|
+
var PROJECT_DETAIL_QUERY = `
|
|
264
|
+
query ProjectDetail($projectId: ID!) {
|
|
265
|
+
node(id: $projectId) {
|
|
266
|
+
__typename
|
|
267
|
+
... on ProjectV2 {
|
|
268
|
+
id
|
|
269
|
+
title
|
|
270
|
+
url
|
|
271
|
+
fields(first: 50) {
|
|
272
|
+
nodes {
|
|
273
|
+
__typename
|
|
274
|
+
... on ProjectV2SingleSelectField {
|
|
275
|
+
id
|
|
276
|
+
name
|
|
277
|
+
options {
|
|
278
|
+
id
|
|
279
|
+
name
|
|
280
|
+
description
|
|
281
|
+
color
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
... on ProjectV2Field {
|
|
285
|
+
id
|
|
286
|
+
name
|
|
287
|
+
dataType
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
items(first: 100) {
|
|
292
|
+
nodes {
|
|
293
|
+
content {
|
|
294
|
+
__typename
|
|
295
|
+
... on Issue {
|
|
296
|
+
repository {
|
|
297
|
+
name
|
|
298
|
+
url
|
|
299
|
+
owner { login }
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
... on PullRequest {
|
|
303
|
+
repository {
|
|
304
|
+
name
|
|
305
|
+
url
|
|
306
|
+
owner { login }
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
pageInfo {
|
|
312
|
+
endCursor
|
|
313
|
+
hasNextPage
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
`;
|
|
320
|
+
var PROJECT_ITEMS_PAGE_QUERY = `
|
|
321
|
+
query ProjectItemsPage($projectId: ID!, $cursor: String) {
|
|
322
|
+
node(id: $projectId) {
|
|
323
|
+
... on ProjectV2 {
|
|
324
|
+
items(first: 100, after: $cursor) {
|
|
325
|
+
nodes {
|
|
326
|
+
content {
|
|
327
|
+
__typename
|
|
328
|
+
... on Issue {
|
|
329
|
+
repository {
|
|
330
|
+
name
|
|
331
|
+
url
|
|
332
|
+
owner { login }
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
... on PullRequest {
|
|
336
|
+
repository {
|
|
337
|
+
name
|
|
338
|
+
url
|
|
339
|
+
owner { login }
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
pageInfo {
|
|
345
|
+
endCursor
|
|
346
|
+
hasNextPage
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
`;
|
|
353
|
+
|
|
354
|
+
// src/github/gh-auth.ts
|
|
355
|
+
import { execFileSync, spawnSync } from "child_process";
|
|
356
|
+
var REQUIRED_GH_SCOPES = ["repo", "read:org", "project"];
|
|
357
|
+
var GhAuthError = class extends Error {
|
|
358
|
+
constructor(code, message) {
|
|
359
|
+
super(message);
|
|
360
|
+
this.code = code;
|
|
361
|
+
this.name = "GhAuthError";
|
|
362
|
+
}
|
|
363
|
+
};
|
|
364
|
+
function ghTokenReadErrorMessage() {
|
|
365
|
+
return "Failed to read a GitHub token from gh CLI. Run 'gh auth status' and try again.";
|
|
366
|
+
}
|
|
367
|
+
function missingGhScopesMessage(missing) {
|
|
368
|
+
return `Run 'gh auth refresh --scopes repo,read:org,project'. Missing scopes: ${missing.join(", ")}`;
|
|
369
|
+
}
|
|
370
|
+
function classifyTokenValidationError(error, source) {
|
|
371
|
+
if (error instanceof GhAuthError) {
|
|
372
|
+
return error;
|
|
373
|
+
}
|
|
374
|
+
if (error instanceof GitHubApiError) {
|
|
375
|
+
if (error.status === 401) {
|
|
376
|
+
return new GhAuthError(
|
|
377
|
+
source === "env" ? "invalid_token" : "token_failed",
|
|
378
|
+
source === "env" ? "GITHUB_GRAPHQL_TOKEN is invalid or expired." : ghTokenReadErrorMessage()
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
const prefix = source === "env" ? "GITHUB_GRAPHQL_TOKEN could not be validated" : "gh CLI token could not be validated";
|
|
382
|
+
return new GhAuthError("token_failed", `${prefix}: ${error.message}`);
|
|
383
|
+
}
|
|
384
|
+
if (error instanceof Error) {
|
|
385
|
+
const prefix = source === "env" ? "GITHUB_GRAPHQL_TOKEN could not be validated" : "gh CLI token could not be validated";
|
|
386
|
+
return new GhAuthError("token_failed", `${prefix}: ${error.message}`);
|
|
387
|
+
}
|
|
388
|
+
return new GhAuthError(
|
|
389
|
+
"token_failed",
|
|
390
|
+
source === "env" ? "GITHUB_GRAPHQL_TOKEN could not be validated." : "gh CLI token could not be validated."
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
function getEnvGitHubToken() {
|
|
394
|
+
const token = process.env.GITHUB_GRAPHQL_TOKEN?.trim();
|
|
395
|
+
return token ? token : null;
|
|
396
|
+
}
|
|
397
|
+
function checkGhInstalled(opts) {
|
|
398
|
+
const execImpl = opts?.execImpl ?? execFileSync;
|
|
399
|
+
try {
|
|
400
|
+
execImpl("gh", ["--version"], { stdio: "pipe" });
|
|
401
|
+
return true;
|
|
402
|
+
} catch (error) {
|
|
403
|
+
const execError = error;
|
|
404
|
+
if (execError.code === "ENOENT") {
|
|
405
|
+
return false;
|
|
406
|
+
}
|
|
407
|
+
throw error;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
function checkGhAuthenticated(opts) {
|
|
411
|
+
const spawnImpl = opts?.spawnImpl ?? spawnSync;
|
|
412
|
+
const result = spawnImpl("gh", ["auth", "status"], {
|
|
413
|
+
encoding: "utf8",
|
|
414
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
415
|
+
});
|
|
416
|
+
if ((result.status ?? 1) !== 0) {
|
|
417
|
+
return { authenticated: false };
|
|
418
|
+
}
|
|
419
|
+
const login = parseLogin((result.stdout ?? "").toString());
|
|
420
|
+
return { authenticated: true, login };
|
|
421
|
+
}
|
|
422
|
+
function checkGhScopes(opts) {
|
|
423
|
+
const spawnImpl = opts?.spawnImpl ?? spawnSync;
|
|
424
|
+
const result = spawnImpl("gh", ["auth", "status"], {
|
|
425
|
+
encoding: "utf8",
|
|
426
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
427
|
+
});
|
|
428
|
+
const output = (result.stdout ?? "").toString();
|
|
429
|
+
const scopes = parseScopes(output);
|
|
430
|
+
if (scopes.length === 0) {
|
|
431
|
+
return { valid: true, missing: [], scopes: [] };
|
|
432
|
+
}
|
|
433
|
+
const normalized = scopes.map((scope) => scope.toLowerCase());
|
|
434
|
+
const missing = REQUIRED_GH_SCOPES.filter(
|
|
435
|
+
(scope) => !normalized.includes(scope)
|
|
436
|
+
);
|
|
437
|
+
return {
|
|
438
|
+
valid: missing.length === 0,
|
|
439
|
+
missing: [...missing],
|
|
440
|
+
scopes
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
function getGhToken(opts) {
|
|
444
|
+
const envToken = opts?.allowEnv === false ? null : getEnvGitHubToken();
|
|
445
|
+
if (envToken) {
|
|
446
|
+
return envToken;
|
|
447
|
+
}
|
|
448
|
+
return getGhTokenWithSource({
|
|
449
|
+
execImpl: opts?.execImpl,
|
|
450
|
+
envToken: void 0
|
|
451
|
+
}).token;
|
|
452
|
+
}
|
|
453
|
+
function getGhTokenWithSource(opts) {
|
|
454
|
+
const hasExplicitEnvToken = opts !== void 0 && Object.prototype.hasOwnProperty.call(opts, "envToken");
|
|
455
|
+
const envToken = hasExplicitEnvToken ? opts.envToken?.trim() ?? null : getEnvGitHubToken();
|
|
456
|
+
if (envToken) {
|
|
457
|
+
return { token: envToken, source: "env" };
|
|
458
|
+
}
|
|
459
|
+
const execImpl = opts?.execImpl ?? execFileSync;
|
|
460
|
+
try {
|
|
461
|
+
const token = execImpl("gh", ["auth", "token"], {
|
|
462
|
+
encoding: "utf8",
|
|
463
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
464
|
+
}).toString().trim();
|
|
465
|
+
if (!token) {
|
|
466
|
+
throw new GhAuthError("token_failed", ghTokenReadErrorMessage());
|
|
467
|
+
}
|
|
468
|
+
return { token, source: "gh" };
|
|
469
|
+
} catch (error) {
|
|
470
|
+
if (error instanceof GhAuthError) {
|
|
471
|
+
throw error;
|
|
472
|
+
}
|
|
473
|
+
throw new GhAuthError("token_failed", ghTokenReadErrorMessage());
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
async function validateGitHubToken(token, source, opts) {
|
|
477
|
+
const createClientImpl = opts?.createClientImpl ?? createClient;
|
|
478
|
+
const validateTokenImpl = opts?.validateTokenImpl ?? validateToken;
|
|
479
|
+
const checkRequiredScopesImpl = opts?.checkRequiredScopesImpl ?? checkRequiredScopes;
|
|
480
|
+
let viewer;
|
|
481
|
+
try {
|
|
482
|
+
const client = createClientImpl(token);
|
|
483
|
+
viewer = await validateTokenImpl(client);
|
|
484
|
+
} catch (error) {
|
|
485
|
+
throw classifyTokenValidationError(error, source);
|
|
486
|
+
}
|
|
487
|
+
const scopeCheck = checkRequiredScopesImpl(viewer.scopes);
|
|
488
|
+
if (!scopeCheck.valid) {
|
|
489
|
+
if (source === "env") {
|
|
490
|
+
throw new GhAuthError(
|
|
491
|
+
"missing_scopes",
|
|
492
|
+
`GITHUB_GRAPHQL_TOKEN is missing required scopes: ${scopeCheck.missing.join(", ")}`
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
throw new GhAuthError(
|
|
496
|
+
"missing_scopes",
|
|
497
|
+
missingGhScopesMessage(scopeCheck.missing)
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
return {
|
|
501
|
+
source,
|
|
502
|
+
token,
|
|
503
|
+
login: viewer.login,
|
|
504
|
+
scopes: viewer.scopes
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
async function resolveGitHubAuth(opts) {
|
|
508
|
+
const envToken = getEnvGitHubToken();
|
|
509
|
+
let envError = null;
|
|
510
|
+
if (envToken) {
|
|
511
|
+
try {
|
|
512
|
+
return await validateGitHubToken(envToken, "env", opts);
|
|
513
|
+
} catch (error) {
|
|
514
|
+
if (error instanceof GhAuthError) {
|
|
515
|
+
envError = error;
|
|
516
|
+
} else {
|
|
517
|
+
throw error;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
try {
|
|
522
|
+
const auth = ensureGhAuth(opts);
|
|
523
|
+
return await validateGitHubToken(auth.token, "gh", opts);
|
|
524
|
+
} catch (error) {
|
|
525
|
+
if (envError && error instanceof GhAuthError) {
|
|
526
|
+
throw envError;
|
|
527
|
+
}
|
|
528
|
+
throw error;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
function ensureGhAuth(opts) {
|
|
532
|
+
const execImpl = opts?.execImpl ?? execFileSync;
|
|
533
|
+
const spawnImpl = opts?.spawnImpl ?? spawnSync;
|
|
534
|
+
if (!checkGhInstalled({ execImpl })) {
|
|
535
|
+
throw new GhAuthError(
|
|
536
|
+
"not_installed",
|
|
537
|
+
"gh CLI is not installed. Install it from https://cli.github.com or set GITHUB_GRAPHQL_TOKEN."
|
|
538
|
+
);
|
|
539
|
+
}
|
|
540
|
+
const auth = checkGhAuthenticated({ spawnImpl });
|
|
541
|
+
if (!auth.authenticated) {
|
|
542
|
+
throw new GhAuthError(
|
|
543
|
+
"not_authenticated",
|
|
544
|
+
"Run 'gh auth login --scopes repo,read:org,project' or set GITHUB_GRAPHQL_TOKEN."
|
|
545
|
+
);
|
|
546
|
+
}
|
|
547
|
+
const scopeCheck = checkGhScopes({ spawnImpl });
|
|
548
|
+
if (!scopeCheck.valid) {
|
|
549
|
+
throw new GhAuthError(
|
|
550
|
+
"missing_scopes",
|
|
551
|
+
missingGhScopesMessage(scopeCheck.missing)
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
const { token } = getGhTokenWithSource({
|
|
555
|
+
execImpl,
|
|
556
|
+
envToken: void 0
|
|
557
|
+
});
|
|
558
|
+
return { login: auth.login ?? "unknown", token, source: "gh" };
|
|
559
|
+
}
|
|
560
|
+
function isInteractiveTerminal() {
|
|
561
|
+
return process.stdin.isTTY === true && process.stdout.isTTY === true;
|
|
562
|
+
}
|
|
563
|
+
function runGhAuthCommand(mode, opts) {
|
|
564
|
+
const spawnImpl = opts?.spawnImpl ?? spawnSync;
|
|
565
|
+
const command = `gh auth ${mode} --scopes ${REQUIRED_GH_SCOPES.join(",")}`;
|
|
566
|
+
const interactive = opts?.interactive ?? isInteractiveTerminal();
|
|
567
|
+
if (!interactive) {
|
|
568
|
+
return {
|
|
569
|
+
mode,
|
|
570
|
+
status: "manual",
|
|
571
|
+
command,
|
|
572
|
+
summary: `Interactive terminal not available. Run '${command}' manually.`
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
const result = spawnImpl(
|
|
576
|
+
"gh",
|
|
577
|
+
["auth", mode, "--scopes", REQUIRED_GH_SCOPES.join(",")],
|
|
578
|
+
{
|
|
579
|
+
stdio: "inherit"
|
|
580
|
+
}
|
|
581
|
+
);
|
|
582
|
+
if ((result.status ?? 1) === 0) {
|
|
583
|
+
return {
|
|
584
|
+
mode,
|
|
585
|
+
status: "applied",
|
|
586
|
+
command,
|
|
587
|
+
summary: `Executed '${command}'.`
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
return {
|
|
591
|
+
mode,
|
|
592
|
+
status: "manual",
|
|
593
|
+
command,
|
|
594
|
+
summary: `Failed to complete '${command}' automatically. Re-run it manually.`
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
function runGhAuthLogin(opts) {
|
|
598
|
+
return runGhAuthCommand("login", opts);
|
|
599
|
+
}
|
|
600
|
+
function runGhAuthRefresh(opts) {
|
|
601
|
+
return runGhAuthCommand("refresh", opts);
|
|
602
|
+
}
|
|
603
|
+
function parseLogin(output) {
|
|
604
|
+
const matched = output.match(
|
|
605
|
+
/Logged in to github\.com account\s+\*?\*?([A-Za-z0-9_-]+)\*?\*?/i
|
|
606
|
+
);
|
|
607
|
+
return matched?.[1];
|
|
608
|
+
}
|
|
609
|
+
function parseScopes(output) {
|
|
610
|
+
const matched = output.match(/Token scopes:\s*(.+)/i);
|
|
611
|
+
if (!matched) {
|
|
612
|
+
return [];
|
|
613
|
+
}
|
|
614
|
+
return matched[1].split(",").map((scope) => scope.trim().replace(/^'+|'+$/g, "")).filter((scope) => scope.length > 0);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
export {
|
|
618
|
+
GitHubApiError,
|
|
619
|
+
GitHubScopeError,
|
|
620
|
+
createClient,
|
|
621
|
+
validateToken,
|
|
622
|
+
checkRequiredScopes,
|
|
623
|
+
listUserProjects,
|
|
624
|
+
getProjectDetail,
|
|
625
|
+
REQUIRED_GH_SCOPES,
|
|
626
|
+
GhAuthError,
|
|
627
|
+
getEnvGitHubToken,
|
|
628
|
+
checkGhInstalled,
|
|
629
|
+
checkGhAuthenticated,
|
|
630
|
+
checkGhScopes,
|
|
631
|
+
getGhToken,
|
|
632
|
+
getGhTokenWithSource,
|
|
633
|
+
validateGitHubToken,
|
|
634
|
+
resolveGitHubAuth,
|
|
635
|
+
ensureGhAuth,
|
|
636
|
+
runGhAuthLogin,
|
|
637
|
+
runGhAuthRefresh
|
|
638
|
+
};
|
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
import {
|
|
20
20
|
handleMissingManagedProjectConfig,
|
|
21
21
|
resolveManagedProjectConfig
|
|
22
|
-
} from "./chunk-
|
|
22
|
+
} from "./chunk-C7G7RJ4G.js";
|
|
23
23
|
|
|
24
24
|
// src/commands/status.ts
|
|
25
25
|
import { readFile } from "fs/promises";
|
|
@@ -30,7 +30,7 @@ var COL_ID = 24;
|
|
|
30
30
|
var COL_STATUS = 14;
|
|
31
31
|
var COL_PID = 8;
|
|
32
32
|
var COL_AGE_TURN = 12;
|
|
33
|
-
var COL_TOKENS =
|
|
33
|
+
var COL_TOKENS = 17;
|
|
34
34
|
var COL_SESSION = 14;
|
|
35
35
|
var COL_ID_HEADER = COL_ID + 2;
|
|
36
36
|
var identity = (s) => s;
|
|
@@ -63,6 +63,11 @@ function compactSessionId(id) {
|
|
|
63
63
|
function fmtTokens(n) {
|
|
64
64
|
return n.toLocaleString("en-US");
|
|
65
65
|
}
|
|
66
|
+
function fmtTokenPair(delta, cumulative) {
|
|
67
|
+
const left = fmtTokens(delta ?? 0);
|
|
68
|
+
const right = fmtTokens(cumulative ?? delta ?? 0);
|
|
69
|
+
return `${left} / ${right}`;
|
|
70
|
+
}
|
|
66
71
|
function fmtAge(startedAt, now) {
|
|
67
72
|
if (!startedAt) return "0m";
|
|
68
73
|
const diffMs = now - new Date(startedAt).getTime();
|
|
@@ -173,7 +178,10 @@ function activeRunRow(run, now, evtWidth, c) {
|
|
|
173
178
|
const turn = run.turnCount ?? 0;
|
|
174
179
|
const ageTurn = pad(`${age}/${turn}`, COL_AGE_TURN);
|
|
175
180
|
const tokens = pad(
|
|
176
|
-
|
|
181
|
+
fmtTokenPair(
|
|
182
|
+
run.tokenUsage?.totalTokens,
|
|
183
|
+
run.tokenUsage?.cumulativeTotalTokens
|
|
184
|
+
),
|
|
177
185
|
COL_TOKENS,
|
|
178
186
|
"right"
|
|
179
187
|
);
|
|
@@ -260,6 +268,15 @@ function truncate(s, len) {
|
|
|
260
268
|
if (s.length <= len) return s;
|
|
261
269
|
return s.slice(0, len - 3) + "...";
|
|
262
270
|
}
|
|
271
|
+
function formatTokenPair(delta, cumulative) {
|
|
272
|
+
return `${delta.toLocaleString("en-US")} / ${cumulative.toLocaleString("en-US")}`;
|
|
273
|
+
}
|
|
274
|
+
function resolveProjectTokenDelta(snapshot) {
|
|
275
|
+
return snapshot.activeRuns.reduce(
|
|
276
|
+
(sum, run) => sum + (run.tokenUsage?.totalTokens ?? 0),
|
|
277
|
+
0
|
|
278
|
+
);
|
|
279
|
+
}
|
|
263
280
|
function renderLegacyStatus(snapshot, noColor) {
|
|
264
281
|
const apply = noColor ? (s) => stripAnsi(s) : (s) => s;
|
|
265
282
|
const lines = [];
|
|
@@ -326,12 +343,13 @@ function renderLegacyStatus(snapshot, noColor) {
|
|
|
326
343
|
lines.push("");
|
|
327
344
|
}
|
|
328
345
|
if (snapshot.codexTotals) {
|
|
346
|
+
const tokenDelta = resolveProjectTokenDelta(snapshot);
|
|
329
347
|
const tokenStr = apply(
|
|
330
|
-
`Tokens: ${
|
|
348
|
+
`Tokens: ${formatTokenPair(tokenDelta, snapshot.codexTotals.totalTokens)} total`
|
|
331
349
|
);
|
|
332
350
|
lines.push(` ${tokenStr}`);
|
|
333
351
|
} else {
|
|
334
|
-
lines.push(" Tokens: 0
|
|
352
|
+
lines.push(" Tokens: 0 / 0 total");
|
|
335
353
|
}
|
|
336
354
|
return lines.join("\n");
|
|
337
355
|
}
|