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