@gh-symphony/cli 0.0.6 → 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;
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.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>;
@@ -1,26 +1,26 @@
1
1
  import { mkdir, writeFile } from "node:fs/promises";
2
2
  import { dirname, join, resolve } from "node:path";
3
- import { loadGlobalConfig, loadTenantConfig, } from "./config.js";
3
+ import { loadGlobalConfig, loadProjectConfig, } from "./config.js";
4
4
  export function resolveRuntimeRoot(configDir) {
5
5
  return resolve(configDir);
6
6
  }
7
- export async function resolveTenantConfig(configDir, requestedTenantId) {
8
- if (requestedTenantId) {
9
- return loadTenantConfig(configDir, requestedTenantId);
7
+ export async function resolveProjectConfig(configDir, requestedProjectId) {
8
+ if (requestedProjectId) {
9
+ return loadProjectConfig(configDir, requestedProjectId);
10
10
  }
11
11
  const global = await loadGlobalConfig(configDir);
12
- if (!global?.activeTenant) {
12
+ if (!global?.activeProject) {
13
13
  return null;
14
14
  }
15
- return loadTenantConfig(configDir, global.activeTenant);
15
+ return loadProjectConfig(configDir, global.activeProject);
16
16
  }
17
- export function orchestratorTenantConfigPath(runtimeRoot, tenantId) {
18
- return join(runtimeRoot, "orchestrator", "tenants", tenantId, "config.json");
17
+ export function orchestratorProjectConfigPath(runtimeRoot, projectId) {
18
+ return join(runtimeRoot, "orchestrator", "projects", projectId, "config.json");
19
19
  }
20
- export async function syncTenantToRuntime(configDir, tenantConfig) {
20
+ export async function syncProjectToRuntime(configDir, projectConfig) {
21
21
  const runtimeRoot = resolveRuntimeRoot(configDir);
22
- const configPath = orchestratorTenantConfigPath(runtimeRoot, tenantConfig.tenantId);
22
+ const configPath = orchestratorProjectConfigPath(runtimeRoot, projectConfig.projectId);
23
23
  await mkdir(dirname(configPath), { recursive: true });
24
- await writeFile(configPath, JSON.stringify(tenantConfig, null, 2) + "\n");
24
+ await writeFile(configPath, JSON.stringify(projectConfig, null, 2) + "\n");
25
25
  return runtimeRoot;
26
26
  }
@@ -0,0 +1,5 @@
1
+ export declare function resolveProjectOrchestratorStatusBaseUrl(input: {
2
+ configDir: string;
3
+ projectId: string;
4
+ env?: NodeJS.ProcessEnv;
5
+ }): Promise<string | null>;
@@ -0,0 +1,27 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { orchestratorPortPath } from "./config.js";
3
+ export async function resolveProjectOrchestratorStatusBaseUrl(input) {
4
+ const env = input.env ?? process.env;
5
+ const explicitBaseUrl = env.ORCHESTRATOR_STATUS_BASE_URL;
6
+ if (explicitBaseUrl) {
7
+ return explicitBaseUrl;
8
+ }
9
+ const host = env.ORCHESTRATOR_STATUS_HOST ?? "127.0.0.1";
10
+ const port = env.ORCHESTRATOR_STATUS_PORT ??
11
+ (await readProjectStatusPort(input.configDir, input.projectId));
12
+ if (!port) {
13
+ return null;
14
+ }
15
+ const urlHost = host.includes(":") && !host.startsWith("[") ? `[${host}]` : host;
16
+ return `http://${urlHost}:${port}`;
17
+ }
18
+ async function readProjectStatusPort(configDir, projectId) {
19
+ try {
20
+ const raw = await readFile(orchestratorPortPath(configDir, projectId), "utf8");
21
+ const port = raw.trim();
22
+ return port.length > 0 ? port : null;
23
+ }
24
+ catch {
25
+ return null;
26
+ }
27
+ }
@@ -2,7 +2,7 @@ export type SkillRuntime = "claude-code" | "codex";
2
2
  export type SkillTemplateContext = {
3
3
  runtime: SkillRuntime | string;
4
4
  projectId: string;
5
- projectTitle: string;
5
+ githubProjectTitle: string;
6
6
  repositories: Array<{
7
7
  owner: string;
8
8
  name: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gh-symphony/cli",
3
- "version": "0.0.6",
3
+ "version": "0.0.7",
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.6",
40
- "@gh-symphony/orchestrator": "0.0.6",
41
- "@gh-symphony/tracker-github": "0.0.6",
42
- "@gh-symphony/worker": "0.0.6"
39
+ "@gh-symphony/orchestrator": "0.0.7",
40
+ "@gh-symphony/core": "0.0.7",
41
+ "@gh-symphony/tracker-github": "0.0.7",
42
+ "@gh-symphony/worker": "0.0.7"
43
43
  },
44
44
  "scripts": {
45
45
  "build": "tsc -p tsconfig.json",