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/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
PatchRelay is a self-hosted harness for running a controlled coding loop per Linear issue on your own machine.
|
|
4
4
|
|
|
5
|
-
It receives Linear webhooks, routes issues to the right local repository, prepares durable issue worktrees, runs Codex sessions through `codex app-server`, and keeps the whole issue loop observable and resumable from the CLI. GitHub webhooks drive reactive loops for CI repair
|
|
5
|
+
It receives Linear webhooks, routes issues to the right local repository, prepares durable issue worktrees, runs Codex sessions through `codex app-server`, and keeps the whole issue loop observable and resumable from the CLI. GitHub webhooks drive reactive loops for CI repair and review fixes. A separate [Merge Steward](./packages/merge-steward) service handles serial queue integration and landing.
|
|
6
6
|
|
|
7
7
|
PatchRelay is the system around the model:
|
|
8
8
|
|
|
@@ -11,7 +11,8 @@ PatchRelay is the system around the model:
|
|
|
11
11
|
- issue-to-repo routing
|
|
12
12
|
- issue worktree and branch lifecycle
|
|
13
13
|
- context packaging, run orchestration, and thread continuity
|
|
14
|
-
- reactive CI repair
|
|
14
|
+
- reactive CI repair and review fix loops
|
|
15
|
+
- queue repair runs triggered by Merge Steward evictions
|
|
15
16
|
- native Linear agent input forwarding into active runs
|
|
16
17
|
- read-only inspection and run reporting
|
|
17
18
|
|
|
@@ -36,7 +37,7 @@ PatchRelay does the deterministic harness work that you do not want to re-implem
|
|
|
36
37
|
- packages the right issue, repo, review, and failure context for each loop
|
|
37
38
|
- creates and reuses one durable worktree and branch per issue lifecycle
|
|
38
39
|
- starts Codex threads for implementation runs
|
|
39
|
-
- triggers reactive runs for CI failures, review feedback, and
|
|
40
|
+
- triggers reactive runs for CI failures, review feedback, and Merge Steward evictions
|
|
40
41
|
- persists enough state to correlate the Linear issue, local workspace, run, and Codex thread
|
|
41
42
|
- reports progress back to Linear and forwards follow-up agent input into active runs
|
|
42
43
|
- exposes CLI and optional read-only inspection surfaces so operators can understand what happened
|
|
@@ -79,8 +80,9 @@ You will also need:
|
|
|
79
80
|
2. PatchRelay verifies the webhook, routes the issue to the right local project, and packages the issue context for the first loop.
|
|
80
81
|
3. Delegated issues create or reuse the issue worktree and launch an implementation run through `codex app-server`.
|
|
81
82
|
4. PatchRelay persists thread ids, run state, and observations so the work stays inspectable and resumable.
|
|
82
|
-
5. GitHub webhooks drive reactive verification and repair loops: CI repair on check failures
|
|
83
|
-
6.
|
|
83
|
+
5. GitHub webhooks drive reactive verification and repair loops: CI repair on check failures and review fix on changes requested.
|
|
84
|
+
6. When the PR is approved and CI is green, PatchRelay adds the `queue` label. Merge Steward takes over — rebasing, validating, and merging the PR. If the steward evicts the PR, PatchRelay triggers a queue repair run.
|
|
85
|
+
7. Native agent prompts and Linear comments can steer the active run. An operator can take over from the exact same worktree when needed.
|
|
84
86
|
|
|
85
87
|
## Factory State Machine
|
|
86
88
|
|
|
@@ -127,6 +129,22 @@ PatchRelay uses repo-local workflow files as prompts for Codex runs:
|
|
|
127
129
|
|
|
128
130
|
These files define how the agent should work in that repository. Keep them short, action-oriented, and human-authored.
|
|
129
131
|
|
|
132
|
+
## Knowledge And Validation Surfaces
|
|
133
|
+
|
|
134
|
+
PatchRelay works best when repository guidance follows progressive disclosure:
|
|
135
|
+
|
|
136
|
+
- keep the root entrypoints short and navigational
|
|
137
|
+
- treat deeper `docs/` content as the durable system of record
|
|
138
|
+
- capture architecture, workflow, and product decisions in versioned files instead of chat history or operator memory
|
|
139
|
+
|
|
140
|
+
PatchRelay should also help agents validate their own work inside the issue loop:
|
|
141
|
+
|
|
142
|
+
- package the smallest useful context for the current run instead of replaying ever-growing transcript history
|
|
143
|
+
- preserve high-signal failure evidence such as review feedback, failing checks, and queue incidents
|
|
144
|
+
- make repo-local validation surfaces legible per worktree so the next run can see what passed, what failed, and what needs repair
|
|
145
|
+
|
|
146
|
+
Keeping those knowledge and validation surfaces clean is part of the harness, not optional documentation polish.
|
|
147
|
+
|
|
130
148
|
## Access Control
|
|
131
149
|
|
|
132
150
|
PatchRelay reacts only for issues that route to a configured project.
|
|
@@ -159,8 +177,6 @@ It creates the local config, env file, and system service units:
|
|
|
159
177
|
- `~/.config/patchrelay/service.env`
|
|
160
178
|
- `~/.config/patchrelay/patchrelay.json`
|
|
161
179
|
- `/etc/systemd/system/patchrelay.service`
|
|
162
|
-
- `/etc/systemd/system/patchrelay-reload.service`
|
|
163
|
-
- `/etc/systemd/system/patchrelay.path`
|
|
164
180
|
|
|
165
181
|
The generated `patchrelay.json` is intentionally minimal, and `patchrelay init` prints the webhook URL, OAuth callback URL, and the Linear app values you need next.
|
|
166
182
|
|
|
@@ -177,23 +193,37 @@ LINEAR_OAUTH_CLIENT_SECRET=replace-with-linear-oauth-client-secret
|
|
|
177
193
|
|
|
178
194
|
Keep service secrets in `service.env`. `runtime.env` is for non-secret overrides such as `PATCHRELAY_DB_PATH` or `PATCHRELAY_LOG_FILE`. Everyday local inspection commands do not require exporting these values in your shell.
|
|
179
195
|
|
|
180
|
-
### 4.
|
|
196
|
+
### 4. Connect PatchRelay to Linear
|
|
197
|
+
|
|
198
|
+
Connect PatchRelay to one Linear workspace:
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
patchrelay linear connect
|
|
202
|
+
patchrelay linear sync
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
This authorizes the workspace once, then caches its teams and projects locally. Workspace auth is separate from repo linking.
|
|
206
|
+
|
|
207
|
+
### 5. Link a GitHub repo
|
|
181
208
|
|
|
182
|
-
|
|
209
|
+
Link repos by GitHub identity, not by local path:
|
|
183
210
|
|
|
184
|
-
|
|
211
|
+
```bash
|
|
212
|
+
patchrelay repo link krasnoperov/usertold --workspace usertold --team USE
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
PatchRelay treats the GitHub repo as the source of truth. It reuses an existing local clone under the managed repo root when `origin` already matches, or clones it automatically when missing. Use `--path <path>` only when you want a non-default local location.
|
|
185
216
|
|
|
186
|
-
The generated `~/.config/patchrelay/patchrelay.json`
|
|
217
|
+
The generated `~/.config/patchrelay/patchrelay.json` stays machine-level service config. Repo links should be created with the CLI, not by hand-editing the file.
|
|
187
218
|
|
|
188
|
-
`patchrelay
|
|
219
|
+
`patchrelay repo link` is idempotent:
|
|
189
220
|
|
|
190
|
-
- it creates or updates the
|
|
191
|
-
- it
|
|
221
|
+
- it creates or updates the linked repo entry
|
|
222
|
+
- it refreshes the selected Linear workspace catalog before resolving teams/projects
|
|
192
223
|
- it reloads the service when it can
|
|
193
|
-
- it reuses or starts the Linear connect flow when the local setup is ready
|
|
194
224
|
- if workflow files or secrets are still missing, it tells you exactly what to fix and can be rerun safely
|
|
195
225
|
|
|
196
|
-
###
|
|
226
|
+
### 6. Add workflow docs to the repo
|
|
197
227
|
|
|
198
228
|
PatchRelay looks for:
|
|
199
229
|
|
|
@@ -204,21 +234,21 @@ REVIEW_WORKFLOW.md
|
|
|
204
234
|
|
|
205
235
|
These files define how the agent should work in that repo.
|
|
206
236
|
|
|
207
|
-
###
|
|
237
|
+
### 7. Validate
|
|
208
238
|
|
|
209
239
|
```bash
|
|
210
240
|
patchrelay doctor
|
|
241
|
+
patchrelay service status
|
|
211
242
|
```
|
|
212
243
|
|
|
213
|
-
###
|
|
244
|
+
### 8. Check linked workspaces and repos
|
|
214
245
|
|
|
215
246
|
```bash
|
|
216
|
-
patchrelay
|
|
247
|
+
patchrelay linear list
|
|
248
|
+
patchrelay repo list
|
|
217
249
|
```
|
|
218
250
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
If you later add another local repo that should use the same Linear installation, run `patchrelay project apply <id> <repo-path>` for that repo too. PatchRelay now reuses the single saved installation automatically when there is no ambiguity, so you usually will not need another browser approval.
|
|
251
|
+
If you later add another local repo from the same workspace, run `patchrelay repo link <owner/repo> --workspace <workspace> --team <team>` again. PatchRelay reuses the existing workspace installation instead of opening a new OAuth flow.
|
|
222
252
|
|
|
223
253
|
Important:
|
|
224
254
|
|
|
@@ -236,16 +266,18 @@ Important:
|
|
|
236
266
|
|
|
237
267
|
Useful commands:
|
|
238
268
|
|
|
239
|
-
- `patchrelay list --active`
|
|
240
|
-
- `patchrelay
|
|
241
|
-
- `patchrelay
|
|
242
|
-
- `patchrelay
|
|
243
|
-
- `patchrelay
|
|
244
|
-
- `patchrelay
|
|
245
|
-
- `patchrelay
|
|
246
|
-
- `patchrelay
|
|
269
|
+
- `patchrelay issue list --active`
|
|
270
|
+
- `patchrelay issue show APP-123`
|
|
271
|
+
- `patchrelay issue watch APP-123`
|
|
272
|
+
- `patchrelay dashboard`
|
|
273
|
+
- `patchrelay issue report APP-123`
|
|
274
|
+
- `patchrelay issue events APP-123 --follow`
|
|
275
|
+
- `patchrelay issue path APP-123 --cd`
|
|
276
|
+
- `patchrelay issue open APP-123`
|
|
277
|
+
- `patchrelay issue retry APP-123`
|
|
278
|
+
- `patchrelay service logs --lines 100`
|
|
247
279
|
|
|
248
|
-
`patchrelay open` is the handoff bridge: it opens Codex in the issue worktree and resumes the existing thread when PatchRelay has one.
|
|
280
|
+
`patchrelay issue open` is the handoff bridge: it opens Codex in the issue worktree and resumes the existing thread when PatchRelay has one.
|
|
249
281
|
|
|
250
282
|
Today that takeover path is intentionally YOLO mode: it launches Codex with `--dangerously-bypass-approvals-and-sandbox`.
|
|
251
283
|
|
|
@@ -261,12 +293,32 @@ PatchRelay keeps enough durable state to answer the questions that matter during
|
|
|
261
293
|
- which files it changed
|
|
262
294
|
- whether the run completed, failed, or needs handoff
|
|
263
295
|
|
|
296
|
+
## Merge Steward
|
|
297
|
+
|
|
298
|
+
[Merge Steward](./packages/merge-steward) is a separate service that owns serial merge queue integration. PatchRelay develops code and produces pull requests. Merge Steward delivers those PRs into production — rebasing onto main, waiting for CI, and merging when green.
|
|
299
|
+
|
|
300
|
+
The two services communicate through GitHub. PatchRelay adds a `queue` label when a PR is ready. The steward processes the queue. On failure, the steward evicts the PR with a check run report, and PatchRelay can trigger a queue repair run in response.
|
|
301
|
+
|
|
302
|
+
The steward now has its own bootstrap flow:
|
|
303
|
+
|
|
304
|
+
```bash
|
|
305
|
+
merge-steward init https://queue.example.com
|
|
306
|
+
merge-steward attach app owner/repo --base-branch main --required-check test,lint
|
|
307
|
+
merge-steward doctor --repo app
|
|
308
|
+
merge-steward service status app
|
|
309
|
+
merge-steward queue status --repo app
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
See [Merge queue](./docs/merge-queue.md) for the full two-service overview and [Merge Steward README](./packages/merge-steward/README.md) for operational details.
|
|
313
|
+
|
|
264
314
|
## Docs
|
|
265
315
|
|
|
266
316
|
Use the README for the product overview and quick start. Use the docs for operating details:
|
|
267
317
|
|
|
318
|
+
- [Merge queue and delivery](./docs/merge-queue.md)
|
|
268
319
|
- [Self-hosting and deployment](./docs/self-hosting.md)
|
|
269
320
|
- [Architecture](./docs/architecture.md)
|
|
321
|
+
- [Design docs index](./docs/design-docs/index.md)
|
|
270
322
|
- [Design principles](./docs/design-docs/core-beliefs.md)
|
|
271
323
|
- [External reference patterns](./docs/references/external-patterns.md)
|
|
272
324
|
- [Security policy](./SECURITY.md)
|
|
@@ -88,7 +88,6 @@ export function buildAgentSessionPlan(params) {
|
|
|
88
88
|
const runType = resolvePlanRunType(params);
|
|
89
89
|
switch (params.factoryState) {
|
|
90
90
|
case "delegated":
|
|
91
|
-
case "preparing":
|
|
92
91
|
return setStatuses(planForRunType(runType, params), ["inProgress", "pending", "pending", "pending"]);
|
|
93
92
|
case "implementing":
|
|
94
93
|
return setStatuses(planForRunType("implementation", params), ["completed", "inProgress", "pending", "pending"]);
|
|
@@ -144,12 +143,6 @@ export function buildAgentSessionPlanForIssue(issue, options) {
|
|
|
144
143
|
...(options?.activeRunType ? { activeRunType: options.activeRunType } : {}),
|
|
145
144
|
});
|
|
146
145
|
}
|
|
147
|
-
export function buildPreparingSessionPlan(runType) {
|
|
148
|
-
return buildAgentSessionPlan({
|
|
149
|
-
factoryState: "preparing",
|
|
150
|
-
pendingRunType: runType,
|
|
151
|
-
});
|
|
152
|
-
}
|
|
153
146
|
export function buildRunningSessionPlan(runType) {
|
|
154
147
|
return buildAgentSessionPlan({
|
|
155
148
|
factoryState: runType === "ci_repair" ? "repairing_ci"
|
package/dist/build-info.json
CHANGED
package/dist/cli/args.js
CHANGED
|
@@ -2,26 +2,22 @@ import { UnknownCommandError, UnknownFlagsError } from "./errors.js";
|
|
|
2
2
|
export const KNOWN_COMMANDS = new Set([
|
|
3
3
|
"version",
|
|
4
4
|
"serve",
|
|
5
|
-
"
|
|
6
|
-
"live",
|
|
7
|
-
"report",
|
|
8
|
-
"events",
|
|
9
|
-
"worktree",
|
|
10
|
-
"open",
|
|
11
|
-
"retry",
|
|
12
|
-
"list",
|
|
5
|
+
"issue",
|
|
13
6
|
"doctor",
|
|
14
7
|
"init",
|
|
15
|
-
"
|
|
8
|
+
"attach",
|
|
9
|
+
"repos",
|
|
10
|
+
"linear",
|
|
11
|
+
"repo",
|
|
12
|
+
"dashboard",
|
|
13
|
+
"dash",
|
|
14
|
+
"d",
|
|
15
|
+
"service",
|
|
16
16
|
"connect",
|
|
17
17
|
"installations",
|
|
18
18
|
"feed",
|
|
19
|
-
"watch",
|
|
20
|
-
"install-service",
|
|
21
|
-
"restart-service",
|
|
22
19
|
"help",
|
|
23
20
|
]);
|
|
24
|
-
const ISSUE_KEY_PATTERN = /^[A-Za-z][A-Za-z0-9]*-\d+$/;
|
|
25
21
|
export function parseArgs(argv) {
|
|
26
22
|
const positionals = [];
|
|
27
23
|
const flags = new Map();
|
|
@@ -60,10 +56,10 @@ export function resolveCommand(parsed) {
|
|
|
60
56
|
return { command: "help", commandArgs: [] };
|
|
61
57
|
}
|
|
62
58
|
if (KNOWN_COMMANDS.has(requestedCommand)) {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
return { command
|
|
59
|
+
const command = requestedCommand === "dash" || requestedCommand === "d"
|
|
60
|
+
? "dashboard"
|
|
61
|
+
: requestedCommand;
|
|
62
|
+
return { command, commandArgs: parsed.positionals.slice(1) };
|
|
67
63
|
}
|
|
68
64
|
throw new UnknownCommandError(requestedCommand);
|
|
69
65
|
}
|
|
@@ -92,7 +88,15 @@ export function assertKnownFlags(parsed, command, allowedFlags) {
|
|
|
92
88
|
if (unknownFlags.length === 0) {
|
|
93
89
|
return;
|
|
94
90
|
}
|
|
95
|
-
throw new UnknownFlagsError(unknownFlags, command === "
|
|
91
|
+
throw new UnknownFlagsError(unknownFlags, command === "repo"
|
|
92
|
+
? "repo"
|
|
93
|
+
: command === "linear"
|
|
94
|
+
? "linear"
|
|
95
|
+
: command === "issue"
|
|
96
|
+
? "issue"
|
|
97
|
+
: command === "service"
|
|
98
|
+
? "service"
|
|
99
|
+
: "root");
|
|
96
100
|
}
|
|
97
101
|
export function parsePositiveIntegerFlag(value, flagName) {
|
|
98
102
|
if (typeof value !== "string") {
|
|
@@ -26,7 +26,7 @@ export async function handleFeedCommand(params) {
|
|
|
26
26
|
const limit = parseLimit(params.parsed.flags.get("limit"));
|
|
27
27
|
const follow = params.parsed.flags.get("follow") === true;
|
|
28
28
|
const issueKey = readOptionalStringFlag(params.parsed, "issue");
|
|
29
|
-
const projectId = readOptionalStringFlag(params.parsed, "
|
|
29
|
+
const projectId = readOptionalStringFlag(params.parsed, "repo");
|
|
30
30
|
const kind = readOptionalStringFlag(params.parsed, "kind");
|
|
31
31
|
const stage = readOptionalStringFlag(params.parsed, "stage");
|
|
32
32
|
const status = readOptionalStringFlag(params.parsed, "status");
|
|
@@ -1,13 +1,53 @@
|
|
|
1
1
|
import { setTimeout as delay } from "node:timers/promises";
|
|
2
2
|
import { getRunTypeFlag, parsePositiveIntegerFlag } from "../args.js";
|
|
3
|
+
import { CliUsageError } from "../errors.js";
|
|
3
4
|
import { formatJson } from "../formatters/json.js";
|
|
4
5
|
import { formatEvents, formatInspect, formatList, formatLive, formatOpen, formatReport, formatRetry, formatWorktree } from "../formatters/text.js";
|
|
5
6
|
import { buildOpenCommand } from "../interactive.js";
|
|
6
7
|
import { writeOutput } from "../output.js";
|
|
8
|
+
export async function handleIssueCommand(params) {
|
|
9
|
+
const subcommand = params.commandArgs[0];
|
|
10
|
+
if (!subcommand) {
|
|
11
|
+
throw new CliUsageError("patchrelay issue requires a subcommand.", "issue");
|
|
12
|
+
}
|
|
13
|
+
const nested = {
|
|
14
|
+
...params,
|
|
15
|
+
commandArgs: params.commandArgs.slice(1),
|
|
16
|
+
};
|
|
17
|
+
switch (subcommand) {
|
|
18
|
+
case "show":
|
|
19
|
+
return await handleInspectCommand(nested);
|
|
20
|
+
case "list":
|
|
21
|
+
return await handleListCommand(nested);
|
|
22
|
+
case "watch": {
|
|
23
|
+
const flags = new Map(params.parsed.flags);
|
|
24
|
+
flags.set("watch", true);
|
|
25
|
+
return await handleLiveCommand({
|
|
26
|
+
...nested,
|
|
27
|
+
parsed: {
|
|
28
|
+
...params.parsed,
|
|
29
|
+
flags,
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
case "report":
|
|
34
|
+
return await handleReportCommand(nested);
|
|
35
|
+
case "events":
|
|
36
|
+
return await handleEventsCommand(nested);
|
|
37
|
+
case "path":
|
|
38
|
+
return await handleWorktreeCommand(nested);
|
|
39
|
+
case "open":
|
|
40
|
+
return await handleOpenCommand(nested);
|
|
41
|
+
case "retry":
|
|
42
|
+
return await handleRetryCommand(nested);
|
|
43
|
+
default:
|
|
44
|
+
throw new CliUsageError(`Unknown issue command: ${subcommand}`, "issue");
|
|
45
|
+
}
|
|
46
|
+
}
|
|
7
47
|
export async function handleInspectCommand(params) {
|
|
8
48
|
const issueKey = params.commandArgs[0];
|
|
9
49
|
if (!issueKey) {
|
|
10
|
-
throw new Error("
|
|
50
|
+
throw new Error("show requires <issueKey>.");
|
|
11
51
|
}
|
|
12
52
|
const result = await params.data.inspect(issueKey);
|
|
13
53
|
if (!result) {
|
|
@@ -19,7 +59,7 @@ export async function handleInspectCommand(params) {
|
|
|
19
59
|
export async function handleLiveCommand(params) {
|
|
20
60
|
const issueKey = params.commandArgs[0];
|
|
21
61
|
if (!issueKey) {
|
|
22
|
-
throw new Error("
|
|
62
|
+
throw new Error(`${params.parsed.flags.get("watch") === true ? "watch" : "status"} requires <issueKey>.`);
|
|
23
63
|
}
|
|
24
64
|
const watch = params.parsed.flags.get("watch") === true;
|
|
25
65
|
do {
|
|
@@ -88,7 +128,7 @@ export async function handleEventsCommand(params) {
|
|
|
88
128
|
export async function handleWorktreeCommand(params) {
|
|
89
129
|
const issueKey = params.commandArgs[0];
|
|
90
130
|
if (!issueKey) {
|
|
91
|
-
throw new Error("
|
|
131
|
+
throw new Error("path requires <issueKey>.");
|
|
92
132
|
}
|
|
93
133
|
const result = params.data.worktree(issueKey);
|
|
94
134
|
if (!result) {
|
|
@@ -150,7 +190,7 @@ export async function handleListCommand(params) {
|
|
|
150
190
|
const result = params.data.list({
|
|
151
191
|
active: params.parsed.flags.get("active") === true,
|
|
152
192
|
failed: params.parsed.flags.get("failed") === true,
|
|
153
|
-
...(typeof params.parsed.flags.get("
|
|
193
|
+
...(typeof params.parsed.flags.get("repo") === "string" ? { project: String(params.parsed.flags.get("repo")) } : {}),
|
|
154
194
|
});
|
|
155
195
|
writeOutput(params.stdout, params.json ? formatJson(result) : formatList(result));
|
|
156
196
|
return 0;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { loadConfig } from "../../config.js";
|
|
2
|
+
import { runConnectFlow, parseTimeoutSeconds } from "../connect-flow.js";
|
|
3
|
+
import { formatJson } from "../formatters/json.js";
|
|
4
|
+
import { writeOutput } from "../output.js";
|
|
5
|
+
import { openExternalUrl } from "../interactive.js";
|
|
6
|
+
export async function handleLinearCommand(params) {
|
|
7
|
+
const subcommand = params.commandArgs[0] ?? "list";
|
|
8
|
+
const config = params.options?.config ?? loadConfig(undefined, { profile: "operator_cli" });
|
|
9
|
+
const data = params.options?.data ?? (await createCliOperatorDataAccess(config));
|
|
10
|
+
try {
|
|
11
|
+
switch (subcommand) {
|
|
12
|
+
case "connect":
|
|
13
|
+
return await runConnectFlow({
|
|
14
|
+
config,
|
|
15
|
+
data,
|
|
16
|
+
stdout: params.stdout,
|
|
17
|
+
noOpen: params.parsed.flags.get("no-open") === true,
|
|
18
|
+
timeoutSeconds: parseTimeoutSeconds(params.parsed.flags.get("timeout"), "linear connect"),
|
|
19
|
+
json: params.json,
|
|
20
|
+
openExternal: params.options?.openExternal ?? openExternalUrl,
|
|
21
|
+
...(params.options?.connectPollIntervalMs !== undefined ? { connectPollIntervalMs: params.options.connectPollIntervalMs } : {}),
|
|
22
|
+
});
|
|
23
|
+
case "list": {
|
|
24
|
+
const result = await data.listLinearWorkspaces();
|
|
25
|
+
writeOutput(params.stdout, params.json
|
|
26
|
+
? formatJson({ ok: true, ...result })
|
|
27
|
+
: result.workspaces.length === 0
|
|
28
|
+
? "No Linear workspaces connected.\n"
|
|
29
|
+
: `${result.workspaces.map((workspace) => {
|
|
30
|
+
const name = workspace.installation.workspaceKey ?? workspace.installation.workspaceName ?? `installation-${workspace.installation.id}`;
|
|
31
|
+
return `${name} repos=${workspace.linkedRepos.length} teams=${workspace.teams.length} projects=${workspace.projects.length}`;
|
|
32
|
+
}).join("\n")}\n`);
|
|
33
|
+
return 0;
|
|
34
|
+
}
|
|
35
|
+
case "sync": {
|
|
36
|
+
const workspace = params.commandArgs[1];
|
|
37
|
+
const result = await data.syncLinearWorkspace(workspace);
|
|
38
|
+
writeOutput(params.stdout, params.json
|
|
39
|
+
? formatJson({ ok: true, ...result })
|
|
40
|
+
: `Synced ${result.installation.workspaceKey ?? result.installation.workspaceName ?? result.installation.id}: ${result.teams.length} teams, ${result.projects.length} projects\n`);
|
|
41
|
+
return 0;
|
|
42
|
+
}
|
|
43
|
+
case "disconnect": {
|
|
44
|
+
const workspace = params.commandArgs[1];
|
|
45
|
+
if (!workspace) {
|
|
46
|
+
throw new Error("patchrelay linear disconnect requires <workspace>.");
|
|
47
|
+
}
|
|
48
|
+
const result = await data.disconnectLinearWorkspace(workspace);
|
|
49
|
+
writeOutput(params.stdout, params.json
|
|
50
|
+
? formatJson({ ok: true, ...result })
|
|
51
|
+
: `Disconnected ${result.installation.workspaceKey ?? result.installation.workspaceName ?? result.installation.id}.\n`);
|
|
52
|
+
return 0;
|
|
53
|
+
}
|
|
54
|
+
default:
|
|
55
|
+
throw new Error(`Unknown linear subcommand: ${subcommand}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
finally {
|
|
59
|
+
if (!params.options?.data) {
|
|
60
|
+
data.close();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
async function createCliOperatorDataAccess(config) {
|
|
65
|
+
const { CliOperatorApiClient } = await import("../operator-client.js");
|
|
66
|
+
return new CliOperatorApiClient(config);
|
|
67
|
+
}
|