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.
- package/README.md +83 -31
- package/dist/agent-session-plan.js +0 -7
- package/dist/build-info.json +3 -3
- package/dist/cli/args.js +22 -18
- package/dist/cli/commands/feed.js +1 -1
- package/dist/cli/commands/issues.js +44 -4
- package/dist/cli/commands/linear.js +67 -0
- package/dist/cli/commands/repo.js +213 -0
- package/dist/cli/commands/setup.js +140 -21
- package/dist/cli/connect-flow.js +5 -3
- package/dist/cli/formatters/text.js +1 -1
- package/dist/cli/help.js +134 -63
- package/dist/cli/index.js +166 -188
- package/dist/cli/interactive.js +25 -0
- package/dist/cli/operator-client.js +11 -0
- package/dist/cli/service-commands.js +11 -4
- package/dist/cli/watch/App.js +1 -1
- package/dist/cli/watch/FactoryStateGraph.js +31 -0
- package/dist/cli/watch/FeedView.js +3 -2
- package/dist/cli/watch/FreshnessBadge.js +13 -0
- package/dist/cli/watch/IssueDetailView.js +9 -2
- package/dist/cli/watch/IssueListView.js +2 -2
- package/dist/cli/watch/IssueRow.js +9 -11
- package/dist/cli/watch/QueueObservationView.js +15 -0
- package/dist/cli/watch/StateHistoryView.js +0 -1
- package/dist/cli/watch/StatusBar.js +5 -2
- package/dist/cli/watch/format-utils.js +7 -0
- package/dist/cli/watch/freshness.js +30 -0
- package/dist/cli/watch/state-visualization.js +147 -0
- package/dist/cli/watch/theme.js +6 -7
- package/dist/cli/watch/use-watch-stream.js +5 -2
- package/dist/cli/watch/watch-state.js +9 -5
- package/dist/config.js +129 -36
- package/dist/db/linear-installation-store.js +23 -0
- package/dist/db/migrations.js +42 -0
- package/dist/db/repository-link-store.js +103 -0
- package/dist/db.js +61 -11
- package/dist/factory-state.js +1 -5
- package/dist/github-webhook-handler.js +115 -46
- package/dist/github-webhooks.js +4 -0
- package/dist/http.js +162 -0
- package/dist/install.js +93 -13
- package/dist/issue-query-service.js +34 -1
- package/dist/linear-client.js +80 -25
- package/dist/merge-queue-incident.js +104 -0
- package/dist/merge-queue-protocol.js +54 -0
- package/dist/preflight.js +28 -1
- package/dist/repository-linking.js +42 -0
- package/dist/run-orchestrator.js +197 -21
- package/dist/runtime-paths.js +0 -8
- package/dist/service.js +94 -49
- package/package.json +8 -7
- package/dist/cli/commands/connect.js +0 -54
- package/dist/cli/commands/project.js +0 -146
- package/dist/merge-queue.js +0 -200
- package/infra/patchrelay-reload.service +0 -6
- 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 {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
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 "
|
|
19
|
+
case "service":
|
|
20
20
|
return "doctor";
|
|
21
|
-
case "
|
|
22
|
-
case "installations":
|
|
21
|
+
case "linear":
|
|
23
22
|
case "feed":
|
|
24
|
-
case "
|
|
23
|
+
case "dashboard":
|
|
25
24
|
return "operator_cli";
|
|
26
|
-
case "
|
|
27
|
-
case "
|
|
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 "
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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 "
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
84
|
-
|
|
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
|
-
|
|
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", "
|
|
137
|
+
assertKnownFlags(parsed, command, ["follow", "limit", "issue", "repo", "kind", "stage", "status", "workflow", "json"]);
|
|
93
138
|
return;
|
|
94
|
-
case "
|
|
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 === "
|
|
135
|
-
writeOutput(stdout, `${helpTextFor(
|
|
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
|
-
|
|
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 === "
|
|
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
|
|
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 === "
|
|
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
|
|
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
|
-
|
|
292
|
-
...(options ? { options } : {}),
|
|
319
|
+
runInteractive,
|
|
293
320
|
});
|
|
294
321
|
}
|
|
295
|
-
if (command === "installations") {
|
|
296
|
-
|
|
297
|
-
|
|
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 === "
|
|
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)) {
|
package/dist/cli/interactive.js
CHANGED
|
@@ -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
|
|
4
|
-
|
|
5
|
-
|
|
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
|
];
|
package/dist/cli/watch/App.js
CHANGED
|
@@ -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(
|
|
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
|
+
}
|