@gh-symphony/cli 0.0.4 → 0.0.5

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.
@@ -43,6 +43,7 @@ type WriteConfigInput = {
43
43
  pollIntervalMs?: number;
44
44
  concurrency?: number;
45
45
  maxAttempts?: number;
46
+ assignedOnly?: boolean;
46
47
  };
47
48
  export declare function writeConfig(configDir: string, input: WriteConfigInput): Promise<void>;
48
49
  export declare function generateTenantId(projectTitle: string, uniqueKey: string): string;
@@ -469,6 +469,7 @@ export async function writeConfig(configDir, input) {
469
469
  bindingId: input.project.id,
470
470
  settings: {
471
471
  projectId: input.project.id,
472
+ ...(input.assignedOnly ? { assignedOnly: true } : {}),
472
473
  },
473
474
  },
474
475
  runtime: {
@@ -151,6 +151,9 @@ const handler = async (args, options) => {
151
151
  return snapshot ?? null;
152
152
  },
153
153
  },
154
+ onRefresh: async () => {
155
+ await service.runOnce({ tenantId });
156
+ },
154
157
  });
155
158
  logLine(green("\u25B2"), `Starting orchestrator for tenant: ${bold(tenantId)}`);
156
159
  logLine(dim("\u00B7"), dim("Press Ctrl+C to stop"));
@@ -0,0 +1,8 @@
1
+ type RefreshRequestOptions = {
2
+ fetchImpl?: typeof fetch;
3
+ timeoutMs?: number;
4
+ env?: NodeJS.ProcessEnv;
5
+ };
6
+ export declare function resolveOrchestratorStatusBaseUrl(env?: NodeJS.ProcessEnv): string;
7
+ export declare function requestOrchestratorRefresh(options?: RefreshRequestOptions): Promise<boolean>;
8
+ export {};
@@ -0,0 +1,21 @@
1
+ export function resolveOrchestratorStatusBaseUrl(env = process.env) {
2
+ const host = env.ORCHESTRATOR_STATUS_HOST ?? "127.0.0.1";
3
+ const port = env.ORCHESTRATOR_STATUS_PORT ?? "4680";
4
+ const urlHost = host.includes(":") && !host.startsWith("[") ? `[${host}]` : host;
5
+ return `http://${urlHost}:${port}`;
6
+ }
7
+ export async function requestOrchestratorRefresh(options = {}) {
8
+ const fetchImpl = options.fetchImpl ?? fetch;
9
+ const timeoutMs = options.timeoutMs ?? 5_000;
10
+ const signal = AbortSignal.timeout(timeoutMs);
11
+ try {
12
+ const response = await fetchImpl(`${resolveOrchestratorStatusBaseUrl(options.env)}/api/v1/refresh`, {
13
+ method: "POST",
14
+ signal,
15
+ });
16
+ return response.ok;
17
+ }
18
+ catch {
19
+ return false;
20
+ }
21
+ }
@@ -4,6 +4,8 @@ import { resolveRuntimeRoot, resolveTenantConfig, syncTenantToRuntime, } from ".
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 { requestOrchestratorRefresh } from "./status-refresh.js";
8
+ const WATCH_REFRESH_TIMEOUT_MS = 1_500;
7
9
  function healthIcon(health) {
8
10
  switch (health) {
9
11
  case "idle":
@@ -166,7 +168,11 @@ const handler = async (args, options) => {
166
168
  if (parsed.watch) {
167
169
  const isTTY = process.stdout.isTTY === true;
168
170
  let terminalWidth = process.stdout.columns ?? 115;
171
+ let runPromise = null;
169
172
  const run = async () => {
173
+ await requestOrchestratorRefresh({
174
+ timeoutMs: WATCH_REFRESH_TIMEOUT_MS,
175
+ });
170
176
  const snapshots = await readAllStatusSnapshots(runtimeRoot);
171
177
  if (options.json || !isTTY) {
172
178
  process.stdout.write(JSON.stringify(snapshots, null, 2) + "\n");
@@ -180,11 +186,20 @@ const handler = async (args, options) => {
180
186
  "\n");
181
187
  }
182
188
  };
189
+ const tick = () => {
190
+ if (runPromise) {
191
+ return;
192
+ }
193
+ runPromise = run().finally(() => {
194
+ runPromise = null;
195
+ });
196
+ };
183
197
  if (isTTY) {
184
198
  process.stdout.write(hideCursor());
185
199
  }
186
- await run();
187
- const interval = setInterval(() => void run(), 2000);
200
+ tick();
201
+ await runPromise;
202
+ const interval = setInterval(tick, 2000);
188
203
  process.on("SIGWINCH", () => {
189
204
  terminalWidth = process.stdout.columns ?? terminalWidth;
190
205
  });
@@ -17,7 +17,7 @@ function displayScopeError(error, retryCommand) {
17
17
  p.note(`gh auth refresh --scopes ${scopeArg}\n\nThen re-run: ${retryCommand}`, "Fix missing scope");
18
18
  }
19
19
  function parseTenantAddFlags(args) {
20
- const flags = { nonInteractive: false };
20
+ const flags = { nonInteractive: false, assignedOnly: false };
21
21
  for (let i = 0; i < args.length; i += 1) {
22
22
  const arg = args[i];
23
23
  const next = args[i + 1];
@@ -33,6 +33,9 @@ function parseTenantAddFlags(args) {
33
33
  flags.runtime = next;
34
34
  i += 1;
35
35
  break;
36
+ case "--assigned-only":
37
+ flags.assignedOnly = true;
38
+ break;
36
39
  }
37
40
  }
38
41
  return flags;
@@ -143,6 +146,7 @@ async function tenantAddNonInteractive(flags, options) {
143
146
  statusField,
144
147
  mappings,
145
148
  runtime,
149
+ assignedOnly: flags.assignedOnly,
146
150
  });
147
151
  if (options.json) {
148
152
  process.stdout.write(JSON.stringify({ tenantId, status: "created" }) + "\n");
@@ -228,7 +232,7 @@ async function tenantAddInteractive(options) {
228
232
  return;
229
233
  }
230
234
  const selectedProjectId = await abortIfCancelled(p.select({
231
- message: "Step 1/3 — Select a GitHub Project:",
235
+ message: "Step 1/4 — Select a GitHub Project:",
232
236
  options: projects.map((proj) => ({
233
237
  value: proj.id,
234
238
  label: `${proj.owner.login}/${proj.title}`,
@@ -256,7 +260,7 @@ async function tenantAddInteractive(options) {
256
260
  return;
257
261
  }
258
262
  const selectedRepos = await abortIfCancelled(p.multiselect({
259
- message: "Step 2/3 — Select repositories to orchestrate:",
263
+ message: "Step 2/4 — Select repositories to orchestrate:",
260
264
  options: projectDetail.linkedRepositories.map((repo) => ({
261
265
  value: repo,
262
266
  label: `${repo.owner}/${repo.name}`,
@@ -287,9 +291,14 @@ async function tenantAddInteractive(options) {
287
291
  }
288
292
  const lifecycleConfig = toWorkflowLifecycleConfig(statusField.name, mappings);
289
293
  p.log.info(`Auto-detected workflow: Active=[${lifecycleConfig.activeStates.join(", ")}] Terminal=[${lifecycleConfig.terminalStates.join(", ")}]`);
290
- // ── Step 4: Runtime selection ────────────────────────────────────────────────
294
+ // ── Step 4: Assignment filter ────────────────────────────────────────────────
295
+ const assignedOnly = await abortIfCancelled(p.confirm({
296
+ message: `Step 3/4 — Only process issues assigned to the authenticated GitHub user?`,
297
+ initialValue: false,
298
+ }));
299
+ // ── Step 5: Runtime selection ────────────────────────────────────────────────
291
300
  const runtime = await abortIfCancelled(p.select({
292
- message: "Step 3/3 — Select AI runtime:",
301
+ message: "Step 4/4 — Select AI runtime:",
293
302
  options: [
294
303
  { value: "codex", label: "OpenAI Codex", hint: "recommended" },
295
304
  { value: "claude-code", label: "Claude Code" },
@@ -308,6 +317,7 @@ async function tenantAddInteractive(options) {
308
317
  `User: ${login}`,
309
318
  `Project: ${projectDetail.title}`,
310
319
  `Repos: ${selectedRepos.map((r) => `${r.owner}/${r.name}`).join(", ")}`,
320
+ `Assigned: ${assignedOnly ? `Only issues assigned to ${login}` : "All project issues"}`,
311
321
  `Runtime: ${runtime}`,
312
322
  `Active: ${lifecycleConfig.activeStates.join(", ")}`,
313
323
  `Terminal: ${lifecycleConfig.terminalStates.join(", ")}`,
@@ -335,6 +345,7 @@ async function tenantAddInteractive(options) {
335
345
  mappings,
336
346
  runtime,
337
347
  agentCommand,
348
+ assignedOnly,
338
349
  });
339
350
  s6.stop("Configuration saved.");
340
351
  }
package/dist/config.d.ts CHANGED
@@ -7,7 +7,14 @@ export type CliGlobalConfig = {
7
7
  activeTenant: string | null;
8
8
  tenants: string[];
9
9
  };
10
- export type CliTenantConfig = OrchestratorTenantConfig & {
10
+ export type CliTenantTrackerSettings = Record<string, string | boolean> & {
11
+ projectId?: string;
12
+ assignedOnly?: boolean;
13
+ };
14
+ export type CliTenantConfig = Omit<OrchestratorTenantConfig, "tracker"> & {
15
+ tracker: Omit<OrchestratorTenantConfig["tracker"], "settings"> & {
16
+ settings?: CliTenantTrackerSettings;
17
+ };
11
18
  workflowMapping?: WorkflowStateConfig;
12
19
  };
13
20
  export type StateRole = "active" | "wait" | "terminal";
@@ -1,7 +1,7 @@
1
1
  // ── Dashboard Renderer (Elixir-parity) ──────────────────────────────────────
2
2
  import { bold, dim, green, red, yellow, cyan, magenta, blue, stripAnsi, } from "../ansi.js";
3
3
  // ── Column widths (from Elixir spec) ─────────────────────────────────────────
4
- const COL_ID = 8;
4
+ const COL_ID = 24;
5
5
  const COL_STAGE = 14;
6
6
  const COL_PID = 8;
7
7
  const COL_AGE_TURN = 12;
@@ -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, COL_STAGE);
155
+ const stage = pad(run.issueState || "\u2014", COL_STAGE);
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;
@@ -31,7 +31,7 @@ export function checkGhAuthenticated(opts) {
31
31
  if ((result.status ?? 1) !== 0) {
32
32
  return { authenticated: false };
33
33
  }
34
- const login = parseLogin((result.stderr ?? "").toString());
34
+ const login = parseLogin((result.stdout ?? "").toString());
35
35
  return { authenticated: true, login };
36
36
  }
37
37
  export function checkGhScopes(opts) {
@@ -40,7 +40,7 @@ export function checkGhScopes(opts) {
40
40
  encoding: "utf8",
41
41
  stdio: ["pipe", "pipe", "pipe"],
42
42
  });
43
- const output = (result.stderr ?? "").toString();
43
+ const output = (result.stdout ?? "").toString();
44
44
  const scopes = parseScopes(output);
45
45
  if (scopes.length === 0) {
46
46
  return { valid: true, missing: [], scopes: [] };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gh-symphony/cli",
3
- "version": "0.0.4",
3
+ "version": "0.0.5",
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.3",
40
- "@gh-symphony/orchestrator": "0.0.3",
41
- "@gh-symphony/tracker-github": "0.0.3",
42
- "@gh-symphony/worker": "0.0.3"
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"
43
43
  },
44
44
  "scripts": {
45
45
  "build": "tsc -p tsconfig.json",