@gh-symphony/cli 0.0.5 → 0.0.7

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.
@@ -1,10 +1,12 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
- import { resolveRuntimeRoot, resolveTenantConfig, syncTenantToRuntime, } from "../orchestrator-runtime.js";
3
+ import { resolveRuntimeRoot, resolveProjectConfig, syncProjectToRuntime, } from "../orchestrator-runtime.js";
4
4
  import { bold, dim, green, red, yellow, cyan, stripAnsi } from "../ansi.js";
5
5
  import { clearScreen, showCursor, hideCursor } from "../ansi.js";
6
6
  import { renderDashboard } from "../dashboard/renderer.js";
7
+ import { resolveProjectOrchestratorStatusBaseUrl } from "../orchestrator-status-endpoint.js";
7
8
  import { requestOrchestratorRefresh } from "./status-refresh.js";
9
+ import { parseCliArgs } from "./parse-cli-args.js";
8
10
  const WATCH_REFRESH_TIMEOUT_MS = 1_500;
9
11
  function healthIcon(health) {
10
12
  switch (health) {
@@ -107,22 +109,22 @@ function renderLegacyStatus(snapshot, noColor) {
107
109
  return lines.join("\n");
108
110
  }
109
111
  function parseStatusArgs(args) {
110
- const parsed = { watch: false };
111
- for (let i = 0; i < args.length; i += 1) {
112
- const arg = args[i];
113
- if (arg === "--watch" || arg === "-w") {
114
- parsed.watch = true;
115
- }
116
- if (arg === "--tenant" || arg === "--tenant-id") {
117
- parsed.tenantId = args[i + 1];
118
- i += 1;
119
- }
120
- }
121
- return parsed;
112
+ const parsed = parseCliArgs(args, {
113
+ watch: { type: "boolean", short: "w" },
114
+ project: { type: "string" },
115
+ "project-id": { type: "string" },
116
+ });
117
+ if ("error" in parsed) {
118
+ return { watch: false, error: parsed.error };
119
+ }
120
+ return {
121
+ watch: Boolean(parsed.values.watch),
122
+ projectId: (parsed.values["project-id"] ?? parsed.values.project),
123
+ };
122
124
  }
123
- async function readStatusSnapshot(runtimeRoot, tenantId) {
125
+ async function readStatusSnapshot(runtimeRoot, projectId) {
124
126
  try {
125
- const statusPath = join(runtimeRoot, "orchestrator", "tenants", tenantId, "status.json");
127
+ const statusPath = join(runtimeRoot, "orchestrator", "projects", projectId, "status.json");
126
128
  const content = await readFile(statusPath, "utf-8");
127
129
  return JSON.parse(content);
128
130
  }
@@ -130,56 +132,47 @@ async function readStatusSnapshot(runtimeRoot, tenantId) {
130
132
  return null;
131
133
  }
132
134
  }
133
- async function readAllStatusSnapshots(runtimeRoot) {
134
- try {
135
- const tenantsDir = join(runtimeRoot, "orchestrator", "tenants");
136
- const { readdir } = await import("node:fs/promises");
137
- const entries = await readdir(tenantsDir, { withFileTypes: true });
138
- const snapshots = [];
139
- for (const entry of entries) {
140
- if (!entry.isDirectory())
141
- continue;
142
- const statusPath = join(tenantsDir, entry.name, "status.json");
143
- try {
144
- const content = await readFile(statusPath, "utf-8");
145
- snapshots.push(JSON.parse(content));
146
- }
147
- catch {
148
- // skip missing/invalid files
149
- }
150
- }
151
- return snapshots;
152
- }
153
- catch {
154
- return [];
155
- }
156
- }
157
135
  const handler = async (args, options) => {
158
136
  const parsed = parseStatusArgs(args);
159
- const tenantConfig = await resolveTenantConfig(options.configDir, parsed.tenantId);
160
- if (!tenantConfig) {
161
- process.stderr.write("No tenant configured. Run 'gh-symphony init' first.\n");
137
+ if (parsed.error) {
138
+ process.stderr.write(`${parsed.error}\n`);
139
+ process.stderr.write("Usage: gh-symphony status [--project-id <project-id>] [--watch]\n");
140
+ process.exitCode = 2;
141
+ return;
142
+ }
143
+ const projectConfig = await resolveProjectConfig(options.configDir, parsed.projectId);
144
+ if (!projectConfig) {
145
+ process.stderr.write("No project configured. Run 'gh-symphony project add' first.\n");
162
146
  process.exitCode = 1;
163
147
  return;
164
148
  }
165
149
  const runtimeRoot = resolveRuntimeRoot(options.configDir);
166
- const tenantId = tenantConfig.tenantId;
167
- await syncTenantToRuntime(options.configDir, tenantConfig);
150
+ const projectId = projectConfig.projectId;
151
+ await syncProjectToRuntime(options.configDir, projectConfig);
168
152
  if (parsed.watch) {
169
153
  const isTTY = process.stdout.isTTY === true;
170
154
  let terminalWidth = process.stdout.columns ?? 115;
171
155
  let runPromise = null;
172
156
  const run = async () => {
157
+ const baseUrl = await resolveProjectOrchestratorStatusBaseUrl({
158
+ configDir: options.configDir,
159
+ projectId,
160
+ });
173
161
  await requestOrchestratorRefresh({
162
+ baseUrl,
174
163
  timeoutMs: WATCH_REFRESH_TIMEOUT_MS,
175
164
  });
176
- const snapshots = await readAllStatusSnapshots(runtimeRoot);
165
+ const snapshot = await readStatusSnapshot(runtimeRoot, projectId);
177
166
  if (options.json || !isTTY) {
178
- process.stdout.write(JSON.stringify(snapshots, null, 2) + "\n");
167
+ process.stdout.write(JSON.stringify(snapshot, null, 2) + "\n");
179
168
  }
180
169
  else {
170
+ if (!snapshot) {
171
+ process.stdout.write(clearScreen() + "Unable to read status snapshot.\n");
172
+ return;
173
+ }
181
174
  process.stdout.write(clearScreen() +
182
- renderDashboard(snapshots, {
175
+ renderDashboard([snapshot], {
183
176
  terminalWidth,
184
177
  noColor: options.noColor,
185
178
  }) +
@@ -213,7 +206,7 @@ const handler = async (args, options) => {
213
206
  await new Promise(() => { });
214
207
  }
215
208
  // Single status query
216
- const snapshot = await readStatusSnapshot(runtimeRoot, tenantId);
209
+ const snapshot = await readStatusSnapshot(runtimeRoot, projectId);
217
210
  if (snapshot) {
218
211
  if (options.json) {
219
212
  process.stdout.write(JSON.stringify(snapshot, null, 2) + "\n");
@@ -1,17 +1,43 @@
1
1
  import { readFile, rm } from "node:fs/promises";
2
- import { daemonPidPath } from "../config.js";
2
+ import { daemonPidPath, orchestratorPortPath } from "../config.js";
3
+ import { parseCliArgs } from "./parse-cli-args.js";
3
4
  function parseStopArgs(args) {
4
- return { force: args.includes("--force") };
5
+ const parsed = parseCliArgs(args, {
6
+ force: { type: "boolean" },
7
+ project: { type: "string" },
8
+ "project-id": { type: "string" },
9
+ });
10
+ if ("error" in parsed) {
11
+ return { force: false, error: parsed.error };
12
+ }
13
+ return {
14
+ force: Boolean(parsed.values.force),
15
+ projectId: (parsed.values["project-id"] ?? parsed.values.project),
16
+ };
5
17
  }
6
18
  const handler = async (args, options) => {
7
- const { force } = parseStopArgs(args);
8
- const pidPath = daemonPidPath(options.configDir);
19
+ const parsed = parseStopArgs(args);
20
+ if (parsed.error) {
21
+ process.stderr.write(`${parsed.error}\n`);
22
+ process.stderr.write("Usage: gh-symphony stop --project-id <project-id> [--force]\n");
23
+ process.exitCode = 2;
24
+ return;
25
+ }
26
+ if (!parsed.projectId) {
27
+ process.stderr.write("Usage: gh-symphony stop --project-id <project-id> [--force]\n");
28
+ process.exitCode = 2;
29
+ return;
30
+ }
31
+ const resolvedForce = parsed.force;
32
+ const resolvedProjectId = parsed.projectId;
33
+ const pidPath = daemonPidPath(options.configDir, resolvedProjectId);
34
+ const portPath = orchestratorPortPath(options.configDir, resolvedProjectId);
9
35
  let pidStr;
10
36
  try {
11
37
  pidStr = await readFile(pidPath, "utf8");
12
38
  }
13
39
  catch {
14
- process.stderr.write("No running daemon found (PID file missing).\n");
40
+ process.stderr.write(`No running daemon found for project "${resolvedProjectId}" (PID file missing).\n`);
15
41
  process.exitCode = 1;
16
42
  return;
17
43
  }
@@ -26,11 +52,12 @@ const handler = async (args, options) => {
26
52
  process.kill(pid, 0);
27
53
  }
28
54
  catch {
29
- process.stdout.write(`Daemon (PID ${pid}) is not running. Cleaning up PID file.\n`);
55
+ process.stdout.write(`Daemon for project "${resolvedProjectId}" (PID ${pid}) is not running. Cleaning up PID file.\n`);
30
56
  await rm(pidPath, { force: true });
57
+ await rm(portPath, { force: true });
31
58
  return;
32
59
  }
33
- const signal = force ? "SIGKILL" : "SIGTERM";
60
+ const signal = resolvedForce ? "SIGKILL" : "SIGTERM";
34
61
  try {
35
62
  process.kill(pid, signal);
36
63
  process.stdout.write(`Sent ${signal} to orchestrator (PID ${pid}).\n`);
@@ -41,6 +68,9 @@ const handler = async (args, options) => {
41
68
  return;
42
69
  }
43
70
  await rm(pidPath, { force: true });
71
+ if (resolvedForce) {
72
+ await rm(portPath, { force: true });
73
+ }
44
74
  process.stdout.write("Daemon stopped.\n");
45
75
  };
46
76
  export default handler;
@@ -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,44 +1,37 @@
1
- import type { OrchestratorTenantConfig, WorkflowLifecycleConfig } from "@gh-symphony/core";
1
+ import type { OrchestratorProjectConfig } 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";
5
- export declare const LOGS_DIR = "logs";
5
+ export declare const ORCHESTRATOR_LOG_FILE = "orchestrator.log";
6
+ export declare const ORCHESTRATOR_PORT_FILE = "port";
6
7
  export type CliGlobalConfig = {
7
- activeTenant: string | null;
8
- tenants: string[];
8
+ activeProject: string | null;
9
+ projects: string[];
9
10
  };
10
- export type CliTenantTrackerSettings = Record<string, string | boolean> & {
11
+ export type CliProjectTrackerSettings = Record<string, string | boolean> & {
11
12
  projectId?: string;
12
13
  assignedOnly?: boolean;
13
14
  };
14
- export type CliTenantConfig = Omit<OrchestratorTenantConfig, "tracker"> & {
15
- tracker: Omit<OrchestratorTenantConfig["tracker"], "settings"> & {
16
- settings?: CliTenantTrackerSettings;
15
+ export type CliProjectConfig = Omit<OrchestratorProjectConfig, "tracker"> & {
16
+ displayName?: string;
17
+ tracker: Omit<OrchestratorProjectConfig["tracker"], "settings"> & {
18
+ settings?: CliProjectTrackerSettings;
17
19
  };
18
- workflowMapping?: WorkflowStateConfig;
19
20
  };
20
21
  export type StateRole = "active" | "wait" | "terminal";
21
22
  export type StateMapping = {
22
23
  role: StateRole;
23
24
  goal?: string;
24
25
  };
25
- export type WorkflowStateConfig = {
26
- stateFieldName: string;
27
- mappings: Record<string, StateMapping>;
28
- lifecycle: WorkflowLifecycleConfig;
29
- };
30
26
  export declare function resolveConfigDir(override?: string): string;
31
27
  export declare function configFilePath(configDir: string): string;
32
- export declare function tenantConfigDir(configDir: string, tenantId: string): string;
33
- export declare function tenantConfigPath(configDir: string, tenantId: string): string;
34
- export declare function workflowMappingPath(configDir: string, tenantId: string): string;
35
- export declare function daemonPidPath(configDir: string): string;
36
- export declare function logsDir(configDir: string): string;
37
- export declare function orchestratorLogPath(configDir: string): string;
28
+ export declare function projectConfigDir(configDir: string, projectId: string): string;
29
+ export declare function projectConfigPath(configDir: string, projectId: string): string;
30
+ export declare function daemonPidPath(configDir: string, projectId: string): string;
31
+ export declare function orchestratorLogPath(configDir: string, projectId: string): string;
32
+ export declare function orchestratorPortPath(configDir: string, projectId: string): string;
38
33
  export declare function loadGlobalConfig(configDir: string): Promise<CliGlobalConfig | null>;
39
34
  export declare function saveGlobalConfig(configDir: string, config: CliGlobalConfig): Promise<void>;
40
- export declare function loadTenantConfig(configDir: string, tenantId: string): Promise<CliTenantConfig | null>;
41
- 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
- export declare function loadActiveTenantConfig(configDir: string): Promise<CliTenantConfig | null>;
35
+ export declare function loadProjectConfig(configDir: string, projectId: string): Promise<CliProjectConfig | null>;
36
+ export declare function saveProjectConfig(configDir: string, projectId: string, config: CliProjectConfig): Promise<void>;
37
+ export declare function loadActiveProjectConfig(configDir: string): Promise<CliProjectConfig | null>;
package/dist/config.js CHANGED
@@ -4,55 +4,56 @@ import { homedir } from "node:os";
4
4
  export const DEFAULT_CONFIG_DIR = join(homedir(), ".gh-symphony");
5
5
  export const CONFIG_FILE = "config.json";
6
6
  export const DAEMON_PID_FILE = "daemon.pid";
7
- export const LOGS_DIR = "logs";
7
+ export const ORCHESTRATOR_LOG_FILE = "orchestrator.log";
8
+ export const ORCHESTRATOR_PORT_FILE = "port";
8
9
  export function resolveConfigDir(override) {
9
10
  return override ?? process.env.GH_SYMPHONY_CONFIG_DIR ?? DEFAULT_CONFIG_DIR;
10
11
  }
11
12
  export function configFilePath(configDir) {
12
13
  return join(configDir, CONFIG_FILE);
13
14
  }
14
- export function tenantConfigDir(configDir, tenantId) {
15
- return join(configDir, "tenants", tenantId);
15
+ export function projectConfigDir(configDir, projectId) {
16
+ return join(configDir, "projects", projectId);
16
17
  }
17
- export function tenantConfigPath(configDir, tenantId) {
18
- return join(tenantConfigDir(configDir, tenantId), "tenant.json");
18
+ export function projectConfigPath(configDir, projectId) {
19
+ return join(projectConfigDir(configDir, projectId), "project.json");
19
20
  }
20
- export function workflowMappingPath(configDir, tenantId) {
21
- return join(tenantConfigDir(configDir, tenantId), "workflow-mapping.json");
21
+ export function daemonPidPath(configDir, projectId) {
22
+ return join(projectConfigDir(configDir, projectId), DAEMON_PID_FILE);
22
23
  }
23
- export function daemonPidPath(configDir) {
24
- return join(configDir, DAEMON_PID_FILE);
24
+ export function orchestratorLogPath(configDir, projectId) {
25
+ return join(projectConfigDir(configDir, projectId), ORCHESTRATOR_LOG_FILE);
25
26
  }
26
- export function logsDir(configDir) {
27
- return join(configDir, LOGS_DIR);
28
- }
29
- export function orchestratorLogPath(configDir) {
30
- return join(logsDir(configDir), "orchestrator.log");
27
+ export function orchestratorPortPath(configDir, projectId) {
28
+ return join(projectConfigDir(configDir, projectId), ORCHESTRATOR_PORT_FILE);
31
29
  }
32
30
  export async function loadGlobalConfig(configDir) {
33
- return readJsonFile(configFilePath(configDir));
31
+ const raw = await readJsonFile(configFilePath(configDir));
32
+ if (!raw) {
33
+ return null;
34
+ }
35
+ return {
36
+ activeProject: typeof raw.activeProject === "string" ? raw.activeProject : null,
37
+ projects: Array.isArray(raw.projects)
38
+ ? raw.projects.filter((projectId) => typeof projectId === "string")
39
+ : [],
40
+ };
34
41
  }
35
42
  export async function saveGlobalConfig(configDir, config) {
36
43
  await writeJsonFile(configFilePath(configDir), config);
37
44
  }
38
- export async function loadTenantConfig(configDir, tenantId) {
39
- return readJsonFile(tenantConfigPath(configDir, tenantId));
40
- }
41
- export async function saveTenantConfig(configDir, tenantId, config) {
42
- await writeJsonFile(tenantConfigPath(configDir, tenantId), config);
43
- }
44
- export async function loadWorkflowMapping(configDir, tenantId) {
45
- return readJsonFile(workflowMappingPath(configDir, tenantId));
45
+ export async function loadProjectConfig(configDir, projectId) {
46
+ return readJsonFile(projectConfigPath(configDir, projectId));
46
47
  }
47
- export async function saveWorkflowMapping(configDir, tenantId, mapping) {
48
- await writeJsonFile(workflowMappingPath(configDir, tenantId), mapping);
48
+ export async function saveProjectConfig(configDir, projectId, config) {
49
+ await writeJsonFile(projectConfigPath(configDir, projectId), config);
49
50
  }
50
- export async function loadActiveTenantConfig(configDir) {
51
+ export async function loadActiveProjectConfig(configDir) {
51
52
  const global = await loadGlobalConfig(configDir);
52
- if (!global?.activeTenant) {
53
+ if (!global?.activeProject) {
53
54
  return null;
54
55
  }
55
- return loadTenantConfig(configDir, global.activeTenant);
56
+ return loadProjectConfig(configDir, global.activeProject);
56
57
  }
57
58
  async function readJsonFile(path) {
58
59
  try {
@@ -1,4 +1,4 @@
1
- import type { TenantStatusSnapshot } from "@gh-symphony/core";
1
+ import type { ProjectStatusSnapshot } from "@gh-symphony/core";
2
2
  export type DashboardOptions = {
3
3
  terminalWidth: number;
4
4
  noColor: boolean;
@@ -6,4 +6,4 @@ export type DashboardOptions = {
6
6
  /** Override Date.now() for deterministic testing */
7
7
  now?: number;
8
8
  };
9
- export declare function renderDashboard(snapshots: TenantStatusSnapshot[], options: DashboardOptions): string;
9
+ export declare function renderDashboard(snapshots: ProjectStatusSnapshot[], options: DashboardOptions): string;
@@ -2,7 +2,7 @@
2
2
  import { bold, dim, green, red, yellow, cyan, magenta, blue, stripAnsi, } from "../ansi.js";
3
3
  // ── Column widths (from Elixir spec) ─────────────────────────────────────────
4
4
  const COL_ID = 24;
5
- const COL_STAGE = 14;
5
+ const COL_STATUS = 14;
6
6
  const COL_PID = 8;
7
7
  const COL_AGE_TURN = 12;
8
8
  const COL_TOKENS = 10;
@@ -81,7 +81,7 @@ const COL_SEPARATORS = 6;
81
81
  function eventColWidth(termWidth) {
82
82
  const fixed = 2 +
83
83
  COL_ID_HEADER +
84
- COL_STAGE +
84
+ COL_STATUS +
85
85
  COL_PID +
86
86
  COL_AGE_TURN +
87
87
  COL_TOKENS +
@@ -140,7 +140,7 @@ function buildSummaryLines(snapshots, options, c) {
140
140
  function tableHeaderRow(c) {
141
141
  const cols = [
142
142
  pad("ID", COL_ID_HEADER),
143
- pad("STAGE", COL_STAGE),
143
+ pad("STATUS", COL_STATUS),
144
144
  pad("PID", COL_PID),
145
145
  pad("AGE/TURN", COL_AGE_TURN),
146
146
  pad("TOKENS", COL_TOKENS),
@@ -152,7 +152,7 @@ function tableHeaderRow(c) {
152
152
  function activeRunRow(run, now, evtWidth, c) {
153
153
  const dot = statusDot(run, c);
154
154
  const id = pad(run.issueIdentifier, COL_ID);
155
- const stage = pad(run.issueState || "\u2014", COL_STAGE);
155
+ const status = pad(run.issueState ?? run.executionPhase ?? "\u2014", COL_STATUS);
156
156
  const pid = pad(run.processId != null ? String(run.processId) : "\u2014", COL_PID);
157
157
  const age = fmtAge(run.startedAt, now);
158
158
  const turn = run.turnCount ?? 0;
@@ -161,7 +161,7 @@ function activeRunRow(run, now, evtWidth, c) {
161
161
  const sessionId = run.runtimeSession?.sessionId ?? run.runtimeSession?.threadId ?? null;
162
162
  const session = pad(compactSessionId(sessionId), COL_SESSION);
163
163
  const event = pad(run.lastEvent ?? "\u2014", evtWidth);
164
- const columns = [id, stage, pid, ageTurn, tokens, session, event].join(" ");
164
+ const columns = [id, status, pid, ageTurn, tokens, session, event].join(" ");
165
165
  return ` ${dot} ${columns}`;
166
166
  }
167
167
  function retryRow(entry, snapshot, now, c) {
package/dist/index.js CHANGED
@@ -49,7 +49,6 @@ const COMMANDS = {
49
49
  logs: () => import("./commands/logs.js"),
50
50
  project: () => import("./commands/project.js"),
51
51
  repo: () => import("./commands/repo.js"),
52
- tenant: () => import("./commands/tenant.js"),
53
52
  config: () => import("./commands/config-cmd.js"),
54
53
  help: () => import("./commands/help.js"),
55
54
  version: () => import("./commands/version.js"),
@@ -1,5 +1,5 @@
1
- import { type CliTenantConfig } from "./config.js";
1
+ import { type CliProjectConfig } from "./config.js";
2
2
  export declare function resolveRuntimeRoot(configDir: string): string;
3
- export declare function resolveTenantConfig(configDir: string, requestedTenantId?: string): Promise<CliTenantConfig | null>;
4
- export declare function orchestratorTenantConfigPath(runtimeRoot: string, tenantId: string): string;
5
- export declare function syncTenantToRuntime(configDir: string, tenantConfig: CliTenantConfig): Promise<string>;
3
+ export declare function resolveProjectConfig(configDir: string, requestedProjectId?: string): Promise<CliProjectConfig | null>;
4
+ export declare function orchestratorProjectConfigPath(runtimeRoot: string, projectId: string): string;
5
+ export declare function syncProjectToRuntime(configDir: string, projectConfig: CliProjectConfig): Promise<string>;