@gh-symphony/cli 0.0.5 → 0.0.6

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 CHANGED
@@ -61,8 +61,8 @@ The interactive wizard will:
61
61
  1. Authenticate via `gh` CLI
62
62
  2. Let you select a **GitHub Project**
63
63
  3. Select repositories to orchestrate
64
- 4. Auto-detect workflow column mappings
65
- 5. Choose an AI runtime (Codex / Claude Code / custom)
64
+ 4. Optionally limit processing to issues assigned to the authenticated user
65
+ 5. Configure the workspace root directory
66
66
  6. Write tenant configuration to `~/.gh-symphony/`
67
67
 
68
68
  ### Tenant Management
@@ -41,7 +41,7 @@ Global Options:
41
41
 
42
42
  Examples:
43
43
  gh-symphony tenant add # Add a tenant (interactive)
44
- gh-symphony tenant add --non-interactive --project <id>
44
+ gh-symphony tenant add --non-interactive --project <id> --workspace-dir <path>
45
45
  gh-symphony tenant list # List all tenants
46
46
  gh-symphony tenant remove <id> # Remove a tenant
47
47
  gh-symphony start # Start orchestrator
@@ -1,6 +1,5 @@
1
1
  import type { GlobalOptions } from "../index.js";
2
2
  import { type ProjectDetail, type ProjectStatusField, type LinkedRepository } from "../github/client.js";
3
- import { type StateMapping } from "../config.js";
4
3
  export declare function abortIfCancelled<T>(input: T | Promise<T>): Promise<Exclude<T, symbol>>;
5
4
  declare const handler: (args: string[], options: GlobalOptions) => Promise<void>;
6
5
  export default handler;
@@ -27,21 +26,7 @@ type WriteConfigInput = {
27
26
  tenantId: string;
28
27
  project: ProjectDetail;
29
28
  repos: LinkedRepository[];
30
- statusField: {
31
- id: string;
32
- name: string;
33
- options: Array<{
34
- id: string;
35
- name: string;
36
- color?: string | null;
37
- }>;
38
- };
39
- mappings: Record<string, StateMapping>;
40
- runtime: string;
41
- agentCommand?: string;
42
- workerCommand?: string;
43
- pollIntervalMs?: number;
44
- concurrency?: number;
29
+ workspaceDir: string;
45
30
  maxAttempts?: number;
46
31
  assignedOnly?: boolean;
47
32
  };
@@ -2,11 +2,10 @@ import * as p from "@clack/prompts";
2
2
  import { createHash } from "node:crypto";
3
3
  import { mkdir, rename, writeFile } from "node:fs/promises";
4
4
  import { basename, dirname, join, relative, resolve } from "node:path";
5
- import { fileURLToPath } from "node:url";
6
5
  import { createClient, validateToken, checkRequiredScopes, listUserProjects, getProjectDetail, GitHubScopeError, } from "../github/client.js";
7
6
  import { inferAllStateRoles, toWorkflowLifecycleConfig, validateStateMapping, } from "../mapping/smart-defaults.js";
8
7
  import { generateWorkflowMarkdown } from "../workflow/generate-workflow-md.js";
9
- import { loadGlobalConfig, saveGlobalConfig, saveTenantConfig, saveWorkflowMapping, } from "../config.js";
8
+ import { loadGlobalConfig, saveGlobalConfig, saveTenantConfig, } from "../config.js";
10
9
  import { getGhToken, ensureGhAuth, GhAuthError } from "../github/gh-auth.js";
11
10
  import { detectEnvironment } from "../detection/environment-detector.js";
12
11
  import { buildContextYaml, writeContextYaml, } from "../context/generate-context-yaml.js";
@@ -436,29 +435,11 @@ async function runInteractiveStandalone(_options) {
436
435
  nextSteps: "Run 'gh-symphony tenant add' to register a tenant.",
437
436
  });
438
437
  }
439
- function resolveWorkerCommand() {
440
- try {
441
- const url = import.meta.resolve("@gh-symphony/worker/dist/index.js");
442
- return `node ${fileURLToPath(url)}`;
443
- }
444
- catch {
445
- return undefined;
446
- }
447
- }
448
438
  export async function writeConfig(configDir, input) {
449
- const lifecycleConfig = toWorkflowLifecycleConfig(input.statusField.name, input.mappings);
450
- // Save workflow mapping
451
- const mappingConfig = {
452
- stateFieldName: input.statusField.name,
453
- mappings: input.mappings,
454
- lifecycle: lifecycleConfig,
455
- };
456
- await saveWorkflowMapping(configDir, input.tenantId, mappingConfig);
457
- // Save tenant config (OrchestratorTenantConfig shape)
458
- const runtimeDir = `${configDir}/tenants/${input.tenantId}/runtime`;
459
439
  await saveTenantConfig(configDir, input.tenantId, {
460
440
  tenantId: input.tenantId,
461
441
  slug: input.tenantId,
442
+ workspaceDir: input.workspaceDir,
462
443
  repositories: input.repos.map((r) => ({
463
444
  owner: r.owner,
464
445
  name: r.name,
@@ -472,13 +453,6 @@ export async function writeConfig(configDir, input) {
472
453
  ...(input.assignedOnly ? { assignedOnly: true } : {}),
473
454
  },
474
455
  },
475
- runtime: {
476
- driver: "local",
477
- workspaceRuntimeDir: runtimeDir,
478
- projectRoot: process.cwd(),
479
- workerCommand: input.workerCommand ?? resolveWorkerCommand(),
480
- },
481
- workflowMapping: mappingConfig,
482
456
  });
483
457
  // Save/update global config
484
458
  const existing = await loadGlobalConfig(configDir);
@@ -490,18 +464,6 @@ export async function writeConfig(configDir, input) {
490
464
  ],
491
465
  };
492
466
  await saveGlobalConfig(configDir, globalConfig);
493
- // Generate WORKFLOW.md for tenant-level fallback
494
- const workflowMd = generateWorkflowMarkdown({
495
- projectId: input.project.id,
496
- stateFieldName: input.statusField.name,
497
- mappings: input.mappings,
498
- lifecycle: lifecycleConfig,
499
- runtime: input.agentCommand ?? input.runtime,
500
- pollIntervalMs: input.pollIntervalMs,
501
- concurrency: input.concurrency,
502
- });
503
- const workflowMdPath = join(configDir, "tenants", input.tenantId, "WORKFLOW.md");
504
- await writeFile(workflowMdPath, workflowMd, "utf8");
505
467
  }
506
468
  export function generateTenantId(projectTitle, uniqueKey) {
507
469
  const slug = projectTitle
@@ -1,7 +1,6 @@
1
1
  import * as p from "@clack/prompts";
2
2
  import { createClient, validateToken, checkRequiredScopes, listUserProjects, getProjectDetail, GitHubScopeError, } from "../github/client.js";
3
3
  import { ensureGhAuth, getGhToken, GhAuthError } from "../github/gh-auth.js";
4
- import { inferAllStateRoles, toWorkflowLifecycleConfig, validateStateMapping, } from "../mapping/smart-defaults.js";
5
4
  import { loadGlobalConfig, saveGlobalConfig, loadTenantConfig, tenantConfigDir, } from "../config.js";
6
5
  import { writeConfig, generateTenantId, abortIfCancelled } from "./init.js";
7
6
  // ── Scope error display ───────────────────────────────────────────────────────
@@ -29,8 +28,8 @@ function parseTenantAddFlags(args) {
29
28
  flags.project = next;
30
29
  i += 1;
31
30
  break;
32
- case "--runtime":
33
- flags.runtime = next;
31
+ case "--workspace-dir":
32
+ flags.workspaceDir = next;
34
33
  i += 1;
35
34
  break;
36
35
  case "--assigned-only":
@@ -115,37 +114,13 @@ async function tenantAddNonInteractive(flags, options) {
115
114
  process.exitCode = 1;
116
115
  return;
117
116
  }
118
- // Auto-map with smart defaults
119
- const statusField = project.statusFields.find((f) => f.name.toLowerCase() === "status") ??
120
- project.statusFields[0];
121
- if (!statusField) {
122
- process.stderr.write("Error: No status field found on the project.\n");
123
- process.exitCode = 1;
124
- return;
125
- }
126
- const columnNames = statusField.options.map((o) => o.name);
127
- const inferred = inferAllStateRoles(columnNames);
128
- const mappings = {};
129
- for (const mapping of inferred) {
130
- if (mapping.role) {
131
- mappings[mapping.columnName] = { role: mapping.role };
132
- }
133
- }
134
- const validation = validateStateMapping(mappings);
135
- if (!validation.valid) {
136
- process.stderr.write(`Error: Cannot auto-map columns. ${validation.errors.join("; ")}\nRun without --non-interactive for manual mapping.\n`);
137
- process.exitCode = 1;
138
- return;
139
- }
140
- const runtime = flags.runtime ?? "codex";
141
117
  const tenantId = generateTenantId(project.title, project.id);
118
+ const workspaceDir = flags.workspaceDir ?? `${options.configDir}/workspaces`;
142
119
  await writeConfig(options.configDir, {
143
120
  tenantId,
144
121
  project,
145
122
  repos: project.linkedRepositories,
146
- statusField,
147
- mappings,
148
- runtime,
123
+ workspaceDir,
149
124
  assignedOnly: flags.assignedOnly,
150
125
  });
151
126
  if (options.json) {
@@ -253,7 +228,7 @@ async function tenantAddInteractive(options) {
253
228
  process.exitCode = 1;
254
229
  return;
255
230
  }
256
- // ── Step 3: Repository selection ────────────────────────────────────────────
231
+ // ── Step 2: Repository selection ────────────────────────────────────────────
257
232
  if (projectDetail.linkedRepositories.length === 0) {
258
233
  p.log.warn("No linked repositories found in this project. Add issues from repositories to the project first.");
259
234
  process.exitCode = 1;
@@ -267,60 +242,28 @@ async function tenantAddInteractive(options) {
267
242
  })),
268
243
  required: true,
269
244
  }));
270
- // ── Step 4: Status column auto-detection ─────────────────────────────────────
271
- const statusField = projectDetail.statusFields.find((f) => f.name.toLowerCase() === "status") ??
272
- projectDetail.statusFields[0];
273
- if (!statusField) {
274
- p.log.error("No status field found on the project. The project needs a single-select 'Status' field.");
275
- process.exitCode = 1;
276
- return;
277
- }
278
- const columnNames = statusField.options.map((o) => o.name);
279
- const inferred = inferAllStateRoles(columnNames);
280
- const mappings = {};
281
- for (const mapping of inferred) {
282
- if (mapping.role) {
283
- mappings[mapping.columnName] = { role: mapping.role };
284
- }
285
- }
286
- const validation = validateStateMapping(mappings);
287
- if (!validation.valid) {
288
- p.log.error(`Cannot auto-map status columns: ${validation.errors.join("; ")}\nRun 'gh-symphony init' to manually configure WORKFLOW.md.`);
289
- process.exitCode = 1;
290
- return;
291
- }
292
- const lifecycleConfig = toWorkflowLifecycleConfig(statusField.name, mappings);
293
- p.log.info(`Auto-detected workflow: Active=[${lifecycleConfig.activeStates.join(", ")}] Terminal=[${lifecycleConfig.terminalStates.join(", ")}]`);
294
- // ── Step 4: Assignment filter ────────────────────────────────────────────────
245
+ // ── Step 3: Assignment filter ────────────────────────────────────────────────
295
246
  const assignedOnly = await abortIfCancelled(p.confirm({
296
- message: `Step 3/4 — Only process issues assigned to the authenticated GitHub user?`,
247
+ message: "Step 3/4 — Only process issues assigned to the authenticated GitHub user?",
297
248
  initialValue: false,
298
249
  }));
299
- // ── Step 5: Runtime selection ────────────────────────────────────────────────
300
- const runtime = await abortIfCancelled(p.select({
301
- message: "Step 4/4 — Select AI runtime:",
302
- options: [
303
- { value: "codex", label: "OpenAI Codex", hint: "recommended" },
304
- { value: "claude-code", label: "Claude Code" },
305
- { value: "custom", label: "Custom command" },
306
- ],
250
+ const workspaceDir = await abortIfCancelled(p.text({
251
+ message: "Step 4/4 Workspace root directory:",
252
+ placeholder: `${options.configDir}/workspaces`,
253
+ defaultValue: `${options.configDir}/workspaces`,
254
+ validate(value) {
255
+ return value.trim().length > 0
256
+ ? undefined
257
+ : "Workspace directory is required.";
258
+ },
307
259
  }));
308
- let agentCommand;
309
- if (runtime === "custom") {
310
- agentCommand = await abortIfCancelled(p.text({
311
- message: "Custom agent command:",
312
- placeholder: "bash -lc my-agent",
313
- }));
314
- }
315
260
  // ── Confirmation ─────────────────────────────────────────────────────────────
316
261
  p.note([
317
262
  `User: ${login}`,
318
263
  `Project: ${projectDetail.title}`,
319
264
  `Repos: ${selectedRepos.map((r) => `${r.owner}/${r.name}`).join(", ")}`,
320
265
  `Assigned: ${assignedOnly ? `Only issues assigned to ${login}` : "All project issues"}`,
321
- `Runtime: ${runtime}`,
322
- `Active: ${lifecycleConfig.activeStates.join(", ")}`,
323
- `Terminal: ${lifecycleConfig.terminalStates.join(", ")}`,
266
+ `Workspace: ${workspaceDir}`,
324
267
  ].join("\n"), "Configuration Summary");
325
268
  const confirmed = await abortIfCancelled(p.confirm({ message: "Apply this configuration?" }));
326
269
  if (!confirmed) {
@@ -337,14 +280,7 @@ async function tenantAddInteractive(options) {
337
280
  tenantId,
338
281
  project: projectDetail,
339
282
  repos: selectedRepos,
340
- statusField: {
341
- id: statusField.id,
342
- name: statusField.name,
343
- options: statusField.options,
344
- },
345
- mappings,
346
- runtime,
347
- agentCommand,
283
+ workspaceDir,
348
284
  assignedOnly,
349
285
  });
350
286
  s6.stop("Configuration saved.");
@@ -355,7 +291,6 @@ async function tenantAddInteractive(options) {
355
291
  process.exitCode = 1;
356
292
  return;
357
293
  }
358
- p.log.info(`WORKFLOW.md generated at ${tenantId}/WORKFLOW.md — edit it to customize your team policy.`);
359
294
  p.outro(`Tenant "${tenantId}" created!\n Run 'gh-symphony start' to begin orchestration.`);
360
295
  }
361
296
  // ── tenant list ───────────────────────────────────────────────────────────────
package/dist/config.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { OrchestratorTenantConfig, WorkflowLifecycleConfig } from "@gh-symphony/core";
1
+ import type { OrchestratorTenantConfig } from "@gh-symphony/core";
2
2
  export declare const DEFAULT_CONFIG_DIR: string;
3
3
  export declare const CONFIG_FILE = "config.json";
4
4
  export declare const DAEMON_PID_FILE = "daemon.pid";
@@ -15,23 +15,16 @@ export type CliTenantConfig = Omit<OrchestratorTenantConfig, "tracker"> & {
15
15
  tracker: Omit<OrchestratorTenantConfig["tracker"], "settings"> & {
16
16
  settings?: CliTenantTrackerSettings;
17
17
  };
18
- workflowMapping?: WorkflowStateConfig;
19
18
  };
20
19
  export type StateRole = "active" | "wait" | "terminal";
21
20
  export type StateMapping = {
22
21
  role: StateRole;
23
22
  goal?: string;
24
23
  };
25
- export type WorkflowStateConfig = {
26
- stateFieldName: string;
27
- mappings: Record<string, StateMapping>;
28
- lifecycle: WorkflowLifecycleConfig;
29
- };
30
24
  export declare function resolveConfigDir(override?: string): string;
31
25
  export declare function configFilePath(configDir: string): string;
32
26
  export declare function tenantConfigDir(configDir: string, tenantId: string): string;
33
27
  export declare function tenantConfigPath(configDir: string, tenantId: string): string;
34
- export declare function workflowMappingPath(configDir: string, tenantId: string): string;
35
28
  export declare function daemonPidPath(configDir: string): string;
36
29
  export declare function logsDir(configDir: string): string;
37
30
  export declare function orchestratorLogPath(configDir: string): string;
@@ -39,6 +32,4 @@ export declare function loadGlobalConfig(configDir: string): Promise<CliGlobalCo
39
32
  export declare function saveGlobalConfig(configDir: string, config: CliGlobalConfig): Promise<void>;
40
33
  export declare function loadTenantConfig(configDir: string, tenantId: string): Promise<CliTenantConfig | null>;
41
34
  export declare function saveTenantConfig(configDir: string, tenantId: string, config: CliTenantConfig): Promise<void>;
42
- export declare function loadWorkflowMapping(configDir: string, tenantId: string): Promise<WorkflowStateConfig | null>;
43
- export declare function saveWorkflowMapping(configDir: string, tenantId: string, mapping: WorkflowStateConfig): Promise<void>;
44
35
  export declare function loadActiveTenantConfig(configDir: string): Promise<CliTenantConfig | null>;
package/dist/config.js CHANGED
@@ -17,9 +17,6 @@ export function tenantConfigDir(configDir, tenantId) {
17
17
  export function tenantConfigPath(configDir, tenantId) {
18
18
  return join(tenantConfigDir(configDir, tenantId), "tenant.json");
19
19
  }
20
- export function workflowMappingPath(configDir, tenantId) {
21
- return join(tenantConfigDir(configDir, tenantId), "workflow-mapping.json");
22
- }
23
20
  export function daemonPidPath(configDir) {
24
21
  return join(configDir, DAEMON_PID_FILE);
25
22
  }
@@ -41,12 +38,6 @@ export async function loadTenantConfig(configDir, tenantId) {
41
38
  export async function saveTenantConfig(configDir, tenantId, config) {
42
39
  await writeJsonFile(tenantConfigPath(configDir, tenantId), config);
43
40
  }
44
- export async function loadWorkflowMapping(configDir, tenantId) {
45
- return readJsonFile(workflowMappingPath(configDir, tenantId));
46
- }
47
- export async function saveWorkflowMapping(configDir, tenantId, mapping) {
48
- await writeJsonFile(workflowMappingPath(configDir, tenantId), mapping);
49
- }
50
41
  export async function loadActiveTenantConfig(configDir) {
51
42
  const global = await loadGlobalConfig(configDir);
52
43
  if (!global?.activeTenant) {
@@ -1,4 +1,4 @@
1
- import { copyFile, mkdir, writeFile } from "node:fs/promises";
1
+ import { mkdir, writeFile } from "node:fs/promises";
2
2
  import { dirname, join, resolve } from "node:path";
3
3
  import { loadGlobalConfig, loadTenantConfig, } from "./config.js";
4
4
  export function resolveRuntimeRoot(configDir) {
@@ -22,20 +22,5 @@ export async function syncTenantToRuntime(configDir, tenantConfig) {
22
22
  const configPath = orchestratorTenantConfigPath(runtimeRoot, tenantConfig.tenantId);
23
23
  await mkdir(dirname(configPath), { recursive: true });
24
24
  await writeFile(configPath, JSON.stringify(tenantConfig, null, 2) + "\n");
25
- // Copy tenant WORKFLOW.md to runtime if it exists
26
- const workflowSrc = join(configDir, "tenants", tenantConfig.tenantId, "WORKFLOW.md");
27
- const workflowDst = join(dirname(configPath), "WORKFLOW.md");
28
- try {
29
- await copyFile(workflowSrc, workflowDst);
30
- }
31
- catch (error) {
32
- // ENOENT is expected for tenants created before WORKFLOW.md scaffolding
33
- if (!(error &&
34
- typeof error === "object" &&
35
- "code" in error &&
36
- error.code === "ENOENT")) {
37
- throw error;
38
- }
39
- }
40
25
  return runtimeRoot;
41
26
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gh-symphony/cli",
3
- "version": "0.0.5",
3
+ "version": "0.0.6",
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/core": "0.0.5",
40
- "@gh-symphony/orchestrator": "0.0.5",
41
- "@gh-symphony/tracker-github": "0.0.5",
42
- "@gh-symphony/worker": "0.0.5"
39
+ "@gh-symphony/core": "0.0.6",
40
+ "@gh-symphony/orchestrator": "0.0.6",
41
+ "@gh-symphony/tracker-github": "0.0.6",
42
+ "@gh-symphony/worker": "0.0.6"
43
43
  },
44
44
  "scripts": {
45
45
  "build": "tsc -p tsconfig.json",