@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
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
GhAuthError,
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
GitHubScopeError,
|
|
5
|
+
checkRequiredScopes,
|
|
6
|
+
createClient,
|
|
7
|
+
getGhTokenWithSource,
|
|
8
|
+
getProjectDetail,
|
|
9
|
+
listUserProjects,
|
|
10
|
+
resolveGitHubAuth,
|
|
11
|
+
validateToken
|
|
12
|
+
} from "./chunk-TILHWBP6.js";
|
|
7
13
|
import {
|
|
8
14
|
loadGlobalConfig,
|
|
9
15
|
saveGlobalConfig,
|
|
@@ -13,360 +19,9 @@ import {
|
|
|
13
19
|
// src/commands/init.ts
|
|
14
20
|
import * as p from "@clack/prompts";
|
|
15
21
|
import { createHash } from "crypto";
|
|
16
|
-
import { mkdir as mkdir3, rename, writeFile as writeFile3 } from "fs/promises";
|
|
22
|
+
import { mkdir as mkdir3, readFile as readFile3, rename as rename2, writeFile as writeFile3 } from "fs/promises";
|
|
17
23
|
import { basename, dirname as dirname2, join as join3, relative, resolve } from "path";
|
|
18
24
|
|
|
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
25
|
// src/mapping/smart-defaults.ts
|
|
371
26
|
var ROLE_PATTERNS = [
|
|
372
27
|
{
|
|
@@ -811,16 +466,6 @@ function generateContextYamlString(context) {
|
|
|
811
466
|
lines.push(` agent_command: ${yamlQuote(context.runtime.agent_command)}`);
|
|
812
467
|
return lines.join("\n") + "\n";
|
|
813
468
|
}
|
|
814
|
-
async function writeContextYaml(outputDir, context) {
|
|
815
|
-
await mkdir(outputDir, { recursive: true });
|
|
816
|
-
const contextPath = `${outputDir}/.gh-symphony/context.yaml`;
|
|
817
|
-
await mkdir(dirname(contextPath), { recursive: true });
|
|
818
|
-
const temporaryPath = `${contextPath}.tmp`;
|
|
819
|
-
const yamlContent = generateContextYamlString(context);
|
|
820
|
-
await writeFile(temporaryPath, yamlContent, "utf8");
|
|
821
|
-
const { rename: rename2 } = await import("fs/promises");
|
|
822
|
-
await rename2(temporaryPath, contextPath);
|
|
823
|
-
}
|
|
824
469
|
function buildContextYaml(params) {
|
|
825
470
|
const columns = params.statusField.options.map((option) => {
|
|
826
471
|
const roleMapping = inferStateRole(option.name);
|
|
@@ -1201,7 +846,7 @@ function resolveRoleAction(role) {
|
|
|
1201
846
|
}
|
|
1202
847
|
|
|
1203
848
|
// src/skills/skill-writer.ts
|
|
1204
|
-
import { mkdir as mkdir2,
|
|
849
|
+
import { mkdir as mkdir2, readFile as readFile2, rename, writeFile as writeFile2 } from "fs/promises";
|
|
1205
850
|
import { join as join2 } from "path";
|
|
1206
851
|
function normalizeRuntimeForSkills(runtime) {
|
|
1207
852
|
if (runtime === "claude-code" || runtime.includes("claude-code")) {
|
|
@@ -1222,44 +867,18 @@ function resolveSkillsDir(repoRoot, runtime) {
|
|
|
1222
867
|
}
|
|
1223
868
|
return null;
|
|
1224
869
|
}
|
|
1225
|
-
|
|
1226
|
-
const skillDir = join2(skillsDir, template.name);
|
|
1227
|
-
const filePath = join2(skillDir, template.fileName);
|
|
1228
|
-
if (!options?.overwrite) {
|
|
1229
|
-
try {
|
|
1230
|
-
await readFile2(filePath, "utf8");
|
|
1231
|
-
return { written: false, path: filePath };
|
|
1232
|
-
} catch (error) {
|
|
1233
|
-
const err = error;
|
|
1234
|
-
if (err.code !== "ENOENT") {
|
|
1235
|
-
throw error;
|
|
1236
|
-
}
|
|
1237
|
-
}
|
|
1238
|
-
}
|
|
1239
|
-
await mkdir2(skillDir, { recursive: true });
|
|
1240
|
-
const content = template.generate(context);
|
|
1241
|
-
const temporaryPath = `${filePath}.tmp`;
|
|
1242
|
-
await writeFile2(temporaryPath, content, "utf8");
|
|
1243
|
-
const { rename: rename2 } = await import("fs/promises");
|
|
1244
|
-
await rename2(temporaryPath, filePath);
|
|
1245
|
-
return { written: true, path: filePath };
|
|
1246
|
-
}
|
|
1247
|
-
async function writeAllSkills(repoRoot, runtime, templates, context, options) {
|
|
870
|
+
function buildSkillFilePlans(repoRoot, runtime, templates, context) {
|
|
1248
871
|
const skillsDir = resolveSkillsDir(repoRoot, runtime);
|
|
1249
872
|
if (!skillsDir) {
|
|
1250
|
-
return {
|
|
1251
|
-
}
|
|
1252
|
-
const written = [];
|
|
1253
|
-
const skipped = [];
|
|
1254
|
-
for (const template of templates) {
|
|
1255
|
-
const result = await writeSkillFile(skillsDir, template, context, options);
|
|
1256
|
-
if (result.written) {
|
|
1257
|
-
written.push(result.path);
|
|
1258
|
-
} else {
|
|
1259
|
-
skipped.push(result.path);
|
|
1260
|
-
}
|
|
873
|
+
return { skillsDir: null, files: [] };
|
|
1261
874
|
}
|
|
1262
|
-
return {
|
|
875
|
+
return {
|
|
876
|
+
skillsDir,
|
|
877
|
+
files: templates.map((template) => ({
|
|
878
|
+
path: join2(skillsDir, template.name, template.fileName),
|
|
879
|
+
content: template.generate(context)
|
|
880
|
+
}))
|
|
881
|
+
};
|
|
1263
882
|
}
|
|
1264
883
|
|
|
1265
884
|
// src/skills/templates/document.ts
|
|
@@ -1797,6 +1416,7 @@ async function abortIfCancelled(input) {
|
|
|
1797
1416
|
}
|
|
1798
1417
|
function parseInitFlags(args) {
|
|
1799
1418
|
const flags = {
|
|
1419
|
+
dryRun: false,
|
|
1800
1420
|
nonInteractive: false,
|
|
1801
1421
|
skipSkills: false,
|
|
1802
1422
|
skipContext: false
|
|
@@ -1805,6 +1425,9 @@ function parseInitFlags(args) {
|
|
|
1805
1425
|
const arg = args[i];
|
|
1806
1426
|
const next = args[i + 1];
|
|
1807
1427
|
switch (arg) {
|
|
1428
|
+
case "--dry-run":
|
|
1429
|
+
flags.dryRun = true;
|
|
1430
|
+
break;
|
|
1808
1431
|
case "--non-interactive":
|
|
1809
1432
|
flags.nonInteractive = true;
|
|
1810
1433
|
break;
|
|
@@ -1832,29 +1455,145 @@ var handler = async (args, options) => {
|
|
|
1832
1455
|
await runNonInteractive(flags, options);
|
|
1833
1456
|
return;
|
|
1834
1457
|
}
|
|
1835
|
-
await runInteractive(options);
|
|
1458
|
+
await runInteractive(flags, options);
|
|
1836
1459
|
};
|
|
1837
1460
|
var init_default = handler;
|
|
1838
|
-
async function
|
|
1461
|
+
async function resolveChangeStatus(path, content, mode) {
|
|
1462
|
+
try {
|
|
1463
|
+
const existing = await readFile3(path, "utf8");
|
|
1464
|
+
if (mode === "create-only") {
|
|
1465
|
+
return "unchanged";
|
|
1466
|
+
}
|
|
1467
|
+
return existing === content ? "unchanged" : "update";
|
|
1468
|
+
} catch (error) {
|
|
1469
|
+
const err = error;
|
|
1470
|
+
if (err.code === "ENOENT") {
|
|
1471
|
+
return "create";
|
|
1472
|
+
}
|
|
1473
|
+
throw error;
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
async function planFileChange(input) {
|
|
1477
|
+
return {
|
|
1478
|
+
...input,
|
|
1479
|
+
status: await resolveChangeStatus(input.path, input.content, input.mode)
|
|
1480
|
+
};
|
|
1481
|
+
}
|
|
1482
|
+
async function writePlannedFile(file) {
|
|
1483
|
+
if (file.status === "unchanged") {
|
|
1484
|
+
return false;
|
|
1485
|
+
}
|
|
1486
|
+
await mkdir3(dirname2(file.path), { recursive: true });
|
|
1487
|
+
const temporaryPath = `${file.path}.tmp`;
|
|
1488
|
+
await writeFile3(temporaryPath, file.content, "utf8");
|
|
1489
|
+
await rename2(temporaryPath, file.path);
|
|
1490
|
+
return true;
|
|
1491
|
+
}
|
|
1492
|
+
function resolveStatusField(projectDetail) {
|
|
1493
|
+
return projectDetail.statusFields.find((f) => f.name.toLowerCase() === "status") ?? projectDetail.statusFields[0] ?? null;
|
|
1494
|
+
}
|
|
1495
|
+
function buildAutomaticStateMappings(statusField) {
|
|
1496
|
+
const mappings = {};
|
|
1497
|
+
for (const mapping of inferAllStateRoles(statusField.options.map((o) => o.name))) {
|
|
1498
|
+
if (mapping.role) {
|
|
1499
|
+
mappings[mapping.columnName] = { role: mapping.role };
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
return mappings;
|
|
1503
|
+
}
|
|
1504
|
+
async function promptStateMappings(statusField, options) {
|
|
1505
|
+
const mappings = {};
|
|
1506
|
+
const inferred = inferAllStateRoles(statusField.options.map((o) => o.name));
|
|
1507
|
+
p.log.info(
|
|
1508
|
+
`Found ${statusField.options.length} status columns on field "${statusField.name}".`
|
|
1509
|
+
);
|
|
1510
|
+
for (const mapping of inferred) {
|
|
1511
|
+
const roleOptions = [
|
|
1512
|
+
{ value: "active", label: "Active (agent works on this)" },
|
|
1513
|
+
{ value: "wait", label: "Wait (human review / hold)" },
|
|
1514
|
+
{ value: "terminal", label: "Terminal (completed)" }
|
|
1515
|
+
];
|
|
1516
|
+
const defaultRole = mapping.role ?? "wait";
|
|
1517
|
+
const sortedOptions = [
|
|
1518
|
+
roleOptions.find((o) => o.value === defaultRole),
|
|
1519
|
+
...roleOptions.filter((o) => o.value !== defaultRole)
|
|
1520
|
+
];
|
|
1521
|
+
const selectedRole = await abortIfCancelled(
|
|
1522
|
+
p.select({
|
|
1523
|
+
message: `${options?.stepLabel ?? "Step 2/2"} \u2014 Map column "${mapping.columnName}":${mapping.confidence === "high" ? " (auto-detected)" : ""}`,
|
|
1524
|
+
options: sortedOptions
|
|
1525
|
+
})
|
|
1526
|
+
);
|
|
1527
|
+
mappings[mapping.columnName] = { role: selectedRole };
|
|
1528
|
+
}
|
|
1529
|
+
return mappings;
|
|
1530
|
+
}
|
|
1531
|
+
async function planWorkflowArtifacts(opts) {
|
|
1532
|
+
const workflowMd = generateWorkflowMarkdown({
|
|
1533
|
+
projectId: opts.projectDetail.id,
|
|
1534
|
+
stateFieldName: opts.statusField.name,
|
|
1535
|
+
mappings: opts.mappings,
|
|
1536
|
+
lifecycle: toWorkflowLifecycleConfig(opts.statusField.name, opts.mappings),
|
|
1537
|
+
runtime: opts.runtime
|
|
1538
|
+
});
|
|
1539
|
+
const workflowPlan = await planFileChange({
|
|
1540
|
+
path: opts.outputPath,
|
|
1541
|
+
label: "WORKFLOW.md",
|
|
1542
|
+
content: workflowMd,
|
|
1543
|
+
mode: "overwrite"
|
|
1544
|
+
});
|
|
1545
|
+
const ecosystemPlan = await planEcosystem({
|
|
1546
|
+
cwd: opts.cwd,
|
|
1547
|
+
projectDetail: opts.projectDetail,
|
|
1548
|
+
statusField: opts.statusField,
|
|
1549
|
+
runtime: opts.runtime,
|
|
1550
|
+
skipSkills: opts.skipSkills,
|
|
1551
|
+
skipContext: opts.skipContext
|
|
1552
|
+
});
|
|
1553
|
+
return {
|
|
1554
|
+
outputPath: opts.outputPath,
|
|
1555
|
+
workflowMd,
|
|
1556
|
+
workflowPlan,
|
|
1557
|
+
ecosystemPlan
|
|
1558
|
+
};
|
|
1559
|
+
}
|
|
1560
|
+
async function writeWorkflowPlan(workflowPlan) {
|
|
1561
|
+
return writePlannedFile(workflowPlan);
|
|
1562
|
+
}
|
|
1563
|
+
function summarizeEnvironment(env) {
|
|
1564
|
+
return [
|
|
1565
|
+
`Package manager ${env.packageManager ?? "none"}${env.lockfile ? ` (${env.lockfile})` : ""}`,
|
|
1566
|
+
`Scripts test=${env.testCommand ?? "none"} | lint=${env.lintCommand ?? "none"} | build=${env.buildCommand ?? "none"}`,
|
|
1567
|
+
`CI ${env.ciPlatform ?? "none"}`,
|
|
1568
|
+
`Monorepo ${env.monorepo ? "yes" : "no"}`,
|
|
1569
|
+
`Existing skills ${env.existingSkills.length === 0 ? "none" : env.existingSkills.join(", ")}`
|
|
1570
|
+
];
|
|
1571
|
+
}
|
|
1572
|
+
async function planEcosystem(opts) {
|
|
1839
1573
|
const { cwd, projectDetail, statusField, runtime, skipSkills, skipContext } = opts;
|
|
1840
1574
|
const ghSymphonyDir = join3(cwd, ".gh-symphony");
|
|
1841
|
-
|
|
1842
|
-
const
|
|
1843
|
-
let contextYamlWritten = false;
|
|
1575
|
+
const environment = await detectEnvironment(cwd);
|
|
1576
|
+
const files = [];
|
|
1844
1577
|
if (!skipContext) {
|
|
1845
1578
|
const contextYaml = buildContextYaml({
|
|
1846
1579
|
projectDetail,
|
|
1847
1580
|
statusField,
|
|
1848
|
-
detectedEnvironment:
|
|
1581
|
+
detectedEnvironment: environment,
|
|
1849
1582
|
runtime: {
|
|
1850
1583
|
agent: runtime,
|
|
1851
1584
|
agent_command: runtime === "codex" ? "bash -lc codex app-server" : runtime === "claude-code" ? "bash -lc claude-code" : runtime
|
|
1852
1585
|
}
|
|
1853
1586
|
});
|
|
1854
|
-
|
|
1855
|
-
|
|
1587
|
+
files.push(
|
|
1588
|
+
await planFileChange({
|
|
1589
|
+
path: join3(ghSymphonyDir, "context.yaml"),
|
|
1590
|
+
label: "Context metadata",
|
|
1591
|
+
content: generateContextYamlString(contextYaml),
|
|
1592
|
+
mode: "overwrite"
|
|
1593
|
+
})
|
|
1594
|
+
);
|
|
1856
1595
|
}
|
|
1857
|
-
const
|
|
1596
|
+
const referenceWorkflow = generateReferenceWorkflow({
|
|
1858
1597
|
runtime,
|
|
1859
1598
|
statusColumns: statusField.options.map((o) => ({
|
|
1860
1599
|
name: o.name,
|
|
@@ -1862,43 +1601,99 @@ async function writeEcosystem(opts) {
|
|
|
1862
1601
|
})),
|
|
1863
1602
|
projectId: projectDetail.id
|
|
1864
1603
|
});
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1604
|
+
files.push(
|
|
1605
|
+
await planFileChange({
|
|
1606
|
+
path: join3(ghSymphonyDir, "reference-workflow.md"),
|
|
1607
|
+
label: "Reference workflow",
|
|
1608
|
+
content: referenceWorkflow,
|
|
1609
|
+
mode: "overwrite"
|
|
1610
|
+
})
|
|
1611
|
+
);
|
|
1612
|
+
const skillsDir = skipSkills ? null : resolveSkillsDir(cwd, runtime);
|
|
1872
1613
|
if (!skipSkills && skillsDir) {
|
|
1873
|
-
const
|
|
1614
|
+
const { files: plannedSkills } = buildSkillFilePlans(
|
|
1615
|
+
cwd,
|
|
1874
1616
|
runtime,
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1617
|
+
ALL_SKILL_TEMPLATES,
|
|
1618
|
+
{
|
|
1619
|
+
runtime,
|
|
1620
|
+
projectId: projectDetail.id,
|
|
1621
|
+
githubProjectTitle: projectDetail.title,
|
|
1622
|
+
repositories: projectDetail.linkedRepositories.map((r) => ({
|
|
1623
|
+
owner: r.owner,
|
|
1624
|
+
name: r.name
|
|
1625
|
+
})),
|
|
1626
|
+
statusColumns: statusField.options.map((o) => ({
|
|
1627
|
+
id: o.id,
|
|
1628
|
+
name: o.name,
|
|
1629
|
+
role: null
|
|
1630
|
+
})),
|
|
1631
|
+
statusFieldId: statusField.id,
|
|
1632
|
+
contextYamlPath: ".gh-symphony/context.yaml",
|
|
1633
|
+
referenceWorkflowPath: ".gh-symphony/reference-workflow.md"
|
|
1634
|
+
}
|
|
1635
|
+
);
|
|
1636
|
+
for (const plannedSkill of plannedSkills) {
|
|
1637
|
+
files.push(
|
|
1638
|
+
await planFileChange({
|
|
1639
|
+
path: plannedSkill.path,
|
|
1640
|
+
label: `Skill ${basename(dirname2(plannedSkill.path))}`,
|
|
1641
|
+
content: plannedSkill.content,
|
|
1642
|
+
mode: "create-only"
|
|
1643
|
+
})
|
|
1644
|
+
);
|
|
1645
|
+
}
|
|
1892
1646
|
}
|
|
1893
1647
|
return {
|
|
1894
1648
|
projectId: projectDetail.id,
|
|
1895
1649
|
githubProjectTitle: projectDetail.title,
|
|
1896
1650
|
runtime,
|
|
1897
1651
|
skillsDir,
|
|
1652
|
+
environment,
|
|
1653
|
+
files
|
|
1654
|
+
};
|
|
1655
|
+
}
|
|
1656
|
+
async function writeEcosystem(opts) {
|
|
1657
|
+
const plan = await planEcosystem(opts);
|
|
1658
|
+
await mkdir3(join3(opts.cwd, ".gh-symphony"), { recursive: true });
|
|
1659
|
+
const contextYamlPath = join3(opts.cwd, ".gh-symphony", "context.yaml");
|
|
1660
|
+
const referenceWorkflowPath = join3(
|
|
1661
|
+
opts.cwd,
|
|
1662
|
+
".gh-symphony",
|
|
1663
|
+
"reference-workflow.md"
|
|
1664
|
+
);
|
|
1665
|
+
let contextYamlWritten = false;
|
|
1666
|
+
let referenceWorkflowWritten = false;
|
|
1667
|
+
const skillsWritten = [];
|
|
1668
|
+
const skillsSkipped = [];
|
|
1669
|
+
for (const file of plan.files) {
|
|
1670
|
+
const written = await writePlannedFile(file);
|
|
1671
|
+
if (file.path === contextYamlPath) {
|
|
1672
|
+
contextYamlWritten = written;
|
|
1673
|
+
continue;
|
|
1674
|
+
}
|
|
1675
|
+
if (file.path === referenceWorkflowPath) {
|
|
1676
|
+
referenceWorkflowWritten = written;
|
|
1677
|
+
continue;
|
|
1678
|
+
}
|
|
1679
|
+
if (file.label.startsWith("Skill ")) {
|
|
1680
|
+
const skillName = basename(dirname2(file.path));
|
|
1681
|
+
if (written) {
|
|
1682
|
+
skillsWritten.push(skillName);
|
|
1683
|
+
} else {
|
|
1684
|
+
skillsSkipped.push(skillName);
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
return {
|
|
1689
|
+
projectId: plan.projectId,
|
|
1690
|
+
githubProjectTitle: plan.githubProjectTitle,
|
|
1691
|
+
runtime: plan.runtime,
|
|
1692
|
+
skillsDir: plan.skillsDir,
|
|
1898
1693
|
contextYamlWritten,
|
|
1899
|
-
referenceWorkflowWritten
|
|
1900
|
-
skillsWritten,
|
|
1901
|
-
skillsSkipped
|
|
1694
|
+
referenceWorkflowWritten,
|
|
1695
|
+
skillsWritten: skillsWritten.sort(),
|
|
1696
|
+
skillsSkipped: skillsSkipped.sort()
|
|
1902
1697
|
};
|
|
1903
1698
|
}
|
|
1904
1699
|
function printEcosystemSummary(result, workflowPath, opts) {
|
|
@@ -1941,10 +1736,65 @@ function printEcosystemSummary(result, workflowPath, opts) {
|
|
|
1941
1736
|
process.stdout.write(lines.map((l) => ` ${l}`).join("\n") + "\n");
|
|
1942
1737
|
}
|
|
1943
1738
|
}
|
|
1739
|
+
function renderDryRunPreview(workflowPath, workflowPlan, ecosystemPlan) {
|
|
1740
|
+
const cwd = process.cwd();
|
|
1741
|
+
const relWorkflow = relative(cwd, workflowPath) || "WORKFLOW.md";
|
|
1742
|
+
const statusIcon = {
|
|
1743
|
+
create: "+",
|
|
1744
|
+
update: "~",
|
|
1745
|
+
unchanged: "="
|
|
1746
|
+
};
|
|
1747
|
+
const lines = [];
|
|
1748
|
+
lines.push("Init dry-run preview");
|
|
1749
|
+
lines.push(
|
|
1750
|
+
`GitHub Project ${ecosystemPlan.githubProjectTitle} (${ecosystemPlan.projectId})`
|
|
1751
|
+
);
|
|
1752
|
+
lines.push(`Runtime ${ecosystemPlan.runtime}`);
|
|
1753
|
+
lines.push("");
|
|
1754
|
+
lines.push("Planned file changes");
|
|
1755
|
+
lines.push(
|
|
1756
|
+
` ${statusIcon[workflowPlan.status]} ${workflowPlan.status.padEnd(9)} WORKFLOW.md ${relWorkflow}`
|
|
1757
|
+
);
|
|
1758
|
+
for (const file of ecosystemPlan.files) {
|
|
1759
|
+
const relPath = relative(cwd, file.path) || file.path;
|
|
1760
|
+
lines.push(
|
|
1761
|
+
` ${statusIcon[file.status]} ${file.status.padEnd(9)} ${file.label.padEnd(36)} ${relPath}`
|
|
1762
|
+
);
|
|
1763
|
+
}
|
|
1764
|
+
lines.push("");
|
|
1765
|
+
lines.push("Detected environment inputs");
|
|
1766
|
+
for (const line of summarizeEnvironment(ecosystemPlan.environment)) {
|
|
1767
|
+
lines.push(` ${line}`);
|
|
1768
|
+
}
|
|
1769
|
+
lines.push("");
|
|
1770
|
+
lines.push("Dry run only. No files were written.");
|
|
1771
|
+
return lines.join("\n") + "\n";
|
|
1772
|
+
}
|
|
1773
|
+
function buildDryRunJsonResult(workflowPath, workflowPlan, ecosystemPlan) {
|
|
1774
|
+
return {
|
|
1775
|
+
dryRun: true,
|
|
1776
|
+
output: workflowPath,
|
|
1777
|
+
projectId: ecosystemPlan.projectId,
|
|
1778
|
+
githubProjectTitle: ecosystemPlan.githubProjectTitle,
|
|
1779
|
+
runtime: ecosystemPlan.runtime,
|
|
1780
|
+
files: [workflowPlan, ...ecosystemPlan.files].map((file) => ({
|
|
1781
|
+
path: file.path,
|
|
1782
|
+
label: file.label,
|
|
1783
|
+
status: file.status,
|
|
1784
|
+
mode: file.mode
|
|
1785
|
+
})),
|
|
1786
|
+
environment: ecosystemPlan.environment
|
|
1787
|
+
};
|
|
1788
|
+
}
|
|
1789
|
+
function printDryRunPreview(workflowPath, workflowPlan, ecosystemPlan) {
|
|
1790
|
+
process.stdout.write(
|
|
1791
|
+
renderDryRunPreview(workflowPath, workflowPlan, ecosystemPlan)
|
|
1792
|
+
);
|
|
1793
|
+
}
|
|
1944
1794
|
async function runNonInteractive(flags, options) {
|
|
1945
1795
|
let token;
|
|
1946
1796
|
try {
|
|
1947
|
-
token =
|
|
1797
|
+
token = getGhTokenWithSource().token;
|
|
1948
1798
|
} catch {
|
|
1949
1799
|
process.stderr.write(
|
|
1950
1800
|
"Error: GitHub token not found. Run 'gh auth login --scopes repo,read:org,project' or set GITHUB_GRAPHQL_TOKEN.\n"
|
|
@@ -1992,20 +1842,13 @@ async function runNonInteractive(flags, options) {
|
|
|
1992
1842
|
process.exitCode = 1;
|
|
1993
1843
|
return;
|
|
1994
1844
|
}
|
|
1995
|
-
const statusField = githubProject
|
|
1845
|
+
const statusField = resolveStatusField(githubProject);
|
|
1996
1846
|
if (!statusField) {
|
|
1997
1847
|
process.stderr.write("Error: No status field found on the project.\n");
|
|
1998
1848
|
process.exitCode = 1;
|
|
1999
1849
|
return;
|
|
2000
1850
|
}
|
|
2001
|
-
const
|
|
2002
|
-
const inferred = inferAllStateRoles(columnNames);
|
|
2003
|
-
const mappings = {};
|
|
2004
|
-
for (const mapping of inferred) {
|
|
2005
|
-
if (mapping.role) {
|
|
2006
|
-
mappings[mapping.columnName] = { role: mapping.role };
|
|
2007
|
-
}
|
|
2008
|
-
}
|
|
1851
|
+
const mappings = buildAutomaticStateMappings(statusField);
|
|
2009
1852
|
const validation = validateStateMapping(mappings);
|
|
2010
1853
|
if (!validation.valid) {
|
|
2011
1854
|
process.stderr.write(
|
|
@@ -2016,16 +1859,30 @@ Run without --non-interactive for manual mapping.
|
|
|
2016
1859
|
process.exitCode = 1;
|
|
2017
1860
|
return;
|
|
2018
1861
|
}
|
|
2019
|
-
const lifecycleConfig = toWorkflowLifecycleConfig(statusField.name, mappings);
|
|
2020
1862
|
const outputPath = resolve(flags.output ?? "WORKFLOW.md");
|
|
2021
|
-
const
|
|
2022
|
-
|
|
2023
|
-
|
|
1863
|
+
const { workflowPlan, ecosystemPlan } = await planWorkflowArtifacts({
|
|
1864
|
+
cwd: process.cwd(),
|
|
1865
|
+
outputPath,
|
|
1866
|
+
projectDetail: githubProject,
|
|
1867
|
+
statusField,
|
|
2024
1868
|
mappings,
|
|
2025
|
-
|
|
2026
|
-
|
|
1869
|
+
runtime: "codex",
|
|
1870
|
+
skipSkills: flags.skipSkills,
|
|
1871
|
+
skipContext: flags.skipContext
|
|
2027
1872
|
});
|
|
2028
|
-
|
|
1873
|
+
if (flags.dryRun) {
|
|
1874
|
+
if (options.json) {
|
|
1875
|
+
process.stdout.write(
|
|
1876
|
+
JSON.stringify(
|
|
1877
|
+
buildDryRunJsonResult(outputPath, workflowPlan, ecosystemPlan)
|
|
1878
|
+
) + "\n"
|
|
1879
|
+
);
|
|
1880
|
+
return;
|
|
1881
|
+
}
|
|
1882
|
+
printDryRunPreview(outputPath, workflowPlan, ecosystemPlan);
|
|
1883
|
+
return;
|
|
1884
|
+
}
|
|
1885
|
+
await writeWorkflowPlan(workflowPlan);
|
|
2029
1886
|
const ecosystemResult = await writeEcosystem({
|
|
2030
1887
|
cwd: process.cwd(),
|
|
2031
1888
|
projectDetail: githubProject,
|
|
@@ -2036,7 +1893,7 @@ Run without --non-interactive for manual mapping.
|
|
|
2036
1893
|
});
|
|
2037
1894
|
if (options.json) {
|
|
2038
1895
|
process.stdout.write(
|
|
2039
|
-
JSON.stringify({ output: outputPath, status:
|
|
1896
|
+
JSON.stringify({ output: outputPath, status: workflowPlan.status }) + "\n"
|
|
2040
1897
|
);
|
|
2041
1898
|
} else {
|
|
2042
1899
|
printEcosystemSummary(ecosystemResult, outputPath, {
|
|
@@ -2045,36 +1902,23 @@ Run without --non-interactive for manual mapping.
|
|
|
2045
1902
|
});
|
|
2046
1903
|
}
|
|
2047
1904
|
}
|
|
2048
|
-
async function runInteractive(options) {
|
|
1905
|
+
async function runInteractive(flags, options) {
|
|
2049
1906
|
p.intro("gh-symphony \u2014 WORKFLOW.md Setup");
|
|
2050
|
-
await runInteractiveStandalone(options);
|
|
1907
|
+
await runInteractiveStandalone(flags, options);
|
|
2051
1908
|
}
|
|
2052
|
-
async function runInteractiveStandalone(_options) {
|
|
1909
|
+
async function runInteractiveStandalone(flags, _options) {
|
|
2053
1910
|
const s1 = p.spinner();
|
|
2054
|
-
s1.start("Checking
|
|
1911
|
+
s1.start("Checking GitHub authentication...");
|
|
2055
1912
|
let client;
|
|
2056
1913
|
try {
|
|
2057
|
-
const
|
|
2058
|
-
|
|
2059
|
-
|
|
1914
|
+
const auth = await resolveGitHubAuth();
|
|
1915
|
+
const sourceLabel = auth.source === "env" ? "GITHUB_GRAPHQL_TOKEN" : "gh CLI";
|
|
1916
|
+
client = createClient(auth.token);
|
|
1917
|
+
s1.stop(`Authenticated via ${sourceLabel} as ${auth.login}`);
|
|
2060
1918
|
} catch (error) {
|
|
2061
1919
|
s1.stop("Authentication failed.");
|
|
2062
1920
|
if (error instanceof GhAuthError) {
|
|
2063
|
-
|
|
2064
|
-
p.log.error(
|
|
2065
|
-
"gh CLI\uAC00 \uC124\uCE58\uB418\uC5B4 \uC788\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. https://cli.github.com \uC5D0\uC11C \uC124\uCE58\uD558\uC138\uC694."
|
|
2066
|
-
);
|
|
2067
|
-
} else if (error.code === "not_authenticated") {
|
|
2068
|
-
p.log.error(
|
|
2069
|
-
"gh auth login --scopes repo,read:org,project \uB97C \uC2E4\uD589\uD558\uC138\uC694."
|
|
2070
|
-
);
|
|
2071
|
-
} else if (error.code === "missing_scopes") {
|
|
2072
|
-
p.log.error(
|
|
2073
|
-
"gh auth refresh --scopes repo,read:org,project \uB97C \uC2E4\uD589\uD558\uC138\uC694."
|
|
2074
|
-
);
|
|
2075
|
-
} else {
|
|
2076
|
-
p.log.error(error.message);
|
|
2077
|
-
}
|
|
1921
|
+
p.log.error(error.message);
|
|
2078
1922
|
} else {
|
|
2079
1923
|
p.log.error(error instanceof Error ? error.message : "Unknown error");
|
|
2080
1924
|
}
|
|
@@ -2092,7 +1936,7 @@ async function runInteractiveStandalone(_options) {
|
|
|
2092
1936
|
} catch (error) {
|
|
2093
1937
|
s2.stop("Failed to load projects.");
|
|
2094
1938
|
if (error instanceof GitHubScopeError) {
|
|
2095
|
-
displayScopeError(error, "gh-symphony init");
|
|
1939
|
+
displayScopeError(error, "gh-symphony workflow init");
|
|
2096
1940
|
} else {
|
|
2097
1941
|
p.log.error(error instanceof Error ? error.message : "Unknown error");
|
|
2098
1942
|
}
|
|
@@ -2129,7 +1973,7 @@ async function runInteractiveStandalone(_options) {
|
|
|
2129
1973
|
process.exitCode = 1;
|
|
2130
1974
|
return;
|
|
2131
1975
|
}
|
|
2132
|
-
const statusField = projectDetail
|
|
1976
|
+
const statusField = resolveStatusField(projectDetail);
|
|
2133
1977
|
if (!statusField) {
|
|
2134
1978
|
p.log.error(
|
|
2135
1979
|
"No status field found on the project. The project needs a single-select 'Status' field."
|
|
@@ -2137,33 +1981,7 @@ async function runInteractiveStandalone(_options) {
|
|
|
2137
1981
|
process.exitCode = 1;
|
|
2138
1982
|
return;
|
|
2139
1983
|
}
|
|
2140
|
-
const
|
|
2141
|
-
const inferred = inferAllStateRoles(columnNames);
|
|
2142
|
-
p.log.info(
|
|
2143
|
-
`Found ${columnNames.length} status columns on field "${statusField.name}".`
|
|
2144
|
-
);
|
|
2145
|
-
const mappings = {};
|
|
2146
|
-
for (const mapping of inferred) {
|
|
2147
|
-
const roleOptions = [
|
|
2148
|
-
{ value: "active", label: "Active (agent works on this)" },
|
|
2149
|
-
{ value: "wait", label: "Wait (human review / hold)" },
|
|
2150
|
-
{ value: "terminal", label: "Terminal (completed)" }
|
|
2151
|
-
];
|
|
2152
|
-
const defaultRole = mapping.role ?? "wait";
|
|
2153
|
-
const sortedOptions = [
|
|
2154
|
-
roleOptions.find((o) => o.value === defaultRole),
|
|
2155
|
-
...roleOptions.filter((o) => o.value !== defaultRole)
|
|
2156
|
-
];
|
|
2157
|
-
const selectedRole = await abortIfCancelled(
|
|
2158
|
-
p.select({
|
|
2159
|
-
message: `Step 2/2 \u2014 Map column "${mapping.columnName}":${mapping.confidence === "high" ? " (auto-detected)" : ""}`,
|
|
2160
|
-
options: sortedOptions
|
|
2161
|
-
})
|
|
2162
|
-
);
|
|
2163
|
-
if (selectedRole !== "skip") {
|
|
2164
|
-
mappings[mapping.columnName] = { role: selectedRole };
|
|
2165
|
-
}
|
|
2166
|
-
}
|
|
1984
|
+
const mappings = await promptStateMappings(statusField);
|
|
2167
1985
|
const validation = validateStateMapping(mappings);
|
|
2168
1986
|
if (!validation.valid) {
|
|
2169
1987
|
p.log.error("Mapping validation failed:");
|
|
@@ -2176,23 +1994,29 @@ async function runInteractiveStandalone(_options) {
|
|
|
2176
1994
|
for (const warn of validation.warnings) {
|
|
2177
1995
|
p.log.warn(` \u26A0 ${warn}`);
|
|
2178
1996
|
}
|
|
2179
|
-
const
|
|
2180
|
-
const
|
|
2181
|
-
|
|
2182
|
-
|
|
1997
|
+
const outputPath = resolve(flags.output ?? "WORKFLOW.md");
|
|
1998
|
+
const { workflowPlan, ecosystemPlan } = await planWorkflowArtifacts({
|
|
1999
|
+
cwd: process.cwd(),
|
|
2000
|
+
outputPath,
|
|
2001
|
+
projectDetail,
|
|
2002
|
+
statusField,
|
|
2183
2003
|
mappings,
|
|
2184
|
-
|
|
2185
|
-
|
|
2004
|
+
runtime: "codex",
|
|
2005
|
+
skipSkills: flags.skipSkills,
|
|
2006
|
+
skipContext: flags.skipContext
|
|
2186
2007
|
});
|
|
2187
|
-
|
|
2188
|
-
|
|
2008
|
+
if (flags.dryRun) {
|
|
2009
|
+
printDryRunPreview(outputPath, workflowPlan, ecosystemPlan);
|
|
2010
|
+
return;
|
|
2011
|
+
}
|
|
2012
|
+
await writeWorkflowPlan(workflowPlan);
|
|
2189
2013
|
const ecosystemResult = await writeEcosystem({
|
|
2190
2014
|
cwd: process.cwd(),
|
|
2191
2015
|
projectDetail,
|
|
2192
2016
|
statusField,
|
|
2193
2017
|
runtime: "codex",
|
|
2194
|
-
skipSkills:
|
|
2195
|
-
skipContext:
|
|
2018
|
+
skipSkills: flags.skipSkills,
|
|
2019
|
+
skipContext: flags.skipContext
|
|
2196
2020
|
});
|
|
2197
2021
|
printEcosystemSummary(ecosystemResult, outputPath, {
|
|
2198
2022
|
interactive: true,
|
|
@@ -2236,15 +2060,18 @@ function generateProjectId(githubProjectTitle, uniqueKey) {
|
|
|
2236
2060
|
}
|
|
2237
2061
|
|
|
2238
2062
|
export {
|
|
2239
|
-
|
|
2240
|
-
createClient,
|
|
2241
|
-
validateToken,
|
|
2242
|
-
checkRequiredScopes,
|
|
2243
|
-
listUserProjects,
|
|
2244
|
-
getProjectDetail,
|
|
2063
|
+
validateStateMapping,
|
|
2245
2064
|
abortIfCancelled,
|
|
2246
2065
|
init_default,
|
|
2066
|
+
resolveStatusField,
|
|
2067
|
+
buildAutomaticStateMappings,
|
|
2068
|
+
promptStateMappings,
|
|
2069
|
+
planWorkflowArtifacts,
|
|
2070
|
+
writeWorkflowPlan,
|
|
2071
|
+
planEcosystem,
|
|
2247
2072
|
writeEcosystem,
|
|
2073
|
+
renderDryRunPreview,
|
|
2074
|
+
buildDryRunJsonResult,
|
|
2248
2075
|
writeConfig,
|
|
2249
2076
|
generateProjectId
|
|
2250
2077
|
};
|