@gh-symphony/cli 0.0.7 → 0.0.8
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 +18 -4
- package/dist/commands/logs.js +14 -9
- package/dist/commands/recover.js +7 -4
- package/dist/commands/run.js +7 -4
- package/dist/commands/start.js +31 -21
- package/dist/commands/status.js +31 -16
- package/dist/commands/stop.js +33 -17
- package/dist/completion.d.ts +1 -0
- package/dist/completion.js +204 -0
- package/dist/index.d.ts +0 -5
- package/dist/index.js +340 -52
- package/dist/project-selection.d.ts +8 -0
- package/dist/project-selection.js +56 -0
- package/package.json +6 -5
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.
|
|
64
|
-
4. Optionally
|
|
65
|
-
5.
|
|
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)
|
package/dist/commands/logs.js
CHANGED
|
@@ -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 {
|
|
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)
|
package/dist/commands/recover.js
CHANGED
|
@@ -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,
|
|
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
|
|
22
|
+
const projectConfig = await resolveManagedProjectConfig({
|
|
23
|
+
configDir: options.configDir,
|
|
24
|
+
requestedProjectId: parsed.projectId,
|
|
25
|
+
});
|
|
22
26
|
if (!projectConfig) {
|
|
23
|
-
|
|
24
|
-
process.exitCode = 1;
|
|
27
|
+
handleMissingManagedProjectConfig();
|
|
25
28
|
return;
|
|
26
29
|
}
|
|
27
30
|
const runtimeRoot = resolveRuntimeRoot(options.configDir);
|
package/dist/commands/run.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { runCli as orchestratorRunCli } from "@gh-symphony/orchestrator";
|
|
2
|
-
import { resolveRuntimeRoot,
|
|
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
|
|
31
|
+
const projectConfig = await resolveManagedProjectConfig({
|
|
32
|
+
configDir: options.configDir,
|
|
33
|
+
requestedProjectId: parsed.projectId,
|
|
34
|
+
});
|
|
31
35
|
if (!projectConfig) {
|
|
32
|
-
|
|
33
|
-
process.exitCode = 1;
|
|
36
|
+
handleMissingManagedProjectConfig();
|
|
34
37
|
return;
|
|
35
38
|
}
|
|
36
39
|
const runtimeRoot = resolveRuntimeRoot(options.configDir);
|
package/dist/commands/start.js
CHANGED
|
@@ -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 {
|
|
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 =
|
|
24
|
-
daemon:
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
121
|
-
process.exitCode = 1;
|
|
131
|
+
handleMissingManagedProjectConfig();
|
|
122
132
|
return;
|
|
123
133
|
}
|
|
124
134
|
const runtimeRoot = resolveRuntimeRoot(options.configDir);
|
package/dist/commands/status.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import { resolveRuntimeRoot,
|
|
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 =
|
|
113
|
-
watch:
|
|
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
|
|
156
|
+
const projectConfig = await resolveManagedProjectConfig({
|
|
157
|
+
configDir: options.configDir,
|
|
158
|
+
requestedProjectId: parsed.projectId,
|
|
159
|
+
});
|
|
144
160
|
if (!projectConfig) {
|
|
145
|
-
|
|
146
|
-
process.exitCode = 1;
|
|
161
|
+
handleMissingManagedProjectConfig();
|
|
147
162
|
return;
|
|
148
163
|
}
|
|
149
164
|
const runtimeRoot = resolveRuntimeRoot(options.configDir);
|
package/dist/commands/stop.js
CHANGED
|
@@ -1,19 +1,32 @@
|
|
|
1
1
|
import { readFile, rm } from "node:fs/promises";
|
|
2
2
|
import { daemonPidPath, orchestratorPortPath } from "../config.js";
|
|
3
|
-
import {
|
|
3
|
+
import { handleMissingManagedProjectConfig, resolveManagedProjectConfig, } from "../project-selection.js";
|
|
4
4
|
function parseStopArgs(args) {
|
|
5
|
-
const parsed =
|
|
6
|
-
force:
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
57
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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 (
|
|
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(
|
|
345
|
+
await versionModule.default([], resolveVersionOptions(argv));
|
|
69
346
|
return;
|
|
70
347
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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.
|
|
3
|
+
"version": "0.0.8",
|
|
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
|
-
"
|
|
40
|
-
"@gh-symphony/core": "0.0.
|
|
41
|
-
"@gh-symphony/
|
|
42
|
-
"@gh-symphony/
|
|
39
|
+
"commander": "^14.0.1",
|
|
40
|
+
"@gh-symphony/core": "0.0.8",
|
|
41
|
+
"@gh-symphony/orchestrator": "0.0.8",
|
|
42
|
+
"@gh-symphony/tracker-github": "0.0.8",
|
|
43
|
+
"@gh-symphony/worker": "0.0.8"
|
|
43
44
|
},
|
|
44
45
|
"scripts": {
|
|
45
46
|
"build": "tsc -p tsconfig.json",
|