@gh-symphony/cli 0.0.18 → 0.0.20

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.
@@ -0,0 +1,431 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ promptProjectRegistrationOptions,
4
+ renderProjectRegistrationSummary
5
+ } from "./chunk-3AWF54PI.js";
6
+ import {
7
+ abortIfCancelled,
8
+ buildAutomaticStateMappings,
9
+ generateProjectId,
10
+ planWorkflowArtifacts,
11
+ promptStateMappings,
12
+ renderDryRunPreview,
13
+ resolveStatusField,
14
+ validateStateMapping,
15
+ writeConfig,
16
+ writeEcosystem,
17
+ writeWorkflowPlan
18
+ } from "./chunk-RN2PACNV.js";
19
+ import "./chunk-EKKT5USP.js";
20
+ import "./chunk-HZVDTAPS.js";
21
+ import "./chunk-M3IFVLQS.js";
22
+ import {
23
+ GhAuthError,
24
+ GitHubScopeError,
25
+ checkRequiredScopes,
26
+ createClient,
27
+ ensureGhAuth,
28
+ getGhToken,
29
+ getProjectDetail,
30
+ listUserProjects,
31
+ validateToken
32
+ } from "./chunk-TILHWBP6.js";
33
+ import "./chunk-XN5ABWZ6.js";
34
+ import "./chunk-MVRF7BES.js";
35
+ import "./chunk-5NV3LSAJ.js";
36
+ import "./chunk-Y6TYJMNT.js";
37
+ import "./chunk-C7G7RJ4G.js";
38
+ import "./chunk-ROGRTUFI.js";
39
+
40
+ // src/commands/setup.ts
41
+ import * as p from "@clack/prompts";
42
+ import { join, resolve } from "path";
43
+ var KNOWN_REQUIRED_SCOPES = ["repo", "read:org", "project"];
44
+ function parseSetupFlags(args) {
45
+ const flags = {
46
+ nonInteractive: false,
47
+ skipSkills: false,
48
+ skipContext: false
49
+ };
50
+ for (let i = 0; i < args.length; i += 1) {
51
+ const arg = args[i];
52
+ const next = args[i + 1];
53
+ switch (arg) {
54
+ case "--non-interactive":
55
+ flags.nonInteractive = true;
56
+ break;
57
+ case "--project":
58
+ flags.project = next;
59
+ i += 1;
60
+ break;
61
+ case "--workspace-dir":
62
+ flags.workspaceDir = next;
63
+ i += 1;
64
+ break;
65
+ case "--assigned-only":
66
+ flags.assignedOnly = true;
67
+ break;
68
+ case "--output":
69
+ flags.output = next;
70
+ i += 1;
71
+ break;
72
+ case "--skip-skills":
73
+ flags.skipSkills = true;
74
+ break;
75
+ case "--skip-context":
76
+ flags.skipContext = true;
77
+ break;
78
+ }
79
+ }
80
+ return flags;
81
+ }
82
+ function displayScopeError(error, retryCommand) {
83
+ const plural = error.requiredScopes.length === 1 ? "" : "s";
84
+ p.log.error(
85
+ `Token is missing required scope${plural}: ${error.requiredScopes.join(", ")}`
86
+ );
87
+ const currentSet = new Set(error.currentScopes.map((s) => s.toLowerCase()));
88
+ const scopesToAdd = KNOWN_REQUIRED_SCOPES.filter((s) => !currentSet.has(s));
89
+ const scopeArg = scopesToAdd.length > 0 ? scopesToAdd.join(",") : error.requiredScopes.join(",");
90
+ p.note(
91
+ `gh auth refresh --scopes ${scopeArg}
92
+
93
+ Then re-run: ${retryCommand}`,
94
+ "Fix missing scope"
95
+ );
96
+ }
97
+ async function resolveProjectDetail(client, projectArg) {
98
+ const projects = await listUserProjects(client);
99
+ if (projects.length === 0) {
100
+ throw new Error(
101
+ "No GitHub Projects found. Create a project first and re-run setup."
102
+ );
103
+ }
104
+ if (projectArg) {
105
+ const match = projects.find(
106
+ (project) => project.id === projectArg || project.url === projectArg
107
+ );
108
+ if (!match) {
109
+ throw new Error(`Project not found: ${projectArg}`);
110
+ }
111
+ return getProjectDetail(client, match.id);
112
+ }
113
+ if (projects.length === 1) {
114
+ return getProjectDetail(client, projects[0].id);
115
+ }
116
+ throw new Error("Error: --project is required when multiple projects exist.");
117
+ }
118
+ async function selectProjectSummary(client) {
119
+ const projects = await listUserProjects(client);
120
+ if (projects.length === 0) {
121
+ throw new Error(
122
+ "No GitHub Projects found. Create a project at https://github.com/orgs/YOUR_ORG/projects and re-run."
123
+ );
124
+ }
125
+ const selectedProjectId = await abortIfCancelled(
126
+ p.select({
127
+ message: "Step 1/3 \u2014 Select a GitHub Project board:",
128
+ options: projects.map((project) => ({
129
+ value: project.id,
130
+ label: `${project.owner.login}/${project.title}`,
131
+ hint: `${project.openItemCount} items`
132
+ })),
133
+ maxItems: 15
134
+ })
135
+ );
136
+ return projects.find((project) => project.id === selectedProjectId);
137
+ }
138
+ function formatRepoSummary(projectDetail, selectedRepos) {
139
+ return selectedRepos.length === projectDetail.linkedRepositories.length ? `${selectedRepos.map((repo) => `${repo.owner}/${repo.name}`).join(", ")} (all ${selectedRepos.length} linked)` : `${selectedRepos.map((repo) => `${repo.owner}/${repo.name}`).join(", ")} (${selectedRepos.length} of ${projectDetail.linkedRepositories.length} linked)`;
140
+ }
141
+ function printNonInteractiveSummary(input) {
142
+ process.stdout.write(
143
+ [
144
+ `GitHub Project ${input.githubProjectTitle} (${input.githubProjectId})`,
145
+ `Managed project ${input.projectId}`,
146
+ `WORKFLOW.md ${input.workflowPath}`,
147
+ `Workspace root ${input.workspaceDir}`,
148
+ "Ready. Run 'gh-symphony start' to begin orchestration."
149
+ ].map((line) => ` ${line}`).join("\n") + "\n"
150
+ );
151
+ }
152
+ var handler = async (args, options) => {
153
+ const flags = parseSetupFlags(args);
154
+ if (flags.nonInteractive) {
155
+ await runNonInteractive(flags, options);
156
+ return;
157
+ }
158
+ await runInteractive(flags, options);
159
+ };
160
+ var setup_default = handler;
161
+ async function runNonInteractive(flags, options) {
162
+ let token;
163
+ try {
164
+ token = getGhToken();
165
+ } catch {
166
+ process.stderr.write(
167
+ "Error: GitHub token not found. Run 'gh auth login --scopes repo,read:org,project' or set GITHUB_GRAPHQL_TOKEN.\n"
168
+ );
169
+ process.exitCode = 1;
170
+ return;
171
+ }
172
+ const client = createClient(token);
173
+ let viewer;
174
+ try {
175
+ viewer = await validateToken(client);
176
+ } catch {
177
+ process.stderr.write("Error: Invalid GitHub token.\n");
178
+ process.exitCode = 1;
179
+ return;
180
+ }
181
+ const scopeCheck = checkRequiredScopes(viewer.scopes);
182
+ if (!scopeCheck.valid) {
183
+ process.stderr.write(
184
+ `Error: Missing required PAT scopes: ${scopeCheck.missing.join(", ")}
185
+ `
186
+ );
187
+ process.exitCode = 1;
188
+ return;
189
+ }
190
+ let projectDetail;
191
+ try {
192
+ projectDetail = await resolveProjectDetail(client, flags.project);
193
+ } catch (error) {
194
+ process.stderr.write(
195
+ `${error instanceof Error ? error.message : "Unknown error"}
196
+ `
197
+ );
198
+ process.exitCode = 1;
199
+ return;
200
+ }
201
+ const statusField = resolveStatusField(projectDetail);
202
+ if (!statusField) {
203
+ process.stderr.write("Error: No status field found on the project.\n");
204
+ process.exitCode = 1;
205
+ return;
206
+ }
207
+ const mappings = buildAutomaticStateMappings(statusField);
208
+ const workflowValidation = validateStateMapping(mappings);
209
+ if (!workflowValidation.valid) {
210
+ process.stderr.write(
211
+ `Error: Cannot auto-map columns. ${workflowValidation.errors.join("; ")}
212
+ Run setup without --non-interactive for manual mapping.
213
+ `
214
+ );
215
+ process.exitCode = 1;
216
+ return;
217
+ }
218
+ const workflowPath = resolve(flags.output ?? "WORKFLOW.md");
219
+ const { workflowPlan } = await planWorkflowArtifacts({
220
+ cwd: process.cwd(),
221
+ outputPath: workflowPath,
222
+ projectDetail,
223
+ statusField,
224
+ mappings,
225
+ runtime: "codex",
226
+ skipSkills: flags.skipSkills,
227
+ skipContext: flags.skipContext
228
+ });
229
+ await writeWorkflowPlan(workflowPlan);
230
+ await writeEcosystem({
231
+ cwd: process.cwd(),
232
+ projectDetail,
233
+ statusField,
234
+ runtime: "codex",
235
+ skipSkills: flags.skipSkills,
236
+ skipContext: flags.skipContext
237
+ });
238
+ const projectId = generateProjectId(projectDetail.title, projectDetail.id);
239
+ const workspaceDir = flags.workspaceDir ?? join(options.configDir, "workspaces");
240
+ await writeConfig(options.configDir, {
241
+ projectId,
242
+ project: projectDetail,
243
+ repos: projectDetail.linkedRepositories,
244
+ workspaceDir,
245
+ assignedOnly: flags.assignedOnly
246
+ });
247
+ if (options.json) {
248
+ process.stdout.write(
249
+ JSON.stringify({
250
+ status: "created",
251
+ output: workflowPath,
252
+ projectId,
253
+ githubProjectId: projectDetail.id
254
+ }) + "\n"
255
+ );
256
+ return;
257
+ }
258
+ printNonInteractiveSummary({
259
+ projectId,
260
+ githubProjectTitle: projectDetail.title,
261
+ githubProjectId: projectDetail.id,
262
+ workflowPath,
263
+ workspaceDir
264
+ });
265
+ }
266
+ async function runInteractive(flags, options) {
267
+ p.intro("gh-symphony \u2014 One-command Setup");
268
+ const authSpinner = p.spinner();
269
+ authSpinner.start("Checking gh CLI authentication...");
270
+ let login;
271
+ let client;
272
+ try {
273
+ const auth = ensureGhAuth();
274
+ login = auth.login;
275
+ client = createClient(auth.token);
276
+ authSpinner.stop(`Authenticated as ${login}`);
277
+ } catch (error) {
278
+ authSpinner.stop("Authentication failed.");
279
+ if (error instanceof GhAuthError) {
280
+ if (error.code === "not_installed") {
281
+ p.log.error("gh CLI\uAC00 \uC124\uCE58\uB418\uC5B4 \uC788\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. https://cli.github.com \uC5D0\uC11C \uC124\uCE58\uD558\uC138\uC694.");
282
+ } else if (error.code === "not_authenticated") {
283
+ p.log.error("gh auth login --scopes repo,read:org,project \uB97C \uC2E4\uD589\uD558\uC138\uC694.");
284
+ } else if (error.code === "missing_scopes") {
285
+ p.log.error("gh auth refresh --scopes repo,read:org,project \uB97C \uC2E4\uD589\uD558\uC138\uC694.");
286
+ } else {
287
+ p.log.error(error.message);
288
+ }
289
+ } else {
290
+ p.log.error(error instanceof Error ? error.message : "Unknown error");
291
+ }
292
+ process.exitCode = 1;
293
+ return;
294
+ }
295
+ const projectsSpinner = p.spinner();
296
+ projectsSpinner.start("Loading GitHub Project boards...");
297
+ let selectedProject;
298
+ try {
299
+ selectedProject = await selectProjectSummary(client);
300
+ projectsSpinner.stop(`Selected: ${selectedProject.title}`);
301
+ } catch (error) {
302
+ projectsSpinner.stop("Failed to load projects.");
303
+ if (error instanceof GitHubScopeError) {
304
+ displayScopeError(error, "gh-symphony setup");
305
+ } else {
306
+ p.log.error(error instanceof Error ? error.message : "Unknown error");
307
+ }
308
+ process.exitCode = 1;
309
+ return;
310
+ }
311
+ const detailSpinner = p.spinner();
312
+ detailSpinner.start("Loading project details...");
313
+ let projectDetail;
314
+ try {
315
+ projectDetail = await getProjectDetail(client, selectedProject.id);
316
+ detailSpinner.stop(`Loaded: ${projectDetail.title}`);
317
+ } catch (error) {
318
+ detailSpinner.stop("Failed to load project details.");
319
+ p.log.error(error instanceof Error ? error.message : "Unknown error");
320
+ process.exitCode = 1;
321
+ return;
322
+ }
323
+ if (projectDetail.linkedRepositories.length === 0) {
324
+ p.log.error(
325
+ "No linked repositories found in this project. Add issues from repositories to the project first."
326
+ );
327
+ process.exitCode = 1;
328
+ return;
329
+ }
330
+ const statusField = resolveStatusField(projectDetail);
331
+ if (!statusField) {
332
+ p.log.error(
333
+ "No status field found on the project. The project needs a single-select 'Status' field."
334
+ );
335
+ process.exitCode = 1;
336
+ return;
337
+ }
338
+ const mappings = await promptStateMappings(statusField, {
339
+ stepLabel: "Step 2/3"
340
+ });
341
+ const workflowValidation = validateStateMapping(mappings);
342
+ if (!workflowValidation.valid) {
343
+ p.log.error("Mapping validation failed:");
344
+ for (const error of workflowValidation.errors) {
345
+ p.log.error(` \u2022 ${error}`);
346
+ }
347
+ process.exitCode = 1;
348
+ return;
349
+ }
350
+ for (const warning of workflowValidation.warnings) {
351
+ p.log.warn(` \u26A0 ${warning}`);
352
+ }
353
+ const {
354
+ assignedOnly: promptAssignedOnly,
355
+ selectedRepos,
356
+ workspaceDir
357
+ } = await promptProjectRegistrationOptions({
358
+ projectDetail,
359
+ defaultWorkspaceDir: flags.workspaceDir ?? join(options.configDir, "workspaces"),
360
+ assignedOnlyMessage: "Step 3/3 \u2014 Only process issues assigned to the authenticated GitHub user?",
361
+ assignedOnlyInitialValue: flags.assignedOnly
362
+ });
363
+ const assignedOnly = flags.assignedOnly || promptAssignedOnly;
364
+ const workflowPath = resolve(flags.output ?? "WORKFLOW.md");
365
+ const { workflowPlan, ecosystemPlan } = await planWorkflowArtifacts({
366
+ cwd: process.cwd(),
367
+ outputPath: workflowPath,
368
+ projectDetail,
369
+ statusField,
370
+ mappings,
371
+ runtime: "codex",
372
+ skipSkills: flags.skipSkills,
373
+ skipContext: flags.skipContext
374
+ });
375
+ const projectId = generateProjectId(projectDetail.title, projectDetail.id);
376
+ p.note(
377
+ [
378
+ renderProjectRegistrationSummary({
379
+ login,
380
+ projectTitle: projectDetail.title,
381
+ repoSummary: formatRepoSummary(projectDetail, selectedRepos),
382
+ assignedOnly,
383
+ workspaceDir
384
+ }),
385
+ "",
386
+ renderDryRunPreview(workflowPath, workflowPlan, ecosystemPlan).trimEnd()
387
+ ].join("\n"),
388
+ "Final summary"
389
+ );
390
+ const confirmed = await abortIfCancelled(
391
+ p.confirm({ message: "Write files and register this managed project?" })
392
+ );
393
+ if (!confirmed) {
394
+ p.cancel("Setup cancelled.");
395
+ process.exitCode = 130;
396
+ return;
397
+ }
398
+ const writeSpinner = p.spinner();
399
+ writeSpinner.start("Writing setup files...");
400
+ try {
401
+ await writeWorkflowPlan(workflowPlan);
402
+ await writeEcosystem({
403
+ cwd: process.cwd(),
404
+ projectDetail,
405
+ statusField,
406
+ runtime: "codex",
407
+ skipSkills: flags.skipSkills,
408
+ skipContext: flags.skipContext
409
+ });
410
+ await writeConfig(options.configDir, {
411
+ projectId,
412
+ project: projectDetail,
413
+ repos: selectedRepos,
414
+ workspaceDir,
415
+ assignedOnly
416
+ });
417
+ writeSpinner.stop("Setup saved.");
418
+ } catch (error) {
419
+ writeSpinner.stop("Setup failed.");
420
+ p.log.error(error instanceof Error ? error.message : "Unknown error");
421
+ process.exitCode = 1;
422
+ return;
423
+ }
424
+ p.outro(
425
+ `Project "${projectId}" is ready.
426
+ Run 'gh-symphony start' to begin orchestration.`
427
+ );
428
+ }
429
+ export {
430
+ setup_default as default
431
+ };
@@ -2,11 +2,11 @@
2
2
  import {
3
3
  shutdownForegroundOrchestrator,
4
4
  start_default
5
- } from "./chunk-ZYYY55WB.js";
6
- import "./chunk-7UBUBSMH.js";
5
+ } from "./chunk-EKKT5USP.js";
6
+ import "./chunk-HZVDTAPS.js";
7
+ import "./chunk-M3IFVLQS.js";
8
+ import "./chunk-TILHWBP6.js";
7
9
  import "./chunk-MVRF7BES.js";
8
- import "./chunk-LZE6YUSB.js";
9
- import "./chunk-OL73UN2X.js";
10
10
  import "./chunk-5NV3LSAJ.js";
11
11
  import "./chunk-C7G7RJ4G.js";
12
12
  import "./chunk-ROGRTUFI.js";
@@ -0,0 +1,165 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/commands/upgrade.ts
4
+ import { readFileSync } from "fs";
5
+ import { execFile as execFileCallback, spawn } from "child_process";
6
+ var PACKAGE_NAME = "@gh-symphony/cli";
7
+ function execFileAsync(file, args, execFileImpl = execFileCallback) {
8
+ return new Promise((resolve, reject) => {
9
+ execFileImpl(file, args, (error, stdout, stderr) => {
10
+ if (error) {
11
+ reject(error);
12
+ return;
13
+ }
14
+ resolve({ stdout, stderr });
15
+ });
16
+ });
17
+ }
18
+ function resolveCurrentCliVersion() {
19
+ if ("0.0.20".length > 0) {
20
+ return "0.0.20";
21
+ }
22
+ const pkg = JSON.parse(
23
+ readFileSync(new URL("../../package.json", import.meta.url), "utf8")
24
+ );
25
+ if (typeof pkg.version === "string" && pkg.version.length > 0) {
26
+ return pkg.version;
27
+ }
28
+ throw new Error("Unable to determine the current CLI version.");
29
+ }
30
+ function resolvePackageManagerExecutable(command, platform = process.platform) {
31
+ return platform === "win32" ? `${command}.cmd` : command;
32
+ }
33
+ async function fetchLatestCliVersion(deps) {
34
+ const runExecFile = deps?.execFileImpl ?? execFileAsync;
35
+ const { stdout } = await runExecFile(
36
+ resolvePackageManagerExecutable("npm", deps?.platform),
37
+ [
38
+ "view",
39
+ PACKAGE_NAME,
40
+ "dist-tags.latest",
41
+ "--json"
42
+ ]
43
+ );
44
+ const raw = stdout.trim();
45
+ if (raw.length === 0) {
46
+ throw new Error("Failed to resolve the latest CLI version from npm.");
47
+ }
48
+ const parsed = JSON.parse(raw);
49
+ if (typeof parsed !== "string" || parsed.length === 0) {
50
+ throw new Error("npm returned an invalid latest version response.");
51
+ }
52
+ return parsed;
53
+ }
54
+ async function detectGlobalPackageManager(deps) {
55
+ const runExecFile = deps?.execFileImpl ?? execFileAsync;
56
+ try {
57
+ const { stdout } = await runExecFile(
58
+ resolvePackageManagerExecutable("npm", deps?.platform),
59
+ ["prefix", "-g"]
60
+ );
61
+ return stdout.toLowerCase().includes("pnpm") ? "pnpm" : "npm";
62
+ } catch {
63
+ return "npm";
64
+ }
65
+ }
66
+ function packageManagerCommand(manager, platform) {
67
+ if (manager === "pnpm") {
68
+ return [
69
+ resolvePackageManagerExecutable("pnpm", platform),
70
+ "add",
71
+ "-g",
72
+ `${PACKAGE_NAME}@latest`
73
+ ];
74
+ }
75
+ return [
76
+ resolvePackageManagerExecutable("npm", platform),
77
+ "install",
78
+ "-g",
79
+ `${PACKAGE_NAME}@latest`
80
+ ];
81
+ }
82
+ function pipeInstallOutput(child) {
83
+ child.stdout?.on("data", (chunk) => {
84
+ process.stderr.write(chunk);
85
+ });
86
+ child.stderr?.on("data", (chunk) => {
87
+ process.stderr.write(chunk);
88
+ });
89
+ }
90
+ async function runUpgradeInstall(manager, deps, jsonOutput = false) {
91
+ const spawnCommand = deps?.spawnImpl ?? spawn;
92
+ const [command, ...args] = packageManagerCommand(manager, deps?.platform);
93
+ await new Promise((resolve, reject) => {
94
+ const child = jsonOutput ? spawnCommand(command, args, {
95
+ stdio: ["ignore", "pipe", "pipe"]
96
+ }) : spawnCommand(command, args, {
97
+ stdio: "inherit"
98
+ });
99
+ if (jsonOutput) {
100
+ pipeInstallOutput(child);
101
+ }
102
+ child.once("error", reject);
103
+ child.once("close", (code) => {
104
+ if (code === 0) {
105
+ resolve();
106
+ return;
107
+ }
108
+ reject(
109
+ new Error(`${command} exited with code ${code ?? "unknown"}.`)
110
+ );
111
+ });
112
+ });
113
+ }
114
+ async function runUpgradeCommand(options, deps) {
115
+ const latestVersion = await fetchLatestCliVersion(deps);
116
+ const currentVersion = deps?.currentVersion ?? resolveCurrentCliVersion();
117
+ if (currentVersion === latestVersion) {
118
+ if (options.json) {
119
+ process.stdout.write(
120
+ JSON.stringify({
121
+ status: "up_to_date",
122
+ currentVersion,
123
+ latestVersion,
124
+ packageManager: null
125
+ }) + "\n"
126
+ );
127
+ } else {
128
+ process.stdout.write(`Already up to date (v${currentVersion})
129
+ `);
130
+ }
131
+ return;
132
+ }
133
+ const manager = await detectGlobalPackageManager(deps);
134
+ if (!options.json) {
135
+ process.stdout.write(
136
+ `Upgrading ${PACKAGE_NAME} from v${currentVersion} to v${latestVersion} using ${manager}...
137
+ `
138
+ );
139
+ }
140
+ await runUpgradeInstall(manager, deps, options.json);
141
+ if (options.json) {
142
+ process.stdout.write(
143
+ JSON.stringify({
144
+ status: "upgraded",
145
+ previousVersion: currentVersion,
146
+ latestVersion,
147
+ packageManager: manager
148
+ }) + "\n"
149
+ );
150
+ } else {
151
+ process.stdout.write(`Upgrade complete (v${latestVersion})
152
+ `);
153
+ }
154
+ }
155
+ var handler = async (_args, options) => {
156
+ await runUpgradeCommand(options);
157
+ };
158
+ var upgrade_default = handler;
159
+ export {
160
+ upgrade_default as default,
161
+ detectGlobalPackageManager,
162
+ fetchLatestCliVersion,
163
+ runUpgradeCommand,
164
+ runUpgradeInstall
165
+ };
@@ -2,7 +2,7 @@
2
2
 
3
3
  // src/commands/version.ts
4
4
  var handler = async (_args, options) => {
5
- const version = "0.0.18";
5
+ const version = "0.0.20";
6
6
  if (options.json) {
7
7
  process.stdout.write(JSON.stringify({ version }) + "\n");
8
8
  } else {