@gh-symphony/cli 0.0.6 → 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 +28 -14
- package/dist/commands/config-cmd.js +9 -9
- package/dist/commands/help.js +12 -12
- package/dist/commands/init.d.ts +3 -3
- package/dist/commands/init.js +27 -26
- package/dist/commands/logs.js +19 -8
- package/dist/commands/parse-cli-args.d.ts +6 -0
- package/dist/commands/parse-cli-args.js +20 -0
- package/dist/commands/project.js +592 -62
- package/dist/commands/recover.js +17 -14
- package/dist/commands/repo.js +13 -13
- package/dist/commands/run.js +19 -16
- package/dist/commands/start.d.ts +11 -0
- package/dist/commands/start.js +165 -122
- package/dist/commands/status-refresh.d.ts +1 -0
- package/dist/commands/status-refresh.js +7 -1
- package/dist/commands/status.js +48 -40
- package/dist/commands/stop.js +53 -7
- package/dist/completion.d.ts +1 -0
- package/dist/completion.js +204 -0
- package/dist/config.d.ts +18 -16
- package/dist/config.js +29 -19
- package/dist/dashboard/renderer.d.ts +2 -2
- package/dist/dashboard/renderer.js +5 -5
- package/dist/index.d.ts +0 -5
- package/dist/index.js +340 -53
- package/dist/orchestrator-runtime.d.ts +4 -4
- package/dist/orchestrator-runtime.js +11 -11
- package/dist/orchestrator-status-endpoint.d.ts +5 -0
- package/dist/orchestrator-status-endpoint.js +27 -0
- package/dist/project-selection.d.ts +8 -0
- package/dist/project-selection.js +56 -0
- package/dist/skills/types.d.ts +1 -1
- package/package.json +6 -5
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) {
|
|
@@ -9,8 +10,8 @@ function parseRecoverArgs(args) {
|
|
|
9
10
|
if (arg === "--dry-run") {
|
|
10
11
|
parsed.dryRun = true;
|
|
11
12
|
}
|
|
12
|
-
if (arg === "--
|
|
13
|
-
parsed.
|
|
13
|
+
if (arg === "--project" || arg === "--project-id") {
|
|
14
|
+
parsed.projectId = args[i + 1];
|
|
14
15
|
i += 1;
|
|
15
16
|
}
|
|
16
17
|
}
|
|
@@ -18,18 +19,20 @@ function parseRecoverArgs(args) {
|
|
|
18
19
|
}
|
|
19
20
|
const handler = async (args, options) => {
|
|
20
21
|
const parsed = parseRecoverArgs(args);
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
const projectConfig = await resolveManagedProjectConfig({
|
|
23
|
+
configDir: options.configDir,
|
|
24
|
+
requestedProjectId: parsed.projectId,
|
|
25
|
+
});
|
|
26
|
+
if (!projectConfig) {
|
|
27
|
+
handleMissingManagedProjectConfig();
|
|
25
28
|
return;
|
|
26
29
|
}
|
|
27
30
|
const runtimeRoot = resolveRuntimeRoot(options.configDir);
|
|
28
|
-
const
|
|
29
|
-
await
|
|
31
|
+
const projectId = projectConfig.projectId;
|
|
32
|
+
await syncProjectToRuntime(options.configDir, projectConfig);
|
|
30
33
|
if (parsed.dryRun) {
|
|
31
34
|
process.stdout.write("Dry run — scanning for stalled runs...\n");
|
|
32
|
-
const candidates = await listRecoverCandidates(runtimeRoot,
|
|
35
|
+
const candidates = await listRecoverCandidates(runtimeRoot, projectId);
|
|
33
36
|
if (options.json) {
|
|
34
37
|
process.stdout.write(JSON.stringify(candidates, null, 2) + "\n");
|
|
35
38
|
return;
|
|
@@ -48,12 +51,12 @@ const handler = async (args, options) => {
|
|
|
48
51
|
"recover",
|
|
49
52
|
"--runtime-root",
|
|
50
53
|
runtimeRoot,
|
|
51
|
-
"--
|
|
52
|
-
|
|
54
|
+
"--project-id",
|
|
55
|
+
projectId,
|
|
53
56
|
]);
|
|
54
57
|
};
|
|
55
58
|
export default handler;
|
|
56
|
-
async function listRecoverCandidates(runtimeRoot,
|
|
59
|
+
async function listRecoverCandidates(runtimeRoot, projectId) {
|
|
57
60
|
const runsDir = join(runtimeRoot, "orchestrator", "runs");
|
|
58
61
|
const candidates = [];
|
|
59
62
|
let entries = [];
|
|
@@ -68,7 +71,7 @@ async function listRecoverCandidates(runtimeRoot, tenantId) {
|
|
|
68
71
|
try {
|
|
69
72
|
const raw = await readFile(runPath, "utf8");
|
|
70
73
|
const run = JSON.parse(raw);
|
|
71
|
-
if (run.
|
|
74
|
+
if (run.projectId !== projectId) {
|
|
72
75
|
continue;
|
|
73
76
|
}
|
|
74
77
|
const reason = detectRecoveryReason(run);
|
package/dist/commands/repo.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { loadActiveProjectConfig, loadGlobalConfig, saveProjectConfig, } from "../config.js";
|
|
2
2
|
const handler = async (args, options) => {
|
|
3
3
|
const [subcommand, ...rest] = args;
|
|
4
4
|
switch (subcommand) {
|
|
@@ -19,9 +19,9 @@ const handler = async (args, options) => {
|
|
|
19
19
|
export default handler;
|
|
20
20
|
// ── 6.4: repo list / add / remove ────────────────────────────────────────────
|
|
21
21
|
async function repoList(options) {
|
|
22
|
-
const ws = await
|
|
22
|
+
const ws = await loadActiveProjectConfig(options.configDir);
|
|
23
23
|
if (!ws) {
|
|
24
|
-
process.stderr.write("No
|
|
24
|
+
process.stderr.write("No project configured.\n");
|
|
25
25
|
process.exitCode = 1;
|
|
26
26
|
return;
|
|
27
27
|
}
|
|
@@ -42,14 +42,14 @@ async function repoAdd(args, options) {
|
|
|
42
42
|
return;
|
|
43
43
|
}
|
|
44
44
|
const global = await loadGlobalConfig(options.configDir);
|
|
45
|
-
if (!global?.
|
|
46
|
-
process.stderr.write("No active
|
|
45
|
+
if (!global?.activeProject) {
|
|
46
|
+
process.stderr.write("No active project.\n");
|
|
47
47
|
process.exitCode = 1;
|
|
48
48
|
return;
|
|
49
49
|
}
|
|
50
|
-
const ws = await
|
|
50
|
+
const ws = await loadActiveProjectConfig(options.configDir);
|
|
51
51
|
if (!ws) {
|
|
52
|
-
process.stderr.write("
|
|
52
|
+
process.stderr.write("Project config missing.\n");
|
|
53
53
|
process.exitCode = 1;
|
|
54
54
|
return;
|
|
55
55
|
}
|
|
@@ -68,7 +68,7 @@ async function repoAdd(args, options) {
|
|
|
68
68
|
name,
|
|
69
69
|
cloneUrl: `https://github.com/${owner}/${name}.git`,
|
|
70
70
|
});
|
|
71
|
-
await
|
|
71
|
+
await saveProjectConfig(options.configDir, global.activeProject, ws);
|
|
72
72
|
process.stdout.write(`Added repository: ${repoSpec}\n`);
|
|
73
73
|
}
|
|
74
74
|
async function repoRemove(args, options) {
|
|
@@ -79,14 +79,14 @@ async function repoRemove(args, options) {
|
|
|
79
79
|
return;
|
|
80
80
|
}
|
|
81
81
|
const global = await loadGlobalConfig(options.configDir);
|
|
82
|
-
if (!global?.
|
|
83
|
-
process.stderr.write("No active
|
|
82
|
+
if (!global?.activeProject) {
|
|
83
|
+
process.stderr.write("No active project.\n");
|
|
84
84
|
process.exitCode = 1;
|
|
85
85
|
return;
|
|
86
86
|
}
|
|
87
|
-
const ws = await
|
|
87
|
+
const ws = await loadActiveProjectConfig(options.configDir);
|
|
88
88
|
if (!ws) {
|
|
89
|
-
process.stderr.write("
|
|
89
|
+
process.stderr.write("Project config missing.\n");
|
|
90
90
|
process.exitCode = 1;
|
|
91
91
|
return;
|
|
92
92
|
}
|
|
@@ -98,6 +98,6 @@ async function repoRemove(args, options) {
|
|
|
98
98
|
return;
|
|
99
99
|
}
|
|
100
100
|
ws.repositories.splice(idx, 1);
|
|
101
|
-
await
|
|
101
|
+
await saveProjectConfig(options.configDir, global.activeProject, ws);
|
|
102
102
|
process.stdout.write(`Removed repository: ${repoSpec}\n`);
|
|
103
103
|
}
|
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,
|
|
@@ -9,8 +10,8 @@ function parseRunArgs(args) {
|
|
|
9
10
|
if (arg === "--watch" || arg === "-w") {
|
|
10
11
|
parsed.watch = true;
|
|
11
12
|
}
|
|
12
|
-
else if (arg === "--
|
|
13
|
-
parsed.
|
|
13
|
+
else if (arg === "--project" || arg === "--project-id") {
|
|
14
|
+
parsed.projectId = args[i + 1];
|
|
14
15
|
i += 1;
|
|
15
16
|
}
|
|
16
17
|
else if (!arg?.startsWith("--")) {
|
|
@@ -27,21 +28,23 @@ const handler = async (args, options) => {
|
|
|
27
28
|
process.exitCode = 2;
|
|
28
29
|
return;
|
|
29
30
|
}
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
const projectConfig = await resolveManagedProjectConfig({
|
|
32
|
+
configDir: options.configDir,
|
|
33
|
+
requestedProjectId: parsed.projectId,
|
|
34
|
+
});
|
|
35
|
+
if (!projectConfig) {
|
|
36
|
+
handleMissingManagedProjectConfig();
|
|
34
37
|
return;
|
|
35
38
|
}
|
|
36
39
|
const runtimeRoot = resolveRuntimeRoot(options.configDir);
|
|
37
|
-
const
|
|
38
|
-
await
|
|
40
|
+
const projectId = projectConfig.projectId;
|
|
41
|
+
await syncProjectToRuntime(options.configDir, projectConfig);
|
|
39
42
|
// Validate the issue identifier belongs to a configured repo
|
|
40
43
|
const [repoSpec] = parsed.issue.split("#");
|
|
41
44
|
if (repoSpec &&
|
|
42
|
-
!
|
|
43
|
-
process.stderr.write(`Repository "${repoSpec}" is not configured in this
|
|
44
|
-
`Configured repos: ${
|
|
45
|
+
!projectConfig.repositories.some((r) => `${r.owner}/${r.name}` === repoSpec)) {
|
|
46
|
+
process.stderr.write(`Repository "${repoSpec}" is not configured in this project.\n` +
|
|
47
|
+
`Configured repos: ${projectConfig.repositories.map((r) => `${r.owner}/${r.name}`).join(", ")}\n`);
|
|
45
48
|
process.exitCode = 1;
|
|
46
49
|
return;
|
|
47
50
|
}
|
|
@@ -50,8 +53,8 @@ const handler = async (args, options) => {
|
|
|
50
53
|
"run-issue",
|
|
51
54
|
"--runtime-root",
|
|
52
55
|
runtimeRoot,
|
|
53
|
-
"--
|
|
54
|
-
|
|
56
|
+
"--project-id",
|
|
57
|
+
projectId,
|
|
55
58
|
"--issue",
|
|
56
59
|
parsed.issue,
|
|
57
60
|
]);
|
|
@@ -61,8 +64,8 @@ const handler = async (args, options) => {
|
|
|
61
64
|
"status",
|
|
62
65
|
"--runtime-root",
|
|
63
66
|
runtimeRoot,
|
|
64
|
-
"--
|
|
65
|
-
|
|
67
|
+
"--project-id",
|
|
68
|
+
projectId,
|
|
66
69
|
]);
|
|
67
70
|
}
|
|
68
71
|
};
|
package/dist/commands/start.d.ts
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
import { rm } from "node:fs/promises";
|
|
1
2
|
import type { GlobalOptions } from "../index.js";
|
|
3
|
+
type ForegroundShutdownOptions = {
|
|
4
|
+
configDir: string;
|
|
5
|
+
projectId: string;
|
|
6
|
+
statusServer: {
|
|
7
|
+
close(): void;
|
|
8
|
+
};
|
|
9
|
+
exit?: (code?: number) => never;
|
|
10
|
+
removePortFile?: typeof rm;
|
|
11
|
+
};
|
|
2
12
|
declare const handler: (args: string[], options: GlobalOptions) => Promise<void>;
|
|
13
|
+
export declare function shutdownForegroundOrchestrator(input: ForegroundShutdownOptions): Promise<never>;
|
|
3
14
|
export default handler;
|
package/dist/commands/start.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import { writeFile, mkdir, readFile } from "node:fs/promises";
|
|
1
|
+
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
|
-
import {
|
|
4
|
+
import { once } from "node:events";
|
|
5
|
+
import { daemonPidPath, orchestratorLogPath, orchestratorPortPath, } from "../config.js";
|
|
5
6
|
import { OrchestratorService, createStore, startOrchestratorStatusServer, } from "@gh-symphony/orchestrator";
|
|
6
|
-
import {
|
|
7
|
+
import { resolveRuntimeRoot, syncProjectToRuntime, } from "../orchestrator-runtime.js";
|
|
8
|
+
import { handleMissingManagedProjectConfig, resolveManagedProjectConfig, } from "../project-selection.js";
|
|
7
9
|
import { bold, dim, green, red, yellow, cyan, setNoColor } from "../ansi.js";
|
|
8
10
|
import { getGhToken } from "../github/gh-auth.js";
|
|
9
11
|
function timestamp() {
|
|
@@ -18,114 +20,122 @@ function logLine(icon, msg) {
|
|
|
18
20
|
}
|
|
19
21
|
// ── Arg parsing ───────────────────────────────────────────────────────────────
|
|
20
22
|
function parseStartArgs(args) {
|
|
21
|
-
const parsed = {
|
|
23
|
+
const parsed = {
|
|
24
|
+
daemon: false,
|
|
25
|
+
};
|
|
22
26
|
for (let i = 0; i < args.length; i += 1) {
|
|
23
27
|
const arg = args[i];
|
|
24
28
|
if (arg === "--daemon" || arg === "-d") {
|
|
25
29
|
parsed.daemon = true;
|
|
30
|
+
continue;
|
|
26
31
|
}
|
|
27
|
-
if (arg === "--
|
|
28
|
-
|
|
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;
|
|
29
39
|
i += 1;
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (arg?.startsWith("-")) {
|
|
43
|
+
parsed.error = `Unknown option '${arg}'`;
|
|
44
|
+
return parsed;
|
|
30
45
|
}
|
|
31
46
|
}
|
|
32
47
|
return parsed;
|
|
33
48
|
}
|
|
34
49
|
// ── Tick logging ──────────────────────────────────────────────────────────────
|
|
35
|
-
function logTickResult(
|
|
36
|
-
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
?
|
|
41
|
-
:
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
logLine(
|
|
45
|
-
if (snap.summary.activeRuns > 0) {
|
|
46
|
-
logLine(cyan("\u25B8"), `${snap.summary.activeRuns} active run(s)`);
|
|
47
|
-
}
|
|
48
|
-
continue;
|
|
49
|
-
}
|
|
50
|
-
// Health changes
|
|
51
|
-
if (prev && prev.health !== snap.health) {
|
|
52
|
-
const icon = snap.health === "degraded" ? red("\u25CF") : green("\u25CF");
|
|
53
|
-
logLine(icon, `Health changed: ${prev.health} \u2192 ${bold(snap.health)}`);
|
|
54
|
-
}
|
|
55
|
-
// New error
|
|
56
|
-
if (snap.lastError && snap.lastError !== prev?.lastError) {
|
|
57
|
-
logLine(red("\u2717"), red(snap.lastError));
|
|
58
|
-
}
|
|
59
|
-
// Error cleared
|
|
60
|
-
if (!snap.lastError && prev?.lastError) {
|
|
61
|
-
logLine(green("\u2713"), green("Error cleared"));
|
|
62
|
-
}
|
|
63
|
-
// Dispatched delta
|
|
64
|
-
const prevDispatched = prev?.summary.dispatched ?? 0;
|
|
65
|
-
if (snap.summary.dispatched > prevDispatched) {
|
|
66
|
-
const delta = snap.summary.dispatched - prevDispatched;
|
|
67
|
-
logLine(yellow("\u25B8"), `Dispatched ${bold(String(delta))} new run(s)`);
|
|
68
|
-
}
|
|
69
|
-
// Active run changes
|
|
70
|
-
const prevRunIds = new Set(prev?.activeRuns.map((r) => r.runId) ?? []);
|
|
71
|
-
for (const run of snap.activeRuns) {
|
|
72
|
-
if (!prevRunIds.has(run.runId)) {
|
|
73
|
-
logLine(cyan("\u25B8"), `Run started: ${bold(run.issueIdentifier)} ${dim("state=")}${run.issueState} ${dim("status=")}${run.status}`);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
// Completed runs (were active, now gone)
|
|
77
|
-
const currentRunIds = new Set(snap.activeRuns.map((r) => r.runId));
|
|
78
|
-
for (const prevRun of prev?.activeRuns ?? []) {
|
|
79
|
-
if (!currentRunIds.has(prevRun.runId)) {
|
|
80
|
-
logLine(green("\u2713"), `Run finished: ${bold(prevRun.issueIdentifier)} ${dim("(")}${prevRun.status}${dim(")")}`);
|
|
81
|
-
}
|
|
50
|
+
function logTickResult(snapshot, prevSnapshot, isFirst) {
|
|
51
|
+
if (isFirst) {
|
|
52
|
+
const healthColor = snapshot.health === "degraded"
|
|
53
|
+
? red
|
|
54
|
+
: snapshot.health === "running"
|
|
55
|
+
? green
|
|
56
|
+
: cyan;
|
|
57
|
+
logLine(green("\u25CF"), `Project ${bold(snapshot.slug)} connected ${dim("(")}${healthColor(snapshot.health)}${dim(")")}`);
|
|
58
|
+
if (snapshot.summary.activeRuns > 0) {
|
|
59
|
+
logLine(cyan("\u25B8"), `${snapshot.summary.activeRuns} active run(s)`);
|
|
82
60
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (prevSnapshot && prevSnapshot.health !== snapshot.health) {
|
|
64
|
+
const icon = snapshot.health === "degraded" ? red("\u25CF") : green("\u25CF");
|
|
65
|
+
logLine(icon, `Health changed: ${prevSnapshot.health} \u2192 ${bold(snapshot.health)}`);
|
|
66
|
+
}
|
|
67
|
+
if (snapshot.lastError && snapshot.lastError !== prevSnapshot?.lastError) {
|
|
68
|
+
logLine(red("\u2717"), red(snapshot.lastError));
|
|
69
|
+
}
|
|
70
|
+
if (!snapshot.lastError && prevSnapshot?.lastError) {
|
|
71
|
+
logLine(green("\u2713"), green("Error cleared"));
|
|
72
|
+
}
|
|
73
|
+
const prevDispatched = prevSnapshot?.summary.dispatched ?? 0;
|
|
74
|
+
if (snapshot.summary.dispatched > prevDispatched) {
|
|
75
|
+
const delta = snapshot.summary.dispatched - prevDispatched;
|
|
76
|
+
logLine(yellow("\u25B8"), `Dispatched ${bold(String(delta))} new run(s)`);
|
|
77
|
+
}
|
|
78
|
+
const prevRunIds = new Set(prevSnapshot?.activeRuns.map((run) => run.runId) ?? []);
|
|
79
|
+
for (const run of snapshot.activeRuns) {
|
|
80
|
+
if (!prevRunIds.has(run.runId)) {
|
|
81
|
+
logLine(cyan("\u25B8"), `Run started: ${bold(run.issueIdentifier)} ${dim("state=")}${run.issueState} ${dim("status=")}${run.status}`);
|
|
100
82
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
snap.summary.recovered !== prev?.summary.recovered ||
|
|
107
|
-
snap.activeRuns.length !== (prev?.activeRuns.length ?? 0) ||
|
|
108
|
-
snap.retryQueue.length !== (prev?.retryQueue.length ?? 0);
|
|
109
|
-
if (!changed) {
|
|
110
|
-
logLine(dim("\u00B7"), dim(`tick \u2014 ${snap.summary.activeRuns} active, ${snap.health}`));
|
|
83
|
+
}
|
|
84
|
+
const currentRunIds = new Set(snapshot.activeRuns.map((run) => run.runId));
|
|
85
|
+
for (const prevRun of prevSnapshot?.activeRuns ?? []) {
|
|
86
|
+
if (!currentRunIds.has(prevRun.runId)) {
|
|
87
|
+
logLine(green("\u2713"), `Run finished: ${bold(prevRun.issueIdentifier)} ${dim("(")}${prevRun.status}${dim(")")}`);
|
|
111
88
|
}
|
|
112
89
|
}
|
|
90
|
+
const prevSuppressed = prevSnapshot?.summary.suppressed ?? 0;
|
|
91
|
+
if (snapshot.summary.suppressed > prevSuppressed) {
|
|
92
|
+
const delta = snapshot.summary.suppressed - prevSuppressed;
|
|
93
|
+
logLine(dim("\u25CB"), dim(`${delta} issue(s) suppressed (already running or at limit)`));
|
|
94
|
+
}
|
|
95
|
+
const prevRecovered = prevSnapshot?.summary.recovered ?? 0;
|
|
96
|
+
if (snapshot.summary.recovered > prevRecovered) {
|
|
97
|
+
const delta = snapshot.summary.recovered - prevRecovered;
|
|
98
|
+
logLine(yellow("\u21BA"), `Recovered ${bold(String(delta))} stalled run(s)`);
|
|
99
|
+
}
|
|
100
|
+
const prevRetryCount = prevSnapshot?.retryQueue.length ?? 0;
|
|
101
|
+
if (snapshot.retryQueue.length > prevRetryCount) {
|
|
102
|
+
const delta = snapshot.retryQueue.length - prevRetryCount;
|
|
103
|
+
logLine(yellow("\u25CC"), `${delta} run(s) queued for retry`);
|
|
104
|
+
}
|
|
105
|
+
const changed = snapshot.health !== prevSnapshot?.health ||
|
|
106
|
+
snapshot.lastError !== prevSnapshot?.lastError ||
|
|
107
|
+
snapshot.summary.dispatched !== prevSnapshot?.summary.dispatched ||
|
|
108
|
+
snapshot.summary.suppressed !== prevSnapshot?.summary.suppressed ||
|
|
109
|
+
snapshot.summary.recovered !== prevSnapshot?.summary.recovered ||
|
|
110
|
+
snapshot.activeRuns.length !== (prevSnapshot?.activeRuns.length ?? 0) ||
|
|
111
|
+
snapshot.retryQueue.length !== (prevSnapshot?.retryQueue.length ?? 0);
|
|
112
|
+
if (!changed) {
|
|
113
|
+
logLine(dim("\u00B7"), dim(`tick \u2014 ${snapshot.summary.activeRuns} active, ${snapshot.health}`));
|
|
114
|
+
}
|
|
113
115
|
}
|
|
114
116
|
// ── Handler ───────────────────────────────────────────────────────────────────
|
|
115
117
|
const handler = async (args, options) => {
|
|
116
118
|
setNoColor(options.noColor);
|
|
117
119
|
const parsed = parseStartArgs(args);
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
process.stderr.write("
|
|
121
|
-
process.exitCode =
|
|
120
|
+
if (parsed.error) {
|
|
121
|
+
process.stderr.write(`${parsed.error}\n`);
|
|
122
|
+
process.stderr.write("Usage: gh-symphony start --project-id <project-id> [--daemon]\n");
|
|
123
|
+
process.exitCode = 2;
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
const projectConfig = await resolveManagedProjectConfig({
|
|
127
|
+
configDir: options.configDir,
|
|
128
|
+
requestedProjectId: parsed.projectId,
|
|
129
|
+
});
|
|
130
|
+
if (!projectConfig) {
|
|
131
|
+
handleMissingManagedProjectConfig();
|
|
122
132
|
return;
|
|
123
133
|
}
|
|
124
134
|
const runtimeRoot = resolveRuntimeRoot(options.configDir);
|
|
125
|
-
const
|
|
126
|
-
await
|
|
135
|
+
const projectId = projectConfig.projectId;
|
|
136
|
+
await syncProjectToRuntime(options.configDir, projectConfig);
|
|
127
137
|
if (parsed.daemon) {
|
|
128
|
-
await startDaemon(options,
|
|
138
|
+
await startDaemon(options, projectId);
|
|
129
139
|
return;
|
|
130
140
|
}
|
|
131
141
|
// ── 5.1: Foreground mode with live logging ────────────────────────────────
|
|
@@ -139,50 +149,53 @@ const handler = async (args, options) => {
|
|
|
139
149
|
}
|
|
140
150
|
}
|
|
141
151
|
const store = createStore(runtimeRoot);
|
|
142
|
-
const service = new OrchestratorService(store);
|
|
143
|
-
|
|
144
|
-
startOrchestratorStatusServer({
|
|
152
|
+
const service = new OrchestratorService(store, projectConfig);
|
|
153
|
+
const statusServer = startOrchestratorStatusServer({
|
|
145
154
|
host: "127.0.0.1",
|
|
146
|
-
port:
|
|
147
|
-
|
|
148
|
-
all: () => service.status(),
|
|
149
|
-
byTenantId: async (id) => {
|
|
150
|
-
const [snapshot] = await service.status(id);
|
|
151
|
-
return snapshot ?? null;
|
|
152
|
-
},
|
|
153
|
-
},
|
|
155
|
+
port: 0,
|
|
156
|
+
getProjectStatus: () => service.status(),
|
|
154
157
|
onRefresh: async () => {
|
|
155
|
-
await service.runOnce(
|
|
158
|
+
await service.runOnce();
|
|
156
159
|
},
|
|
157
160
|
});
|
|
158
|
-
|
|
161
|
+
await persistStatusServerPort(options.configDir, projectId, statusServer);
|
|
162
|
+
logLine(green("\u25B2"), `Starting orchestrator for project: ${bold(projectId)}`);
|
|
159
163
|
logLine(dim("\u00B7"), dim("Press Ctrl+C to stop"));
|
|
160
164
|
let running = true;
|
|
161
|
-
|
|
165
|
+
let shuttingDown = false;
|
|
166
|
+
const shutdown = async () => {
|
|
167
|
+
if (shuttingDown) {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
shuttingDown = true;
|
|
162
171
|
running = false;
|
|
163
|
-
|
|
164
|
-
|
|
172
|
+
await shutdownForegroundOrchestrator({
|
|
173
|
+
configDir: options.configDir,
|
|
174
|
+
projectId,
|
|
175
|
+
statusServer,
|
|
176
|
+
});
|
|
165
177
|
};
|
|
166
|
-
process.on("SIGINT",
|
|
167
|
-
|
|
168
|
-
|
|
178
|
+
process.on("SIGINT", () => {
|
|
179
|
+
void shutdown();
|
|
180
|
+
});
|
|
181
|
+
process.on("SIGTERM", () => {
|
|
182
|
+
void shutdown();
|
|
183
|
+
});
|
|
184
|
+
let prevSnapshot = null;
|
|
169
185
|
let isFirst = true;
|
|
170
186
|
while (running) {
|
|
171
187
|
try {
|
|
172
|
-
const
|
|
173
|
-
logTickResult(
|
|
188
|
+
const snapshot = await service.runOnce();
|
|
189
|
+
logTickResult(snapshot, prevSnapshot, isFirst);
|
|
174
190
|
if (!isFirst) {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
if (!currentRunIds.has(prevRun.runId)) {
|
|
180
|
-
await tailWorkerLog(runtimeRoot, prevRun.runId, prevRun.issueIdentifier);
|
|
181
|
-
}
|
|
191
|
+
const currentRunIds = new Set(snapshot.activeRuns.map((run) => run.runId));
|
|
192
|
+
for (const prevRun of prevSnapshot?.activeRuns ?? []) {
|
|
193
|
+
if (!currentRunIds.has(prevRun.runId)) {
|
|
194
|
+
await tailWorkerLog(runtimeRoot, prevRun.runId, prevRun.issueIdentifier);
|
|
182
195
|
}
|
|
183
196
|
}
|
|
184
197
|
}
|
|
185
|
-
|
|
198
|
+
prevSnapshot = snapshot;
|
|
186
199
|
isFirst = false;
|
|
187
200
|
}
|
|
188
201
|
catch (error) {
|
|
@@ -192,6 +205,24 @@ const handler = async (args, options) => {
|
|
|
192
205
|
await new Promise((r) => setTimeout(r, 30_000));
|
|
193
206
|
}
|
|
194
207
|
};
|
|
208
|
+
export async function shutdownForegroundOrchestrator(input) {
|
|
209
|
+
logLine(yellow("\u25BC"), "Shutting down...");
|
|
210
|
+
try {
|
|
211
|
+
input.statusServer.close();
|
|
212
|
+
}
|
|
213
|
+
catch (error) {
|
|
214
|
+
logLine(red("\u2717"), red(`Failed to close status server: ${error instanceof Error ? error.message : "Unknown error"}`));
|
|
215
|
+
}
|
|
216
|
+
try {
|
|
217
|
+
await (input.removePortFile ?? rm)(orchestratorPortPath(input.configDir, input.projectId), {
|
|
218
|
+
force: true,
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
catch (error) {
|
|
222
|
+
logLine(yellow("\u26A0"), `Failed to remove persisted status port: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
223
|
+
}
|
|
224
|
+
return (input.exit ?? process.exit)(0);
|
|
225
|
+
}
|
|
195
226
|
async function tailWorkerLog(runtimeRoot, runId, issueIdentifier) {
|
|
196
227
|
try {
|
|
197
228
|
const logPath = join(runtimeRoot, "orchestrator", "runs", runId, "worker.log");
|
|
@@ -211,12 +242,12 @@ async function tailWorkerLog(runtimeRoot, runId, issueIdentifier) {
|
|
|
211
242
|
}
|
|
212
243
|
export default handler;
|
|
213
244
|
// ── 5.2: Daemon mode ─────────────────────────────────────────────────────────
|
|
214
|
-
async function startDaemon(options,
|
|
215
|
-
const logPath = orchestratorLogPath(options.configDir);
|
|
216
|
-
await mkdir(
|
|
245
|
+
async function startDaemon(options, projectId) {
|
|
246
|
+
const logPath = orchestratorLogPath(options.configDir, projectId);
|
|
247
|
+
await mkdir(dirname(logPath), { recursive: true });
|
|
217
248
|
const { openSync } = await import("node:fs");
|
|
218
249
|
const logFd = openSync(logPath, "a");
|
|
219
|
-
const child = spawn(process.execPath, [process.argv[1], "start", "--
|
|
250
|
+
const child = spawn(process.execPath, [process.argv[1], "start", "--project", projectId], {
|
|
220
251
|
cwd: process.cwd(),
|
|
221
252
|
env: {
|
|
222
253
|
...process.env,
|
|
@@ -225,7 +256,7 @@ async function startDaemon(options, tenantId) {
|
|
|
225
256
|
detached: true,
|
|
226
257
|
stdio: ["ignore", logFd, logFd],
|
|
227
258
|
});
|
|
228
|
-
const pidPath = daemonPidPath(options.configDir);
|
|
259
|
+
const pidPath = daemonPidPath(options.configDir, projectId);
|
|
229
260
|
await mkdir(dirname(pidPath), { recursive: true });
|
|
230
261
|
await writeFile(pidPath, String(child.pid), "utf8");
|
|
231
262
|
child.unref();
|
|
@@ -233,5 +264,17 @@ async function startDaemon(options, tenantId) {
|
|
|
233
264
|
closeSync(logFd);
|
|
234
265
|
process.stdout.write(`Orchestrator started in background (PID: ${child.pid}).\n` +
|
|
235
266
|
`Logs: ${logPath}\n` +
|
|
236
|
-
`Stop with: gh-symphony stop\n`);
|
|
267
|
+
`Stop with: gh-symphony project stop --project-id ${projectId}\n`);
|
|
268
|
+
}
|
|
269
|
+
async function persistStatusServerPort(configDir, projectId, statusServer) {
|
|
270
|
+
if (!statusServer.listening) {
|
|
271
|
+
await once(statusServer, "listening");
|
|
272
|
+
}
|
|
273
|
+
const address = statusServer.address();
|
|
274
|
+
if (!address || typeof address !== "object") {
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
const portPath = orchestratorPortPath(configDir, projectId);
|
|
278
|
+
await mkdir(dirname(portPath), { recursive: true });
|
|
279
|
+
await writeFile(portPath, `${address.port}\n`, "utf8");
|
|
237
280
|
}
|
|
@@ -2,6 +2,7 @@ type RefreshRequestOptions = {
|
|
|
2
2
|
fetchImpl?: typeof fetch;
|
|
3
3
|
timeoutMs?: number;
|
|
4
4
|
env?: NodeJS.ProcessEnv;
|
|
5
|
+
baseUrl?: string | null;
|
|
5
6
|
};
|
|
6
7
|
export declare function resolveOrchestratorStatusBaseUrl(env?: NodeJS.ProcessEnv): string;
|
|
7
8
|
export declare function requestOrchestratorRefresh(options?: RefreshRequestOptions): Promise<boolean>;
|
|
@@ -8,8 +8,14 @@ export async function requestOrchestratorRefresh(options = {}) {
|
|
|
8
8
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
9
9
|
const timeoutMs = options.timeoutMs ?? 5_000;
|
|
10
10
|
const signal = AbortSignal.timeout(timeoutMs);
|
|
11
|
+
const baseUrl = "baseUrl" in options
|
|
12
|
+
? options.baseUrl
|
|
13
|
+
: resolveOrchestratorStatusBaseUrl(options.env);
|
|
14
|
+
if (!baseUrl) {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
11
17
|
try {
|
|
12
|
-
const response = await fetchImpl(`${
|
|
18
|
+
const response = await fetchImpl(`${baseUrl}/api/v1/refresh`, {
|
|
13
19
|
method: "POST",
|
|
14
20
|
signal,
|
|
15
21
|
});
|