patchrelay 0.6.1 → 0.7.0
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/dist/build-info.json +3 -3
- package/dist/cli/args.js +11 -3
- package/dist/cli/commands/project.js +128 -122
- package/dist/cli/errors.js +20 -0
- package/dist/cli/help.js +108 -0
- package/dist/cli/index.js +51 -71
- package/dist/cli/output.js +4 -0
- package/dist/index.js +12 -10
- package/package.json +1 -1
package/dist/build-info.json
CHANGED
package/dist/cli/args.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { UnknownCommandError, UnknownFlagsError } from "./errors.js";
|
|
1
2
|
export const KNOWN_COMMANDS = new Set([
|
|
2
3
|
"version",
|
|
3
4
|
"serve",
|
|
@@ -25,6 +26,10 @@ export function parseArgs(argv) {
|
|
|
25
26
|
const flags = new Map();
|
|
26
27
|
for (let index = 0; index < argv.length; index += 1) {
|
|
27
28
|
const value = argv[index];
|
|
29
|
+
if (value === "-h" || value === "--help") {
|
|
30
|
+
flags.set("help", true);
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
28
33
|
if (!value.startsWith("--")) {
|
|
29
34
|
positionals.push(value);
|
|
30
35
|
continue;
|
|
@@ -59,7 +64,10 @@ export function resolveCommand(parsed) {
|
|
|
59
64
|
if (ISSUE_KEY_PATTERN.test(requestedCommand)) {
|
|
60
65
|
return { command: "inspect", commandArgs: parsed.positionals };
|
|
61
66
|
}
|
|
62
|
-
throw new
|
|
67
|
+
throw new UnknownCommandError(requestedCommand);
|
|
68
|
+
}
|
|
69
|
+
export function hasHelpFlag(parsed) {
|
|
70
|
+
return parsed.flags.get("help") === true;
|
|
63
71
|
}
|
|
64
72
|
export function getStageFlag(value) {
|
|
65
73
|
if (typeof value !== "string") {
|
|
@@ -78,12 +86,12 @@ export function parseCsvFlag(value) {
|
|
|
78
86
|
.filter(Boolean);
|
|
79
87
|
}
|
|
80
88
|
export function assertKnownFlags(parsed, command, allowedFlags) {
|
|
81
|
-
const allowed = new Set(allowedFlags);
|
|
89
|
+
const allowed = new Set(["help", ...allowedFlags]);
|
|
82
90
|
const unknownFlags = [...parsed.flags.keys()].filter((flag) => !allowed.has(flag)).sort();
|
|
83
91
|
if (unknownFlags.length === 0) {
|
|
84
92
|
return;
|
|
85
93
|
}
|
|
86
|
-
throw new
|
|
94
|
+
throw new UnknownFlagsError(unknownFlags, command === "project" || command === "project apply" ? "project" : "root");
|
|
87
95
|
}
|
|
88
96
|
export function parsePositiveIntegerFlag(value, flagName) {
|
|
89
97
|
if (typeof value !== "string") {
|
|
@@ -1,140 +1,146 @@
|
|
|
1
1
|
import { loadConfig } from "../../config.js";
|
|
2
2
|
import { installUserServiceUnits, upsertProjectInConfig } from "../../install.js";
|
|
3
|
-
import {
|
|
4
|
-
import { parseCsvFlag } from "../args.js";
|
|
3
|
+
import { hasHelpFlag, parseCsvFlag } from "../args.js";
|
|
5
4
|
import { runConnectFlow, parseTimeoutSeconds } from "../connect-flow.js";
|
|
6
|
-
import {
|
|
5
|
+
import { CliUsageError } from "../errors.js";
|
|
7
6
|
import { formatJson } from "../formatters/json.js";
|
|
7
|
+
import { projectHelpText } from "../help.js";
|
|
8
8
|
import { writeOutput } from "../output.js";
|
|
9
9
|
import { installServiceCommands, tryManageService } from "../service-commands.js";
|
|
10
10
|
export async function handleProjectCommand(params) {
|
|
11
|
+
if (hasHelpFlag(params.parsed)) {
|
|
12
|
+
writeOutput(params.stdout, `${projectHelpText()}\n`);
|
|
13
|
+
return 0;
|
|
14
|
+
}
|
|
15
|
+
if (params.commandArgs.length === 0) {
|
|
16
|
+
throw new CliUsageError("patchrelay project requires a subcommand.", "project");
|
|
17
|
+
}
|
|
18
|
+
const subcommand = params.commandArgs[0];
|
|
19
|
+
if (subcommand !== "apply") {
|
|
20
|
+
throw new CliUsageError(`Unknown project command: ${subcommand}`, "project");
|
|
21
|
+
}
|
|
22
|
+
const projectId = params.commandArgs[1];
|
|
23
|
+
const repoPath = params.commandArgs[2];
|
|
24
|
+
if (!projectId || !repoPath) {
|
|
25
|
+
throw new CliUsageError("patchrelay project apply requires <id> and <repo-path>.", "project");
|
|
26
|
+
}
|
|
27
|
+
const result = await upsertProjectInConfig({
|
|
28
|
+
id: projectId,
|
|
29
|
+
repoPath,
|
|
30
|
+
issueKeyPrefixes: parseCsvFlag(params.parsed.flags.get("issue-prefix")),
|
|
31
|
+
linearTeamIds: parseCsvFlag(params.parsed.flags.get("team-id")),
|
|
32
|
+
});
|
|
33
|
+
const serviceUnits = await installUserServiceUnits();
|
|
34
|
+
const noConnect = params.parsed.flags.get("no-connect") === true;
|
|
35
|
+
const lines = [
|
|
36
|
+
`Config file: ${result.configPath}`,
|
|
37
|
+
`${result.status === "created" ? "Created" : result.status === "updated" ? "Updated" : "Verified"} project ${result.project.id} for ${result.project.repoPath}`,
|
|
38
|
+
result.project.issueKeyPrefixes.length > 0 ? `Issue key prefixes: ${result.project.issueKeyPrefixes.join(", ")}` : undefined,
|
|
39
|
+
result.project.linearTeamIds.length > 0 ? `Linear team ids: ${result.project.linearTeamIds.join(", ")}` : undefined,
|
|
40
|
+
`Service unit: ${serviceUnits.unitPath} (${serviceUnits.serviceStatus})`,
|
|
41
|
+
`Watcher unit: ${serviceUnits.pathUnitPath} (${serviceUnits.pathStatus})`,
|
|
42
|
+
].filter(Boolean);
|
|
43
|
+
let fullConfig;
|
|
11
44
|
try {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const noConnect = params.parsed.flags.get("no-connect") === true;
|
|
29
|
-
const lines = [
|
|
30
|
-
`Config file: ${result.configPath}`,
|
|
31
|
-
`${result.status === "created" ? "Created" : result.status === "updated" ? "Updated" : "Verified"} project ${result.project.id} for ${result.project.repoPath}`,
|
|
32
|
-
result.project.issueKeyPrefixes.length > 0 ? `Issue key prefixes: ${result.project.issueKeyPrefixes.join(", ")}` : undefined,
|
|
33
|
-
result.project.linearTeamIds.length > 0 ? `Linear team ids: ${result.project.linearTeamIds.join(", ")}` : undefined,
|
|
34
|
-
`Service unit: ${serviceUnits.unitPath} (${serviceUnits.serviceStatus})`,
|
|
35
|
-
`Watcher unit: ${serviceUnits.pathUnitPath} (${serviceUnits.pathStatus})`,
|
|
36
|
-
].filter(Boolean);
|
|
37
|
-
let fullConfig;
|
|
38
|
-
try {
|
|
39
|
-
fullConfig = loadConfig(undefined, { profile: "doctor" });
|
|
40
|
-
}
|
|
41
|
-
catch (error) {
|
|
42
|
-
if (params.json) {
|
|
43
|
-
writeOutput(params.stdout, formatJson({
|
|
44
|
-
...result,
|
|
45
|
-
serviceUnits,
|
|
46
|
-
readiness: {
|
|
47
|
-
ok: false,
|
|
48
|
-
error: error instanceof Error ? error.message : String(error),
|
|
49
|
-
},
|
|
50
|
-
connect: {
|
|
51
|
-
attempted: false,
|
|
52
|
-
skipped: "missing_env",
|
|
53
|
-
},
|
|
54
|
-
}));
|
|
55
|
-
return 0;
|
|
56
|
-
}
|
|
57
|
-
lines.push(`Linear connect was skipped: ${error instanceof Error ? error.message : String(error)}`);
|
|
58
|
-
lines.push("Finish the required env vars and rerun `patchrelay project apply`.");
|
|
59
|
-
writeOutput(params.stdout, `${lines.join("\n")}\n`);
|
|
45
|
+
fullConfig = loadConfig(undefined, { profile: "doctor" });
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
if (params.json) {
|
|
49
|
+
writeOutput(params.stdout, formatJson({
|
|
50
|
+
...result,
|
|
51
|
+
serviceUnits,
|
|
52
|
+
readiness: {
|
|
53
|
+
ok: false,
|
|
54
|
+
error: error instanceof Error ? error.message : String(error),
|
|
55
|
+
},
|
|
56
|
+
connect: {
|
|
57
|
+
attempted: false,
|
|
58
|
+
skipped: "missing_env",
|
|
59
|
+
},
|
|
60
|
+
}));
|
|
60
61
|
return 0;
|
|
61
62
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
63
|
+
lines.push(`Linear connect was skipped: ${error instanceof Error ? error.message : String(error)}`);
|
|
64
|
+
lines.push("Finish the required env vars and rerun `patchrelay project apply`.");
|
|
65
|
+
writeOutput(params.stdout, `${lines.join("\n")}\n`);
|
|
66
|
+
return 0;
|
|
67
|
+
}
|
|
68
|
+
const { runPreflight } = await import("../../preflight.js");
|
|
69
|
+
const report = await runPreflight(fullConfig);
|
|
70
|
+
const failedChecks = report.checks.filter((check) => check.status === "fail");
|
|
71
|
+
if (failedChecks.length > 0) {
|
|
72
|
+
if (params.json) {
|
|
73
|
+
writeOutput(params.stdout, formatJson({
|
|
74
|
+
...result,
|
|
75
|
+
serviceUnits,
|
|
76
|
+
readiness: report,
|
|
77
|
+
connect: {
|
|
78
|
+
attempted: false,
|
|
79
|
+
skipped: "preflight_failed",
|
|
80
|
+
},
|
|
81
|
+
}));
|
|
81
82
|
return 0;
|
|
82
83
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
84
|
+
lines.push("Linear connect was skipped because PatchRelay is not ready yet:");
|
|
85
|
+
lines.push(...failedChecks.map((check) => `- [${check.scope}] ${check.message}`));
|
|
86
|
+
lines.push("Fix the failures above and rerun `patchrelay project apply`.");
|
|
87
|
+
writeOutput(params.stdout, `${lines.join("\n")}\n`);
|
|
88
|
+
return 0;
|
|
89
|
+
}
|
|
90
|
+
const serviceState = await tryManageService(params.runInteractive, installServiceCommands());
|
|
91
|
+
if (!serviceState.ok) {
|
|
92
|
+
throw new Error(`Project was saved, but PatchRelay could not be reloaded: ${serviceState.error}`);
|
|
93
|
+
}
|
|
94
|
+
const cliData = params.options?.data ?? (await createCliDataAccess(fullConfig));
|
|
95
|
+
try {
|
|
96
|
+
if (params.json) {
|
|
97
|
+
const connectResult = noConnect ? undefined : await cliData.connect(projectId);
|
|
98
|
+
writeOutput(params.stdout, formatJson({
|
|
99
|
+
...result,
|
|
100
|
+
serviceUnits,
|
|
101
|
+
readiness: report,
|
|
102
|
+
serviceReloaded: true,
|
|
103
|
+
...(noConnect
|
|
104
|
+
? {
|
|
105
|
+
connect: {
|
|
106
|
+
attempted: false,
|
|
107
|
+
skipped: "no_connect",
|
|
108
|
+
},
|
|
109
|
+
}
|
|
110
|
+
: {
|
|
111
|
+
connect: {
|
|
112
|
+
attempted: true,
|
|
113
|
+
result: connectResult,
|
|
114
|
+
},
|
|
115
|
+
}),
|
|
116
|
+
}));
|
|
117
|
+
return 0;
|
|
86
118
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
const connectResult = noConnect ? undefined : await cliData.connect(projectId);
|
|
91
|
-
writeOutput(params.stdout, formatJson({
|
|
92
|
-
...result,
|
|
93
|
-
serviceUnits,
|
|
94
|
-
readiness: report,
|
|
95
|
-
serviceReloaded: true,
|
|
96
|
-
...(noConnect
|
|
97
|
-
? {
|
|
98
|
-
connect: {
|
|
99
|
-
attempted: false,
|
|
100
|
-
skipped: "no_connect",
|
|
101
|
-
},
|
|
102
|
-
}
|
|
103
|
-
: {
|
|
104
|
-
connect: {
|
|
105
|
-
attempted: true,
|
|
106
|
-
result: connectResult,
|
|
107
|
-
},
|
|
108
|
-
}),
|
|
109
|
-
}));
|
|
110
|
-
return 0;
|
|
111
|
-
}
|
|
112
|
-
if (noConnect) {
|
|
113
|
-
lines.push("Project saved and PatchRelay was reloaded.");
|
|
114
|
-
lines.push(`Next: patchrelay connect --project ${result.project.id}`);
|
|
115
|
-
writeOutput(params.stdout, `${lines.join("\n")}\n`);
|
|
116
|
-
return 0;
|
|
117
|
-
}
|
|
119
|
+
if (noConnect) {
|
|
120
|
+
lines.push("Project saved and PatchRelay was reloaded.");
|
|
121
|
+
lines.push(`Next: patchrelay connect --project ${result.project.id}`);
|
|
118
122
|
writeOutput(params.stdout, `${lines.join("\n")}\n`);
|
|
119
|
-
return
|
|
120
|
-
config: fullConfig,
|
|
121
|
-
data: cliData,
|
|
122
|
-
stdout: params.stdout,
|
|
123
|
-
noOpen: params.parsed.flags.get("no-open") === true,
|
|
124
|
-
timeoutSeconds: parseTimeoutSeconds(params.parsed.flags.get("timeout"), "project apply"),
|
|
125
|
-
projectId,
|
|
126
|
-
...(params.options?.openExternal ? { openExternal: params.options.openExternal } : {}),
|
|
127
|
-
...(params.options?.connectPollIntervalMs !== undefined ? { connectPollIntervalMs: params.options.connectPollIntervalMs } : {}),
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
|
-
finally {
|
|
131
|
-
if (!params.options?.data) {
|
|
132
|
-
cliData.close();
|
|
133
|
-
}
|
|
123
|
+
return 0;
|
|
134
124
|
}
|
|
125
|
+
writeOutput(params.stdout, `${lines.join("\n")}\n`);
|
|
126
|
+
return await runConnectFlow({
|
|
127
|
+
config: fullConfig,
|
|
128
|
+
data: cliData,
|
|
129
|
+
stdout: params.stdout,
|
|
130
|
+
noOpen: params.parsed.flags.get("no-open") === true,
|
|
131
|
+
timeoutSeconds: parseTimeoutSeconds(params.parsed.flags.get("timeout"), "project apply"),
|
|
132
|
+
projectId,
|
|
133
|
+
...(params.options?.openExternal ? { openExternal: params.options.openExternal } : {}),
|
|
134
|
+
...(params.options?.connectPollIntervalMs !== undefined ? { connectPollIntervalMs: params.options.connectPollIntervalMs } : {}),
|
|
135
|
+
});
|
|
135
136
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
137
|
+
finally {
|
|
138
|
+
if (!params.options?.data) {
|
|
139
|
+
cliData.close();
|
|
140
|
+
}
|
|
139
141
|
}
|
|
140
142
|
}
|
|
143
|
+
async function createCliDataAccess(config) {
|
|
144
|
+
const { CliDataAccess } = await import("../data.js");
|
|
145
|
+
return new CliDataAccess(config);
|
|
146
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export class CliUsageError extends Error {
|
|
2
|
+
helpTopic;
|
|
3
|
+
constructor(message, helpTopic = "root") {
|
|
4
|
+
super(message);
|
|
5
|
+
this.helpTopic = helpTopic;
|
|
6
|
+
this.name = "CliUsageError";
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export class UnknownCommandError extends CliUsageError {
|
|
10
|
+
constructor(command) {
|
|
11
|
+
super(`Unknown command: ${command}`);
|
|
12
|
+
this.name = "UnknownCommandError";
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export class UnknownFlagsError extends CliUsageError {
|
|
16
|
+
constructor(flags, helpTopic = "root") {
|
|
17
|
+
super(`Unknown flag${flags.length === 1 ? "" : "s"}: ${flags.map((flag) => `--${flag}`).join(", ")}`, helpTopic);
|
|
18
|
+
this.name = "UnknownFlagsError";
|
|
19
|
+
}
|
|
20
|
+
}
|
package/dist/cli/help.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
export function rootHelpText() {
|
|
2
|
+
return [
|
|
3
|
+
"PatchRelay",
|
|
4
|
+
"",
|
|
5
|
+
"patchrelay is a local service and CLI that connects Linear issue delegation to Codex worktrees on your machine.",
|
|
6
|
+
"",
|
|
7
|
+
"Usage:",
|
|
8
|
+
" patchrelay <command> [args] [flags]",
|
|
9
|
+
" patchrelay <issueKey> # shorthand for `patchrelay inspect <issueKey>`",
|
|
10
|
+
"",
|
|
11
|
+
"First-time setup:",
|
|
12
|
+
" 1. patchrelay init <public-https-url>",
|
|
13
|
+
" 2. Fill in ~/.config/patchrelay/service.env",
|
|
14
|
+
" 3. patchrelay project apply <id> <repo-path>",
|
|
15
|
+
" 4. Edit the generated project workflows if needed, then add those workflow files to the repo",
|
|
16
|
+
" 5. patchrelay doctor",
|
|
17
|
+
"",
|
|
18
|
+
"Why init needs the public URL:",
|
|
19
|
+
" Linear must reach PatchRelay at a public HTTPS origin for both the webhook endpoint",
|
|
20
|
+
" and the OAuth callback. `patchrelay init` writes that origin to `server.public_base_url`.",
|
|
21
|
+
"",
|
|
22
|
+
"Default behavior:",
|
|
23
|
+
" PatchRelay already defaults the local bind address, database path, log path, worktree",
|
|
24
|
+
" root, and Codex runner settings. In the normal",
|
|
25
|
+
" case you only need the public URL, the required secrets, and at least one project.",
|
|
26
|
+
" `patchrelay init` installs the user service and config watcher, and `project apply`",
|
|
27
|
+
" upserts the repo config and reuses or starts the Linear connection flow.",
|
|
28
|
+
"",
|
|
29
|
+
"Commands:",
|
|
30
|
+
" version [--json] Show the installed PatchRelay build version",
|
|
31
|
+
" init <public-base-url> [--force] [--json] Bootstrap the machine-level PatchRelay home",
|
|
32
|
+
" project apply <id> <repo-path> [--issue-prefix <prefixes>] [--team-id <ids>] [--no-connect] [--no-open] [--timeout <seconds>] [--json]",
|
|
33
|
+
" Upsert one local repository and connect it to Linear when ready",
|
|
34
|
+
" doctor [--json] Check secrets, paths, configured workflow files, git, and codex",
|
|
35
|
+
" install-service [--force] [--write-only] [--json] Reinstall the systemd user service and watcher",
|
|
36
|
+
" restart-service [--json] Reload-or-restart the systemd user service",
|
|
37
|
+
" connect [--project <projectId>] [--no-open] [--timeout <seconds>] [--json]",
|
|
38
|
+
" Advanced: start or reuse a Linear installation directly",
|
|
39
|
+
" installations [--json] Show connected Linear installations",
|
|
40
|
+
" feed [--follow] [--limit <count>] [--issue <issueKey>] [--project <projectId>] [--json]",
|
|
41
|
+
" Show a live operator feed from the daemon",
|
|
42
|
+
" serve Run the local PatchRelay service",
|
|
43
|
+
" inspect <issueKey> Show the latest known issue state",
|
|
44
|
+
" live <issueKey> [--watch] [--json] Show the active run status",
|
|
45
|
+
" report <issueKey> [--stage <workflow>] [--stage-run <id>] [--json]",
|
|
46
|
+
" Show finished workflow reports",
|
|
47
|
+
" events <issueKey> [--stage-run <id>] [--method <name>] [--follow] [--json]",
|
|
48
|
+
" Show raw thread events",
|
|
49
|
+
" worktree <issueKey> [--cd] [--json] Print the issue worktree path",
|
|
50
|
+
" open <issueKey> [--print] [--json] Open Codex in the issue worktree",
|
|
51
|
+
" retry <issueKey> [--stage <workflow>] [--reason <text>] [--json]",
|
|
52
|
+
" Requeue a workflow",
|
|
53
|
+
" list [--active] [--failed] [--project <projectId>] [--json]",
|
|
54
|
+
" List tracked issues",
|
|
55
|
+
"",
|
|
56
|
+
"Environment options:",
|
|
57
|
+
" --help, -h Show help for the root command or current command group",
|
|
58
|
+
"",
|
|
59
|
+
"Automation env vars:",
|
|
60
|
+
" PATCHRELAY_CONFIG Override the config file path",
|
|
61
|
+
" PATCHRELAY_DB_PATH Override the SQLite database path",
|
|
62
|
+
" PATCHRELAY_LOG_FILE Override the log file path",
|
|
63
|
+
" PATCHRELAY_LOG_LEVEL Override the log level",
|
|
64
|
+
"",
|
|
65
|
+
"Examples:",
|
|
66
|
+
" patchrelay init https://patchrelay.example.com",
|
|
67
|
+
" patchrelay project apply app /absolute/path/to/repo",
|
|
68
|
+
" patchrelay doctor",
|
|
69
|
+
" patchrelay USE-54",
|
|
70
|
+
" patchrelay version --json",
|
|
71
|
+
"",
|
|
72
|
+
"Command help:",
|
|
73
|
+
" patchrelay help",
|
|
74
|
+
" patchrelay help project",
|
|
75
|
+
" patchrelay project --help",
|
|
76
|
+
].join("\n");
|
|
77
|
+
}
|
|
78
|
+
export function projectHelpText() {
|
|
79
|
+
return [
|
|
80
|
+
"Usage:",
|
|
81
|
+
" patchrelay project apply <id> <repo-path> [options]",
|
|
82
|
+
"",
|
|
83
|
+
"Commands:",
|
|
84
|
+
" apply <id> <repo-path> Create or update a repository entry in the local PatchRelay config",
|
|
85
|
+
"",
|
|
86
|
+
"Options:",
|
|
87
|
+
" --issue-prefix <prefixes> Comma-separated issue key prefixes for routing",
|
|
88
|
+
" --team-id <ids> Comma-separated Linear team ids for routing",
|
|
89
|
+
" --no-connect Save the project without starting or reusing Linear OAuth",
|
|
90
|
+
" --no-open Do not open the browser during connect",
|
|
91
|
+
" --timeout <seconds> Override the connect wait timeout",
|
|
92
|
+
" --json Emit structured JSON output",
|
|
93
|
+
" --help, -h Show this help",
|
|
94
|
+
"",
|
|
95
|
+
"Behavior:",
|
|
96
|
+
" `patchrelay project apply` is the idempotent happy-path command. It updates",
|
|
97
|
+
" the local config, reruns readiness checks, reloads the service when ready,",
|
|
98
|
+
" and reuses or starts the Linear connect flow unless `--no-connect` is set.",
|
|
99
|
+
"",
|
|
100
|
+
"Examples:",
|
|
101
|
+
" patchrelay project apply app /absolute/path/to/repo",
|
|
102
|
+
" patchrelay project apply app /absolute/path/to/repo --issue-prefix APP",
|
|
103
|
+
" patchrelay project apply app /absolute/path/to/repo --team-id team-123 --no-connect",
|
|
104
|
+
].join("\n");
|
|
105
|
+
}
|
|
106
|
+
export function helpTextFor(topic) {
|
|
107
|
+
return topic === "project" ? projectHelpText() : rootHelpText();
|
|
108
|
+
}
|
package/dist/cli/index.js
CHANGED
|
@@ -1,72 +1,16 @@
|
|
|
1
1
|
import { loadConfig } from "../config.js";
|
|
2
2
|
import { getBuildInfo } from "../build-info.js";
|
|
3
|
-
import {
|
|
4
|
-
import { assertKnownFlags, parseArgs, resolveCommand } from "./args.js";
|
|
3
|
+
import { assertKnownFlags, hasHelpFlag, parseArgs, resolveCommand } from "./args.js";
|
|
5
4
|
import { handleConnectCommand, handleInstallationsCommand } from "./commands/connect.js";
|
|
6
5
|
import { handleFeedCommand } from "./commands/feed.js";
|
|
7
6
|
import { handleEventsCommand, handleInspectCommand, handleListCommand, handleLiveCommand, handleOpenCommand, handleReportCommand, handleRetryCommand, handleWorktreeCommand, } from "./commands/issues.js";
|
|
8
7
|
import { handleProjectCommand } from "./commands/project.js";
|
|
9
8
|
import { handleInitCommand, handleInstallServiceCommand, handleRestartServiceCommand } from "./commands/setup.js";
|
|
10
|
-
import {
|
|
9
|
+
import { CliUsageError } from "./errors.js";
|
|
11
10
|
import { formatJson } from "./formatters/json.js";
|
|
11
|
+
import { helpTextFor, rootHelpText } from "./help.js";
|
|
12
12
|
import { runInteractiveCommand } from "./interactive.js";
|
|
13
|
-
import { formatDoctor, writeOutput } from "./output.js";
|
|
14
|
-
function helpText() {
|
|
15
|
-
return [
|
|
16
|
-
"PatchRelay",
|
|
17
|
-
"",
|
|
18
|
-
"patchrelay is a local service and CLI that connects Linear issue delegation to Codex worktrees on your machine.",
|
|
19
|
-
"",
|
|
20
|
-
"Usage:",
|
|
21
|
-
" patchrelay <command> [args] [flags]",
|
|
22
|
-
" patchrelay <issueKey> # shorthand for `patchrelay inspect <issueKey>`",
|
|
23
|
-
"",
|
|
24
|
-
"First-time setup:",
|
|
25
|
-
" 1. patchrelay init <public-https-url>",
|
|
26
|
-
" 2. Fill in ~/.config/patchrelay/service.env",
|
|
27
|
-
" 3. patchrelay project apply <id> <repo-path>",
|
|
28
|
-
" 4. Edit the generated project workflows if needed, then add those workflow files to the repo",
|
|
29
|
-
" 5. patchrelay doctor",
|
|
30
|
-
"",
|
|
31
|
-
"Why init needs the public URL:",
|
|
32
|
-
" Linear must reach PatchRelay at a public HTTPS origin for both the webhook endpoint",
|
|
33
|
-
" and the OAuth callback. `patchrelay init` writes that origin to `server.public_base_url`.",
|
|
34
|
-
"",
|
|
35
|
-
"Default behavior:",
|
|
36
|
-
" PatchRelay already defaults the local bind address, database path, log path, worktree",
|
|
37
|
-
" root, and Codex runner settings. In the normal",
|
|
38
|
-
" case you only need the public URL, the required secrets, and at least one project.",
|
|
39
|
-
" `patchrelay init` installs the user service and config watcher, and `project apply`",
|
|
40
|
-
" upserts the repo config and reuses or starts the Linear connection flow.",
|
|
41
|
-
"",
|
|
42
|
-
"Commands:",
|
|
43
|
-
" version [--json] Show the installed PatchRelay build version",
|
|
44
|
-
" init <public-base-url> [--force] [--json] Bootstrap the machine-level PatchRelay home",
|
|
45
|
-
" project apply <id> <repo-path> [--issue-prefix <prefixes>] [--team-id <ids>] [--no-connect] [--no-open] [--timeout <seconds>] [--json]",
|
|
46
|
-
" Upsert one local repository and connect it to Linear when ready",
|
|
47
|
-
" doctor [--json] Check secrets, paths, configured workflow files, git, and codex",
|
|
48
|
-
" install-service [--force] [--write-only] [--json] Reinstall the systemd user service and watcher",
|
|
49
|
-
" restart-service [--json] Reload-or-restart the systemd user service",
|
|
50
|
-
" connect [--project <projectId>] [--no-open] [--timeout <seconds>] [--json]",
|
|
51
|
-
" Advanced: start or reuse a Linear installation directly",
|
|
52
|
-
" installations [--json] Show connected Linear installations",
|
|
53
|
-
" feed [--follow] [--limit <count>] [--issue <issueKey>] [--project <projectId>] [--json]",
|
|
54
|
-
" Show a live operator feed from the daemon",
|
|
55
|
-
" serve Run the local PatchRelay service",
|
|
56
|
-
" inspect <issueKey> Show the latest known issue state",
|
|
57
|
-
" live <issueKey> [--watch] [--json] Show the active run status",
|
|
58
|
-
" report <issueKey> [--stage <workflow>] [--stage-run <id>] [--json]",
|
|
59
|
-
" Show finished workflow reports",
|
|
60
|
-
" events <issueKey> [--stage-run <id>] [--method <name>] [--follow] [--json]",
|
|
61
|
-
" Show raw thread events",
|
|
62
|
-
" worktree <issueKey> [--cd] [--json] Print the issue worktree path",
|
|
63
|
-
" open <issueKey> [--print] [--json] Open Codex in the issue worktree",
|
|
64
|
-
" retry <issueKey> [--stage <workflow>] [--reason <text>] [--json]",
|
|
65
|
-
" Requeue a workflow",
|
|
66
|
-
" list [--active] [--failed] [--project <projectId>] [--json]",
|
|
67
|
-
" List tracked issues",
|
|
68
|
-
].join("\n");
|
|
69
|
-
}
|
|
13
|
+
import { formatDoctor, writeOutput, writeUsageError } from "./output.js";
|
|
70
14
|
function getCommandConfigProfile(command) {
|
|
71
15
|
switch (command) {
|
|
72
16
|
case "version":
|
|
@@ -168,12 +112,25 @@ export async function runCli(argv, options) {
|
|
|
168
112
|
validateFlags(command, commandArgs, parsed);
|
|
169
113
|
}
|
|
170
114
|
catch (error) {
|
|
115
|
+
if (error instanceof CliUsageError) {
|
|
116
|
+
writeUsageError(stderr, error);
|
|
117
|
+
return 1;
|
|
118
|
+
}
|
|
171
119
|
writeOutput(stderr, `${error instanceof Error ? error.message : String(error)}\n`);
|
|
172
120
|
return 1;
|
|
173
121
|
}
|
|
174
122
|
const json = parsed.flags.get("json") === true;
|
|
175
123
|
if (command === "help") {
|
|
176
|
-
|
|
124
|
+
const topic = commandArgs[0];
|
|
125
|
+
if (topic === "project") {
|
|
126
|
+
writeOutput(stdout, `${helpTextFor("project")}\n`);
|
|
127
|
+
return 0;
|
|
128
|
+
}
|
|
129
|
+
if (topic) {
|
|
130
|
+
writeUsageError(stderr, new CliUsageError(`Unknown help topic: ${topic}`));
|
|
131
|
+
return 1;
|
|
132
|
+
}
|
|
133
|
+
writeOutput(stdout, `${rootHelpText()}\n`);
|
|
177
134
|
return 0;
|
|
178
135
|
}
|
|
179
136
|
if (command === "version") {
|
|
@@ -181,6 +138,10 @@ export async function runCli(argv, options) {
|
|
|
181
138
|
writeOutput(stdout, json ? formatJson(buildInfo) : `${buildInfo.version}\n`);
|
|
182
139
|
return 0;
|
|
183
140
|
}
|
|
141
|
+
if (hasHelpFlag(parsed)) {
|
|
142
|
+
writeOutput(stdout, `${helpTextFor(command === "project" ? "project" : "root")}\n`);
|
|
143
|
+
return 0;
|
|
144
|
+
}
|
|
184
145
|
if (command === "serve") {
|
|
185
146
|
return -1;
|
|
186
147
|
}
|
|
@@ -216,15 +177,25 @@ export async function runCli(argv, options) {
|
|
|
216
177
|
});
|
|
217
178
|
}
|
|
218
179
|
if (command === "project") {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
180
|
+
try {
|
|
181
|
+
return await handleProjectCommand({
|
|
182
|
+
commandArgs,
|
|
183
|
+
parsed,
|
|
184
|
+
json,
|
|
185
|
+
stdout,
|
|
186
|
+
stderr,
|
|
187
|
+
runInteractive,
|
|
188
|
+
...(options ? { options } : {}),
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
catch (error) {
|
|
192
|
+
if (error instanceof CliUsageError) {
|
|
193
|
+
writeUsageError(stderr, error);
|
|
194
|
+
return 1;
|
|
195
|
+
}
|
|
196
|
+
writeOutput(stderr, `${error instanceof Error ? error.message : String(error)}\n`);
|
|
197
|
+
return 1;
|
|
198
|
+
}
|
|
228
199
|
}
|
|
229
200
|
const config = options?.config ??
|
|
230
201
|
loadConfig(undefined, {
|
|
@@ -233,11 +204,12 @@ export async function runCli(argv, options) {
|
|
|
233
204
|
let data = options?.data;
|
|
234
205
|
try {
|
|
235
206
|
if (command === "doctor") {
|
|
207
|
+
const { runPreflight } = await import("../preflight.js");
|
|
236
208
|
const report = await runPreflight(config);
|
|
237
209
|
writeOutput(stdout, json ? formatJson(report) : formatDoctor(report));
|
|
238
210
|
return report.ok ? 0 : 1;
|
|
239
211
|
}
|
|
240
|
-
data ??=
|
|
212
|
+
data ??= await createCliDataAccess(config);
|
|
241
213
|
if (command === "inspect") {
|
|
242
214
|
return await handleInspectCommand({ commandArgs, parsed, json, stdout, data, config, runInteractive });
|
|
243
215
|
}
|
|
@@ -290,6 +262,10 @@ export async function runCli(argv, options) {
|
|
|
290
262
|
throw new Error(`Unknown command: ${command}`);
|
|
291
263
|
}
|
|
292
264
|
catch (error) {
|
|
265
|
+
if (error instanceof CliUsageError) {
|
|
266
|
+
writeUsageError(stderr, error);
|
|
267
|
+
return 1;
|
|
268
|
+
}
|
|
293
269
|
writeOutput(stderr, `${error instanceof Error ? error.message : String(error)}\n`);
|
|
294
270
|
return 1;
|
|
295
271
|
}
|
|
@@ -299,3 +275,7 @@ export async function runCli(argv, options) {
|
|
|
299
275
|
}
|
|
300
276
|
}
|
|
301
277
|
}
|
|
278
|
+
async function createCliDataAccess(config) {
|
|
279
|
+
const { CliDataAccess } = await import("./data.js");
|
|
280
|
+
return new CliDataAccess(config);
|
|
281
|
+
}
|
package/dist/cli/output.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
import { helpTextFor } from "./help.js";
|
|
1
2
|
export function writeOutput(stream, text) {
|
|
2
3
|
stream.write(text);
|
|
3
4
|
}
|
|
5
|
+
export function writeUsageError(stream, error) {
|
|
6
|
+
writeOutput(stream, `${helpTextFor(error.helpTopic)}\n\nError: ${error.message}\n`);
|
|
7
|
+
}
|
|
4
8
|
export function formatDoctor(report) {
|
|
5
9
|
const lines = ["PatchRelay doctor", ""];
|
|
6
10
|
for (const check of report.checks) {
|
package/dist/index.js
CHANGED
|
@@ -1,22 +1,24 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { dirname } from "node:path";
|
|
3
3
|
import { runCli } from "./cli/index.js";
|
|
4
|
-
import { CodexAppServerClient } from "./codex-app-server.js";
|
|
5
|
-
import { getAdjacentEnvFilePaths, loadConfig } from "./config.js";
|
|
6
|
-
import { PatchRelayDatabase } from "./db.js";
|
|
7
|
-
import { enforceRuntimeFilePermissions, enforceServiceEnvPermissions } from "./file-permissions.js";
|
|
8
|
-
import { buildHttpServer } from "./http.js";
|
|
9
|
-
import { DatabaseBackedLinearClientProvider } from "./linear-client.js";
|
|
10
|
-
import { createLogger } from "./logging.js";
|
|
11
|
-
import { runPreflight } from "./preflight.js";
|
|
12
|
-
import { PatchRelayService } from "./service.js";
|
|
13
|
-
import { ensureDir } from "./utils.js";
|
|
14
4
|
async function main() {
|
|
15
5
|
const cliExitCode = await runCli(process.argv.slice(2));
|
|
16
6
|
if (cliExitCode !== -1) {
|
|
17
7
|
process.exitCode = cliExitCode;
|
|
18
8
|
return;
|
|
19
9
|
}
|
|
10
|
+
const [{ CodexAppServerClient }, { getAdjacentEnvFilePaths, loadConfig }, { PatchRelayDatabase }, { enforceRuntimeFilePermissions, enforceServiceEnvPermissions }, { buildHttpServer }, { DatabaseBackedLinearClientProvider }, { createLogger }, { runPreflight }, { PatchRelayService }, { ensureDir },] = await Promise.all([
|
|
11
|
+
import("./codex-app-server.js"),
|
|
12
|
+
import("./config.js"),
|
|
13
|
+
import("./db.js"),
|
|
14
|
+
import("./file-permissions.js"),
|
|
15
|
+
import("./http.js"),
|
|
16
|
+
import("./linear-client.js"),
|
|
17
|
+
import("./logging.js"),
|
|
18
|
+
import("./preflight.js"),
|
|
19
|
+
import("./service.js"),
|
|
20
|
+
import("./utils.js"),
|
|
21
|
+
]);
|
|
20
22
|
const configPath = process.env.PATCHRELAY_CONFIG;
|
|
21
23
|
const config = loadConfig(configPath);
|
|
22
24
|
await enforceServiceEnvPermissions(getAdjacentEnvFilePaths(configPath).serviceEnvPath);
|