@gh-symphony/cli 0.0.6 → 0.0.8

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,9 +1,11 @@
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, syncProjectToRuntime, } from "../orchestrator-runtime.js";
4
+ import { handleMissingManagedProjectConfig, resolveManagedProjectConfig, } from "../project-selection.js";
4
5
  import { bold, dim, green, red, yellow, cyan, stripAnsi } from "../ansi.js";
5
6
  import { clearScreen, showCursor, hideCursor } from "../ansi.js";
6
7
  import { renderDashboard } from "../dashboard/renderer.js";
8
+ import { resolveProjectOrchestratorStatusBaseUrl } from "../orchestrator-status-endpoint.js";
7
9
  import { requestOrchestratorRefresh } from "./status-refresh.js";
8
10
  const WATCH_REFRESH_TIMEOUT_MS = 1_500;
9
11
  function healthIcon(health) {
@@ -107,22 +109,35 @@ function renderLegacyStatus(snapshot, noColor) {
107
109
  return lines.join("\n");
108
110
  }
109
111
  function parseStatusArgs(args) {
110
- const parsed = { watch: false };
112
+ const parsed = {
113
+ watch: false,
114
+ };
111
115
  for (let i = 0; i < args.length; i += 1) {
112
116
  const arg = args[i];
113
117
  if (arg === "--watch" || arg === "-w") {
114
118
  parsed.watch = true;
119
+ continue;
115
120
  }
116
- if (arg === "--tenant" || arg === "--tenant-id") {
117
- parsed.tenantId = args[i + 1];
121
+ if (arg === "--project" || arg === "--project-id") {
122
+ const value = args[i + 1];
123
+ if (!value || value.startsWith("-")) {
124
+ parsed.error = `Option '${arg}' argument missing`;
125
+ return parsed;
126
+ }
127
+ parsed.projectId = value;
118
128
  i += 1;
129
+ continue;
130
+ }
131
+ if (arg?.startsWith("-")) {
132
+ parsed.error = `Unknown option '${arg}'`;
133
+ return parsed;
119
134
  }
120
135
  }
121
136
  return parsed;
122
137
  }
123
- async function readStatusSnapshot(runtimeRoot, tenantId) {
138
+ async function readStatusSnapshot(runtimeRoot, projectId) {
124
139
  try {
125
- const statusPath = join(runtimeRoot, "orchestrator", "tenants", tenantId, "status.json");
140
+ const statusPath = join(runtimeRoot, "orchestrator", "projects", projectId, "status.json");
126
141
  const content = await readFile(statusPath, "utf-8");
127
142
  return JSON.parse(content);
128
143
  }
@@ -130,56 +145,49 @@ async function readStatusSnapshot(runtimeRoot, tenantId) {
130
145
  return null;
131
146
  }
132
147
  }
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
148
  const handler = async (args, options) => {
158
149
  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");
162
- process.exitCode = 1;
150
+ if (parsed.error) {
151
+ process.stderr.write(`${parsed.error}\n`);
152
+ process.stderr.write("Usage: gh-symphony status [--project-id <project-id>] [--watch]\n");
153
+ process.exitCode = 2;
154
+ return;
155
+ }
156
+ const projectConfig = await resolveManagedProjectConfig({
157
+ configDir: options.configDir,
158
+ requestedProjectId: parsed.projectId,
159
+ });
160
+ if (!projectConfig) {
161
+ handleMissingManagedProjectConfig();
163
162
  return;
164
163
  }
165
164
  const runtimeRoot = resolveRuntimeRoot(options.configDir);
166
- const tenantId = tenantConfig.tenantId;
167
- await syncTenantToRuntime(options.configDir, tenantConfig);
165
+ const projectId = projectConfig.projectId;
166
+ await syncProjectToRuntime(options.configDir, projectConfig);
168
167
  if (parsed.watch) {
169
168
  const isTTY = process.stdout.isTTY === true;
170
169
  let terminalWidth = process.stdout.columns ?? 115;
171
170
  let runPromise = null;
172
171
  const run = async () => {
172
+ const baseUrl = await resolveProjectOrchestratorStatusBaseUrl({
173
+ configDir: options.configDir,
174
+ projectId,
175
+ });
173
176
  await requestOrchestratorRefresh({
177
+ baseUrl,
174
178
  timeoutMs: WATCH_REFRESH_TIMEOUT_MS,
175
179
  });
176
- const snapshots = await readAllStatusSnapshots(runtimeRoot);
180
+ const snapshot = await readStatusSnapshot(runtimeRoot, projectId);
177
181
  if (options.json || !isTTY) {
178
- process.stdout.write(JSON.stringify(snapshots, null, 2) + "\n");
182
+ process.stdout.write(JSON.stringify(snapshot, null, 2) + "\n");
179
183
  }
180
184
  else {
185
+ if (!snapshot) {
186
+ process.stdout.write(clearScreen() + "Unable to read status snapshot.\n");
187
+ return;
188
+ }
181
189
  process.stdout.write(clearScreen() +
182
- renderDashboard(snapshots, {
190
+ renderDashboard([snapshot], {
183
191
  terminalWidth,
184
192
  noColor: options.noColor,
185
193
  }) +
@@ -213,7 +221,7 @@ const handler = async (args, options) => {
213
221
  await new Promise(() => { });
214
222
  }
215
223
  // Single status query
216
- const snapshot = await readStatusSnapshot(runtimeRoot, tenantId);
224
+ const snapshot = await readStatusSnapshot(runtimeRoot, projectId);
217
225
  if (snapshot) {
218
226
  if (options.json) {
219
227
  process.stdout.write(JSON.stringify(snapshot, null, 2) + "\n");
@@ -1,17 +1,59 @@
1
1
  import { readFile, rm } from "node:fs/promises";
2
- import { daemonPidPath } from "../config.js";
2
+ import { daemonPidPath, orchestratorPortPath } from "../config.js";
3
+ import { handleMissingManagedProjectConfig, resolveManagedProjectConfig, } from "../project-selection.js";
3
4
  function parseStopArgs(args) {
4
- return { force: args.includes("--force") };
5
+ const parsed = {
6
+ force: false,
7
+ };
8
+ for (let i = 0; i < args.length; i += 1) {
9
+ const arg = args[i];
10
+ if (arg === "--force") {
11
+ parsed.force = true;
12
+ continue;
13
+ }
14
+ if (arg === "--project" || arg === "--project-id") {
15
+ const value = args[i + 1];
16
+ if (!value || value.startsWith("-")) {
17
+ parsed.error = `Option '${arg}' argument missing`;
18
+ return parsed;
19
+ }
20
+ parsed.projectId = value;
21
+ i += 1;
22
+ continue;
23
+ }
24
+ if (arg?.startsWith("-")) {
25
+ parsed.error = `Unknown option '${arg}'`;
26
+ return parsed;
27
+ }
28
+ }
29
+ return parsed;
5
30
  }
6
31
  const handler = async (args, options) => {
7
- const { force } = parseStopArgs(args);
8
- const pidPath = daemonPidPath(options.configDir);
32
+ const parsed = parseStopArgs(args);
33
+ if (parsed.error) {
34
+ process.stderr.write(`${parsed.error}\n`);
35
+ process.stderr.write("Usage: gh-symphony stop --project-id <project-id> [--force]\n");
36
+ process.exitCode = 2;
37
+ return;
38
+ }
39
+ const resolvedForce = parsed.force;
40
+ const projectConfig = await resolveManagedProjectConfig({
41
+ configDir: options.configDir,
42
+ requestedProjectId: parsed.projectId,
43
+ });
44
+ if (!projectConfig) {
45
+ handleMissingManagedProjectConfig();
46
+ return;
47
+ }
48
+ const resolvedProjectId = projectConfig.projectId;
49
+ const pidPath = daemonPidPath(options.configDir, resolvedProjectId);
50
+ const portPath = orchestratorPortPath(options.configDir, resolvedProjectId);
9
51
  let pidStr;
10
52
  try {
11
53
  pidStr = await readFile(pidPath, "utf8");
12
54
  }
13
55
  catch {
14
- process.stderr.write("No running daemon found (PID file missing).\n");
56
+ process.stderr.write(`No running daemon found for project "${resolvedProjectId}" (PID file missing).\n`);
15
57
  process.exitCode = 1;
16
58
  return;
17
59
  }
@@ -26,11 +68,12 @@ const handler = async (args, options) => {
26
68
  process.kill(pid, 0);
27
69
  }
28
70
  catch {
29
- process.stdout.write(`Daemon (PID ${pid}) is not running. Cleaning up PID file.\n`);
71
+ process.stdout.write(`Daemon for project "${resolvedProjectId}" (PID ${pid}) is not running. Cleaning up PID file.\n`);
30
72
  await rm(pidPath, { force: true });
73
+ await rm(portPath, { force: true });
31
74
  return;
32
75
  }
33
- const signal = force ? "SIGKILL" : "SIGTERM";
76
+ const signal = resolvedForce ? "SIGKILL" : "SIGTERM";
34
77
  try {
35
78
  process.kill(pid, signal);
36
79
  process.stdout.write(`Sent ${signal} to orchestrator (PID ${pid}).\n`);
@@ -41,6 +84,9 @@ const handler = async (args, options) => {
41
84
  return;
42
85
  }
43
86
  await rm(pidPath, { force: true });
87
+ if (resolvedForce) {
88
+ await rm(portPath, { force: true });
89
+ }
44
90
  process.stdout.write("Daemon stopped.\n");
45
91
  };
46
92
  export default handler;
@@ -0,0 +1 @@
1
+ export declare function renderCompletionScript(shell: "bash" | "zsh" | "fish"): string;
@@ -0,0 +1,204 @@
1
+ const TOP_LEVEL_COMMANDS = [
2
+ "init",
3
+ "start",
4
+ "stop",
5
+ "status",
6
+ "run",
7
+ "recover",
8
+ "logs",
9
+ "project",
10
+ "repo",
11
+ "config",
12
+ "completion",
13
+ "help",
14
+ "version",
15
+ ];
16
+ const GLOBAL_OPTIONS = [
17
+ "--config",
18
+ "--config-dir",
19
+ "--verbose",
20
+ "-v",
21
+ "--json",
22
+ "--no-color",
23
+ "--help",
24
+ "-h",
25
+ "--version",
26
+ "-V",
27
+ ];
28
+ const GLOBAL_OPTIONS_WITH_VALUES = ["--config", "--config-dir"];
29
+ const COMMAND_OPTIONS = {
30
+ completion: ["bash", "zsh", "fish"],
31
+ start: ["--project-id", "--project", "--daemon", "-d", ...GLOBAL_OPTIONS],
32
+ stop: ["--project-id", "--project", "--force", ...GLOBAL_OPTIONS],
33
+ status: ["--project-id", "--project", "--watch", "-w", ...GLOBAL_OPTIONS],
34
+ run: ["--project-id", "--project", "--watch", "-w", ...GLOBAL_OPTIONS],
35
+ recover: ["--project-id", "--project", "--dry-run", ...GLOBAL_OPTIONS],
36
+ logs: [
37
+ "--project-id",
38
+ "--project",
39
+ "--follow",
40
+ "-f",
41
+ "--issue",
42
+ "--run",
43
+ "--level",
44
+ ...GLOBAL_OPTIONS,
45
+ ],
46
+ project: ["add", "list", "remove", "start", "stop", "switch", "status"],
47
+ "project:add": [
48
+ "--non-interactive",
49
+ "--project",
50
+ "--workspace-dir",
51
+ "--assigned-only",
52
+ ...GLOBAL_OPTIONS,
53
+ ],
54
+ "project:list": [...GLOBAL_OPTIONS],
55
+ "project:remove": [...GLOBAL_OPTIONS],
56
+ "project:start": [
57
+ "--project-id",
58
+ "--project",
59
+ "--daemon",
60
+ "-d",
61
+ ...GLOBAL_OPTIONS,
62
+ ],
63
+ "project:stop": ["--project-id", "--project", "--force", ...GLOBAL_OPTIONS],
64
+ "project:switch": [...GLOBAL_OPTIONS],
65
+ "project:status": [
66
+ "--project-id",
67
+ "--project",
68
+ "--watch",
69
+ "-w",
70
+ ...GLOBAL_OPTIONS,
71
+ ],
72
+ repo: ["list", "add", "remove"],
73
+ "repo:list": [...GLOBAL_OPTIONS],
74
+ "repo:add": [...GLOBAL_OPTIONS],
75
+ "repo:remove": [...GLOBAL_OPTIONS],
76
+ config: ["show", "set", "edit"],
77
+ "config:show": [...GLOBAL_OPTIONS],
78
+ "config:set": [...GLOBAL_OPTIONS],
79
+ "config:edit": [...GLOBAL_OPTIONS],
80
+ };
81
+ function quoteWords(values) {
82
+ return values.join(" ");
83
+ }
84
+ function renderBashCasePatterns() {
85
+ return Object.entries(COMMAND_OPTIONS)
86
+ .map(([key, values]) => {
87
+ const [command, subcommand] = key.split(":");
88
+ if (!subcommand) {
89
+ if (command === "completion") {
90
+ return ` completion)\n COMPREPLY=( $(compgen -W "${quoteWords(values)}" -- "$cur") )\n return\n ;;`;
91
+ }
92
+ if (command === "project" ||
93
+ command === "repo" ||
94
+ command === "config") {
95
+ return ` ${command})\n COMPREPLY=( $(compgen -W "${quoteWords(values)}" -- "$cur") )\n return\n ;;`;
96
+ }
97
+ return ` ${command})\n COMPREPLY=( $(compgen -W "${quoteWords(values)}" -- "$cur") )\n return\n ;;`;
98
+ }
99
+ return ` ${command}:${subcommand})\n COMPREPLY=( $(compgen -W "${quoteWords(values)}" -- "$cur") )\n return\n ;;`;
100
+ })
101
+ .join("\n");
102
+ }
103
+ function renderFishLines() {
104
+ const lines = GLOBAL_OPTIONS.map((option) => option.startsWith("--")
105
+ ? `complete -c gh-symphony -f -l ${option.slice(2)}`
106
+ : `complete -c gh-symphony -f -s ${option.slice(1)}`);
107
+ for (const command of TOP_LEVEL_COMMANDS) {
108
+ lines.push(`complete -c gh-symphony -f -n '__fish_use_subcommand' -a '${command}'`);
109
+ }
110
+ for (const subcommand of COMMAND_OPTIONS.project ?? []) {
111
+ lines.push(`complete -c gh-symphony -f -n '__fish_seen_subcommand_from project' -a '${subcommand}'`);
112
+ }
113
+ for (const subcommand of COMMAND_OPTIONS.repo ?? []) {
114
+ lines.push(`complete -c gh-symphony -f -n '__fish_seen_subcommand_from repo' -a '${subcommand}'`);
115
+ }
116
+ for (const subcommand of COMMAND_OPTIONS.config ?? []) {
117
+ lines.push(`complete -c gh-symphony -f -n '__fish_seen_subcommand_from config' -a '${subcommand}'`);
118
+ }
119
+ for (const shell of COMMAND_OPTIONS.completion ?? []) {
120
+ lines.push(`complete -c gh-symphony -f -n '__fish_seen_subcommand_from completion' -a '${shell}'`);
121
+ }
122
+ return lines.join("\n");
123
+ }
124
+ export function renderCompletionScript(shell) {
125
+ if (shell === "fish") {
126
+ return `${renderFishLines()}\n`;
127
+ }
128
+ const bashFunction = `# shellcheck shell=bash
129
+ _gh_symphony_find_context() {
130
+ GH_SYMPHONY_COMMAND=""
131
+ GH_SYMPHONY_SUBCOMMAND=""
132
+
133
+ local idx=1
134
+ local token=""
135
+ local expects_value=0
136
+
137
+ while (( idx < COMP_CWORD )); do
138
+ token="\${COMP_WORDS[idx]}"
139
+
140
+ if (( expects_value )); then
141
+ expects_value=0
142
+ (( idx++ ))
143
+ continue
144
+ fi
145
+
146
+ case "\${token}" in
147
+ ${GLOBAL_OPTIONS_WITH_VALUES.map((option) => `${option}`).join("|")})
148
+ expects_value=1
149
+ ;;
150
+ ${GLOBAL_OPTIONS_WITH_VALUES.map((option) => `${option}=*`).join("|")})
151
+ ;;
152
+ ${GLOBAL_OPTIONS.filter((option) => !GLOBAL_OPTIONS_WITH_VALUES.includes(option)).join("|")})
153
+ ;;
154
+ -*)
155
+ ;;
156
+ *)
157
+ if [[ -z "\${GH_SYMPHONY_COMMAND}" ]]; then
158
+ GH_SYMPHONY_COMMAND="\${token}"
159
+ elif [[ -z "\${GH_SYMPHONY_SUBCOMMAND}" ]]; then
160
+ GH_SYMPHONY_SUBCOMMAND="\${token}"
161
+ fi
162
+ ;;
163
+ esac
164
+
165
+ (( idx++ ))
166
+ done
167
+ }
168
+
169
+ _gh_symphony_completion() {
170
+ local cur prev path
171
+ cur="\${COMP_WORDS[COMP_CWORD]}"
172
+ prev=""
173
+ if (( COMP_CWORD > 0 )); then
174
+ prev="\${COMP_WORDS[COMP_CWORD-1]}"
175
+ fi
176
+
177
+ _gh_symphony_find_context
178
+ path="\${GH_SYMPHONY_COMMAND}"
179
+
180
+ if [[ -z "\${path}" ]]; then
181
+ COMPREPLY=( $(compgen -W "${quoteWords(TOP_LEVEL_COMMANDS)} ${quoteWords(GLOBAL_OPTIONS)}" -- "$cur") )
182
+ return
183
+ fi
184
+
185
+ if [[ "\${path}" == "project" || "\${path}" == "repo" || "\${path}" == "config" || "\${path}" == "completion" ]]; then
186
+ if [[ -n "\${GH_SYMPHONY_SUBCOMMAND}" ]]; then
187
+ path="\${path}:\${GH_SYMPHONY_SUBCOMMAND}"
188
+ fi
189
+ fi
190
+
191
+ case "\${path}" in
192
+ ${renderBashCasePatterns()}
193
+ esac
194
+ }
195
+ `;
196
+ if (shell === "zsh") {
197
+ return `autoload -Uz compinit && compinit
198
+ autoload -U +X bashcompinit && bashcompinit
199
+ ${bashFunction}complete -F _gh_symphony_completion gh-symphony
200
+ `;
201
+ }
202
+ return `${bashFunction}complete -F _gh_symphony_completion gh-symphony
203
+ `;
204
+ }
package/dist/config.d.ts CHANGED
@@ -1,19 +1,21 @@
1
- import type { OrchestratorTenantConfig } 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
20
  };
19
21
  export type StateRole = "active" | "wait" | "terminal";
@@ -23,13 +25,13 @@ export type StateMapping = {
23
25
  };
24
26
  export declare function resolveConfigDir(override?: string): string;
25
27
  export declare function configFilePath(configDir: string): string;
26
- export declare function tenantConfigDir(configDir: string, tenantId: string): string;
27
- export declare function tenantConfigPath(configDir: string, tenantId: string): string;
28
- export declare function daemonPidPath(configDir: string): string;
29
- export declare function logsDir(configDir: string): string;
30
- 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;
31
33
  export declare function loadGlobalConfig(configDir: string): Promise<CliGlobalConfig | null>;
32
34
  export declare function saveGlobalConfig(configDir: string, config: CliGlobalConfig): Promise<void>;
33
- export declare function loadTenantConfig(configDir: string, tenantId: string): Promise<CliTenantConfig | null>;
34
- export declare function saveTenantConfig(configDir: string, tenantId: string, config: CliTenantConfig): Promise<void>;
35
- 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,46 +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 daemonPidPath(configDir) {
21
- return join(configDir, DAEMON_PID_FILE);
21
+ export function daemonPidPath(configDir, projectId) {
22
+ return join(projectConfigDir(configDir, projectId), DAEMON_PID_FILE);
22
23
  }
23
- export function logsDir(configDir) {
24
- return join(configDir, LOGS_DIR);
24
+ export function orchestratorLogPath(configDir, projectId) {
25
+ return join(projectConfigDir(configDir, projectId), ORCHESTRATOR_LOG_FILE);
25
26
  }
26
- export function orchestratorLogPath(configDir) {
27
- return join(logsDir(configDir), "orchestrator.log");
27
+ export function orchestratorPortPath(configDir, projectId) {
28
+ return join(projectConfigDir(configDir, projectId), ORCHESTRATOR_PORT_FILE);
28
29
  }
29
30
  export async function loadGlobalConfig(configDir) {
30
- 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
+ };
31
41
  }
32
42
  export async function saveGlobalConfig(configDir, config) {
33
43
  await writeJsonFile(configFilePath(configDir), config);
34
44
  }
35
- export async function loadTenantConfig(configDir, tenantId) {
36
- return readJsonFile(tenantConfigPath(configDir, tenantId));
45
+ export async function loadProjectConfig(configDir, projectId) {
46
+ return readJsonFile(projectConfigPath(configDir, projectId));
37
47
  }
38
- export async function saveTenantConfig(configDir, tenantId, config) {
39
- await writeJsonFile(tenantConfigPath(configDir, tenantId), config);
48
+ export async function saveProjectConfig(configDir, projectId, config) {
49
+ await writeJsonFile(projectConfigPath(configDir, projectId), config);
40
50
  }
41
- export async function loadActiveTenantConfig(configDir) {
51
+ export async function loadActiveProjectConfig(configDir) {
42
52
  const global = await loadGlobalConfig(configDir);
43
- if (!global?.activeTenant) {
53
+ if (!global?.activeProject) {
44
54
  return null;
45
55
  }
46
- return loadTenantConfig(configDir, global.activeTenant);
56
+ return loadProjectConfig(configDir, global.activeProject);
47
57
  }
48
58
  async function readJsonFile(path) {
49
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.d.ts CHANGED
@@ -5,10 +5,5 @@ export type GlobalOptions = {
5
5
  json: boolean;
6
6
  noColor: boolean;
7
7
  };
8
- export declare function parseGlobalOptions(argv: string[]): {
9
- options: GlobalOptions;
10
- command: string;
11
- args: string[];
12
- };
13
8
  export type CommandHandler = (args: string[], options: GlobalOptions) => Promise<void>;
14
9
  export declare function runCli(argv: string[]): Promise<void>;