@gh-symphony/cli 0.0.16 → 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-N7YXKG6V.js +16 -0
- package/dist/worker-entry.js +16 -10
- package/package.json +4 -4
- package/dist/chunk-TH5QPO3Y.js +0 -67
- package/dist/version-VBB62JWI.js +0 -30
|
@@ -0,0 +1,362 @@
|
|
|
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
|
+
export {
|
|
355
|
+
GitHubApiError,
|
|
356
|
+
GitHubScopeError,
|
|
357
|
+
createClient,
|
|
358
|
+
validateToken,
|
|
359
|
+
checkRequiredScopes,
|
|
360
|
+
listUserProjects,
|
|
361
|
+
getProjectDetail
|
|
362
|
+
};
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// src/github/gh-auth.ts
|
|
4
4
|
import { execFileSync, spawnSync } from "child_process";
|
|
5
|
-
var
|
|
5
|
+
var REQUIRED_GH_SCOPES = ["repo", "read:org", "project"];
|
|
6
6
|
var GhAuthError = class extends Error {
|
|
7
7
|
constructor(code, message) {
|
|
8
8
|
super(message);
|
|
@@ -47,7 +47,7 @@ function checkGhScopes(opts) {
|
|
|
47
47
|
return { valid: true, missing: [], scopes: [] };
|
|
48
48
|
}
|
|
49
49
|
const normalized = scopes.map((scope) => scope.toLowerCase());
|
|
50
|
-
const missing =
|
|
50
|
+
const missing = REQUIRED_GH_SCOPES.filter(
|
|
51
51
|
(scope) => !normalized.includes(scope)
|
|
52
52
|
);
|
|
53
53
|
return {
|
|
@@ -124,7 +124,11 @@ function parseScopes(output) {
|
|
|
124
124
|
}
|
|
125
125
|
|
|
126
126
|
export {
|
|
127
|
+
REQUIRED_GH_SCOPES,
|
|
127
128
|
GhAuthError,
|
|
129
|
+
checkGhInstalled,
|
|
130
|
+
checkGhAuthenticated,
|
|
131
|
+
checkGhScopes,
|
|
128
132
|
getGhToken,
|
|
129
133
|
ensureGhAuth
|
|
130
134
|
};
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
loadGlobalConfig,
|
|
4
|
+
loadProjectConfig
|
|
5
|
+
} from "./chunk-ROGRTUFI.js";
|
|
6
|
+
|
|
7
|
+
// src/project-selection.ts
|
|
8
|
+
import * as p from "@clack/prompts";
|
|
9
|
+
function isInteractiveTerminal() {
|
|
10
|
+
return process.stdin.isTTY === true && process.stdout.isTTY === true;
|
|
11
|
+
}
|
|
12
|
+
function explicitProjectRequiredMessage() {
|
|
13
|
+
return "Multiple projects are configured. Re-run with --project-id in non-interactive environments.\n";
|
|
14
|
+
}
|
|
15
|
+
async function inspectManagedProjectSelection(input) {
|
|
16
|
+
if (input.requestedProjectId) {
|
|
17
|
+
const projectConfig = await loadProjectConfig(
|
|
18
|
+
input.configDir,
|
|
19
|
+
input.requestedProjectId
|
|
20
|
+
);
|
|
21
|
+
if (!projectConfig) {
|
|
22
|
+
return {
|
|
23
|
+
kind: "requested_project_missing",
|
|
24
|
+
projectId: input.requestedProjectId,
|
|
25
|
+
message: `Project "${input.requestedProjectId}" is not configured. Run 'gh-symphony project add' or choose an existing project.`
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
kind: "resolved",
|
|
30
|
+
projectId: input.requestedProjectId,
|
|
31
|
+
projectConfig
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
const global = await loadGlobalConfig(input.configDir);
|
|
35
|
+
if (!global) {
|
|
36
|
+
return {
|
|
37
|
+
kind: "missing_global_config",
|
|
38
|
+
message: "No CLI configuration found. Run 'gh-symphony project add' first."
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
const projectIds = global.projects ?? [];
|
|
42
|
+
if (projectIds.length === 0) {
|
|
43
|
+
return {
|
|
44
|
+
kind: "no_projects",
|
|
45
|
+
message: "No managed projects are configured. Run 'gh-symphony project add' first."
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
if (projectIds.length > 1 && !isInteractiveTerminal()) {
|
|
49
|
+
return {
|
|
50
|
+
kind: "multiple_projects_require_selection",
|
|
51
|
+
message: explicitProjectRequiredMessage().trimEnd()
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
if (global.activeProject) {
|
|
55
|
+
const projectConfig = await loadProjectConfig(
|
|
56
|
+
input.configDir,
|
|
57
|
+
global.activeProject
|
|
58
|
+
);
|
|
59
|
+
if (!projectConfig) {
|
|
60
|
+
return {
|
|
61
|
+
kind: "active_project_missing",
|
|
62
|
+
projectId: global.activeProject,
|
|
63
|
+
message: `Active project "${global.activeProject}" is configured in config.json but its project config is missing. Re-run 'gh-symphony project add' or 'gh-symphony project switch'.`
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
kind: "resolved",
|
|
68
|
+
projectId: global.activeProject,
|
|
69
|
+
projectConfig
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
if (projectIds.length === 1) {
|
|
73
|
+
const projectId = projectIds[0];
|
|
74
|
+
const projectConfig = await loadProjectConfig(input.configDir, projectId);
|
|
75
|
+
if (!projectConfig) {
|
|
76
|
+
return {
|
|
77
|
+
kind: "configured_project_missing",
|
|
78
|
+
projectId,
|
|
79
|
+
message: `Configured project "${projectId}" is missing its project config file. Re-run 'gh-symphony project add'.`
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
kind: "resolved",
|
|
84
|
+
projectId,
|
|
85
|
+
projectConfig
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
kind: "multiple_projects_require_selection",
|
|
90
|
+
message: "Multiple projects are configured and no active project is set. Run 'gh-symphony project switch' or re-run with --project-id."
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
async function resolveManagedProjectConfig(input) {
|
|
94
|
+
if (input.requestedProjectId) {
|
|
95
|
+
return loadProjectConfig(input.configDir, input.requestedProjectId);
|
|
96
|
+
}
|
|
97
|
+
const global = await loadGlobalConfig(input.configDir);
|
|
98
|
+
const projectIds = global?.projects ?? [];
|
|
99
|
+
if (projectIds.length === 0) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
if (projectIds.length === 1) {
|
|
103
|
+
return loadProjectConfig(input.configDir, projectIds[0]);
|
|
104
|
+
}
|
|
105
|
+
if (!isInteractiveTerminal()) {
|
|
106
|
+
process.stderr.write(explicitProjectRequiredMessage());
|
|
107
|
+
process.exitCode = 1;
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
const projects = await Promise.all(
|
|
111
|
+
projectIds.map(async (projectId) => ({
|
|
112
|
+
projectId,
|
|
113
|
+
config: await loadProjectConfig(input.configDir, projectId)
|
|
114
|
+
}))
|
|
115
|
+
);
|
|
116
|
+
const selected = await p.select({
|
|
117
|
+
message: "Select a project:",
|
|
118
|
+
options: projects.map(({ projectId, config }) => ({
|
|
119
|
+
value: projectId,
|
|
120
|
+
label: config?.displayName ?? config?.slug ?? projectId,
|
|
121
|
+
hint: projectId === global?.activeProject ? "current" : config && config.displayName && config.displayName !== projectId ? projectId : void 0
|
|
122
|
+
})),
|
|
123
|
+
maxItems: 10
|
|
124
|
+
});
|
|
125
|
+
if (p.isCancel(selected)) {
|
|
126
|
+
p.cancel("Cancelled.");
|
|
127
|
+
process.exitCode = 130;
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
return loadProjectConfig(input.configDir, selected);
|
|
131
|
+
}
|
|
132
|
+
function handleMissingManagedProjectConfig() {
|
|
133
|
+
if (process.exitCode) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
process.stderr.write(
|
|
137
|
+
"No project configured. Run 'gh-symphony project add' first.\n"
|
|
138
|
+
);
|
|
139
|
+
process.exitCode = 1;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export {
|
|
143
|
+
inspectManagedProjectSelection,
|
|
144
|
+
resolveManagedProjectConfig,
|
|
145
|
+
handleMissingManagedProjectConfig
|
|
146
|
+
};
|