@gh-symphony/cli 0.0.7 → 0.0.9

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 CHANGED
@@ -25,6 +25,20 @@ Verify the installation:
25
25
  gh-symphony --version
26
26
  ```
27
27
 
28
+ Enable shell completion:
29
+
30
+ ```bash
31
+ gh-symphony completion bash >> ~/.bashrc
32
+ gh-symphony completion zsh >> ~/.zshrc
33
+ gh-symphony completion fish > ~/.config/fish/completions/gh-symphony.fish
34
+ ```
35
+
36
+ If your `zsh` config does not already initialize completion, add this before the generated script line:
37
+
38
+ ```bash
39
+ autoload -Uz compinit && compinit
40
+ ```
41
+
28
42
  ## 2. Set Repository
29
43
 
30
44
  Navigate to the repository you want to orchestrate, then run:
@@ -60,10 +74,9 @@ The interactive wizard will:
60
74
 
61
75
  1. Authenticate via `gh` CLI
62
76
  2. Let you select a **GitHub Project**
63
- 3. Select repositories to orchestrate
64
- 4. Optionally limit processing to issues assigned to the authenticated user
65
- 5. Configure the workspace root directory
66
- 6. Write project configuration to `~/.gh-symphony/`
77
+ 3. Optionally limit processing to issues assigned to the authenticated user
78
+ 4. Optionally customize advanced settings for repository filtering and workspace root directory
79
+ 5. Write project configuration to `~/.gh-symphony/`
67
80
 
68
81
  ### Project Management
69
82
 
@@ -126,6 +139,7 @@ Orchestration:
126
139
  run <issue> Dispatch a single issue
127
140
  recover Recover stalled runs
128
141
  logs View orchestrator logs
142
+ completion <shell> Print shell completion for bash/zsh/fish
129
143
 
130
144
  Project Management:
131
145
  project add Add a new project (interactive wizard)
@@ -2,7 +2,8 @@ import { readFile, readdir } from "node:fs/promises";
2
2
  import { join, resolve } from "node:path";
3
3
  import { createReadStream } from "node:fs";
4
4
  import { createInterface } from "node:readline";
5
- import { loadActiveProjectConfig, loadProjectConfig, orchestratorLogPath, } from "../config.js";
5
+ import { orchestratorLogPath } from "../config.js";
6
+ import { handleMissingManagedProjectConfig, resolveManagedProjectConfig, } from "../project-selection.js";
6
7
  function parseLogsArgs(args) {
7
8
  const parsed = { follow: false };
8
9
  for (let i = 0; i < args.length; i += 1) {
@@ -31,14 +32,6 @@ function parseLogsArgs(args) {
31
32
  }
32
33
  const handler = async (args, options) => {
33
34
  const parsed = parseLogsArgs(args);
34
- const projectConfig = parsed.projectId
35
- ? await loadProjectConfig(options.configDir, parsed.projectId)
36
- : await loadActiveProjectConfig(options.configDir);
37
- if (!projectConfig) {
38
- process.stderr.write("No project configured. Run 'gh-symphony project add' first.\n");
39
- process.exitCode = 1;
40
- return;
41
- }
42
35
  // If --run is specified, read that run's events
43
36
  if (parsed.run) {
44
37
  const eventsPath = join(resolve(options.configDir), "orchestrator", "runs", parsed.run, "events.ndjson");
@@ -47,6 +40,8 @@ const handler = async (args, options) => {
47
40
  const lines = content.trim().split("\n").filter(Boolean);
48
41
  for (const line of lines) {
49
42
  const event = JSON.parse(line);
43
+ if (parsed.projectId && event.projectId !== parsed.projectId)
44
+ continue;
50
45
  if (parsed.level && event.level !== parsed.level)
51
46
  continue;
52
47
  if (parsed.issue && event.issueIdentifier !== parsed.issue)
@@ -62,6 +57,14 @@ const handler = async (args, options) => {
62
57
  }
63
58
  // Default: read orchestrator log or scan all events
64
59
  if (parsed.follow) {
60
+ const projectConfig = await resolveManagedProjectConfig({
61
+ configDir: options.configDir,
62
+ requestedProjectId: parsed.projectId,
63
+ });
64
+ if (!projectConfig) {
65
+ handleMissingManagedProjectConfig();
66
+ return;
67
+ }
65
68
  const logPath = orchestratorLogPath(options.configDir, projectConfig.projectId);
66
69
  try {
67
70
  const stream = createReadStream(logPath, { encoding: "utf8" });
@@ -103,6 +106,8 @@ const handler = async (args, options) => {
103
106
  const lines = content.trim().split("\n").filter(Boolean);
104
107
  for (const line of lines) {
105
108
  const event = JSON.parse(line);
109
+ if (parsed.projectId && event.projectId !== parsed.projectId)
110
+ continue;
106
111
  if (parsed.level && event.level !== parsed.level)
107
112
  continue;
108
113
  if (parsed.issue && event.issueIdentifier !== parsed.issue)
@@ -364,7 +364,7 @@ async function projectAddNonInteractive(flags, options) {
364
364
  return;
365
365
  }
366
366
  const projectId = generateProjectId(project.title, project.id);
367
- const workspaceDir = flags.workspaceDir ?? `${options.configDir}/workspaces`;
367
+ const workspaceDir = flags.workspaceDir ?? join(options.configDir, "workspaces");
368
368
  await writeConfig(options.configDir, {
369
369
  projectId,
370
370
  project,
@@ -382,6 +382,7 @@ async function projectAddNonInteractive(flags, options) {
382
382
  }
383
383
  async function projectAddInteractive(options) {
384
384
  p.intro("gh-symphony - Project Setup");
385
+ const defaultWorkspaceDir = join(options.configDir, "workspaces");
385
386
  const existingConfig = await loadGlobalConfig(options.configDir);
386
387
  if (existingConfig) {
387
388
  const action = await abortIfCancelled(p.select({
@@ -451,7 +452,7 @@ async function projectAddInteractive(options) {
451
452
  return;
452
453
  }
453
454
  const selectedProjectId = await abortIfCancelled(p.select({
454
- message: "Step 1/4 - Select a GitHub Project board:",
455
+ message: "Step 1/2 - Select a GitHub Project board:",
455
456
  options: projects.map((project) => ({
456
457
  value: project.id,
457
458
  label: `${project.owner.login}/${project.title}`,
@@ -477,32 +478,49 @@ async function projectAddInteractive(options) {
477
478
  process.exitCode = 1;
478
479
  return;
479
480
  }
480
- const selectedRepos = await abortIfCancelled(p.multiselect({
481
- message: "Step 2/4 - Select repositories to orchestrate:",
482
- options: projectDetail.linkedRepositories.map((repo) => ({
483
- value: repo,
484
- label: `${repo.owner}/${repo.name}`,
485
- })),
486
- required: true,
487
- }));
488
481
  const assignedOnly = await abortIfCancelled(p.confirm({
489
- message: "Step 3/4 - Only process issues assigned to the authenticated GitHub user?",
482
+ message: "Step 2/2 - Only process issues assigned to the authenticated GitHub user?",
490
483
  initialValue: false,
491
484
  }));
492
- const workspaceDir = await abortIfCancelled(p.text({
493
- message: "Step 4/4 - Workspace root directory:",
494
- placeholder: `${options.configDir}/workspaces`,
495
- defaultValue: `${options.configDir}/workspaces`,
496
- validate(value) {
497
- return value.trim().length > 0
498
- ? undefined
499
- : "Workspace directory is required.";
500
- },
485
+ const customizeAdvancedOptions = await abortIfCancelled(p.confirm({
486
+ message: "Customize advanced options? (default: No)",
487
+ initialValue: false,
501
488
  }));
489
+ let selectedRepos = projectDetail.linkedRepositories;
490
+ let workspaceDir = defaultWorkspaceDir;
491
+ if (customizeAdvancedOptions) {
492
+ const filterRepositories = await abortIfCancelled(p.confirm({
493
+ message: "Filter specific repositories? (default: No)",
494
+ initialValue: false,
495
+ }));
496
+ if (filterRepositories) {
497
+ selectedRepos = await abortIfCancelled(p.multiselect({
498
+ message: "Select repositories to orchestrate:",
499
+ options: projectDetail.linkedRepositories.map((repo) => ({
500
+ value: repo,
501
+ label: `${repo.owner}/${repo.name}`,
502
+ })),
503
+ required: true,
504
+ }));
505
+ }
506
+ workspaceDir = await abortIfCancelled(p.text({
507
+ message: "Workspace root directory:",
508
+ placeholder: defaultWorkspaceDir,
509
+ defaultValue: defaultWorkspaceDir,
510
+ validate(value) {
511
+ return value.trim().length > 0
512
+ ? undefined
513
+ : "Workspace directory is required.";
514
+ },
515
+ }));
516
+ }
517
+ const repoSummary = selectedRepos.length === projectDetail.linkedRepositories.length
518
+ ? `${selectedRepos.map((repo) => `${repo.owner}/${repo.name}`).join(", ")} (all ${selectedRepos.length} linked)`
519
+ : `${selectedRepos.map((repo) => `${repo.owner}/${repo.name}`).join(", ")} (${selectedRepos.length} of ${projectDetail.linkedRepositories.length} linked)`;
502
520
  p.note([
503
521
  `User: ${login}`,
504
522
  `Project: ${projectDetail.title}`,
505
- `Repos: ${selectedRepos.map((repo) => `${repo.owner}/${repo.name}`).join(", ")}`,
523
+ `Repos: ${repoSummary}`,
506
524
  `Assigned: ${assignedOnly ? `Only issues assigned to ${login}` : "All project issues"}`,
507
525
  `Workspace: ${workspaceDir}`,
508
526
  ].join("\n"), "Configuration Summary");
@@ -1,7 +1,8 @@
1
1
  import { readFile, readdir } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
3
  import { runCli as orchestratorRunCli } from "@gh-symphony/orchestrator";
4
- import { resolveRuntimeRoot, resolveProjectConfig, syncProjectToRuntime, } from "../orchestrator-runtime.js";
4
+ import { resolveRuntimeRoot, syncProjectToRuntime, } from "../orchestrator-runtime.js";
5
+ import { handleMissingManagedProjectConfig, resolveManagedProjectConfig, } from "../project-selection.js";
5
6
  function parseRecoverArgs(args) {
6
7
  const parsed = { dryRun: false };
7
8
  for (let i = 0; i < args.length; i += 1) {
@@ -18,10 +19,12 @@ function parseRecoverArgs(args) {
18
19
  }
19
20
  const handler = async (args, options) => {
20
21
  const parsed = parseRecoverArgs(args);
21
- const projectConfig = await resolveProjectConfig(options.configDir, parsed.projectId);
22
+ const projectConfig = await resolveManagedProjectConfig({
23
+ configDir: options.configDir,
24
+ requestedProjectId: parsed.projectId,
25
+ });
22
26
  if (!projectConfig) {
23
- process.stderr.write("No project configured. Run 'gh-symphony project add' first.\n");
24
- process.exitCode = 1;
27
+ handleMissingManagedProjectConfig();
25
28
  return;
26
29
  }
27
30
  const runtimeRoot = resolveRuntimeRoot(options.configDir);
@@ -1,5 +1,6 @@
1
1
  import { runCli as orchestratorRunCli } from "@gh-symphony/orchestrator";
2
- import { resolveRuntimeRoot, resolveProjectConfig, syncProjectToRuntime, } from "../orchestrator-runtime.js";
2
+ import { resolveRuntimeRoot, syncProjectToRuntime, } from "../orchestrator-runtime.js";
3
+ import { handleMissingManagedProjectConfig, resolveManagedProjectConfig, } from "../project-selection.js";
3
4
  function parseRunArgs(args) {
4
5
  const parsed = {
5
6
  watch: false,
@@ -27,10 +28,12 @@ const handler = async (args, options) => {
27
28
  process.exitCode = 2;
28
29
  return;
29
30
  }
30
- const projectConfig = await resolveProjectConfig(options.configDir, parsed.projectId);
31
+ const projectConfig = await resolveManagedProjectConfig({
32
+ configDir: options.configDir,
33
+ requestedProjectId: parsed.projectId,
34
+ });
31
35
  if (!projectConfig) {
32
- process.stderr.write("No project configured. Run 'gh-symphony project add' first.\n");
33
- process.exitCode = 1;
36
+ handleMissingManagedProjectConfig();
34
37
  return;
35
38
  }
36
39
  const runtimeRoot = resolveRuntimeRoot(options.configDir);
@@ -2,10 +2,10 @@ import { writeFile, mkdir, readFile, rm } from "node:fs/promises";
2
2
  import { dirname, join } from "node:path";
3
3
  import { spawn } from "node:child_process";
4
4
  import { once } from "node:events";
5
- import { parseCliArgs } from "./parse-cli-args.js";
6
5
  import { daemonPidPath, orchestratorLogPath, orchestratorPortPath, } from "../config.js";
7
6
  import { OrchestratorService, createStore, startOrchestratorStatusServer, } from "@gh-symphony/orchestrator";
8
- import { resolveProjectConfig, resolveRuntimeRoot, syncProjectToRuntime, } from "../orchestrator-runtime.js";
7
+ import { resolveRuntimeRoot, syncProjectToRuntime, } from "../orchestrator-runtime.js";
8
+ import { handleMissingManagedProjectConfig, resolveManagedProjectConfig, } from "../project-selection.js";
9
9
  import { bold, dim, green, red, yellow, cyan, setNoColor } from "../ansi.js";
10
10
  import { getGhToken } from "../github/gh-auth.js";
11
11
  function timestamp() {
@@ -20,18 +20,31 @@ function logLine(icon, msg) {
20
20
  }
21
21
  // ── Arg parsing ───────────────────────────────────────────────────────────────
22
22
  function parseStartArgs(args) {
23
- const parsed = parseCliArgs(args, {
24
- daemon: { type: "boolean", short: "d" },
25
- project: { type: "string" },
26
- "project-id": { type: "string" },
27
- });
28
- if ("error" in parsed) {
29
- return { daemon: false, error: parsed.error };
30
- }
31
- return {
32
- daemon: Boolean(parsed.values.daemon),
33
- projectId: (parsed.values["project-id"] ?? parsed.values.project),
23
+ const parsed = {
24
+ daemon: false,
34
25
  };
26
+ for (let i = 0; i < args.length; i += 1) {
27
+ const arg = args[i];
28
+ if (arg === "--daemon" || arg === "-d") {
29
+ parsed.daemon = true;
30
+ continue;
31
+ }
32
+ if (arg === "--project" || arg === "--project-id") {
33
+ const value = args[i + 1];
34
+ if (!value || value.startsWith("-")) {
35
+ parsed.error = `Option '${arg}' argument missing`;
36
+ return parsed;
37
+ }
38
+ parsed.projectId = value;
39
+ i += 1;
40
+ continue;
41
+ }
42
+ if (arg?.startsWith("-")) {
43
+ parsed.error = `Unknown option '${arg}'`;
44
+ return parsed;
45
+ }
46
+ }
47
+ return parsed;
35
48
  }
36
49
  // ── Tick logging ──────────────────────────────────────────────────────────────
37
50
  function logTickResult(snapshot, prevSnapshot, isFirst) {
@@ -110,15 +123,12 @@ const handler = async (args, options) => {
110
123
  process.exitCode = 2;
111
124
  return;
112
125
  }
113
- if (!parsed.projectId) {
114
- process.stderr.write("Usage: gh-symphony start --project-id <project-id> [--daemon]\n");
115
- process.exitCode = 2;
116
- return;
117
- }
118
- const projectConfig = await resolveProjectConfig(options.configDir, parsed.projectId);
126
+ const projectConfig = await resolveManagedProjectConfig({
127
+ configDir: options.configDir,
128
+ requestedProjectId: parsed.projectId,
129
+ });
119
130
  if (!projectConfig) {
120
- process.stderr.write("No project configured. Run 'gh-symphony project add' first.\n");
121
- process.exitCode = 1;
131
+ handleMissingManagedProjectConfig();
122
132
  return;
123
133
  }
124
134
  const runtimeRoot = resolveRuntimeRoot(options.configDir);
@@ -1,12 +1,12 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
- import { resolveRuntimeRoot, resolveProjectConfig, syncProjectToRuntime, } from "../orchestrator-runtime.js";
3
+ import { resolveRuntimeRoot, syncProjectToRuntime, } from "../orchestrator-runtime.js";
4
+ import { handleMissingManagedProjectConfig, resolveManagedProjectConfig, } from "../project-selection.js";
4
5
  import { bold, dim, green, red, yellow, cyan, stripAnsi } from "../ansi.js";
5
6
  import { clearScreen, showCursor, hideCursor } from "../ansi.js";
6
7
  import { renderDashboard } from "../dashboard/renderer.js";
7
8
  import { resolveProjectOrchestratorStatusBaseUrl } from "../orchestrator-status-endpoint.js";
8
9
  import { requestOrchestratorRefresh } from "./status-refresh.js";
9
- import { parseCliArgs } from "./parse-cli-args.js";
10
10
  const WATCH_REFRESH_TIMEOUT_MS = 1_500;
11
11
  function healthIcon(health) {
12
12
  switch (health) {
@@ -109,18 +109,31 @@ function renderLegacyStatus(snapshot, noColor) {
109
109
  return lines.join("\n");
110
110
  }
111
111
  function parseStatusArgs(args) {
112
- const parsed = parseCliArgs(args, {
113
- watch: { type: "boolean", short: "w" },
114
- project: { type: "string" },
115
- "project-id": { type: "string" },
116
- });
117
- if ("error" in parsed) {
118
- return { watch: false, error: parsed.error };
119
- }
120
- return {
121
- watch: Boolean(parsed.values.watch),
122
- projectId: (parsed.values["project-id"] ?? parsed.values.project),
112
+ const parsed = {
113
+ watch: false,
123
114
  };
115
+ for (let i = 0; i < args.length; i += 1) {
116
+ const arg = args[i];
117
+ if (arg === "--watch" || arg === "-w") {
118
+ parsed.watch = true;
119
+ continue;
120
+ }
121
+ if (arg === "--project" || arg === "--project-id") {
122
+ const value = args[i + 1];
123
+ if (!value || value.startsWith("-")) {
124
+ parsed.error = `Option '${arg}' argument missing`;
125
+ return parsed;
126
+ }
127
+ parsed.projectId = value;
128
+ i += 1;
129
+ continue;
130
+ }
131
+ if (arg?.startsWith("-")) {
132
+ parsed.error = `Unknown option '${arg}'`;
133
+ return parsed;
134
+ }
135
+ }
136
+ return parsed;
124
137
  }
125
138
  async function readStatusSnapshot(runtimeRoot, projectId) {
126
139
  try {
@@ -140,10 +153,12 @@ const handler = async (args, options) => {
140
153
  process.exitCode = 2;
141
154
  return;
142
155
  }
143
- const projectConfig = await resolveProjectConfig(options.configDir, parsed.projectId);
156
+ const projectConfig = await resolveManagedProjectConfig({
157
+ configDir: options.configDir,
158
+ requestedProjectId: parsed.projectId,
159
+ });
144
160
  if (!projectConfig) {
145
- process.stderr.write("No project configured. Run 'gh-symphony project add' first.\n");
146
- process.exitCode = 1;
161
+ handleMissingManagedProjectConfig();
147
162
  return;
148
163
  }
149
164
  const runtimeRoot = resolveRuntimeRoot(options.configDir);
@@ -1,19 +1,32 @@
1
1
  import { readFile, rm } from "node:fs/promises";
2
2
  import { daemonPidPath, orchestratorPortPath } from "../config.js";
3
- import { parseCliArgs } from "./parse-cli-args.js";
3
+ import { handleMissingManagedProjectConfig, resolveManagedProjectConfig, } from "../project-selection.js";
4
4
  function parseStopArgs(args) {
5
- const parsed = parseCliArgs(args, {
6
- force: { type: "boolean" },
7
- project: { type: "string" },
8
- "project-id": { type: "string" },
9
- });
10
- if ("error" in parsed) {
11
- return { force: false, error: parsed.error };
12
- }
13
- return {
14
- force: Boolean(parsed.values.force),
15
- projectId: (parsed.values["project-id"] ?? parsed.values.project),
5
+ const parsed = {
6
+ force: false,
16
7
  };
8
+ for (let i = 0; i < args.length; i += 1) {
9
+ const arg = args[i];
10
+ if (arg === "--force") {
11
+ parsed.force = true;
12
+ continue;
13
+ }
14
+ if (arg === "--project" || arg === "--project-id") {
15
+ const value = args[i + 1];
16
+ if (!value || value.startsWith("-")) {
17
+ parsed.error = `Option '${arg}' argument missing`;
18
+ return parsed;
19
+ }
20
+ parsed.projectId = value;
21
+ i += 1;
22
+ continue;
23
+ }
24
+ if (arg?.startsWith("-")) {
25
+ parsed.error = `Unknown option '${arg}'`;
26
+ return parsed;
27
+ }
28
+ }
29
+ return parsed;
17
30
  }
18
31
  const handler = async (args, options) => {
19
32
  const parsed = parseStopArgs(args);
@@ -23,13 +36,16 @@ const handler = async (args, options) => {
23
36
  process.exitCode = 2;
24
37
  return;
25
38
  }
26
- if (!parsed.projectId) {
27
- process.stderr.write("Usage: gh-symphony stop --project-id <project-id> [--force]\n");
28
- process.exitCode = 2;
39
+ const resolvedForce = parsed.force;
40
+ const projectConfig = await resolveManagedProjectConfig({
41
+ configDir: options.configDir,
42
+ requestedProjectId: parsed.projectId,
43
+ });
44
+ if (!projectConfig) {
45
+ handleMissingManagedProjectConfig();
29
46
  return;
30
47
  }
31
- const resolvedForce = parsed.force;
32
- const resolvedProjectId = parsed.projectId;
48
+ const resolvedProjectId = projectConfig.projectId;
33
49
  const pidPath = daemonPidPath(options.configDir, resolvedProjectId);
34
50
  const portPath = orchestratorPortPath(options.configDir, resolvedProjectId);
35
51
  let pidStr;
@@ -0,0 +1 @@
1
+ export declare function renderCompletionScript(shell: "bash" | "zsh" | "fish"): string;
@@ -0,0 +1,204 @@
1
+ const TOP_LEVEL_COMMANDS = [
2
+ "init",
3
+ "start",
4
+ "stop",
5
+ "status",
6
+ "run",
7
+ "recover",
8
+ "logs",
9
+ "project",
10
+ "repo",
11
+ "config",
12
+ "completion",
13
+ "help",
14
+ "version",
15
+ ];
16
+ const GLOBAL_OPTIONS = [
17
+ "--config",
18
+ "--config-dir",
19
+ "--verbose",
20
+ "-v",
21
+ "--json",
22
+ "--no-color",
23
+ "--help",
24
+ "-h",
25
+ "--version",
26
+ "-V",
27
+ ];
28
+ const GLOBAL_OPTIONS_WITH_VALUES = ["--config", "--config-dir"];
29
+ const COMMAND_OPTIONS = {
30
+ completion: ["bash", "zsh", "fish"],
31
+ start: ["--project-id", "--project", "--daemon", "-d", ...GLOBAL_OPTIONS],
32
+ stop: ["--project-id", "--project", "--force", ...GLOBAL_OPTIONS],
33
+ status: ["--project-id", "--project", "--watch", "-w", ...GLOBAL_OPTIONS],
34
+ run: ["--project-id", "--project", "--watch", "-w", ...GLOBAL_OPTIONS],
35
+ recover: ["--project-id", "--project", "--dry-run", ...GLOBAL_OPTIONS],
36
+ logs: [
37
+ "--project-id",
38
+ "--project",
39
+ "--follow",
40
+ "-f",
41
+ "--issue",
42
+ "--run",
43
+ "--level",
44
+ ...GLOBAL_OPTIONS,
45
+ ],
46
+ project: ["add", "list", "remove", "start", "stop", "switch", "status"],
47
+ "project:add": [
48
+ "--non-interactive",
49
+ "--project",
50
+ "--workspace-dir",
51
+ "--assigned-only",
52
+ ...GLOBAL_OPTIONS,
53
+ ],
54
+ "project:list": [...GLOBAL_OPTIONS],
55
+ "project:remove": [...GLOBAL_OPTIONS],
56
+ "project:start": [
57
+ "--project-id",
58
+ "--project",
59
+ "--daemon",
60
+ "-d",
61
+ ...GLOBAL_OPTIONS,
62
+ ],
63
+ "project:stop": ["--project-id", "--project", "--force", ...GLOBAL_OPTIONS],
64
+ "project:switch": [...GLOBAL_OPTIONS],
65
+ "project:status": [
66
+ "--project-id",
67
+ "--project",
68
+ "--watch",
69
+ "-w",
70
+ ...GLOBAL_OPTIONS,
71
+ ],
72
+ repo: ["list", "add", "remove"],
73
+ "repo:list": [...GLOBAL_OPTIONS],
74
+ "repo:add": [...GLOBAL_OPTIONS],
75
+ "repo:remove": [...GLOBAL_OPTIONS],
76
+ config: ["show", "set", "edit"],
77
+ "config:show": [...GLOBAL_OPTIONS],
78
+ "config:set": [...GLOBAL_OPTIONS],
79
+ "config:edit": [...GLOBAL_OPTIONS],
80
+ };
81
+ function quoteWords(values) {
82
+ return values.join(" ");
83
+ }
84
+ function renderBashCasePatterns() {
85
+ return Object.entries(COMMAND_OPTIONS)
86
+ .map(([key, values]) => {
87
+ const [command, subcommand] = key.split(":");
88
+ if (!subcommand) {
89
+ if (command === "completion") {
90
+ return ` completion)\n COMPREPLY=( $(compgen -W "${quoteWords(values)}" -- "$cur") )\n return\n ;;`;
91
+ }
92
+ if (command === "project" ||
93
+ command === "repo" ||
94
+ command === "config") {
95
+ return ` ${command})\n COMPREPLY=( $(compgen -W "${quoteWords(values)}" -- "$cur") )\n return\n ;;`;
96
+ }
97
+ return ` ${command})\n COMPREPLY=( $(compgen -W "${quoteWords(values)}" -- "$cur") )\n return\n ;;`;
98
+ }
99
+ return ` ${command}:${subcommand})\n COMPREPLY=( $(compgen -W "${quoteWords(values)}" -- "$cur") )\n return\n ;;`;
100
+ })
101
+ .join("\n");
102
+ }
103
+ function renderFishLines() {
104
+ const lines = GLOBAL_OPTIONS.map((option) => option.startsWith("--")
105
+ ? `complete -c gh-symphony -f -l ${option.slice(2)}`
106
+ : `complete -c gh-symphony -f -s ${option.slice(1)}`);
107
+ for (const command of TOP_LEVEL_COMMANDS) {
108
+ lines.push(`complete -c gh-symphony -f -n '__fish_use_subcommand' -a '${command}'`);
109
+ }
110
+ for (const subcommand of COMMAND_OPTIONS.project ?? []) {
111
+ lines.push(`complete -c gh-symphony -f -n '__fish_seen_subcommand_from project' -a '${subcommand}'`);
112
+ }
113
+ for (const subcommand of COMMAND_OPTIONS.repo ?? []) {
114
+ lines.push(`complete -c gh-symphony -f -n '__fish_seen_subcommand_from repo' -a '${subcommand}'`);
115
+ }
116
+ for (const subcommand of COMMAND_OPTIONS.config ?? []) {
117
+ lines.push(`complete -c gh-symphony -f -n '__fish_seen_subcommand_from config' -a '${subcommand}'`);
118
+ }
119
+ for (const shell of COMMAND_OPTIONS.completion ?? []) {
120
+ lines.push(`complete -c gh-symphony -f -n '__fish_seen_subcommand_from completion' -a '${shell}'`);
121
+ }
122
+ return lines.join("\n");
123
+ }
124
+ export function renderCompletionScript(shell) {
125
+ if (shell === "fish") {
126
+ return `${renderFishLines()}\n`;
127
+ }
128
+ const bashFunction = `# shellcheck shell=bash
129
+ _gh_symphony_find_context() {
130
+ GH_SYMPHONY_COMMAND=""
131
+ GH_SYMPHONY_SUBCOMMAND=""
132
+
133
+ local idx=1
134
+ local token=""
135
+ local expects_value=0
136
+
137
+ while (( idx < COMP_CWORD )); do
138
+ token="\${COMP_WORDS[idx]}"
139
+
140
+ if (( expects_value )); then
141
+ expects_value=0
142
+ (( idx++ ))
143
+ continue
144
+ fi
145
+
146
+ case "\${token}" in
147
+ ${GLOBAL_OPTIONS_WITH_VALUES.map((option) => `${option}`).join("|")})
148
+ expects_value=1
149
+ ;;
150
+ ${GLOBAL_OPTIONS_WITH_VALUES.map((option) => `${option}=*`).join("|")})
151
+ ;;
152
+ ${GLOBAL_OPTIONS.filter((option) => !GLOBAL_OPTIONS_WITH_VALUES.includes(option)).join("|")})
153
+ ;;
154
+ -*)
155
+ ;;
156
+ *)
157
+ if [[ -z "\${GH_SYMPHONY_COMMAND}" ]]; then
158
+ GH_SYMPHONY_COMMAND="\${token}"
159
+ elif [[ -z "\${GH_SYMPHONY_SUBCOMMAND}" ]]; then
160
+ GH_SYMPHONY_SUBCOMMAND="\${token}"
161
+ fi
162
+ ;;
163
+ esac
164
+
165
+ (( idx++ ))
166
+ done
167
+ }
168
+
169
+ _gh_symphony_completion() {
170
+ local cur prev path
171
+ cur="\${COMP_WORDS[COMP_CWORD]}"
172
+ prev=""
173
+ if (( COMP_CWORD > 0 )); then
174
+ prev="\${COMP_WORDS[COMP_CWORD-1]}"
175
+ fi
176
+
177
+ _gh_symphony_find_context
178
+ path="\${GH_SYMPHONY_COMMAND}"
179
+
180
+ if [[ -z "\${path}" ]]; then
181
+ COMPREPLY=( $(compgen -W "${quoteWords(TOP_LEVEL_COMMANDS)} ${quoteWords(GLOBAL_OPTIONS)}" -- "$cur") )
182
+ return
183
+ fi
184
+
185
+ if [[ "\${path}" == "project" || "\${path}" == "repo" || "\${path}" == "config" || "\${path}" == "completion" ]]; then
186
+ if [[ -n "\${GH_SYMPHONY_SUBCOMMAND}" ]]; then
187
+ path="\${path}:\${GH_SYMPHONY_SUBCOMMAND}"
188
+ fi
189
+ fi
190
+
191
+ case "\${path}" in
192
+ ${renderBashCasePatterns()}
193
+ esac
194
+ }
195
+ `;
196
+ if (shell === "zsh") {
197
+ return `autoload -Uz compinit && compinit
198
+ autoload -U +X bashcompinit && bashcompinit
199
+ ${bashFunction}complete -F _gh_symphony_completion gh-symphony
200
+ `;
201
+ }
202
+ return `${bashFunction}complete -F _gh_symphony_completion gh-symphony
203
+ `;
204
+ }
package/dist/index.d.ts CHANGED
@@ -5,10 +5,5 @@ export type GlobalOptions = {
5
5
  json: boolean;
6
6
  noColor: boolean;
7
7
  };
8
- export declare function parseGlobalOptions(argv: string[]): {
9
- options: GlobalOptions;
10
- command: string;
11
- args: string[];
12
- };
13
8
  export type CommandHandler = (args: string[], options: GlobalOptions) => Promise<void>;
14
9
  export declare function runCli(argv: string[]): Promise<void>;
package/dist/index.js CHANGED
@@ -1,44 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import { realpathSync } from "node:fs";
3
3
  import { pathToFileURL } from "node:url";
4
+ import { Command, CommanderError, InvalidArgumentError, Option, } from "commander";
4
5
  import { resolveConfigDir } from "./config.js";
5
- export function parseGlobalOptions(argv) {
6
- const globalFlags = {
7
- configDir: resolveConfigDir(),
8
- verbose: false,
9
- json: false,
10
- noColor: false,
11
- };
12
- const remaining = [];
13
- let i = 0;
14
- while (i < argv.length) {
15
- const arg = argv[i];
16
- if (arg === "--config" || arg === "--config-dir") {
17
- globalFlags.configDir = resolveConfigDir(argv[i + 1]);
18
- i += 2;
19
- continue;
20
- }
21
- if (arg === "--verbose" || arg === "-v") {
22
- globalFlags.verbose = true;
23
- i += 1;
24
- continue;
25
- }
26
- if (arg === "--json") {
27
- globalFlags.json = true;
28
- i += 1;
29
- continue;
30
- }
31
- if (arg === "--no-color") {
32
- globalFlags.noColor = true;
33
- i += 1;
34
- continue;
35
- }
36
- remaining.push(arg);
37
- i += 1;
38
- }
39
- const [command = "help", ...args] = remaining;
40
- return { options: globalFlags, command, args };
41
- }
6
+ import { renderCompletionScript } from "./completion.js";
42
7
  const COMMANDS = {
43
8
  init: () => import("./commands/init.js"),
44
9
  start: () => import("./commands/start.js"),
@@ -50,32 +15,355 @@ const COMMANDS = {
50
15
  project: () => import("./commands/project.js"),
51
16
  repo: () => import("./commands/repo.js"),
52
17
  config: () => import("./commands/config-cmd.js"),
53
- help: () => import("./commands/help.js"),
54
18
  version: () => import("./commands/version.js"),
55
19
  };
56
- export async function runCli(argv) {
57
- const { options, command, args } = parseGlobalOptions(argv);
20
+ function addGlobalOptions(command) {
21
+ return command
22
+ .option("--config <dir>", "Config directory")
23
+ .addOption(new Option("--config-dir <dir>").hideHelp())
24
+ .option("-v, --verbose", "Enable verbose output")
25
+ .option("--json", "Output in JSON format")
26
+ .option("--no-color", "Disable color output");
27
+ }
28
+ function resolveGlobalOptions(values) {
29
+ const configInput = typeof values.config === "string"
30
+ ? values.config
31
+ : typeof values.configDir === "string"
32
+ ? values.configDir
33
+ : undefined;
34
+ const options = {
35
+ configDir: resolveConfigDir(configInput),
36
+ verbose: Boolean(values.verbose),
37
+ json: Boolean(values.json),
38
+ noColor: Boolean(values.noColor),
39
+ };
58
40
  if (options.noColor) {
59
41
  process.env.NO_COLOR = "1";
60
42
  }
61
- if (command === "--help" || command === "-h") {
62
- const helpModule = await COMMANDS.help();
63
- await helpModule.default(args, options);
43
+ return options;
44
+ }
45
+ function resolveProjectId(values) {
46
+ return values.projectId ?? values.project;
47
+ }
48
+ function pushOption(args, flag, value) {
49
+ if (typeof value === "string" && value.length > 0) {
50
+ args.push(flag, value);
64
51
  return;
65
52
  }
66
- if (command === "--version" || command === "-V") {
53
+ if (value === true) {
54
+ args.push(flag);
55
+ }
56
+ }
57
+ async function invokeHandler(key, args, values) {
58
+ const module = await COMMANDS[key]();
59
+ await module.default(args, resolveGlobalOptions(values));
60
+ }
61
+ function shellArgument(value) {
62
+ if (value === "bash" || value === "zsh" || value === "fish") {
63
+ return value;
64
+ }
65
+ throw new InvalidArgumentError("Shell must be one of: bash, zsh, fish");
66
+ }
67
+ function hasVersionFlag(argv) {
68
+ return argv.some((arg) => arg === "--version" || arg === "-V");
69
+ }
70
+ function resolveVersionOptions(argv) {
71
+ const options = {
72
+ configDir: resolveConfigDir(),
73
+ verbose: argv.some((arg) => arg === "--verbose" || arg === "-v"),
74
+ json: argv.includes("--json"),
75
+ noColor: argv.includes("--no-color"),
76
+ };
77
+ if (options.noColor) {
78
+ process.env.NO_COLOR = "1";
79
+ }
80
+ return options;
81
+ }
82
+ function createProgram() {
83
+ let actionInvoked = false;
84
+ const markInvoked = () => {
85
+ actionInvoked = true;
86
+ };
87
+ const program = addGlobalOptions(new Command()
88
+ .name("gh-symphony")
89
+ .description("AI Coding Agent Orchestrator")
90
+ .exitOverride()
91
+ .helpOption("-h, --help", "Show help")
92
+ .addHelpCommand("help [command]", "Show help for command")
93
+ .showHelpAfterError("(run with --help for usage)")
94
+ .option("-V, --version", "Show version"));
95
+ addGlobalOptions(program
96
+ .command("init")
97
+ .description("Interactive project setup wizard")
98
+ .allowExcessArguments(false)).action(async function () {
99
+ markInvoked();
100
+ await invokeHandler("init", [], this.optsWithGlobals());
101
+ });
102
+ addGlobalOptions(program
103
+ .command("start")
104
+ .description("Start the orchestrator")
105
+ .option("-d, --daemon", "Start in daemon mode")
106
+ .option("--project-id <projectId>", "Project identifier")
107
+ .addOption(new Option("--project <projectId>").hideHelp())
108
+ .allowExcessArguments(false)).action(async function () {
109
+ markInvoked();
110
+ const values = this.optsWithGlobals();
111
+ const args = [];
112
+ pushOption(args, "--project-id", resolveProjectId(values));
113
+ pushOption(args, "--daemon", values.daemon);
114
+ await invokeHandler("start", args, values);
115
+ });
116
+ addGlobalOptions(program
117
+ .command("stop")
118
+ .description("Stop the background orchestrator")
119
+ .option("--force", "Force stop with SIGKILL")
120
+ .option("--project-id <projectId>", "Project identifier")
121
+ .addOption(new Option("--project <projectId>").hideHelp())
122
+ .allowExcessArguments(false)).action(async function () {
123
+ markInvoked();
124
+ const values = this.optsWithGlobals();
125
+ const args = [];
126
+ pushOption(args, "--project-id", resolveProjectId(values));
127
+ pushOption(args, "--force", values.force);
128
+ await invokeHandler("stop", args, values);
129
+ });
130
+ addGlobalOptions(program
131
+ .command("status")
132
+ .description("Show orchestrator status")
133
+ .option("-w, --watch", "Watch status continuously")
134
+ .option("--project-id <projectId>", "Project identifier")
135
+ .addOption(new Option("--project <projectId>").hideHelp())
136
+ .allowExcessArguments(false)).action(async function () {
137
+ markInvoked();
138
+ const values = this.optsWithGlobals();
139
+ const args = [];
140
+ pushOption(args, "--project-id", resolveProjectId(values));
141
+ pushOption(args, "--watch", values.watch);
142
+ await invokeHandler("status", args, values);
143
+ });
144
+ addGlobalOptions(program
145
+ .command("run")
146
+ .description("Dispatch a single issue")
147
+ .argument("<issue>", "Issue identifier")
148
+ .option("-w, --watch", "Watch status after dispatch")
149
+ .option("--project-id <projectId>", "Project identifier")
150
+ .addOption(new Option("--project <projectId>").hideHelp())
151
+ .allowExcessArguments(false)).action(async function (issue) {
152
+ markInvoked();
153
+ const values = this.optsWithGlobals();
154
+ const args = [issue];
155
+ pushOption(args, "--project-id", resolveProjectId(values));
156
+ pushOption(args, "--watch", values.watch);
157
+ await invokeHandler("run", args, values);
158
+ });
159
+ addGlobalOptions(program
160
+ .command("recover")
161
+ .description("Recover stalled runs")
162
+ .option("--dry-run", "Show recoverable runs without recovering")
163
+ .option("--project-id <projectId>", "Project identifier")
164
+ .addOption(new Option("--project <projectId>").hideHelp())
165
+ .allowExcessArguments(false)).action(async function () {
166
+ markInvoked();
167
+ const values = this.optsWithGlobals();
168
+ const args = [];
169
+ pushOption(args, "--project-id", resolveProjectId(values));
170
+ pushOption(args, "--dry-run", values.dryRun);
171
+ await invokeHandler("recover", args, values);
172
+ });
173
+ addGlobalOptions(program
174
+ .command("logs")
175
+ .description("View orchestrator logs")
176
+ .option("-f, --follow", "Follow new log lines")
177
+ .option("--issue <issue>", "Filter by issue identifier")
178
+ .option("--run <runId>", "Read events for a specific run")
179
+ .option("--level <level>", "Filter by log level")
180
+ .option("--project-id <projectId>", "Project identifier")
181
+ .addOption(new Option("--project <projectId>").hideHelp())
182
+ .allowExcessArguments(false)).action(async function () {
183
+ markInvoked();
184
+ const values = this.optsWithGlobals();
185
+ const args = [];
186
+ pushOption(args, "--project-id", resolveProjectId(values));
187
+ pushOption(args, "--follow", values.follow);
188
+ pushOption(args, "--issue", values.issue);
189
+ pushOption(args, "--run", values.run);
190
+ pushOption(args, "--level", values.level);
191
+ await invokeHandler("logs", args, values);
192
+ });
193
+ const project = addGlobalOptions(program.command("project").description("Manage configured projects"));
194
+ project.action(async function () {
195
+ markInvoked();
196
+ await invokeHandler("project", [], this.optsWithGlobals());
197
+ });
198
+ addGlobalOptions(project
199
+ .command("add")
200
+ .description("Add a new project")
201
+ .option("--non-interactive", "Run without prompts")
202
+ .option("--project <id>", "GitHub Project ID")
203
+ .option("--workspace-dir <path>", "Workspace directory")
204
+ .option("--assigned-only", "Limit processing to assigned issues")
205
+ .allowExcessArguments(false)).action(async function () {
206
+ markInvoked();
207
+ const values = this.optsWithGlobals();
208
+ const args = [];
209
+ pushOption(args, "--non-interactive", values.nonInteractive);
210
+ pushOption(args, "--project", values.project);
211
+ pushOption(args, "--workspace-dir", values.workspaceDir);
212
+ pushOption(args, "--assigned-only", values.assignedOnly);
213
+ await invokeHandler("project", ["add", ...args], values);
214
+ });
215
+ addGlobalOptions(project.command("list").description("List configured projects")).action(async function () {
216
+ markInvoked();
217
+ const values = this.optsWithGlobals();
218
+ await invokeHandler("project", ["list"], values);
219
+ });
220
+ addGlobalOptions(project
221
+ .command("remove")
222
+ .description("Remove a project")
223
+ .argument("<projectId>", "Project identifier")
224
+ .allowExcessArguments(false)).action(async function (projectId) {
225
+ markInvoked();
226
+ await invokeHandler("project", ["remove", projectId], this.optsWithGlobals());
227
+ });
228
+ addGlobalOptions(project
229
+ .command("start")
230
+ .description("Start a specific project")
231
+ .option("-d, --daemon", "Start in daemon mode")
232
+ .option("--project-id <projectId>", "Project identifier")
233
+ .addOption(new Option("--project <projectId>").hideHelp())
234
+ .allowExcessArguments(false)).action(async function () {
235
+ markInvoked();
236
+ const values = this.optsWithGlobals();
237
+ const args = ["start"];
238
+ pushOption(args, "--project-id", resolveProjectId(values));
239
+ pushOption(args, "--daemon", values.daemon);
240
+ await invokeHandler("project", args, values);
241
+ });
242
+ addGlobalOptions(project
243
+ .command("stop")
244
+ .description("Stop a specific project")
245
+ .option("--force", "Force stop with SIGKILL")
246
+ .option("--project-id <projectId>", "Project identifier")
247
+ .addOption(new Option("--project <projectId>").hideHelp())
248
+ .allowExcessArguments(false)).action(async function () {
249
+ markInvoked();
250
+ const values = this.optsWithGlobals();
251
+ const args = ["stop"];
252
+ pushOption(args, "--project-id", resolveProjectId(values));
253
+ pushOption(args, "--force", values.force);
254
+ await invokeHandler("project", args, values);
255
+ });
256
+ addGlobalOptions(project.command("switch").description("Switch the active project")).action(async function () {
257
+ markInvoked();
258
+ await invokeHandler("project", ["switch"], this.optsWithGlobals());
259
+ });
260
+ addGlobalOptions(project
261
+ .command("status")
262
+ .description("Show status for a specific project")
263
+ .option("-w, --watch", "Watch status continuously")
264
+ .option("--project-id <projectId>", "Project identifier")
265
+ .addOption(new Option("--project <projectId>").hideHelp())
266
+ .allowExcessArguments(false)).action(async function () {
267
+ markInvoked();
268
+ const values = this.optsWithGlobals();
269
+ const args = ["status"];
270
+ pushOption(args, "--project-id", resolveProjectId(values));
271
+ pushOption(args, "--watch", values.watch);
272
+ await invokeHandler("project", args, values);
273
+ });
274
+ const repo = addGlobalOptions(program
275
+ .command("repo")
276
+ .description("Manage repositories in the active project"));
277
+ repo.action(async function () {
278
+ markInvoked();
279
+ await invokeHandler("repo", [], this.optsWithGlobals());
280
+ });
281
+ addGlobalOptions(repo.command("list").description("List repositories")).action(async function () {
282
+ markInvoked();
283
+ await invokeHandler("repo", ["list"], this.optsWithGlobals());
284
+ });
285
+ addGlobalOptions(repo
286
+ .command("add")
287
+ .description("Add a repository")
288
+ .argument("<owner/name>", "Repository spec")
289
+ .allowExcessArguments(false)).action(async function (repoSpec) {
290
+ markInvoked();
291
+ await invokeHandler("repo", ["add", repoSpec], this.optsWithGlobals());
292
+ });
293
+ addGlobalOptions(repo
294
+ .command("remove")
295
+ .description("Remove a repository")
296
+ .argument("<owner/name>", "Repository spec")
297
+ .allowExcessArguments(false)).action(async function (repoSpec) {
298
+ markInvoked();
299
+ await invokeHandler("repo", ["remove", repoSpec], this.optsWithGlobals());
300
+ });
301
+ const config = addGlobalOptions(program.command("config").description("Manage CLI configuration"));
302
+ config.action(async function () {
303
+ markInvoked();
304
+ await invokeHandler("config", [], this.optsWithGlobals());
305
+ });
306
+ addGlobalOptions(config.command("show").description("Show configuration")).action(async function () {
307
+ markInvoked();
308
+ await invokeHandler("config", ["show"], this.optsWithGlobals());
309
+ });
310
+ addGlobalOptions(config
311
+ .command("set")
312
+ .description("Set a configuration value")
313
+ .argument("<key>", "Configuration key")
314
+ .argument("<value>", "Configuration value")
315
+ .allowExcessArguments(false)).action(async function (key, value) {
316
+ markInvoked();
317
+ await invokeHandler("config", ["set", key, value], this.optsWithGlobals());
318
+ });
319
+ addGlobalOptions(config.command("edit").description("Open config in $EDITOR")).action(async function () {
320
+ markInvoked();
321
+ await invokeHandler("config", ["edit"], this.optsWithGlobals());
322
+ });
323
+ addGlobalOptions(program
324
+ .command("completion")
325
+ .description("Print shell completion script")
326
+ .argument("<shell>", "Shell name", shellArgument)
327
+ .allowExcessArguments(false)).action(async function (shell) {
328
+ markInvoked();
329
+ process.stdout.write(renderCompletionScript(shell));
330
+ });
331
+ program
332
+ .command("version")
333
+ .description("Show version")
334
+ .allowExcessArguments(false)
335
+ .action(async function () {
336
+ markInvoked();
337
+ await invokeHandler("version", [], this.optsWithGlobals());
338
+ });
339
+ return { program, wasInvoked: () => actionInvoked };
340
+ }
341
+ export async function runCli(argv) {
342
+ const { program, wasInvoked } = createProgram();
343
+ if (hasVersionFlag(argv)) {
67
344
  const versionModule = await COMMANDS.version();
68
- await versionModule.default(args, options);
345
+ await versionModule.default([], resolveVersionOptions(argv));
69
346
  return;
70
347
  }
71
- const loader = COMMANDS[command];
72
- if (!loader) {
73
- process.stderr.write(`Unknown command: ${command}\nRun 'gh-symphony help' for usage.\n`);
74
- process.exitCode = 2;
75
- return;
348
+ try {
349
+ await program.parseAsync(["node", "gh-symphony", ...argv], {
350
+ from: "node",
351
+ });
352
+ if (!wasInvoked()) {
353
+ program.outputHelp();
354
+ }
355
+ }
356
+ catch (error) {
357
+ if (error instanceof CommanderError &&
358
+ error.code === "commander.helpDisplayed") {
359
+ return;
360
+ }
361
+ if (error instanceof CommanderError) {
362
+ process.exitCode = error.exitCode;
363
+ return;
364
+ }
365
+ throw error;
76
366
  }
77
- const module = await loader();
78
- await module.default(args, options);
79
367
  }
80
368
  async function main() {
81
369
  await runCli(process.argv.slice(2));
@@ -0,0 +1,8 @@
1
+ import { type CliProjectConfig } from "./config.js";
2
+ type ResolveProjectSelectionInput = {
3
+ configDir: string;
4
+ requestedProjectId?: string;
5
+ };
6
+ export declare function resolveManagedProjectConfig(input: ResolveProjectSelectionInput): Promise<CliProjectConfig | null>;
7
+ export declare function handleMissingManagedProjectConfig(): void;
8
+ export {};
@@ -0,0 +1,56 @@
1
+ import * as p from "@clack/prompts";
2
+ import { loadGlobalConfig, loadProjectConfig, } from "./config.js";
3
+ function isInteractiveTerminal() {
4
+ return process.stdin.isTTY === true && process.stdout.isTTY === true;
5
+ }
6
+ function explicitProjectRequiredMessage() {
7
+ return "Multiple projects are configured. Re-run with --project-id in non-interactive environments.\n";
8
+ }
9
+ export async function resolveManagedProjectConfig(input) {
10
+ if (input.requestedProjectId) {
11
+ return loadProjectConfig(input.configDir, input.requestedProjectId);
12
+ }
13
+ const global = await loadGlobalConfig(input.configDir);
14
+ const projectIds = global?.projects ?? [];
15
+ if (projectIds.length === 0) {
16
+ return null;
17
+ }
18
+ if (projectIds.length === 1) {
19
+ return loadProjectConfig(input.configDir, projectIds[0]);
20
+ }
21
+ if (!isInteractiveTerminal()) {
22
+ process.stderr.write(explicitProjectRequiredMessage());
23
+ process.exitCode = 1;
24
+ return null;
25
+ }
26
+ const projects = await Promise.all(projectIds.map(async (projectId) => ({
27
+ projectId,
28
+ config: await loadProjectConfig(input.configDir, projectId),
29
+ })));
30
+ const selected = await p.select({
31
+ message: "Select a project:",
32
+ options: projects.map(({ projectId, config }) => ({
33
+ value: projectId,
34
+ label: config?.displayName ?? config?.slug ?? projectId,
35
+ hint: projectId === global?.activeProject
36
+ ? "current"
37
+ : config && config.displayName && config.displayName !== projectId
38
+ ? projectId
39
+ : undefined,
40
+ })),
41
+ maxItems: 10,
42
+ });
43
+ if (p.isCancel(selected)) {
44
+ p.cancel("Cancelled.");
45
+ process.exitCode = 130;
46
+ return null;
47
+ }
48
+ return loadProjectConfig(input.configDir, selected);
49
+ }
50
+ export function handleMissingManagedProjectConfig() {
51
+ if (process.exitCode) {
52
+ return;
53
+ }
54
+ process.stderr.write("No project configured. Run 'gh-symphony project add' first.\n");
55
+ process.exitCode = 1;
56
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gh-symphony/cli",
3
- "version": "0.0.7",
3
+ "version": "0.0.9",
4
4
  "license": "MIT",
5
5
  "author": "hojinzs",
6
6
  "description": "Interactive CLI for GitHub Symphony orchestration",
@@ -36,10 +36,11 @@
36
36
  },
37
37
  "dependencies": {
38
38
  "@clack/prompts": "^0.9.1",
39
- "@gh-symphony/orchestrator": "0.0.7",
40
- "@gh-symphony/core": "0.0.7",
41
- "@gh-symphony/tracker-github": "0.0.7",
42
- "@gh-symphony/worker": "0.0.7"
39
+ "commander": "^14.0.1",
40
+ "@gh-symphony/core": "0.0.9",
41
+ "@gh-symphony/worker": "0.0.9",
42
+ "@gh-symphony/tracker-github": "0.0.9",
43
+ "@gh-symphony/orchestrator": "0.0.9"
43
44
  },
44
45
  "scripts": {
45
46
  "build": "tsc -p tsconfig.json",