@gh-symphony/cli 0.0.14 → 0.0.15
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/dist/chunk-5NV3LSAJ.js +11 -0
- package/dist/chunk-6HBZC3BE.js +468 -0
- package/dist/chunk-76QPITKI.js +109 -0
- package/dist/chunk-IWR4UQEJ.js +2250 -0
- package/dist/chunk-JO3AXHQI.js +130 -0
- package/dist/chunk-M7OSMUTN.js +874 -0
- package/dist/chunk-MVRF7BES.js +68 -0
- package/dist/chunk-RNWX7DQU.js +4617 -0
- package/dist/chunk-ROGRTUFI.js +108 -0
- package/dist/chunk-TH5QPO3Y.js +67 -0
- package/dist/config-cmd-AZ7POMAA.js +110 -0
- package/dist/index.d.ts +5 -4
- package/dist/index.js +568 -356
- package/dist/init-EZXQAXZM.js +17 -0
- package/dist/logs-6LNGT2GF.js +188 -0
- package/dist/project-3ELXQ35D.js +678 -0
- package/dist/recover-T6ME6C56.js +130 -0
- package/dist/repo-R3XBIVAX.js +121 -0
- package/dist/run-DYINRZHK.js +107 -0
- package/dist/start-PIFQMIC2.js +15 -0
- package/dist/status-3WK5BWRZ.js +11 -0
- package/dist/stop-AA3AP5M6.js +9 -0
- package/dist/version-VBB62JWI.js +30 -0
- package/package.json +9 -4
- package/dist/ansi.d.ts +0 -15
- package/dist/ansi.js +0 -53
- package/dist/commands/config-cmd.d.ts +0 -3
- package/dist/commands/config-cmd.js +0 -90
- package/dist/commands/help.d.ts +0 -3
- package/dist/commands/help.js +0 -55
- package/dist/commands/init.d.ts +0 -34
- package/dist/commands/init.js +0 -477
- package/dist/commands/logs.d.ts +0 -3
- package/dist/commands/logs.js +0 -184
- package/dist/commands/project.d.ts +0 -3
- package/dist/commands/project.js +0 -649
- package/dist/commands/recover.d.ts +0 -3
- package/dist/commands/recover.js +0 -119
- package/dist/commands/repo.d.ts +0 -3
- package/dist/commands/repo.js +0 -103
- package/dist/commands/run.d.ts +0 -3
- package/dist/commands/run.js +0 -95
- package/dist/commands/start.d.ts +0 -20
- package/dist/commands/start.js +0 -344
- package/dist/commands/status-refresh.d.ts +0 -9
- package/dist/commands/status-refresh.js +0 -27
- package/dist/commands/status.d.ts +0 -3
- package/dist/commands/status.js +0 -237
- package/dist/commands/stop.d.ts +0 -3
- package/dist/commands/stop.js +0 -92
- package/dist/commands/version.d.ts +0 -3
- package/dist/commands/version.js +0 -21
- package/dist/completion.d.ts +0 -1
- package/dist/completion.js +0 -204
- package/dist/config.d.ts +0 -38
- package/dist/config.js +0 -82
- package/dist/context/context-types.d.ts +0 -36
- package/dist/context/context-types.js +0 -1
- package/dist/context/generate-context-yaml.d.ts +0 -15
- package/dist/context/generate-context-yaml.js +0 -129
- package/dist/dashboard/renderer.d.ts +0 -9
- package/dist/dashboard/renderer.js +0 -220
- package/dist/detection/environment-detector.d.ts +0 -11
- package/dist/detection/environment-detector.js +0 -140
- package/dist/github/client.d.ts +0 -71
- package/dist/github/client.js +0 -348
- package/dist/github/gh-auth.d.ts +0 -34
- package/dist/github/gh-auth.js +0 -110
- package/dist/mapping/smart-defaults.d.ts +0 -17
- package/dist/mapping/smart-defaults.js +0 -86
- package/dist/orchestrator-runtime.d.ts +0 -1
- package/dist/orchestrator-runtime.js +0 -4
- package/dist/orchestrator-status-endpoint.d.ts +0 -5
- package/dist/orchestrator-status-endpoint.js +0 -27
- package/dist/project-selection.d.ts +0 -8
- package/dist/project-selection.js +0 -56
- package/dist/skills/skill-writer.d.ts +0 -14
- package/dist/skills/skill-writer.js +0 -62
- package/dist/skills/templates/commit.d.ts +0 -2
- package/dist/skills/templates/commit.js +0 -45
- package/dist/skills/templates/document.d.ts +0 -7
- package/dist/skills/templates/document.js +0 -16
- package/dist/skills/templates/gh-project.d.ts +0 -2
- package/dist/skills/templates/gh-project.js +0 -88
- package/dist/skills/templates/gh-symphony.d.ts +0 -2
- package/dist/skills/templates/gh-symphony.js +0 -125
- package/dist/skills/templates/index.d.ts +0 -8
- package/dist/skills/templates/index.js +0 -28
- package/dist/skills/templates/land.d.ts +0 -2
- package/dist/skills/templates/land.js +0 -59
- package/dist/skills/templates/pull.d.ts +0 -2
- package/dist/skills/templates/pull.js +0 -41
- package/dist/skills/templates/push.d.ts +0 -2
- package/dist/skills/templates/push.js +0 -36
- package/dist/skills/types.d.ts +0 -23
- package/dist/skills/types.js +0 -1
- package/dist/workflow/generate-reference-workflow.d.ts +0 -9
- package/dist/workflow/generate-reference-workflow.js +0 -261
- package/dist/workflow/generate-workflow-md.d.ts +0 -12
- package/dist/workflow/generate-workflow-md.js +0 -134
package/dist/commands/recover.js
DELETED
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
import { readFile, readdir } from "node:fs/promises";
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
import { runCli as orchestratorRunCli } from "@gh-symphony/orchestrator";
|
|
4
|
-
import { resolveRuntimeRoot, } from "../orchestrator-runtime.js";
|
|
5
|
-
import { handleMissingManagedProjectConfig, resolveManagedProjectConfig, } from "../project-selection.js";
|
|
6
|
-
function parseRecoverArgs(args) {
|
|
7
|
-
const parsed = { dryRun: false };
|
|
8
|
-
for (let i = 0; i < args.length; i += 1) {
|
|
9
|
-
const arg = args[i];
|
|
10
|
-
if (arg === "--dry-run") {
|
|
11
|
-
parsed.dryRun = true;
|
|
12
|
-
}
|
|
13
|
-
if (arg === "--project" || arg === "--project-id") {
|
|
14
|
-
parsed.projectId = args[i + 1];
|
|
15
|
-
i += 1;
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
return parsed;
|
|
19
|
-
}
|
|
20
|
-
const handler = async (args, options) => {
|
|
21
|
-
const parsed = parseRecoverArgs(args);
|
|
22
|
-
const projectConfig = await resolveManagedProjectConfig({
|
|
23
|
-
configDir: options.configDir,
|
|
24
|
-
requestedProjectId: parsed.projectId,
|
|
25
|
-
});
|
|
26
|
-
if (!projectConfig) {
|
|
27
|
-
handleMissingManagedProjectConfig();
|
|
28
|
-
return;
|
|
29
|
-
}
|
|
30
|
-
const runtimeRoot = resolveRuntimeRoot(options.configDir);
|
|
31
|
-
const projectId = projectConfig.projectId;
|
|
32
|
-
if (parsed.dryRun) {
|
|
33
|
-
process.stdout.write("Dry run — scanning for stalled runs...\n");
|
|
34
|
-
const candidates = await listRecoverCandidates(runtimeRoot, projectId);
|
|
35
|
-
if (options.json) {
|
|
36
|
-
process.stdout.write(JSON.stringify(candidates, null, 2) + "\n");
|
|
37
|
-
return;
|
|
38
|
-
}
|
|
39
|
-
if (candidates.length === 0) {
|
|
40
|
-
process.stdout.write("No recoverable runs found.\n");
|
|
41
|
-
return;
|
|
42
|
-
}
|
|
43
|
-
for (const candidate of candidates) {
|
|
44
|
-
process.stdout.write(`${candidate.issueIdentifier} (${candidate.runId}) — ${candidate.reason}\n`);
|
|
45
|
-
}
|
|
46
|
-
return;
|
|
47
|
-
}
|
|
48
|
-
process.stdout.write("Recovering stalled runs...\n");
|
|
49
|
-
await orchestratorRunCli([
|
|
50
|
-
"recover",
|
|
51
|
-
"--runtime-root",
|
|
52
|
-
runtimeRoot,
|
|
53
|
-
"--project-id",
|
|
54
|
-
projectId,
|
|
55
|
-
]);
|
|
56
|
-
};
|
|
57
|
-
export default handler;
|
|
58
|
-
async function listRecoverCandidates(runtimeRoot, projectId) {
|
|
59
|
-
const runsDir = join(runtimeRoot, "projects", projectId, "runs");
|
|
60
|
-
const candidates = [];
|
|
61
|
-
let entries = [];
|
|
62
|
-
try {
|
|
63
|
-
entries = await readdir(runsDir);
|
|
64
|
-
}
|
|
65
|
-
catch {
|
|
66
|
-
return candidates;
|
|
67
|
-
}
|
|
68
|
-
for (const entry of entries) {
|
|
69
|
-
const runPath = join(runsDir, entry, "run.json");
|
|
70
|
-
try {
|
|
71
|
-
const raw = await readFile(runPath, "utf8");
|
|
72
|
-
const run = JSON.parse(raw);
|
|
73
|
-
if (run.projectId !== projectId) {
|
|
74
|
-
continue;
|
|
75
|
-
}
|
|
76
|
-
const reason = detectRecoveryReason(run);
|
|
77
|
-
if (!reason) {
|
|
78
|
-
continue;
|
|
79
|
-
}
|
|
80
|
-
candidates.push({
|
|
81
|
-
runId: run.runId,
|
|
82
|
-
issueIdentifier: run.issueIdentifier,
|
|
83
|
-
status: run.status,
|
|
84
|
-
reason,
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
catch {
|
|
88
|
-
// Skip malformed or partial run records.
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
return candidates;
|
|
92
|
-
}
|
|
93
|
-
function detectRecoveryReason(run) {
|
|
94
|
-
if (run.processId) {
|
|
95
|
-
const startedAt = run.startedAt ? new Date(run.startedAt).getTime() : 0;
|
|
96
|
-
const runningForMs = Date.now() - startedAt;
|
|
97
|
-
if (isProcessRunning(run.processId) && runningForMs > 30 * 60 * 1000) {
|
|
98
|
-
return "worker appears stuck";
|
|
99
|
-
}
|
|
100
|
-
if (!isProcessRunning(run.processId)) {
|
|
101
|
-
return "worker process is no longer running";
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
if (run.status === "retrying" &&
|
|
105
|
-
run.nextRetryAt &&
|
|
106
|
-
new Date(run.nextRetryAt).getTime() <= Date.now()) {
|
|
107
|
-
return "retry window has elapsed";
|
|
108
|
-
}
|
|
109
|
-
return null;
|
|
110
|
-
}
|
|
111
|
-
function isProcessRunning(pid) {
|
|
112
|
-
try {
|
|
113
|
-
process.kill(pid, 0);
|
|
114
|
-
return true;
|
|
115
|
-
}
|
|
116
|
-
catch {
|
|
117
|
-
return false;
|
|
118
|
-
}
|
|
119
|
-
}
|
package/dist/commands/repo.d.ts
DELETED
package/dist/commands/repo.js
DELETED
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
import { loadActiveProjectConfig, loadGlobalConfig, saveProjectConfig, } from "../config.js";
|
|
2
|
-
const handler = async (args, options) => {
|
|
3
|
-
const [subcommand, ...rest] = args;
|
|
4
|
-
switch (subcommand) {
|
|
5
|
-
case "list":
|
|
6
|
-
await repoList(options);
|
|
7
|
-
break;
|
|
8
|
-
case "add":
|
|
9
|
-
await repoAdd(rest, options);
|
|
10
|
-
break;
|
|
11
|
-
case "remove":
|
|
12
|
-
await repoRemove(rest, options);
|
|
13
|
-
break;
|
|
14
|
-
default:
|
|
15
|
-
process.stderr.write("Usage: gh-symphony repo <list|add|remove> [repo]\n");
|
|
16
|
-
process.exitCode = 2;
|
|
17
|
-
}
|
|
18
|
-
};
|
|
19
|
-
export default handler;
|
|
20
|
-
// ── 6.4: repo list / add / remove ────────────────────────────────────────────
|
|
21
|
-
async function repoList(options) {
|
|
22
|
-
const ws = await loadActiveProjectConfig(options.configDir);
|
|
23
|
-
if (!ws) {
|
|
24
|
-
process.stderr.write("No project configured.\n");
|
|
25
|
-
process.exitCode = 1;
|
|
26
|
-
return;
|
|
27
|
-
}
|
|
28
|
-
if (options.json) {
|
|
29
|
-
process.stdout.write(JSON.stringify(ws.repositories, null, 2) + "\n");
|
|
30
|
-
return;
|
|
31
|
-
}
|
|
32
|
-
process.stdout.write("Repositories:\n");
|
|
33
|
-
for (const repo of ws.repositories) {
|
|
34
|
-
process.stdout.write(` ${repo.owner}/${repo.name}\n`);
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
async function repoAdd(args, options) {
|
|
38
|
-
const [repoSpec] = args;
|
|
39
|
-
if (!repoSpec || !repoSpec.includes("/")) {
|
|
40
|
-
process.stderr.write("Usage: gh-symphony repo add <owner/name>\n");
|
|
41
|
-
process.exitCode = 2;
|
|
42
|
-
return;
|
|
43
|
-
}
|
|
44
|
-
const global = await loadGlobalConfig(options.configDir);
|
|
45
|
-
if (!global?.activeProject) {
|
|
46
|
-
process.stderr.write("No active project.\n");
|
|
47
|
-
process.exitCode = 1;
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
const ws = await loadActiveProjectConfig(options.configDir);
|
|
51
|
-
if (!ws) {
|
|
52
|
-
process.stderr.write("Project config missing.\n");
|
|
53
|
-
process.exitCode = 1;
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
const [owner, name] = repoSpec.split("/");
|
|
57
|
-
if (!owner || !name) {
|
|
58
|
-
process.stderr.write("Invalid repo format. Use: owner/name\n");
|
|
59
|
-
process.exitCode = 2;
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
if (ws.repositories.some((r) => r.owner === owner && r.name === name)) {
|
|
63
|
-
process.stdout.write(`Repository ${repoSpec} is already configured.\n`);
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
ws.repositories.push({
|
|
67
|
-
owner,
|
|
68
|
-
name,
|
|
69
|
-
cloneUrl: `https://github.com/${owner}/${name}.git`,
|
|
70
|
-
});
|
|
71
|
-
await saveProjectConfig(options.configDir, global.activeProject, ws);
|
|
72
|
-
process.stdout.write(`Added repository: ${repoSpec}\n`);
|
|
73
|
-
}
|
|
74
|
-
async function repoRemove(args, options) {
|
|
75
|
-
const [repoSpec] = args;
|
|
76
|
-
if (!repoSpec || !repoSpec.includes("/")) {
|
|
77
|
-
process.stderr.write("Usage: gh-symphony repo remove <owner/name>\n");
|
|
78
|
-
process.exitCode = 2;
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
const global = await loadGlobalConfig(options.configDir);
|
|
82
|
-
if (!global?.activeProject) {
|
|
83
|
-
process.stderr.write("No active project.\n");
|
|
84
|
-
process.exitCode = 1;
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
|
-
const ws = await loadActiveProjectConfig(options.configDir);
|
|
88
|
-
if (!ws) {
|
|
89
|
-
process.stderr.write("Project config missing.\n");
|
|
90
|
-
process.exitCode = 1;
|
|
91
|
-
return;
|
|
92
|
-
}
|
|
93
|
-
const [owner, name] = repoSpec.split("/");
|
|
94
|
-
const idx = ws.repositories.findIndex((r) => r.owner === owner && r.name === name);
|
|
95
|
-
if (idx === -1) {
|
|
96
|
-
process.stderr.write(`Repository ${repoSpec} is not configured.\n`);
|
|
97
|
-
process.exitCode = 1;
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
ws.repositories.splice(idx, 1);
|
|
101
|
-
await saveProjectConfig(options.configDir, global.activeProject, ws);
|
|
102
|
-
process.stdout.write(`Removed repository: ${repoSpec}\n`);
|
|
103
|
-
}
|
package/dist/commands/run.d.ts
DELETED
package/dist/commands/run.js
DELETED
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
import { runCli as orchestratorRunCli } from "@gh-symphony/orchestrator";
|
|
2
|
-
import { resolveRuntimeRoot, } from "../orchestrator-runtime.js";
|
|
3
|
-
import { handleMissingManagedProjectConfig, resolveManagedProjectConfig, } from "../project-selection.js";
|
|
4
|
-
function parseRunArgs(args) {
|
|
5
|
-
const parsed = {
|
|
6
|
-
watch: false,
|
|
7
|
-
};
|
|
8
|
-
for (let i = 0; i < args.length; i += 1) {
|
|
9
|
-
const arg = args[i];
|
|
10
|
-
if (arg === "--watch" || arg === "-w") {
|
|
11
|
-
parsed.watch = true;
|
|
12
|
-
}
|
|
13
|
-
else if (arg === "--project" || arg === "--project-id") {
|
|
14
|
-
const value = args[i + 1];
|
|
15
|
-
if (!value || value.startsWith("-")) {
|
|
16
|
-
parsed.error = `Option '${arg}' argument missing`;
|
|
17
|
-
return parsed;
|
|
18
|
-
}
|
|
19
|
-
parsed.projectId = value;
|
|
20
|
-
i += 1;
|
|
21
|
-
}
|
|
22
|
-
else if (arg === "--log-level") {
|
|
23
|
-
const value = args[i + 1];
|
|
24
|
-
if (!value || value.startsWith("-")) {
|
|
25
|
-
parsed.error = `Option '${arg}' argument missing`;
|
|
26
|
-
return parsed;
|
|
27
|
-
}
|
|
28
|
-
parsed.logLevel = value;
|
|
29
|
-
i += 1;
|
|
30
|
-
}
|
|
31
|
-
else if (!arg?.startsWith("-")) {
|
|
32
|
-
// Positional arg = issue identifier
|
|
33
|
-
parsed.issue = arg;
|
|
34
|
-
}
|
|
35
|
-
else {
|
|
36
|
-
parsed.error = `Unknown option '${arg}'`;
|
|
37
|
-
return parsed;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
return parsed;
|
|
41
|
-
}
|
|
42
|
-
const handler = async (args, options) => {
|
|
43
|
-
const parsed = parseRunArgs(args);
|
|
44
|
-
if (parsed.error) {
|
|
45
|
-
process.stderr.write(`${parsed.error}\n`);
|
|
46
|
-
process.exitCode = 2;
|
|
47
|
-
return;
|
|
48
|
-
}
|
|
49
|
-
if (!parsed.issue) {
|
|
50
|
-
process.stderr.write("Usage: gh-symphony run <owner/repo#number>\n");
|
|
51
|
-
process.exitCode = 2;
|
|
52
|
-
return;
|
|
53
|
-
}
|
|
54
|
-
const projectConfig = await resolveManagedProjectConfig({
|
|
55
|
-
configDir: options.configDir,
|
|
56
|
-
requestedProjectId: parsed.projectId,
|
|
57
|
-
});
|
|
58
|
-
if (!projectConfig) {
|
|
59
|
-
handleMissingManagedProjectConfig();
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
const runtimeRoot = resolveRuntimeRoot(options.configDir);
|
|
63
|
-
const projectId = projectConfig.projectId;
|
|
64
|
-
// Validate the issue identifier belongs to a configured repo
|
|
65
|
-
const [repoSpec] = parsed.issue.split("#");
|
|
66
|
-
if (repoSpec &&
|
|
67
|
-
!projectConfig.repositories.some((r) => `${r.owner}/${r.name}` === repoSpec)) {
|
|
68
|
-
process.stderr.write(`Repository "${repoSpec}" is not configured in this project.\n` +
|
|
69
|
-
`Configured repos: ${projectConfig.repositories.map((r) => `${r.owner}/${r.name}`).join(", ")}\n`);
|
|
70
|
-
process.exitCode = 1;
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
73
|
-
process.stdout.write(`Dispatching issue: ${parsed.issue}\n`);
|
|
74
|
-
await orchestratorRunCli([
|
|
75
|
-
"run-issue",
|
|
76
|
-
"--runtime-root",
|
|
77
|
-
runtimeRoot,
|
|
78
|
-
"--project-id",
|
|
79
|
-
projectId,
|
|
80
|
-
"--issue",
|
|
81
|
-
parsed.issue,
|
|
82
|
-
...(parsed.logLevel ? ["--log-level", parsed.logLevel] : []),
|
|
83
|
-
]);
|
|
84
|
-
if (parsed.watch) {
|
|
85
|
-
process.stdout.write("\nWatching for status changes...\n");
|
|
86
|
-
await orchestratorRunCli([
|
|
87
|
-
"status",
|
|
88
|
-
"--runtime-root",
|
|
89
|
-
runtimeRoot,
|
|
90
|
-
"--project-id",
|
|
91
|
-
projectId,
|
|
92
|
-
]);
|
|
93
|
-
}
|
|
94
|
-
};
|
|
95
|
-
export default handler;
|
package/dist/commands/start.d.ts
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { rm } from "node:fs/promises";
|
|
2
|
-
import type { GlobalOptions } from "../index.js";
|
|
3
|
-
import { releaseProjectLock, type ProjectLockHandle } from "@gh-symphony/orchestrator";
|
|
4
|
-
type ForegroundShutdownOptions = {
|
|
5
|
-
configDir: string;
|
|
6
|
-
projectId: string;
|
|
7
|
-
statusServer: {
|
|
8
|
-
close(): void;
|
|
9
|
-
};
|
|
10
|
-
projectLock?: ProjectLockHandle | null;
|
|
11
|
-
service?: {
|
|
12
|
-
shutdown(): Promise<void>;
|
|
13
|
-
};
|
|
14
|
-
exit?: (code?: number) => never;
|
|
15
|
-
removePortFile?: typeof rm;
|
|
16
|
-
releaseLock?: typeof releaseProjectLock;
|
|
17
|
-
};
|
|
18
|
-
declare const handler: (args: string[], options: GlobalOptions) => Promise<void>;
|
|
19
|
-
export declare function shutdownForegroundOrchestrator(input: ForegroundShutdownOptions): Promise<never>;
|
|
20
|
-
export default handler;
|
package/dist/commands/start.js
DELETED
|
@@ -1,344 +0,0 @@
|
|
|
1
|
-
import { writeFile, mkdir, readFile, rm } from "node:fs/promises";
|
|
2
|
-
import { dirname, join } from "node:path";
|
|
3
|
-
import { spawn } from "node:child_process";
|
|
4
|
-
import { once } from "node:events";
|
|
5
|
-
import { daemonPidPath, orchestratorLogPath, orchestratorPortPath, } from "../config.js";
|
|
6
|
-
import { OrchestratorService, acquireProjectLock, createStore, releaseProjectLock, resolveOrchestratorLogLevel, startOrchestratorStatusServer, } from "@gh-symphony/orchestrator";
|
|
7
|
-
import { resolveRuntimeRoot, } from "../orchestrator-runtime.js";
|
|
8
|
-
import { handleMissingManagedProjectConfig, resolveManagedProjectConfig, } from "../project-selection.js";
|
|
9
|
-
import { bold, dim, green, red, yellow, cyan, setNoColor } from "../ansi.js";
|
|
10
|
-
import { getGhToken } from "../github/gh-auth.js";
|
|
11
|
-
function timestamp() {
|
|
12
|
-
const now = new Date();
|
|
13
|
-
const hh = String(now.getHours()).padStart(2, "0");
|
|
14
|
-
const mm = String(now.getMinutes()).padStart(2, "0");
|
|
15
|
-
const ss = String(now.getSeconds()).padStart(2, "0");
|
|
16
|
-
return dim(`${hh}:${mm}:${ss}`);
|
|
17
|
-
}
|
|
18
|
-
function logLine(icon, msg) {
|
|
19
|
-
process.stdout.write(`${timestamp()} ${icon} ${msg}\n`);
|
|
20
|
-
}
|
|
21
|
-
// ── Arg parsing ───────────────────────────────────────────────────────────────
|
|
22
|
-
function parseStartArgs(args) {
|
|
23
|
-
const parsed = {
|
|
24
|
-
daemon: false,
|
|
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 === "--log-level") {
|
|
43
|
-
const value = args[i + 1];
|
|
44
|
-
if (!value || value.startsWith("-")) {
|
|
45
|
-
parsed.error = `Option '${arg}' argument missing`;
|
|
46
|
-
return parsed;
|
|
47
|
-
}
|
|
48
|
-
parsed.logLevel = value;
|
|
49
|
-
i += 1;
|
|
50
|
-
continue;
|
|
51
|
-
}
|
|
52
|
-
if (arg?.startsWith("-")) {
|
|
53
|
-
parsed.error = `Unknown option '${arg}'`;
|
|
54
|
-
return parsed;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
return parsed;
|
|
58
|
-
}
|
|
59
|
-
// ── Tick logging ──────────────────────────────────────────────────────────────
|
|
60
|
-
function logTickResult(snapshot, prevSnapshot, isFirst) {
|
|
61
|
-
if (isFirst) {
|
|
62
|
-
const healthColor = snapshot.health === "degraded"
|
|
63
|
-
? red
|
|
64
|
-
: snapshot.health === "running"
|
|
65
|
-
? green
|
|
66
|
-
: cyan;
|
|
67
|
-
logLine(green("\u25CF"), `Project ${bold(snapshot.slug)} connected ${dim("(")}${healthColor(snapshot.health)}${dim(")")}`);
|
|
68
|
-
if (snapshot.summary.activeRuns > 0) {
|
|
69
|
-
logLine(cyan("\u25B8"), `${snapshot.summary.activeRuns} active run(s)`);
|
|
70
|
-
}
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
73
|
-
if (prevSnapshot && prevSnapshot.health !== snapshot.health) {
|
|
74
|
-
const icon = snapshot.health === "degraded" ? red("\u25CF") : green("\u25CF");
|
|
75
|
-
logLine(icon, `Health changed: ${prevSnapshot.health} \u2192 ${bold(snapshot.health)}`);
|
|
76
|
-
}
|
|
77
|
-
if (snapshot.lastError && snapshot.lastError !== prevSnapshot?.lastError) {
|
|
78
|
-
logLine(red("\u2717"), red(snapshot.lastError));
|
|
79
|
-
}
|
|
80
|
-
if (!snapshot.lastError && prevSnapshot?.lastError) {
|
|
81
|
-
logLine(green("\u2713"), green("Error cleared"));
|
|
82
|
-
}
|
|
83
|
-
const prevDispatched = prevSnapshot?.summary.dispatched ?? 0;
|
|
84
|
-
if (snapshot.summary.dispatched > prevDispatched) {
|
|
85
|
-
const delta = snapshot.summary.dispatched - prevDispatched;
|
|
86
|
-
logLine(yellow("\u25B8"), `Dispatched ${bold(String(delta))} new run(s)`);
|
|
87
|
-
}
|
|
88
|
-
const prevRunIds = new Set(prevSnapshot?.activeRuns.map((run) => run.runId) ?? []);
|
|
89
|
-
for (const run of snapshot.activeRuns) {
|
|
90
|
-
if (!prevRunIds.has(run.runId)) {
|
|
91
|
-
logLine(cyan("\u25B8"), `Run started: ${bold(run.issueIdentifier)} ${dim("state=")}${run.issueState} ${dim("status=")}${run.status}`);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
const currentRunIds = new Set(snapshot.activeRuns.map((run) => run.runId));
|
|
95
|
-
for (const prevRun of prevSnapshot?.activeRuns ?? []) {
|
|
96
|
-
if (!currentRunIds.has(prevRun.runId)) {
|
|
97
|
-
logLine(green("\u2713"), `Run finished: ${bold(prevRun.issueIdentifier)} ${dim("(")}${prevRun.status}${dim(")")}`);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
const prevSuppressed = prevSnapshot?.summary.suppressed ?? 0;
|
|
101
|
-
if (snapshot.summary.suppressed > prevSuppressed) {
|
|
102
|
-
const delta = snapshot.summary.suppressed - prevSuppressed;
|
|
103
|
-
logLine(dim("\u25CB"), dim(`${delta} issue(s) suppressed (already running or at limit)`));
|
|
104
|
-
}
|
|
105
|
-
const prevRecovered = prevSnapshot?.summary.recovered ?? 0;
|
|
106
|
-
if (snapshot.summary.recovered > prevRecovered) {
|
|
107
|
-
const delta = snapshot.summary.recovered - prevRecovered;
|
|
108
|
-
logLine(yellow("\u21BA"), `Recovered ${bold(String(delta))} stalled run(s)`);
|
|
109
|
-
}
|
|
110
|
-
const prevRetryCount = prevSnapshot?.retryQueue.length ?? 0;
|
|
111
|
-
if (snapshot.retryQueue.length > prevRetryCount) {
|
|
112
|
-
const delta = snapshot.retryQueue.length - prevRetryCount;
|
|
113
|
-
logLine(yellow("\u25CC"), `${delta} run(s) queued for retry`);
|
|
114
|
-
}
|
|
115
|
-
const changed = snapshot.health !== prevSnapshot?.health ||
|
|
116
|
-
snapshot.lastError !== prevSnapshot?.lastError ||
|
|
117
|
-
snapshot.summary.dispatched !== prevSnapshot?.summary.dispatched ||
|
|
118
|
-
snapshot.summary.suppressed !== prevSnapshot?.summary.suppressed ||
|
|
119
|
-
snapshot.summary.recovered !== prevSnapshot?.summary.recovered ||
|
|
120
|
-
snapshot.activeRuns.length !== (prevSnapshot?.activeRuns.length ?? 0) ||
|
|
121
|
-
snapshot.retryQueue.length !== (prevSnapshot?.retryQueue.length ?? 0);
|
|
122
|
-
if (!changed) {
|
|
123
|
-
logLine(dim("\u00B7"), dim(`tick \u2014 ${snapshot.summary.activeRuns} active, ${snapshot.health}`));
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
// ── Handler ───────────────────────────────────────────────────────────────────
|
|
127
|
-
const handler = async (args, options) => {
|
|
128
|
-
setNoColor(options.noColor);
|
|
129
|
-
const parsed = parseStartArgs(args);
|
|
130
|
-
if (parsed.error) {
|
|
131
|
-
process.stderr.write(`${parsed.error}\n`);
|
|
132
|
-
process.stderr.write("Usage: gh-symphony start --project-id <project-id> [--daemon]\n");
|
|
133
|
-
process.exitCode = 2;
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
|
-
const projectConfig = await resolveManagedProjectConfig({
|
|
137
|
-
configDir: options.configDir,
|
|
138
|
-
requestedProjectId: parsed.projectId,
|
|
139
|
-
});
|
|
140
|
-
if (!projectConfig) {
|
|
141
|
-
handleMissingManagedProjectConfig();
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
|
-
const runtimeRoot = resolveRuntimeRoot(options.configDir);
|
|
145
|
-
const projectId = projectConfig.projectId;
|
|
146
|
-
let logLevel;
|
|
147
|
-
try {
|
|
148
|
-
logLevel = resolveOrchestratorLogLevel(parsed.logLevel ?? process.env.SYMPHONY_LOG_LEVEL);
|
|
149
|
-
}
|
|
150
|
-
catch (error) {
|
|
151
|
-
process.stderr.write(`${error instanceof Error ? error.message : "Unsupported log level"}\n`);
|
|
152
|
-
process.exitCode = 2;
|
|
153
|
-
return;
|
|
154
|
-
}
|
|
155
|
-
if (parsed.daemon) {
|
|
156
|
-
await startDaemon(options, projectId, parsed.logLevel);
|
|
157
|
-
return;
|
|
158
|
-
}
|
|
159
|
-
// ── 5.1: Foreground mode with live logging ────────────────────────────────
|
|
160
|
-
if (!process.env.GITHUB_GRAPHQL_TOKEN) {
|
|
161
|
-
try {
|
|
162
|
-
process.env.GITHUB_GRAPHQL_TOKEN = getGhToken();
|
|
163
|
-
}
|
|
164
|
-
catch {
|
|
165
|
-
// gh CLI not installed/authenticated — GITHUB_GRAPHQL_TOKEN stays unset
|
|
166
|
-
// Workers will fail if token is needed but not available
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
let projectLock = null;
|
|
170
|
-
try {
|
|
171
|
-
projectLock = await acquireProjectLock({
|
|
172
|
-
runtimeRoot,
|
|
173
|
-
projectId,
|
|
174
|
-
});
|
|
175
|
-
const store = createStore(runtimeRoot);
|
|
176
|
-
const service = new OrchestratorService(store, projectConfig, {
|
|
177
|
-
logLevel,
|
|
178
|
-
});
|
|
179
|
-
const statusServer = startOrchestratorStatusServer({
|
|
180
|
-
host: "127.0.0.1",
|
|
181
|
-
port: 0,
|
|
182
|
-
getProjectStatus: () => service.status(),
|
|
183
|
-
onRefresh: async () => {
|
|
184
|
-
await service.runOnce();
|
|
185
|
-
},
|
|
186
|
-
});
|
|
187
|
-
await persistStatusServerPort(options.configDir, projectId, statusServer);
|
|
188
|
-
logLine(green("\u25B2"), `Starting orchestrator for project: ${bold(projectId)}`);
|
|
189
|
-
logLine(dim("\u00B7"), dim("Press Ctrl+C to stop"));
|
|
190
|
-
let running = true;
|
|
191
|
-
let shuttingDown = false;
|
|
192
|
-
let shutdownPromise = null;
|
|
193
|
-
const shutdown = async () => {
|
|
194
|
-
if (shuttingDown) {
|
|
195
|
-
return shutdownPromise;
|
|
196
|
-
}
|
|
197
|
-
shuttingDown = true;
|
|
198
|
-
running = false;
|
|
199
|
-
const heldLock = projectLock;
|
|
200
|
-
projectLock = null;
|
|
201
|
-
shutdownPromise = shutdownForegroundOrchestrator({
|
|
202
|
-
configDir: options.configDir,
|
|
203
|
-
projectId,
|
|
204
|
-
statusServer,
|
|
205
|
-
projectLock: heldLock,
|
|
206
|
-
service,
|
|
207
|
-
});
|
|
208
|
-
return shutdownPromise;
|
|
209
|
-
};
|
|
210
|
-
process.on("SIGINT", () => {
|
|
211
|
-
void shutdown();
|
|
212
|
-
});
|
|
213
|
-
process.on("SIGTERM", () => {
|
|
214
|
-
void shutdown();
|
|
215
|
-
});
|
|
216
|
-
let prevSnapshot = null;
|
|
217
|
-
let isFirst = true;
|
|
218
|
-
while (running) {
|
|
219
|
-
try {
|
|
220
|
-
const snapshot = await service.runOnce();
|
|
221
|
-
logTickResult(snapshot, prevSnapshot, isFirst);
|
|
222
|
-
if (!isFirst) {
|
|
223
|
-
const currentRunIds = new Set(snapshot.activeRuns.map((run) => run.runId));
|
|
224
|
-
for (const prevRun of prevSnapshot?.activeRuns ?? []) {
|
|
225
|
-
if (!currentRunIds.has(prevRun.runId)) {
|
|
226
|
-
await tailWorkerLog(runtimeRoot, projectId, prevRun.runId, prevRun.issueIdentifier);
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
prevSnapshot = snapshot;
|
|
231
|
-
isFirst = false;
|
|
232
|
-
}
|
|
233
|
-
catch (error) {
|
|
234
|
-
logLine(red("\u2717"), red(`Tick error: ${error instanceof Error ? error.message : "Unknown error"}`));
|
|
235
|
-
}
|
|
236
|
-
if (!running) {
|
|
237
|
-
if (shutdownPromise) {
|
|
238
|
-
await shutdownPromise;
|
|
239
|
-
}
|
|
240
|
-
break;
|
|
241
|
-
}
|
|
242
|
-
// Poll interval: default 30s
|
|
243
|
-
await new Promise((r) => setTimeout(r, 30_000));
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
finally {
|
|
247
|
-
await releaseProjectLock(projectLock);
|
|
248
|
-
}
|
|
249
|
-
};
|
|
250
|
-
export async function shutdownForegroundOrchestrator(input) {
|
|
251
|
-
logLine(yellow("\u25BC"), "Shutting down...");
|
|
252
|
-
// Drain active workers before tearing down infrastructure so that child
|
|
253
|
-
// processes receive SIGTERM/SIGKILL and do not become orphans.
|
|
254
|
-
if (input.service) {
|
|
255
|
-
try {
|
|
256
|
-
await input.service.shutdown();
|
|
257
|
-
}
|
|
258
|
-
catch (error) {
|
|
259
|
-
logLine(red("\u2717"), red(`Failed to shut down workers: ${error instanceof Error ? error.message : "Unknown error"}`));
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
try {
|
|
263
|
-
input.statusServer.close();
|
|
264
|
-
}
|
|
265
|
-
catch (error) {
|
|
266
|
-
logLine(red("\u2717"), red(`Failed to close status server: ${error instanceof Error ? error.message : "Unknown error"}`));
|
|
267
|
-
}
|
|
268
|
-
try {
|
|
269
|
-
await (input.removePortFile ?? rm)(orchestratorPortPath(input.configDir, input.projectId), {
|
|
270
|
-
force: true,
|
|
271
|
-
});
|
|
272
|
-
}
|
|
273
|
-
catch (error) {
|
|
274
|
-
logLine(yellow("\u26A0"), `Failed to remove persisted status port: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
275
|
-
}
|
|
276
|
-
try {
|
|
277
|
-
await (input.releaseLock ?? releaseProjectLock)(input.projectLock);
|
|
278
|
-
}
|
|
279
|
-
catch (error) {
|
|
280
|
-
logLine(yellow("\u26A0"), `Failed to release project lock: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
281
|
-
}
|
|
282
|
-
return (input.exit ?? process.exit)(0);
|
|
283
|
-
}
|
|
284
|
-
async function tailWorkerLog(runtimeRoot, projectId, runId, issueIdentifier) {
|
|
285
|
-
try {
|
|
286
|
-
const logPath = join(runtimeRoot, "projects", projectId, "runs", runId, "worker.log");
|
|
287
|
-
const content = await readFile(logPath, "utf8");
|
|
288
|
-
const lines = content.split("\n").filter((l) => l.trim());
|
|
289
|
-
if (lines.length === 0)
|
|
290
|
-
return;
|
|
291
|
-
const tail = lines.slice(-30);
|
|
292
|
-
logLine(red("\u2717"), red(`Worker stderr (${issueIdentifier}):`));
|
|
293
|
-
for (const line of tail) {
|
|
294
|
-
process.stdout.write(` ${dim(line)}\n`);
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
catch {
|
|
298
|
-
// worker.log 없거나 읽기 실패 시 무시
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
export default handler;
|
|
302
|
-
// ── 5.2: Daemon mode ─────────────────────────────────────────────────────────
|
|
303
|
-
async function startDaemon(options, projectId, logLevel) {
|
|
304
|
-
const logPath = orchestratorLogPath(options.configDir, projectId);
|
|
305
|
-
await mkdir(dirname(logPath), { recursive: true });
|
|
306
|
-
const { openSync } = await import("node:fs");
|
|
307
|
-
const logFd = openSync(logPath, "a");
|
|
308
|
-
const child = spawn(process.execPath, [
|
|
309
|
-
process.argv[1],
|
|
310
|
-
"start",
|
|
311
|
-
"--project",
|
|
312
|
-
projectId,
|
|
313
|
-
...(logLevel ? ["--log-level", logLevel] : []),
|
|
314
|
-
], {
|
|
315
|
-
cwd: process.cwd(),
|
|
316
|
-
env: {
|
|
317
|
-
...process.env,
|
|
318
|
-
GH_SYMPHONY_CONFIG_DIR: options.configDir,
|
|
319
|
-
},
|
|
320
|
-
detached: true,
|
|
321
|
-
stdio: ["ignore", logFd, logFd],
|
|
322
|
-
});
|
|
323
|
-
const pidPath = daemonPidPath(options.configDir, projectId);
|
|
324
|
-
await mkdir(dirname(pidPath), { recursive: true });
|
|
325
|
-
await writeFile(pidPath, String(child.pid), "utf8");
|
|
326
|
-
child.unref();
|
|
327
|
-
const { closeSync } = await import("node:fs");
|
|
328
|
-
closeSync(logFd);
|
|
329
|
-
process.stdout.write(`Orchestrator started in background (PID: ${child.pid}).\n` +
|
|
330
|
-
`Logs: ${logPath}\n` +
|
|
331
|
-
`Stop with: gh-symphony project stop --project-id ${projectId}\n`);
|
|
332
|
-
}
|
|
333
|
-
async function persistStatusServerPort(configDir, projectId, statusServer) {
|
|
334
|
-
if (!statusServer.listening) {
|
|
335
|
-
await once(statusServer, "listening");
|
|
336
|
-
}
|
|
337
|
-
const address = statusServer.address();
|
|
338
|
-
if (!address || typeof address !== "object") {
|
|
339
|
-
return;
|
|
340
|
-
}
|
|
341
|
-
const portPath = orchestratorPortPath(configDir, projectId);
|
|
342
|
-
await mkdir(dirname(portPath), { recursive: true });
|
|
343
|
-
await writeFile(portPath, `${address.port}\n`, "utf8");
|
|
344
|
-
}
|