@gh-symphony/cli 0.0.2 → 0.0.4
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 +142 -0
- package/dist/commands/init.d.ts +0 -1
- package/dist/commands/init.js +3 -135
- package/dist/index.js +2 -1
- package/package.json +5 -5
package/README.md
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# @gh-symphony/cli
|
|
2
|
+
|
|
3
|
+
Interactive CLI for GitHub Symphony — a multi-tenant AI coding agent orchestration platform.
|
|
4
|
+
|
|
5
|
+
## Requirements
|
|
6
|
+
|
|
7
|
+
The following tools must be installed before using the CLI:
|
|
8
|
+
|
|
9
|
+
- **[Node.js](https://nodejs.org/)** (v24+) with npm
|
|
10
|
+
- **[Git](https://git-scm.com/)**
|
|
11
|
+
- **[GitHub CLI (`gh`)](https://cli.github.com/)** — authenticated with required scopes:
|
|
12
|
+
```bash
|
|
13
|
+
gh auth login --scopes repo,read:org,project
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## 1. Install Package
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install -g @gh-symphony/cli
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Verify the installation:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
gh-symphony --version
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## 2. Set Repository
|
|
29
|
+
|
|
30
|
+
Navigate to the repository you want to orchestrate, then run:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
gh-symphony init
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
The interactive wizard will:
|
|
37
|
+
|
|
38
|
+
1. Authenticate via `gh` CLI
|
|
39
|
+
2. Let you select a **GitHub Project** to bind
|
|
40
|
+
3. Map project status columns to workflow phases (active / wait / terminal)
|
|
41
|
+
4. Generate `WORKFLOW.md` and supporting files in the repository
|
|
42
|
+
|
|
43
|
+
### Customizing Agent Behavior
|
|
44
|
+
|
|
45
|
+
`gh-symphony init` generates skill files under `.codex/skills/` (or `.claude/skills/` for Claude Code). These skills define how the AI agent handles commits, pushes, pulls, and project status transitions.
|
|
46
|
+
|
|
47
|
+
You can further customize the agent's behavior by editing `WORKFLOW.md` — this is the policy layer that controls what the agent does at each workflow phase.
|
|
48
|
+
|
|
49
|
+
> Currently supported runtimes: **Codex**, **Claude Code**
|
|
50
|
+
|
|
51
|
+
## 3. Set Orchestrator Runner (Tenant)
|
|
52
|
+
|
|
53
|
+
On the machine where you want the orchestrator to run, register a tenant:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
gh-symphony tenant add
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
The interactive wizard will:
|
|
60
|
+
|
|
61
|
+
1. Authenticate via `gh` CLI
|
|
62
|
+
2. Let you select a **GitHub Project**
|
|
63
|
+
3. Select repositories to orchestrate
|
|
64
|
+
4. Auto-detect workflow column mappings
|
|
65
|
+
5. Choose an AI runtime (Codex / Claude Code / custom)
|
|
66
|
+
6. Write tenant configuration to `~/.gh-symphony/`
|
|
67
|
+
|
|
68
|
+
### Tenant Management
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
gh-symphony tenant list # List all configured tenants
|
|
72
|
+
gh-symphony tenant remove <id> # Remove a tenant
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## 4. Run the Orchestrator
|
|
76
|
+
|
|
77
|
+
### Foreground
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
gh-symphony start
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Background (daemon)
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
gh-symphony start --daemon # Start in background
|
|
87
|
+
gh-symphony stop # Stop the daemon
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Monitor
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
gh-symphony status # Show current status
|
|
94
|
+
gh-symphony status --watch # Live dashboard
|
|
95
|
+
gh-symphony logs # View event logs
|
|
96
|
+
gh-symphony logs --follow # Stream logs in real-time
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Dispatch a Single Issue
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
gh-symphony run org/repo#123
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Recover Stalled Runs
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
gh-symphony recover # Recover stalled runs
|
|
109
|
+
gh-symphony recover --dry-run # Preview what would be recovered
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Command Reference
|
|
113
|
+
|
|
114
|
+
```
|
|
115
|
+
Setup:
|
|
116
|
+
init Interactive repository setup wizard
|
|
117
|
+
config show Show current configuration
|
|
118
|
+
config set Set a configuration value
|
|
119
|
+
config edit Open config in $EDITOR
|
|
120
|
+
|
|
121
|
+
Orchestration:
|
|
122
|
+
start Start the orchestrator (foreground)
|
|
123
|
+
start --daemon Start the orchestrator (background)
|
|
124
|
+
stop Stop the background orchestrator
|
|
125
|
+
status Show orchestrator status
|
|
126
|
+
run <issue> Dispatch a single issue
|
|
127
|
+
recover Recover stalled runs
|
|
128
|
+
logs View orchestrator logs
|
|
129
|
+
|
|
130
|
+
Tenant Management:
|
|
131
|
+
tenant add Add a new tenant (interactive wizard)
|
|
132
|
+
tenant list List all configured tenants
|
|
133
|
+
tenant remove Remove a tenant
|
|
134
|
+
|
|
135
|
+
Global Options:
|
|
136
|
+
--config <dir> Config directory (default: ~/.gh-symphony)
|
|
137
|
+
--verbose Enable verbose output
|
|
138
|
+
--json Output in JSON format
|
|
139
|
+
--no-color Disable color output
|
|
140
|
+
--help, -h Show help
|
|
141
|
+
--version, -V Show version
|
|
142
|
+
```
|
package/dist/commands/init.d.ts
CHANGED
|
@@ -22,7 +22,6 @@ export type EcosystemResult = {
|
|
|
22
22
|
skillsWritten: string[];
|
|
23
23
|
skillsSkipped: string[];
|
|
24
24
|
};
|
|
25
|
-
export declare function resolveTenantRuntime(configDir: string, tenantId: string, tenantWorkerCommand?: string): Promise<string>;
|
|
26
25
|
export declare function writeEcosystem(opts: EcosystemOptions): Promise<EcosystemResult>;
|
|
27
26
|
type WriteConfigInput = {
|
|
28
27
|
tenantId: string;
|
package/dist/commands/init.js
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import * as p from "@clack/prompts";
|
|
2
|
-
import { parseWorkflowMarkdown } from "@gh-symphony/core";
|
|
3
2
|
import { createHash } from "node:crypto";
|
|
4
|
-
import { mkdir,
|
|
3
|
+
import { mkdir, rename, writeFile } from "node:fs/promises";
|
|
5
4
|
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
6
5
|
import { fileURLToPath } from "node:url";
|
|
7
6
|
import { createClient, validateToken, checkRequiredScopes, listUserProjects, getProjectDetail, GitHubScopeError, } from "../github/client.js";
|
|
8
7
|
import { inferAllStateRoles, toWorkflowLifecycleConfig, validateStateMapping, } from "../mapping/smart-defaults.js";
|
|
9
8
|
import { generateWorkflowMarkdown } from "../workflow/generate-workflow-md.js";
|
|
10
|
-
import { loadGlobalConfig,
|
|
9
|
+
import { loadGlobalConfig, saveGlobalConfig, saveTenantConfig, saveWorkflowMapping, } from "../config.js";
|
|
11
10
|
import { getGhToken, ensureGhAuth, GhAuthError } from "../github/gh-auth.js";
|
|
12
11
|
import { detectEnvironment } from "../detection/environment-detector.js";
|
|
13
12
|
import { buildContextYaml, writeContextYaml, } from "../context/generate-context-yaml.js";
|
|
@@ -76,43 +75,6 @@ const handler = async (args, options) => {
|
|
|
76
75
|
await runInteractive(options);
|
|
77
76
|
};
|
|
78
77
|
export default handler;
|
|
79
|
-
function inferAgentRuntimeFromCommand(command) {
|
|
80
|
-
if (!command) {
|
|
81
|
-
return null;
|
|
82
|
-
}
|
|
83
|
-
if (command.includes("claude-code")) {
|
|
84
|
-
return "claude-code";
|
|
85
|
-
}
|
|
86
|
-
if (command.includes("codex")) {
|
|
87
|
-
return "codex";
|
|
88
|
-
}
|
|
89
|
-
return null;
|
|
90
|
-
}
|
|
91
|
-
function isWorkerBootstrapCommand(command) {
|
|
92
|
-
return (command.includes("@gh-symphony/worker/dist/index.js") ||
|
|
93
|
-
command.includes("packages/worker/dist/index.js"));
|
|
94
|
-
}
|
|
95
|
-
function isMissingAgentEnvError(error) {
|
|
96
|
-
return (error instanceof Error &&
|
|
97
|
-
error.message.includes("Workflow front matter requires environment variable"));
|
|
98
|
-
}
|
|
99
|
-
export async function resolveTenantRuntime(configDir, tenantId, tenantWorkerCommand) {
|
|
100
|
-
const workflowPath = join(configDir, "tenants", tenantId, "WORKFLOW.md");
|
|
101
|
-
try {
|
|
102
|
-
const workflowMarkdown = await readFile(workflowPath, "utf8");
|
|
103
|
-
const agentCommand = parseWorkflowMarkdown(workflowMarkdown, {}).agentCommand;
|
|
104
|
-
if (!isWorkerBootstrapCommand(agentCommand)) {
|
|
105
|
-
return agentCommand;
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
catch (error) {
|
|
109
|
-
const err = error;
|
|
110
|
-
if (err.code !== "ENOENT" && !isMissingAgentEnvError(error)) {
|
|
111
|
-
throw error;
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
return inferAgentRuntimeFromCommand(tenantWorkerCommand) ?? "codex";
|
|
115
|
-
}
|
|
116
78
|
export async function writeEcosystem(opts) {
|
|
117
79
|
const { cwd, projectDetail, statusField, runtime, skipSkills, skipContext } = opts;
|
|
118
80
|
const ghSymphonyDir = join(cwd, ".gh-symphony");
|
|
@@ -327,103 +289,9 @@ async function runNonInteractive(flags, options) {
|
|
|
327
289
|
// ── Interactive mode: WORKFLOW.md generation ─────────────────────────────────
|
|
328
290
|
async function runInteractive(options) {
|
|
329
291
|
p.intro("gh-symphony — WORKFLOW.md Setup");
|
|
330
|
-
// Case A: tenant(s) already configured
|
|
331
|
-
const globalConfig = await loadGlobalConfig(options.configDir);
|
|
332
|
-
if (globalConfig?.tenants?.length) {
|
|
333
|
-
await runInteractiveFromTenant(globalConfig, options);
|
|
334
|
-
return;
|
|
335
|
-
}
|
|
336
|
-
// Case B: no tenants — standalone WORKFLOW.md generation
|
|
337
292
|
await runInteractiveStandalone(options);
|
|
338
293
|
}
|
|
339
|
-
// ──
|
|
340
|
-
async function runInteractiveFromTenant(globalConfig, options) {
|
|
341
|
-
const tenants = globalConfig.tenants;
|
|
342
|
-
let tenantId;
|
|
343
|
-
if (tenants.length === 1) {
|
|
344
|
-
tenantId = tenants[0];
|
|
345
|
-
}
|
|
346
|
-
else {
|
|
347
|
-
// Multiple tenants: ask which one to base WORKFLOW.md on
|
|
348
|
-
const tenantConfigs = await Promise.all(tenants.map(async (id) => {
|
|
349
|
-
const cfg = await loadTenantConfig(options.configDir, id);
|
|
350
|
-
return { id, label: cfg?.slug ?? id };
|
|
351
|
-
}));
|
|
352
|
-
tenantId = await abortIfCancelled(p.select({
|
|
353
|
-
message: "Select a tenant to base WORKFLOW.md on:",
|
|
354
|
-
options: tenantConfigs.map((t) => ({
|
|
355
|
-
value: t.id,
|
|
356
|
-
label: t.label,
|
|
357
|
-
hint: globalConfig.activeTenant === t.id ? "active" : undefined,
|
|
358
|
-
})),
|
|
359
|
-
}));
|
|
360
|
-
}
|
|
361
|
-
const tenantConfig = await loadTenantConfig(options.configDir, tenantId);
|
|
362
|
-
if (!tenantConfig) {
|
|
363
|
-
p.log.error(`Tenant config not found for "${tenantId}".`);
|
|
364
|
-
process.exitCode = 1;
|
|
365
|
-
return;
|
|
366
|
-
}
|
|
367
|
-
const lifecycle = tenantConfig.workflowMapping?.lifecycle;
|
|
368
|
-
if (!lifecycle) {
|
|
369
|
-
p.log.error(`Tenant "${tenantId}" has no workflow lifecycle config. Run 'gh-symphony tenant add' first.`);
|
|
370
|
-
process.exitCode = 1;
|
|
371
|
-
return;
|
|
372
|
-
}
|
|
373
|
-
const mappings = {};
|
|
374
|
-
const workflowMapping = tenantConfig.workflowMapping;
|
|
375
|
-
if (workflowMapping) {
|
|
376
|
-
Object.assign(mappings, workflowMapping.mappings);
|
|
377
|
-
}
|
|
378
|
-
const projectId = tenantConfig.tracker.settings?.projectId;
|
|
379
|
-
const stateFieldName = workflowMapping?.stateFieldName ?? lifecycle.stateFieldName;
|
|
380
|
-
const runtime = await resolveTenantRuntime(options.configDir, tenantId, tenantConfig.runtime.workerCommand);
|
|
381
|
-
const workflowMd = generateWorkflowMarkdown({
|
|
382
|
-
projectId: projectId ?? "",
|
|
383
|
-
stateFieldName,
|
|
384
|
-
mappings,
|
|
385
|
-
lifecycle,
|
|
386
|
-
runtime,
|
|
387
|
-
});
|
|
388
|
-
const outputPath = resolve("WORKFLOW.md");
|
|
389
|
-
await writeFile(outputPath, workflowMd, "utf8");
|
|
390
|
-
const projId = tenantConfig.tracker.settings?.projectId;
|
|
391
|
-
let ecosystemResult = null;
|
|
392
|
-
let token;
|
|
393
|
-
try {
|
|
394
|
-
token = getGhToken();
|
|
395
|
-
}
|
|
396
|
-
catch {
|
|
397
|
-
// getGhToken failed — token stays undefined; ecosystem write proceeds best-effort
|
|
398
|
-
}
|
|
399
|
-
if (token && projId) {
|
|
400
|
-
try {
|
|
401
|
-
const client = createClient(token);
|
|
402
|
-
const fullProject = await getProjectDetail(client, projId);
|
|
403
|
-
const sf = fullProject.statusFields.find((f) => f.name.toLowerCase() === stateFieldName.toLowerCase()) ?? fullProject.statusFields[0];
|
|
404
|
-
if (sf) {
|
|
405
|
-
ecosystemResult = await writeEcosystem({
|
|
406
|
-
cwd: process.cwd(),
|
|
407
|
-
projectDetail: fullProject,
|
|
408
|
-
statusField: sf,
|
|
409
|
-
runtime,
|
|
410
|
-
skipSkills: false,
|
|
411
|
-
skipContext: false,
|
|
412
|
-
});
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
catch {
|
|
416
|
-
// best-effort: don't fail init if GitHub API is unreachable
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
if (ecosystemResult) {
|
|
420
|
-
printEcosystemSummary(ecosystemResult, outputPath, { interactive: true });
|
|
421
|
-
}
|
|
422
|
-
else {
|
|
423
|
-
p.outro(`WORKFLOW.md generated at ${outputPath}`);
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
// ── Case B: Standalone WORKFLOW.md generation (no tenant) ────────────────────
|
|
294
|
+
// ── Interactive WORKFLOW.md generation ────────────────────────────────────────
|
|
427
295
|
async function runInteractiveStandalone(_options) {
|
|
428
296
|
const s1 = p.spinner();
|
|
429
297
|
s1.start("Checking gh CLI authentication...");
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { realpathSync } from "node:fs";
|
|
2
3
|
import { pathToFileURL } from "node:url";
|
|
3
4
|
import { resolveConfigDir } from "./config.js";
|
|
4
5
|
export function parseGlobalOptions(argv) {
|
|
@@ -81,7 +82,7 @@ async function main() {
|
|
|
81
82
|
await runCli(process.argv.slice(2));
|
|
82
83
|
}
|
|
83
84
|
if (process.argv[1] &&
|
|
84
|
-
import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
85
|
+
import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) {
|
|
85
86
|
main().catch((error) => {
|
|
86
87
|
process.stderr.write(`${error instanceof Error ? error.message : "Unknown error"}\n`);
|
|
87
88
|
process.exitCode = 1;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gh-symphony/cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "hojinzs",
|
|
6
6
|
"description": "Interactive CLI for GitHub Symphony orchestration",
|
|
@@ -36,10 +36,10 @@
|
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"@clack/prompts": "^0.9.1",
|
|
39
|
-
"@gh-symphony/
|
|
40
|
-
"@gh-symphony/
|
|
41
|
-
"@gh-symphony/
|
|
42
|
-
"@gh-symphony/
|
|
39
|
+
"@gh-symphony/core": "0.0.3",
|
|
40
|
+
"@gh-symphony/orchestrator": "0.0.3",
|
|
41
|
+
"@gh-symphony/tracker-github": "0.0.3",
|
|
42
|
+
"@gh-symphony/worker": "0.0.3"
|
|
43
43
|
},
|
|
44
44
|
"scripts": {
|
|
45
45
|
"build": "tsc -p tsconfig.json",
|