patchrelay 0.26.0 → 0.29.1

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.
Files changed (57) hide show
  1. package/README.md +83 -31
  2. package/dist/agent-session-plan.js +0 -7
  3. package/dist/build-info.json +3 -3
  4. package/dist/cli/args.js +22 -18
  5. package/dist/cli/commands/feed.js +1 -1
  6. package/dist/cli/commands/issues.js +44 -4
  7. package/dist/cli/commands/linear.js +67 -0
  8. package/dist/cli/commands/repo.js +213 -0
  9. package/dist/cli/commands/setup.js +140 -21
  10. package/dist/cli/connect-flow.js +5 -3
  11. package/dist/cli/formatters/text.js +1 -1
  12. package/dist/cli/help.js +134 -63
  13. package/dist/cli/index.js +166 -188
  14. package/dist/cli/interactive.js +25 -0
  15. package/dist/cli/operator-client.js +11 -0
  16. package/dist/cli/service-commands.js +11 -4
  17. package/dist/cli/watch/App.js +1 -1
  18. package/dist/cli/watch/FactoryStateGraph.js +31 -0
  19. package/dist/cli/watch/FeedView.js +3 -2
  20. package/dist/cli/watch/FreshnessBadge.js +13 -0
  21. package/dist/cli/watch/IssueDetailView.js +9 -2
  22. package/dist/cli/watch/IssueListView.js +2 -2
  23. package/dist/cli/watch/IssueRow.js +9 -11
  24. package/dist/cli/watch/QueueObservationView.js +15 -0
  25. package/dist/cli/watch/StateHistoryView.js +0 -1
  26. package/dist/cli/watch/StatusBar.js +5 -2
  27. package/dist/cli/watch/format-utils.js +7 -0
  28. package/dist/cli/watch/freshness.js +30 -0
  29. package/dist/cli/watch/state-visualization.js +147 -0
  30. package/dist/cli/watch/theme.js +6 -7
  31. package/dist/cli/watch/use-watch-stream.js +5 -2
  32. package/dist/cli/watch/watch-state.js +9 -5
  33. package/dist/config.js +129 -36
  34. package/dist/db/linear-installation-store.js +23 -0
  35. package/dist/db/migrations.js +42 -0
  36. package/dist/db/repository-link-store.js +103 -0
  37. package/dist/db.js +61 -11
  38. package/dist/factory-state.js +1 -5
  39. package/dist/github-webhook-handler.js +115 -46
  40. package/dist/github-webhooks.js +4 -0
  41. package/dist/http.js +162 -0
  42. package/dist/install.js +93 -13
  43. package/dist/issue-query-service.js +34 -1
  44. package/dist/linear-client.js +80 -25
  45. package/dist/merge-queue-incident.js +104 -0
  46. package/dist/merge-queue-protocol.js +54 -0
  47. package/dist/preflight.js +28 -1
  48. package/dist/repository-linking.js +42 -0
  49. package/dist/run-orchestrator.js +197 -21
  50. package/dist/runtime-paths.js +0 -8
  51. package/dist/service.js +94 -49
  52. package/package.json +8 -7
  53. package/dist/cli/commands/connect.js +0 -54
  54. package/dist/cli/commands/project.js +0 -146
  55. package/dist/merge-queue.js +0 -200
  56. package/infra/patchrelay-reload.service +0 -6
  57. package/infra/patchrelay.path +0 -13
package/dist/cli/index.js CHANGED
@@ -1,36 +1,29 @@
1
1
  import { loadConfig } from "../config.js";
2
2
  import { getBuildInfo } from "../build-info.js";
3
3
  import { assertKnownFlags, hasHelpFlag, parseArgs, resolveCommand } from "./args.js";
4
- import { handleConnectCommand, handleInstallationsCommand } from "./commands/connect.js";
5
4
  import { handleFeedCommand } from "./commands/feed.js";
6
- import { handleEventsCommand, handleInspectCommand, handleListCommand, handleLiveCommand, handleOpenCommand, handleReportCommand, handleRetryCommand, handleWorktreeCommand, } from "./commands/issues.js";
7
- import { handleProjectCommand } from "./commands/project.js";
8
- import { handleInitCommand, handleInstallServiceCommand, handleRestartServiceCommand } from "./commands/setup.js";
5
+ import { handleIssueCommand, } from "./commands/issues.js";
6
+ import { handleLinearCommand } from "./commands/linear.js";
7
+ import { handleRepoCommand } from "./commands/repo.js";
8
+ import { handleInitCommand, handleServiceCommand } from "./commands/setup.js";
9
9
  import { CliUsageError } from "./errors.js";
10
10
  import { formatJson } from "./formatters/json.js";
11
11
  import { helpTextFor, rootHelpText } from "./help.js";
12
- import { runInteractiveCommand } from "./interactive.js";
12
+ import { runBufferedCommand, runInteractiveCommand } from "./interactive.js";
13
13
  import { formatDoctor, writeOutput, writeUsageError } from "./output.js";
14
14
  function getCommandConfigProfile(command) {
15
15
  switch (command) {
16
16
  case "version":
17
17
  return "service";
18
18
  case "doctor":
19
- case "install-service":
19
+ case "service":
20
20
  return "doctor";
21
- case "connect":
22
- case "installations":
21
+ case "linear":
23
22
  case "feed":
24
- case "watch":
23
+ case "dashboard":
25
24
  return "operator_cli";
26
- case "inspect":
27
- case "live":
28
- case "report":
29
- case "events":
30
- case "worktree":
31
- case "open":
32
- case "retry":
33
- case "list":
25
+ case "repo":
26
+ case "issue":
34
27
  return "cli";
35
28
  default:
36
29
  return "service";
@@ -45,61 +38,107 @@ function validateFlags(command, commandArgs, parsed) {
45
38
  case "serve":
46
39
  assertKnownFlags(parsed, command, []);
47
40
  return;
48
- case "inspect":
49
- assertKnownFlags(parsed, command, ["json"]);
50
- return;
51
- case "live":
52
- assertKnownFlags(parsed, command, ["watch", "json"]);
53
- return;
54
- case "report":
55
- assertKnownFlags(parsed, command, ["run-type", "run", "json"]);
56
- return;
57
- case "events":
58
- assertKnownFlags(parsed, command, ["run", "method", "follow", "json"]);
59
- return;
60
- case "worktree":
61
- assertKnownFlags(parsed, command, ["cd", "json"]);
62
- return;
63
- case "open":
64
- assertKnownFlags(parsed, command, ["print", "json"]);
65
- return;
66
- case "retry":
67
- assertKnownFlags(parsed, command, ["run-type", "reason", "json"]);
68
- return;
69
- case "list":
70
- assertKnownFlags(parsed, command, ["active", "failed", "project", "json"]);
71
- return;
41
+ case "issue": {
42
+ switch (commandArgs[0]) {
43
+ case "show":
44
+ assertKnownFlags(parsed, "issue", ["json"]);
45
+ return;
46
+ case "list":
47
+ assertKnownFlags(parsed, "issue", ["active", "failed", "repo", "json"]);
48
+ return;
49
+ case "watch":
50
+ assertKnownFlags(parsed, "issue", ["json"]);
51
+ return;
52
+ case "report":
53
+ assertKnownFlags(parsed, "issue", ["run-type", "run", "json"]);
54
+ return;
55
+ case "events":
56
+ assertKnownFlags(parsed, "issue", ["run", "method", "follow", "json"]);
57
+ return;
58
+ case "path":
59
+ assertKnownFlags(parsed, "issue", ["cd", "json"]);
60
+ return;
61
+ case "open":
62
+ assertKnownFlags(parsed, "issue", ["print", "json"]);
63
+ return;
64
+ case "retry":
65
+ assertKnownFlags(parsed, "issue", ["run-type", "reason", "json"]);
66
+ return;
67
+ default:
68
+ assertKnownFlags(parsed, "issue", []);
69
+ return;
70
+ }
71
+ }
72
72
  case "doctor":
73
73
  assertKnownFlags(parsed, command, ["json"]);
74
74
  return;
75
75
  case "init":
76
76
  assertKnownFlags(parsed, command, ["force", "json", "public-base-url"]);
77
77
  return;
78
- case "project":
79
- if (commandArgs[0] === "apply") {
80
- assertKnownFlags(parsed, "project apply", ["issue-prefix", "team-id", "no-connect", "no-open", "timeout", "json"]);
81
- return;
78
+ case "linear":
79
+ switch (commandArgs[0]) {
80
+ case undefined:
81
+ case "list":
82
+ assertKnownFlags(parsed, "linear", ["json"]);
83
+ return;
84
+ case "connect":
85
+ assertKnownFlags(parsed, "linear", ["no-open", "timeout", "json"]);
86
+ return;
87
+ case "sync":
88
+ case "disconnect":
89
+ assertKnownFlags(parsed, "linear", ["json"]);
90
+ return;
91
+ default:
92
+ assertKnownFlags(parsed, "linear", []);
93
+ return;
82
94
  }
83
- assertKnownFlags(parsed, command, []);
84
- return;
95
+ case "repo":
96
+ switch (commandArgs[0]) {
97
+ case undefined:
98
+ case "list":
99
+ case "show":
100
+ case "unlink":
101
+ case "sync":
102
+ assertKnownFlags(parsed, "repo", ["json"]);
103
+ return;
104
+ case "link":
105
+ assertKnownFlags(parsed, "repo", ["workspace", "team", "project", "prefix", "path", "json"]);
106
+ return;
107
+ default:
108
+ assertKnownFlags(parsed, "repo", []);
109
+ return;
110
+ }
111
+ case "attach":
112
+ case "repos":
85
113
  case "connect":
86
- assertKnownFlags(parsed, command, ["project", "no-open", "timeout", "json"]);
87
- return;
88
114
  case "installations":
89
- assertKnownFlags(parsed, command, ["json"]);
115
+ throw new CliUsageError(`${command} has been removed. Use \`patchrelay linear ...\` and \`patchrelay repo ...\` instead.`);
116
+ return;
117
+ case "service":
118
+ if (commandArgs[0] === "install") {
119
+ assertKnownFlags(parsed, "service", ["force", "write-only", "json"]);
120
+ return;
121
+ }
122
+ if (commandArgs[0] === "restart") {
123
+ assertKnownFlags(parsed, "service", ["json"]);
124
+ return;
125
+ }
126
+ if (commandArgs[0] === "status") {
127
+ assertKnownFlags(parsed, "service", ["json"]);
128
+ return;
129
+ }
130
+ if (commandArgs[0] === "logs") {
131
+ assertKnownFlags(parsed, "service", ["lines", "json"]);
132
+ return;
133
+ }
134
+ assertKnownFlags(parsed, "service", []);
90
135
  return;
91
136
  case "feed":
92
- assertKnownFlags(parsed, command, ["follow", "limit", "issue", "project", "kind", "stage", "status", "workflow", "json"]);
137
+ assertKnownFlags(parsed, command, ["follow", "limit", "issue", "repo", "kind", "stage", "status", "workflow", "json"]);
93
138
  return;
94
- case "watch":
139
+ case "dashboard":
95
140
  assertKnownFlags(parsed, command, ["issue"]);
96
141
  return;
97
- case "install-service":
98
- assertKnownFlags(parsed, command, ["force", "write-only", "json"]);
99
- return;
100
- case "restart-service":
101
- assertKnownFlags(parsed, command, ["json"]);
102
- return;
103
142
  default:
104
143
  return;
105
144
  }
@@ -131,8 +170,8 @@ export async function runCli(argv, options) {
131
170
  const json = parsed.flags.get("json") === true;
132
171
  if (command === "help") {
133
172
  const topic = commandArgs[0];
134
- if (topic === "project") {
135
- writeOutput(stdout, `${helpTextFor("project")}\n`);
173
+ if (topic === "linear" || topic === "repo" || topic === "issue" || topic === "service") {
174
+ writeOutput(stdout, `${helpTextFor(topic)}\n`);
136
175
  return 0;
137
176
  }
138
177
  if (topic) {
@@ -148,13 +187,28 @@ export async function runCli(argv, options) {
148
187
  return 0;
149
188
  }
150
189
  if (hasHelpFlag(parsed)) {
151
- writeOutput(stdout, `${helpTextFor(command === "project" ? "project" : "root")}\n`);
190
+ const helpTopic = command === "linear"
191
+ ? "linear"
192
+ : command === "repo"
193
+ ? "repo"
194
+ : command === "issue" || command === "service"
195
+ ? command
196
+ : "root";
197
+ writeOutput(stdout, `${helpTextFor(helpTopic)}\n`);
152
198
  return 0;
153
199
  }
154
200
  if (command === "serve") {
155
201
  return -1;
156
202
  }
157
203
  const runInteractive = options?.runInteractive ?? runInteractiveCommand;
204
+ const runCommand = options?.runCommand
205
+ ?? (options?.runInteractive
206
+ ? async (command, args) => ({
207
+ exitCode: await options.runInteractive(command, args),
208
+ stdout: "",
209
+ stderr: "",
210
+ })
211
+ : runBufferedCommand);
158
212
  if (command === "init") {
159
213
  return await handleInitCommand({
160
214
  commandArgs,
@@ -163,37 +217,57 @@ export async function runCli(argv, options) {
163
217
  stdout,
164
218
  stderr,
165
219
  runInteractive,
220
+ runCommand,
166
221
  });
167
222
  }
168
- if (command === "install-service") {
169
- return await handleInstallServiceCommand({
170
- commandArgs,
171
- parsed,
172
- json,
173
- stdout,
174
- stderr,
175
- runInteractive,
176
- });
177
- }
178
- if (command === "restart-service") {
179
- return await handleRestartServiceCommand({
180
- commandArgs,
181
- parsed,
182
- json,
183
- stdout,
184
- stderr,
185
- runInteractive,
186
- });
187
- }
188
- if (command === "project") {
223
+ if (command === "service") {
189
224
  try {
190
- return await handleProjectCommand({
225
+ return await handleServiceCommand({
191
226
  commandArgs,
192
227
  parsed,
193
228
  json,
194
229
  stdout,
195
230
  stderr,
196
231
  runInteractive,
232
+ runCommand,
233
+ });
234
+ }
235
+ catch (error) {
236
+ if (error instanceof CliUsageError) {
237
+ writeUsageError(stderr, error);
238
+ return 1;
239
+ }
240
+ writeOutput(stderr, `${error instanceof Error ? error.message : String(error)}\n`);
241
+ return 1;
242
+ }
243
+ }
244
+ if (command === "linear") {
245
+ try {
246
+ return await handleLinearCommand({
247
+ commandArgs,
248
+ parsed,
249
+ json,
250
+ stdout,
251
+ ...(options ? { options } : {}),
252
+ });
253
+ }
254
+ catch (error) {
255
+ if (error instanceof CliUsageError) {
256
+ writeUsageError(stderr, error);
257
+ return 1;
258
+ }
259
+ writeOutput(stderr, `${error instanceof Error ? error.message : String(error)}\n`);
260
+ return 1;
261
+ }
262
+ }
263
+ if (command === "repo") {
264
+ try {
265
+ return await handleRepoCommand({
266
+ commandArgs,
267
+ parsed,
268
+ json,
269
+ stdout,
270
+ runCommand,
197
271
  ...(options ? { options } : {}),
198
272
  });
199
273
  }
@@ -229,81 +303,25 @@ export async function runCli(argv, options) {
229
303
  writeOutput(stdout, json ? formatJson(doctorReport) : formatDoctor(doctorReport, cliVersion, serviceVersion));
230
304
  return report.ok ? 0 : 1;
231
305
  }
232
- if (command === "inspect") {
233
- const issueData = await ensureIssueDataAccess(data, config);
234
- if (!data) {
235
- data = issueData;
236
- ownsData = true;
237
- }
238
- return await handleInspectCommand({ commandArgs, parsed, json, stdout, data: issueData, config, runInteractive });
239
- }
240
- if (command === "live") {
241
- const issueData = await ensureIssueDataAccess(data, config);
242
- if (!data) {
243
- data = issueData;
244
- ownsData = true;
245
- }
246
- return await handleLiveCommand({ commandArgs, parsed, json, stdout, data: issueData, config, runInteractive });
247
- }
248
- if (command === "report") {
249
- const issueData = await ensureIssueDataAccess(data, config);
250
- if (!data) {
251
- data = issueData;
252
- ownsData = true;
253
- }
254
- return await handleReportCommand({ commandArgs, parsed, json, stdout, data: issueData, config, runInteractive });
255
- }
256
- if (command === "events") {
306
+ if (command === "issue") {
257
307
  const issueData = await ensureIssueDataAccess(data, config);
258
308
  if (!data) {
259
309
  data = issueData;
260
310
  ownsData = true;
261
311
  }
262
- return await handleEventsCommand({ commandArgs, parsed, json, stdout, data: issueData, config, runInteractive });
263
- }
264
- if (command === "worktree") {
265
- const issueData = await ensureIssueDataAccess(data, config);
266
- if (!data) {
267
- data = issueData;
268
- ownsData = true;
269
- }
270
- return await handleWorktreeCommand({ commandArgs, parsed, json, stdout, data: issueData, config, runInteractive });
271
- }
272
- if (command === "open") {
273
- const issueData = await ensureIssueDataAccess(data, config);
274
- if (!data) {
275
- data = issueData;
276
- ownsData = true;
277
- }
278
- return await handleOpenCommand({ commandArgs, parsed, json, stdout, data: issueData, config, runInteractive });
279
- }
280
- if (command === "connect") {
281
- const operatorData = await ensureConnectDataAccess(data, config);
282
- if (!data) {
283
- data = operatorData;
284
- ownsData = true;
285
- }
286
- return await handleConnectCommand({
312
+ return await handleIssueCommand({
313
+ commandArgs,
287
314
  parsed,
288
315
  json,
289
316
  stdout,
317
+ data: issueData,
290
318
  config,
291
- data: operatorData,
292
- ...(options ? { options } : {}),
319
+ runInteractive,
293
320
  });
294
321
  }
295
- if (command === "installations") {
296
- const operatorData = await ensureInstallationsDataAccess(data, config);
297
- if (!data) {
298
- data = operatorData;
299
- ownsData = true;
300
- }
301
- return await handleInstallationsCommand({
302
- json,
303
- stdout,
304
- data: operatorData,
305
- config,
306
- });
322
+ if (command === "attach" || command === "repos" || command === "connect" || command === "installations") {
323
+ writeOutput(stderr, `${command} has been removed. Use \`patchrelay linear ...\` and \`patchrelay repo ...\` instead.\n`);
324
+ return 1;
307
325
  }
308
326
  if (command === "feed") {
309
327
  const operatorData = parsed.flags.get("follow") === true
@@ -320,26 +338,10 @@ export async function runCli(argv, options) {
320
338
  data: operatorData,
321
339
  });
322
340
  }
323
- if (command === "watch") {
341
+ if (command === "dashboard") {
324
342
  const { handleWatchCommand } = await import("./commands/watch.js");
325
343
  return await handleWatchCommand({ config, parsed });
326
344
  }
327
- if (command === "retry") {
328
- const issueData = await ensureIssueDataAccess(data, config);
329
- if (!data) {
330
- data = issueData;
331
- ownsData = true;
332
- }
333
- return await handleRetryCommand({ commandArgs, parsed, json, stdout, data: issueData, config, runInteractive });
334
- }
335
- if (command === "list") {
336
- const issueData = await ensureIssueDataAccess(data, config);
337
- if (!data) {
338
- data = issueData;
339
- ownsData = true;
340
- }
341
- return await handleListCommand({ commandArgs, parsed, json, stdout, data: issueData, config, runInteractive });
342
- }
343
345
  throw new Error(`Unknown command: ${command}`);
344
346
  }
345
347
  catch (error) {
@@ -373,27 +375,9 @@ async function ensureIssueDataAccess(data, config) {
373
375
  }
374
376
  return await createCliDataAccess(config);
375
377
  }
376
- async function ensureConnectDataAccess(data, config) {
377
- if (data) {
378
- if (hasConnectDataAccess(data)) {
379
- return data;
380
- }
381
- throw new Error("The connect command requires HTTP-backed OAuth CLI data access.");
382
- }
383
- return await createCliOperatorDataAccess(config);
384
- }
385
378
  function isIssueDataAccess(data) {
386
379
  return !!data && typeof data === "object" && "inspect" in data && typeof data.inspect === "function";
387
380
  }
388
- async function ensureInstallationsDataAccess(data, config) {
389
- if (data) {
390
- if (hasInstallationsDataAccess(data)) {
391
- return data;
392
- }
393
- throw new Error("The installations command requires HTTP-backed installation data access.");
394
- }
395
- return await createCliOperatorDataAccess(config);
396
- }
397
381
  async function ensureFeedListDataAccess(data, config) {
398
382
  if (data) {
399
383
  if (hasFeedListDataAccess(data)) {
@@ -403,12 +387,6 @@ async function ensureFeedListDataAccess(data, config) {
403
387
  }
404
388
  return await createCliOperatorDataAccess(config);
405
389
  }
406
- function hasConnectDataAccess(data) {
407
- return !!data && typeof data === "object" && "connect" in data && typeof data.connect === "function";
408
- }
409
- function hasInstallationsDataAccess(data) {
410
- return !!data && typeof data === "object" && "listInstallations" in data && typeof data.listInstallations === "function";
411
- }
412
390
  async function ensureFeedFollowDataAccess(data, config) {
413
391
  if (data) {
414
392
  if (hasFeedFollowDataAccess(data)) {
@@ -27,6 +27,31 @@ export async function runInteractiveCommand(command, args) {
27
27
  });
28
28
  });
29
29
  }
30
+ export async function runBufferedCommand(command, args) {
31
+ return await new Promise((resolve, reject) => {
32
+ const child = spawn(command, args, {
33
+ stdio: ["ignore", "pipe", "pipe"],
34
+ });
35
+ let stdout = "";
36
+ let stderr = "";
37
+ child.stdout?.setEncoding("utf8");
38
+ child.stderr?.setEncoding("utf8");
39
+ child.stdout?.on("data", (chunk) => {
40
+ stdout += chunk;
41
+ });
42
+ child.stderr?.on("data", (chunk) => {
43
+ stderr += chunk;
44
+ });
45
+ child.on("error", reject);
46
+ child.on("exit", (code, signal) => {
47
+ resolve({
48
+ exitCode: signal ? 1 : (code ?? 0),
49
+ stdout,
50
+ stderr,
51
+ });
52
+ });
53
+ });
54
+ }
30
55
  export async function openExternalUrl(url) {
31
56
  const candidates = process.platform === "darwin"
32
57
  ? [{ command: "open", args: [url] }]
@@ -18,6 +18,17 @@ export class CliOperatorApiClient {
18
18
  async listInstallations() {
19
19
  return await this.requestJson("/api/installations");
20
20
  }
21
+ async listLinearWorkspaces() {
22
+ return await this.requestJson("/api/linear/workspaces");
23
+ }
24
+ async syncLinearWorkspace(workspace) {
25
+ return await this.requestJson("/api/linear/workspaces/sync", {
26
+ ...(workspace ? { workspace } : {}),
27
+ }, { method: "POST" });
28
+ }
29
+ async disconnectLinearWorkspace(workspace) {
30
+ return await this.requestJson(`/api/linear/workspaces/${encodeURIComponent(workspace)}`, undefined, { method: "DELETE" });
31
+ }
21
32
  async listOperatorFeed(options) {
22
33
  return await this.requestJson("/api/feed", {
23
34
  ...(options?.limit && options.limit > 0 ? { limit: String(options.limit) } : {}),
@@ -1,10 +1,18 @@
1
+ function summarizeCommandOutput(result) {
2
+ const parts = [result.stderr.trim(), result.stdout.trim()].filter(Boolean);
3
+ return parts.length > 0 ? `\n${parts.join("\n")}` : "";
4
+ }
1
5
  export async function runServiceCommands(runner, commands) {
6
+ const results = [];
2
7
  for (const entry of commands) {
3
- const exitCode = await runner(entry.command, entry.args);
4
- if (exitCode !== 0) {
5
- throw new Error(`Command failed with exit code ${exitCode}: ${entry.command} ${entry.args.join(" ")}`);
8
+ const result = await runner(entry.command, entry.args);
9
+ const commandResult = { ...entry, ...result };
10
+ results.push(commandResult);
11
+ if (result.exitCode !== 0) {
12
+ throw new Error(`Command failed with exit code ${result.exitCode}: ${entry.command} ${entry.args.join(" ")}${summarizeCommandOutput(result)}`);
6
13
  }
7
14
  }
15
+ return results;
8
16
  }
9
17
  export async function tryManageService(runner, commands) {
10
18
  try {
@@ -18,7 +26,6 @@ export async function tryManageService(runner, commands) {
18
26
  export function installServiceCommands() {
19
27
  return [
20
28
  { command: "sudo", args: ["systemctl", "daemon-reload"] },
21
- { command: "sudo", args: ["systemctl", "enable", "--now", "patchrelay.path"] },
22
29
  { command: "sudo", args: ["systemctl", "enable", "patchrelay.service"] },
23
30
  { command: "sudo", args: ["systemctl", "reload-or-restart", "patchrelay.service"] },
24
31
  ];
@@ -193,5 +193,5 @@ export function App({ baseUrl, bearerToken, initialIssueKey }) {
193
193
  }
194
194
  }
195
195
  });
196
- return (_jsx(Box, { flexDirection: "column", children: state.view === "list" ? (_jsx(IssueListView, { issues: filtered, allIssues: state.issues, selectedIndex: state.selectedIndex, connected: state.connected, filter: state.filter, totalCount: state.issues.length })) : state.view === "detail" ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(IssueDetailView, { issue: state.issues.find((i) => i.issueKey === state.activeDetailKey), timeline: state.timeline, follow: state.follow, activeRunStartedAt: state.activeRunStartedAt, activeRunId: state.activeRunId, tokenUsage: state.tokenUsage, diffSummary: state.diffSummary, plan: state.plan, issueContext: state.issueContext, detailTab: state.detailTab, timelineMode: state.timelineMode, rawRuns: state.rawRuns, rawFeedEvents: state.rawFeedEvents }), promptMode && (_jsxs(Box, { children: [_jsx(Text, { color: "yellow", children: "prompt> " }), _jsx(Text, { children: promptBuffer }), _jsx(Text, { dimColor: true, children: "_" })] })), promptStatus && !promptMode && (_jsx(Text, { dimColor: true, children: promptStatus }))] })) : (_jsx(FeedView, { events: state.feedEvents, connected: state.connected })) }));
196
+ return (_jsx(Box, { flexDirection: "column", children: state.view === "list" ? (_jsx(IssueListView, { issues: filtered, allIssues: state.issues, selectedIndex: state.selectedIndex, connected: state.connected, lastServerMessageAt: state.lastServerMessageAt, filter: state.filter, totalCount: state.issues.length })) : state.view === "detail" ? (_jsxs(Box, { flexDirection: "column", children: [state.activeDetailKey && (_jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: "Issues" }), _jsx(Text, { dimColor: true, children: " \u203A " }), _jsx(Text, { bold: true, children: state.activeDetailKey }), _jsx(Text, { dimColor: true, children: " \u203A " }), _jsx(Text, { dimColor: true, children: state.detailTab === "timeline" ? "Timeline" : "History" })] })), _jsx(IssueDetailView, { issue: state.issues.find((i) => i.issueKey === state.activeDetailKey), timeline: state.timeline, follow: state.follow, activeRunStartedAt: state.activeRunStartedAt, activeRunId: state.activeRunId, tokenUsage: state.tokenUsage, diffSummary: state.diffSummary, plan: state.plan, issueContext: state.issueContext, detailTab: state.detailTab, timelineMode: state.timelineMode, rawRuns: state.rawRuns, rawFeedEvents: state.rawFeedEvents, connected: state.connected, lastServerMessageAt: state.lastServerMessageAt }), promptMode && (_jsxs(Box, { children: [_jsx(Text, { color: "yellow", children: "prompt> " }), _jsx(Text, { children: promptBuffer }), _jsx(Text, { dimColor: true, children: "_" })] })), promptStatus && !promptMode && (_jsx(Text, { dimColor: true, children: promptStatus }))] })) : (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: "Issues" }), _jsx(Text, { dimColor: true, children: " \u203A " }), _jsx(Text, { bold: true, children: "Operator Feed" })] }), _jsx(FeedView, { events: state.feedEvents, connected: state.connected, lastServerMessageAt: state.lastServerMessageAt })] })) }));
197
197
  }
@@ -0,0 +1,31 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { Box, Text } from "ink";
3
+ function statusColor(status) {
4
+ switch (status) {
5
+ case "current":
6
+ return "cyan";
7
+ case "visited":
8
+ return "green";
9
+ case "upcoming":
10
+ return "gray";
11
+ }
12
+ }
13
+ function statusPrefix(status) {
14
+ switch (status) {
15
+ case "current":
16
+ return "*";
17
+ case "visited":
18
+ return "+";
19
+ case "upcoming":
20
+ return " ";
21
+ }
22
+ }
23
+ function NodePill({ node }) {
24
+ return (_jsxs(Text, { color: statusColor(node.status), bold: node.status === "current", children: ["[", statusPrefix(node.status), " ", node.label, "]"] }));
25
+ }
26
+ function NodeRow({ label, nodes, connector = " -> ", }) {
27
+ return (_jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: label.padEnd(11, " ") }), nodes.map((node, index) => (_jsxs(Box, { children: [index > 0 && _jsx(Text, { dimColor: true, children: connector }), _jsx(NodePill, { node: node })] }, node.state)))] }));
28
+ }
29
+ export function FactoryStateGraph({ main, prLoops, queueLoop, exits, }) {
30
+ return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { bold: true, children: "State Graph" }), _jsx(NodeRow, { label: "main", nodes: main }), _jsx(NodeRow, { label: "pr loops", nodes: prLoops, connector: " " }), _jsx(NodeRow, { label: "queue loop", nodes: queueLoop, connector: " " }), _jsx(NodeRow, { label: "exits", nodes: exits, connector: " " })] }));
31
+ }
@@ -1,6 +1,7 @@
1
1
  import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { Box, Text } from "ink";
3
3
  import { HelpBar } from "./HelpBar.js";
4
+ import { FreshnessBadge } from "./FreshnessBadge.js";
4
5
  const TAIL_SIZE = 30;
5
6
  const KIND_COLORS = {
6
7
  stage: "cyan",
@@ -20,8 +21,8 @@ function FeedEventRow({ event }) {
20
21
  const kindColor = KIND_COLORS[event.kind] ?? "white";
21
22
  return (_jsxs(Box, { children: [_jsxs(Text, { dimColor: true, children: [formatTime(event.at), " "] }), _jsx(Text, { color: kindColor, children: (event.status ?? event.kind).padEnd(14) }), event.issueKey && _jsx(Text, { bold: true, children: ` ${event.issueKey.padEnd(9)}` }), _jsxs(Text, { children: [" ", event.summary] })] }));
22
23
  }
23
- export function FeedView({ events, connected }) {
24
+ export function FeedView({ events, connected, lastServerMessageAt }) {
24
25
  const visible = events.length > TAIL_SIZE ? events.slice(-TAIL_SIZE) : events;
25
26
  const skipped = events.length - visible.length;
26
- return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsx(Text, { bold: true, children: "Operator Feed" }), _jsx(Text, { color: connected ? "green" : "red", children: connected ? "\u25cf connected" : "\u25cb disconnected" })] }), _jsx(Box, { marginTop: 1, flexDirection: "column", children: events.length === 0 ? (_jsx(Text, { dimColor: true, children: "No feed events yet." })) : (_jsxs(_Fragment, { children: [skipped > 0 && _jsxs(Text, { dimColor: true, children: [" ... ", skipped, " earlier"] }), visible.map((event) => (_jsx(FeedEventRow, { event: event }, event.id)))] })) }), _jsx(Box, { marginTop: 1, children: _jsx(HelpBar, { view: "feed" }) })] }));
27
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsx(Text, { bold: true, children: "Operator Feed" }), _jsx(FreshnessBadge, { connected: connected, lastServerMessageAt: lastServerMessageAt })] }), _jsx(Box, { marginTop: 1, flexDirection: "column", children: events.length === 0 ? (_jsx(Text, { dimColor: true, children: "No feed events yet." })) : (_jsxs(_Fragment, { children: [skipped > 0 && _jsxs(Text, { dimColor: true, children: [" ... ", skipped, " earlier"] }), visible.map((event) => (_jsx(FeedEventRow, { event: event }, event.id)))] })) }), _jsx(Box, { marginTop: 1, children: _jsx(HelpBar, { view: "feed" }) })] }));
27
28
  }
@@ -0,0 +1,13 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useEffect, useReducer } from "react";
3
+ import { Text } from "ink";
4
+ import { describePatchRelayFreshness } from "./freshness.js";
5
+ export function FreshnessBadge({ connected, lastServerMessageAt }) {
6
+ const [, tick] = useReducer((value) => value + 1, 0);
7
+ useEffect(() => {
8
+ const id = setInterval(tick, 5_000);
9
+ return () => clearInterval(id);
10
+ }, []);
11
+ const freshness = describePatchRelayFreshness(connected, lastServerMessageAt);
12
+ return _jsx(Text, { color: freshness.color, children: freshness.label });
13
+ }