@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 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
+ ```
@@ -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;
@@ -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, readFile, rename, writeFile } from "node:fs/promises";
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, loadTenantConfig, saveGlobalConfig, saveTenantConfig, saveWorkflowMapping, } from "../config.js";
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
- // ── Case A: Generate WORKFLOW.md from existing tenant config ─────────────────
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.2",
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/orchestrator": "0.0.2",
40
- "@gh-symphony/core": "0.0.2",
41
- "@gh-symphony/worker": "0.0.2",
42
- "@gh-symphony/tracker-github": "0.0.2"
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",