@gh-symphony/cli 0.0.1 → 0.0.2
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/ansi.d.ts +15 -0
- package/dist/ansi.js +53 -0
- package/dist/commands/config-cmd.js +11 -27
- package/dist/commands/help.js +14 -6
- package/dist/commands/init.d.ts +30 -7
- package/dist/commands/init.js +421 -284
- package/dist/commands/logs.js +4 -4
- package/dist/commands/project.js +34 -34
- package/dist/commands/recover.js +14 -14
- package/dist/commands/repo.js +13 -13
- package/dist/commands/run.js +16 -16
- package/dist/commands/start.js +61 -37
- package/dist/commands/status.js +60 -63
- package/dist/commands/tenant.d.ts +3 -0
- package/dist/commands/tenant.js +402 -0
- package/dist/config.d.ts +20 -19
- package/dist/config.js +17 -17
- package/dist/context/context-types.d.ts +36 -0
- package/dist/context/context-types.js +1 -0
- package/dist/context/generate-context-yaml.d.ts +15 -0
- package/dist/context/generate-context-yaml.js +129 -0
- package/dist/dashboard/renderer.d.ts +9 -0
- package/dist/dashboard/renderer.js +220 -0
- package/dist/detection/environment-detector.d.ts +11 -0
- package/dist/detection/environment-detector.js +140 -0
- package/dist/github/client.d.ts +11 -0
- package/dist/github/client.js +59 -11
- package/dist/github/gh-auth.d.ts +34 -0
- package/dist/github/gh-auth.js +110 -0
- package/dist/index.js +1 -0
- package/dist/mapping/smart-defaults.d.ts +9 -25
- package/dist/mapping/smart-defaults.js +52 -125
- package/dist/orchestrator-runtime.d.ts +4 -4
- package/dist/orchestrator-runtime.js +27 -12
- package/dist/skills/skill-writer.d.ts +14 -0
- package/dist/skills/skill-writer.js +62 -0
- package/dist/skills/templates/commit.d.ts +2 -0
- package/dist/skills/templates/commit.js +45 -0
- package/dist/skills/templates/document.d.ts +7 -0
- package/dist/skills/templates/document.js +16 -0
- package/dist/skills/templates/gh-project.d.ts +2 -0
- package/dist/skills/templates/gh-project.js +88 -0
- package/dist/skills/templates/gh-symphony.d.ts +2 -0
- package/dist/skills/templates/gh-symphony.js +125 -0
- package/dist/skills/templates/index.d.ts +8 -0
- package/dist/skills/templates/index.js +28 -0
- package/dist/skills/templates/land.d.ts +2 -0
- package/dist/skills/templates/land.js +59 -0
- package/dist/skills/templates/pull.d.ts +2 -0
- package/dist/skills/templates/pull.js +41 -0
- package/dist/skills/templates/push.d.ts +2 -0
- package/dist/skills/templates/push.js +36 -0
- package/dist/skills/types.d.ts +23 -0
- package/dist/skills/types.js +1 -0
- package/dist/workflow/generate-reference-workflow.d.ts +9 -0
- package/dist/workflow/generate-reference-workflow.js +261 -0
- package/dist/workflow/generate-workflow-md.d.ts +12 -0
- package/dist/workflow/generate-workflow-md.js +134 -0
- package/package.json +5 -4
package/dist/commands/logs.js
CHANGED
|
@@ -2,7 +2,7 @@ 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 { loadActiveTenantConfig, orchestratorLogPath } from "../config.js";
|
|
6
6
|
function parseLogsArgs(args) {
|
|
7
7
|
const parsed = { follow: false };
|
|
8
8
|
for (let i = 0; i < args.length; i += 1) {
|
|
@@ -27,9 +27,9 @@ function parseLogsArgs(args) {
|
|
|
27
27
|
}
|
|
28
28
|
const handler = async (args, options) => {
|
|
29
29
|
const parsed = parseLogsArgs(args);
|
|
30
|
-
const
|
|
31
|
-
if (!
|
|
32
|
-
process.stderr.write("No
|
|
30
|
+
const tenantConfig = await loadActiveTenantConfig(options.configDir);
|
|
31
|
+
if (!tenantConfig) {
|
|
32
|
+
process.stderr.write("No tenant configured. Run 'gh-symphony init' first.\n");
|
|
33
33
|
process.exitCode = 1;
|
|
34
34
|
return;
|
|
35
35
|
}
|
package/dist/commands/project.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as p from "@clack/prompts";
|
|
2
|
-
import { loadGlobalConfig, saveGlobalConfig,
|
|
2
|
+
import { loadGlobalConfig, saveGlobalConfig, loadTenantConfig, } from "../config.js";
|
|
3
3
|
const handler = async (args, options) => {
|
|
4
4
|
const [subcommand] = args;
|
|
5
5
|
switch (subcommand) {
|
|
@@ -21,81 +21,81 @@ export default handler;
|
|
|
21
21
|
// ── 6.1: project list ────────────────────────────────────────────────────────
|
|
22
22
|
async function projectList(options) {
|
|
23
23
|
const global = await loadGlobalConfig(options.configDir);
|
|
24
|
-
if (!global || global.
|
|
25
|
-
process.stdout.write("No
|
|
24
|
+
if (!global || global.tenants.length === 0) {
|
|
25
|
+
process.stdout.write("No tenants configured. Run 'gh-symphony init'.\n");
|
|
26
26
|
return;
|
|
27
27
|
}
|
|
28
28
|
if (options.json) {
|
|
29
29
|
const configs = [];
|
|
30
|
-
for (const
|
|
31
|
-
const
|
|
30
|
+
for (const tId of global.tenants) {
|
|
31
|
+
const t = await loadTenantConfig(options.configDir, tId);
|
|
32
32
|
configs.push({
|
|
33
|
-
id:
|
|
34
|
-
active:
|
|
35
|
-
repos:
|
|
33
|
+
id: tId,
|
|
34
|
+
active: tId === global.activeTenant,
|
|
35
|
+
repos: t?.repositories.length ?? 0,
|
|
36
36
|
});
|
|
37
37
|
}
|
|
38
38
|
process.stdout.write(JSON.stringify(configs, null, 2) + "\n");
|
|
39
39
|
return;
|
|
40
40
|
}
|
|
41
|
-
process.stdout.write("
|
|
42
|
-
for (const
|
|
43
|
-
const
|
|
44
|
-
const active =
|
|
45
|
-
const repos =
|
|
46
|
-
process.stdout.write(` ${
|
|
41
|
+
process.stdout.write("Tenants:\n\n");
|
|
42
|
+
for (const tId of global.tenants) {
|
|
43
|
+
const t = await loadTenantConfig(options.configDir, tId);
|
|
44
|
+
const active = tId === global.activeTenant ? " (active)" : "";
|
|
45
|
+
const repos = t?.repositories.length ?? 0;
|
|
46
|
+
process.stdout.write(` ${tId}${active} — ${repos} repo${repos === 1 ? "" : "s"}\n`);
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
49
|
// ── 6.2: project switch ──────────────────────────────────────────────────────
|
|
50
50
|
async function projectSwitch(options) {
|
|
51
51
|
const global = await loadGlobalConfig(options.configDir);
|
|
52
|
-
if (!global || global.
|
|
53
|
-
process.stderr.write("No
|
|
52
|
+
if (!global || global.tenants.length === 0) {
|
|
53
|
+
process.stderr.write("No tenants configured. Run 'gh-symphony init'.\n");
|
|
54
54
|
process.exitCode = 1;
|
|
55
55
|
return;
|
|
56
56
|
}
|
|
57
|
-
if (global.
|
|
58
|
-
process.stdout.write(`Only one
|
|
57
|
+
if (global.tenants.length === 1) {
|
|
58
|
+
process.stdout.write(`Only one tenant exists: ${global.tenants[0]}\n`);
|
|
59
59
|
return;
|
|
60
60
|
}
|
|
61
61
|
const selected = await p.select({
|
|
62
|
-
message: "Select
|
|
63
|
-
options: global.
|
|
64
|
-
value:
|
|
65
|
-
label:
|
|
66
|
-
hint:
|
|
62
|
+
message: "Select tenant to activate:",
|
|
63
|
+
options: global.tenants.map((tId) => ({
|
|
64
|
+
value: tId,
|
|
65
|
+
label: tId,
|
|
66
|
+
hint: tId === global.activeTenant ? "current" : undefined,
|
|
67
67
|
})),
|
|
68
68
|
});
|
|
69
69
|
if (p.isCancel(selected)) {
|
|
70
70
|
p.cancel("Cancelled.");
|
|
71
71
|
return;
|
|
72
72
|
}
|
|
73
|
-
global.
|
|
73
|
+
global.activeTenant = selected;
|
|
74
74
|
await saveGlobalConfig(options.configDir, global);
|
|
75
|
-
process.stdout.write(`Switched to
|
|
75
|
+
process.stdout.write(`Switched to tenant: ${selected}\n`);
|
|
76
76
|
}
|
|
77
77
|
// ── 6.3: project status ──────────────────────────────────────────────────────
|
|
78
78
|
async function projectStatus(options) {
|
|
79
79
|
const global = await loadGlobalConfig(options.configDir);
|
|
80
|
-
if (!global?.
|
|
81
|
-
process.stderr.write("No active
|
|
80
|
+
if (!global?.activeTenant) {
|
|
81
|
+
process.stderr.write("No active tenant.\n");
|
|
82
82
|
process.exitCode = 1;
|
|
83
83
|
return;
|
|
84
84
|
}
|
|
85
|
-
const
|
|
86
|
-
if (!
|
|
87
|
-
process.stderr.write(`
|
|
85
|
+
const t = await loadTenantConfig(options.configDir, global.activeTenant);
|
|
86
|
+
if (!t) {
|
|
87
|
+
process.stderr.write(`Tenant config missing: ${global.activeTenant}\n`);
|
|
88
88
|
process.exitCode = 1;
|
|
89
89
|
return;
|
|
90
90
|
}
|
|
91
91
|
if (options.json) {
|
|
92
|
-
process.stdout.write(JSON.stringify(
|
|
92
|
+
process.stdout.write(JSON.stringify(t, null, 2) + "\n");
|
|
93
93
|
return;
|
|
94
94
|
}
|
|
95
|
-
process.stdout.write(`
|
|
96
|
-
process.stdout.write(`Tracker: ${
|
|
95
|
+
process.stdout.write(`Tenant: ${t.tenantId}\n`);
|
|
96
|
+
process.stdout.write(`Tracker: ${t.tracker.adapter} (${t.tracker.bindingId})\n`);
|
|
97
97
|
process.stdout.write(`Repositories:\n`);
|
|
98
|
-
for (const repo of
|
|
98
|
+
for (const repo of t.repositories) {
|
|
99
99
|
process.stdout.write(` - ${repo.owner}/${repo.name}\n`);
|
|
100
100
|
}
|
|
101
101
|
}
|
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
|
-
import { runCli as orchestratorRunCli } from "@
|
|
4
|
-
import { resolveRuntimeRoot,
|
|
3
|
+
import { runCli as orchestratorRunCli } from "@gh-symphony/orchestrator";
|
|
4
|
+
import { resolveRuntimeRoot, resolveTenantConfig, syncTenantToRuntime, } 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 === "--tenant" || arg === "--tenant-id") {
|
|
13
|
+
parsed.tenantId = 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 tenantConfig = await resolveTenantConfig(options.configDir, parsed.tenantId);
|
|
22
|
+
if (!tenantConfig) {
|
|
23
|
+
process.stderr.write("No tenant configured. Run 'gh-symphony init' 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 tenantId = tenantConfig.tenantId;
|
|
29
|
+
await syncTenantToRuntime(options.configDir, tenantConfig);
|
|
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, tenantId);
|
|
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
|
+
"--tenant-id",
|
|
52
|
+
tenantId,
|
|
53
53
|
]);
|
|
54
54
|
};
|
|
55
55
|
export default handler;
|
|
56
|
-
async function listRecoverCandidates(runtimeRoot,
|
|
56
|
+
async function listRecoverCandidates(runtimeRoot, tenantId) {
|
|
57
57
|
const runsDir = join(runtimeRoot, "orchestrator", "runs");
|
|
58
58
|
const candidates = [];
|
|
59
59
|
let entries = [];
|
|
@@ -68,7 +68,7 @@ async function listRecoverCandidates(runtimeRoot, workspaceId) {
|
|
|
68
68
|
try {
|
|
69
69
|
const raw = await readFile(runPath, "utf8");
|
|
70
70
|
const run = JSON.parse(raw);
|
|
71
|
-
if (run.
|
|
71
|
+
if (run.tenantId !== tenantId) {
|
|
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 { loadActiveTenantConfig, loadGlobalConfig, saveTenantConfig, } 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 loadActiveTenantConfig(options.configDir);
|
|
23
23
|
if (!ws) {
|
|
24
|
-
process.stderr.write("No
|
|
24
|
+
process.stderr.write("No tenant 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?.activeTenant) {
|
|
46
|
+
process.stderr.write("No active tenant.\n");
|
|
47
47
|
process.exitCode = 1;
|
|
48
48
|
return;
|
|
49
49
|
}
|
|
50
|
-
const ws = await
|
|
50
|
+
const ws = await loadActiveTenantConfig(options.configDir);
|
|
51
51
|
if (!ws) {
|
|
52
|
-
process.stderr.write("
|
|
52
|
+
process.stderr.write("Tenant 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 saveTenantConfig(options.configDir, global.activeTenant, 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?.activeTenant) {
|
|
83
|
+
process.stderr.write("No active tenant.\n");
|
|
84
84
|
process.exitCode = 1;
|
|
85
85
|
return;
|
|
86
86
|
}
|
|
87
|
-
const ws = await
|
|
87
|
+
const ws = await loadActiveTenantConfig(options.configDir);
|
|
88
88
|
if (!ws) {
|
|
89
|
-
process.stderr.write("
|
|
89
|
+
process.stderr.write("Tenant 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 saveTenantConfig(options.configDir, global.activeTenant, ws);
|
|
102
102
|
process.stdout.write(`Removed repository: ${repoSpec}\n`);
|
|
103
103
|
}
|
package/dist/commands/run.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { runCli as orchestratorRunCli } from "@
|
|
2
|
-
import { resolveRuntimeRoot,
|
|
1
|
+
import { runCli as orchestratorRunCli } from "@gh-symphony/orchestrator";
|
|
2
|
+
import { resolveRuntimeRoot, resolveTenantConfig, syncTenantToRuntime, } 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 === "--tenant" || arg === "--tenant-id") {
|
|
13
|
+
parsed.tenantId = 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 tenantConfig = await resolveTenantConfig(options.configDir, parsed.tenantId);
|
|
31
|
+
if (!tenantConfig) {
|
|
32
|
+
process.stderr.write("No tenant configured. Run 'gh-symphony init' 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 tenantId = tenantConfig.tenantId;
|
|
38
|
+
await syncTenantToRuntime(options.configDir, tenantConfig);
|
|
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
|
+
!tenantConfig.repositories.some((r) => `${r.owner}/${r.name}` === repoSpec)) {
|
|
43
|
+
process.stderr.write(`Repository "${repoSpec}" is not configured in this tenant.\n` +
|
|
44
|
+
`Configured repos: ${tenantConfig.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
|
+
"--tenant-id",
|
|
54
|
+
tenantId,
|
|
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
|
+
"--tenant-id",
|
|
65
|
+
tenantId,
|
|
66
66
|
]);
|
|
67
67
|
}
|
|
68
68
|
};
|
package/dist/commands/start.js
CHANGED
|
@@ -1,24 +1,11 @@
|
|
|
1
|
-
import { writeFile, mkdir } from "node:fs/promises";
|
|
2
|
-
import { dirname } from "node:path";
|
|
1
|
+
import { writeFile, mkdir, readFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
3
|
import { spawn } from "node:child_process";
|
|
4
4
|
import { daemonPidPath, orchestratorLogPath, logsDir } from "../config.js";
|
|
5
|
-
import { OrchestratorService, createStore, startOrchestratorStatusServer, } from "@
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const _bold = (s) => `${ESC}1m${s}${ESC}0m`;
|
|
10
|
-
const _dim = (s) => `${ESC}2m${s}${ESC}0m`;
|
|
11
|
-
const _green = (s) => `${ESC}32m${s}${ESC}0m`;
|
|
12
|
-
const _red = (s) => `${ESC}31m${s}${ESC}0m`;
|
|
13
|
-
const _yellow = (s) => `${ESC}33m${s}${ESC}0m`;
|
|
14
|
-
const _cyan = (s) => `${ESC}36m${s}${ESC}0m`;
|
|
15
|
-
let noColor = false;
|
|
16
|
-
const bold = (s) => (noColor ? s : _bold(s));
|
|
17
|
-
const dim = (s) => (noColor ? s : _dim(s));
|
|
18
|
-
const green = (s) => (noColor ? s : _green(s));
|
|
19
|
-
const red = (s) => (noColor ? s : _red(s));
|
|
20
|
-
const yellow = (s) => (noColor ? s : _yellow(s));
|
|
21
|
-
const cyan = (s) => (noColor ? s : _cyan(s));
|
|
5
|
+
import { OrchestratorService, createStore, startOrchestratorStatusServer, } from "@gh-symphony/orchestrator";
|
|
6
|
+
import { resolveTenantConfig, resolveRuntimeRoot, syncTenantToRuntime, } from "../orchestrator-runtime.js";
|
|
7
|
+
import { bold, dim, green, red, yellow, cyan, setNoColor } from "../ansi.js";
|
|
8
|
+
import { getGhToken } from "../github/gh-auth.js";
|
|
22
9
|
function timestamp() {
|
|
23
10
|
const now = new Date();
|
|
24
11
|
const hh = String(now.getHours()).padStart(2, "0");
|
|
@@ -37,8 +24,8 @@ function parseStartArgs(args) {
|
|
|
37
24
|
if (arg === "--daemon" || arg === "-d") {
|
|
38
25
|
parsed.daemon = true;
|
|
39
26
|
}
|
|
40
|
-
if (arg === "--
|
|
41
|
-
parsed.
|
|
27
|
+
if (arg === "--tenant" || arg === "--tenant-id") {
|
|
28
|
+
parsed.tenantId = args[i + 1];
|
|
42
29
|
i += 1;
|
|
43
30
|
}
|
|
44
31
|
}
|
|
@@ -47,14 +34,14 @@ function parseStartArgs(args) {
|
|
|
47
34
|
// ── Tick logging ──────────────────────────────────────────────────────────────
|
|
48
35
|
function logTickResult(snapshots, prevSnapshots, isFirst) {
|
|
49
36
|
for (const snap of snapshots) {
|
|
50
|
-
const prev = prevSnapshots.find((p) => p.
|
|
37
|
+
const prev = prevSnapshots.find((p) => p.tenantId === snap.tenantId);
|
|
51
38
|
if (isFirst) {
|
|
52
39
|
const healthColor = snap.health === "degraded"
|
|
53
40
|
? red
|
|
54
41
|
: snap.health === "running"
|
|
55
42
|
? green
|
|
56
43
|
: cyan;
|
|
57
|
-
logLine(green("\u25CF"), `
|
|
44
|
+
logLine(green("\u25CF"), `Tenant ${bold(snap.slug)} connected ${dim("(")}${healthColor(snap.health)}${dim(")")}`);
|
|
58
45
|
if (snap.summary.activeRuns > 0) {
|
|
59
46
|
logLine(cyan("\u25B8"), `${snap.summary.activeRuns} active run(s)`);
|
|
60
47
|
}
|
|
@@ -83,7 +70,7 @@ function logTickResult(snapshots, prevSnapshots, isFirst) {
|
|
|
83
70
|
const prevRunIds = new Set(prev?.activeRuns.map((r) => r.runId) ?? []);
|
|
84
71
|
for (const run of snap.activeRuns) {
|
|
85
72
|
if (!prevRunIds.has(run.runId)) {
|
|
86
|
-
logLine(cyan("\u25B8"), `Run started: ${bold(run.issueIdentifier)} ${dim("
|
|
73
|
+
logLine(cyan("\u25B8"), `Run started: ${bold(run.issueIdentifier)} ${dim("state=")}${run.issueState} ${dim("status=")}${run.status}`);
|
|
87
74
|
}
|
|
88
75
|
}
|
|
89
76
|
// Completed runs (were active, now gone)
|
|
@@ -126,37 +113,46 @@ function logTickResult(snapshots, prevSnapshots, isFirst) {
|
|
|
126
113
|
}
|
|
127
114
|
// ── Handler ───────────────────────────────────────────────────────────────────
|
|
128
115
|
const handler = async (args, options) => {
|
|
129
|
-
|
|
116
|
+
setNoColor(options.noColor);
|
|
130
117
|
const parsed = parseStartArgs(args);
|
|
131
|
-
const
|
|
132
|
-
if (!
|
|
133
|
-
process.stderr.write("No
|
|
118
|
+
const tenantConfig = await resolveTenantConfig(options.configDir, parsed.tenantId);
|
|
119
|
+
if (!tenantConfig) {
|
|
120
|
+
process.stderr.write("No tenant configured. Run 'gh-symphony init' first.\n");
|
|
134
121
|
process.exitCode = 1;
|
|
135
122
|
return;
|
|
136
123
|
}
|
|
137
124
|
const runtimeRoot = resolveRuntimeRoot(options.configDir);
|
|
138
|
-
const
|
|
139
|
-
await
|
|
125
|
+
const tenantId = tenantConfig.tenantId;
|
|
126
|
+
await syncTenantToRuntime(options.configDir, tenantConfig);
|
|
140
127
|
if (parsed.daemon) {
|
|
141
|
-
await startDaemon(options,
|
|
128
|
+
await startDaemon(options, tenantId);
|
|
142
129
|
return;
|
|
143
130
|
}
|
|
144
131
|
// ── 5.1: Foreground mode with live logging ────────────────────────────────
|
|
132
|
+
if (!process.env.GITHUB_GRAPHQL_TOKEN) {
|
|
133
|
+
try {
|
|
134
|
+
process.env.GITHUB_GRAPHQL_TOKEN = getGhToken();
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
// gh CLI not installed/authenticated — GITHUB_GRAPHQL_TOKEN stays unset
|
|
138
|
+
// Workers will fail if token is needed but not available
|
|
139
|
+
}
|
|
140
|
+
}
|
|
145
141
|
const store = createStore(runtimeRoot);
|
|
146
142
|
const service = new OrchestratorService(store);
|
|
147
143
|
// Start status server
|
|
148
144
|
startOrchestratorStatusServer({
|
|
149
145
|
host: "127.0.0.1",
|
|
150
146
|
port: 4680,
|
|
151
|
-
|
|
147
|
+
getTenantStatus: {
|
|
152
148
|
all: () => service.status(),
|
|
153
|
-
|
|
149
|
+
byTenantId: async (id) => {
|
|
154
150
|
const [snapshot] = await service.status(id);
|
|
155
151
|
return snapshot ?? null;
|
|
156
152
|
},
|
|
157
153
|
},
|
|
158
154
|
});
|
|
159
|
-
logLine(green("\u25B2"), `Starting orchestrator for
|
|
155
|
+
logLine(green("\u25B2"), `Starting orchestrator for tenant: ${bold(tenantId)}`);
|
|
160
156
|
logLine(dim("\u00B7"), dim("Press Ctrl+C to stop"));
|
|
161
157
|
let running = true;
|
|
162
158
|
const shutdown = () => {
|
|
@@ -170,8 +166,19 @@ const handler = async (args, options) => {
|
|
|
170
166
|
let isFirst = true;
|
|
171
167
|
while (running) {
|
|
172
168
|
try {
|
|
173
|
-
const snapshots = await service.runOnce({
|
|
169
|
+
const snapshots = await service.runOnce({ tenantId });
|
|
174
170
|
logTickResult(snapshots, prevSnapshots, isFirst);
|
|
171
|
+
if (!isFirst) {
|
|
172
|
+
for (const snap of snapshots) {
|
|
173
|
+
const prev = prevSnapshots.find((p) => p.tenantId === snap.tenantId);
|
|
174
|
+
const currentRunIds = new Set(snap.activeRuns.map((r) => r.runId));
|
|
175
|
+
for (const prevRun of prev?.activeRuns ?? []) {
|
|
176
|
+
if (!currentRunIds.has(prevRun.runId)) {
|
|
177
|
+
await tailWorkerLog(runtimeRoot, prevRun.runId, prevRun.issueIdentifier);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
175
182
|
prevSnapshots = snapshots;
|
|
176
183
|
isFirst = false;
|
|
177
184
|
}
|
|
@@ -182,14 +189,31 @@ const handler = async (args, options) => {
|
|
|
182
189
|
await new Promise((r) => setTimeout(r, 30_000));
|
|
183
190
|
}
|
|
184
191
|
};
|
|
192
|
+
async function tailWorkerLog(runtimeRoot, runId, issueIdentifier) {
|
|
193
|
+
try {
|
|
194
|
+
const logPath = join(runtimeRoot, "orchestrator", "runs", runId, "worker.log");
|
|
195
|
+
const content = await readFile(logPath, "utf8");
|
|
196
|
+
const lines = content.split("\n").filter((l) => l.trim());
|
|
197
|
+
if (lines.length === 0)
|
|
198
|
+
return;
|
|
199
|
+
const tail = lines.slice(-30);
|
|
200
|
+
logLine(red("\u2717"), red(`Worker stderr (${issueIdentifier}):`));
|
|
201
|
+
for (const line of tail) {
|
|
202
|
+
process.stdout.write(` ${dim(line)}\n`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
// worker.log 없거나 읽기 실패 시 무시
|
|
207
|
+
}
|
|
208
|
+
}
|
|
185
209
|
export default handler;
|
|
186
210
|
// ── 5.2: Daemon mode ─────────────────────────────────────────────────────────
|
|
187
|
-
async function startDaemon(options,
|
|
211
|
+
async function startDaemon(options, tenantId) {
|
|
188
212
|
const logPath = orchestratorLogPath(options.configDir);
|
|
189
213
|
await mkdir(logsDir(options.configDir), { recursive: true });
|
|
190
214
|
const { openSync } = await import("node:fs");
|
|
191
215
|
const logFd = openSync(logPath, "a");
|
|
192
|
-
const child = spawn(process.execPath, [process.argv[1], "start", "--
|
|
216
|
+
const child = spawn(process.execPath, [process.argv[1], "start", "--tenant", tenantId], {
|
|
193
217
|
cwd: process.cwd(),
|
|
194
218
|
env: {
|
|
195
219
|
...process.env,
|