patchrelay 0.6.0 → 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.
@@ -7,10 +7,10 @@ const fallbackBuildInfo = {
7
7
  commit: "unknown",
8
8
  builtAt: "unknown",
9
9
  };
10
- export function getBuildInfo() {
11
- const buildInfoPath = path.resolve(process.cwd(), "dist/build-info.json");
12
- const fallbackPath = getBundledAssetPath("dist/build-info.json");
13
- const resolvedPath = existsSync(buildInfoPath) ? buildInfoPath : fallbackPath;
10
+ export function getBuildInfo(paths) {
11
+ const fallbackPath = paths?.bundledPath ?? getBundledAssetPath("dist/build-info.json");
12
+ const cwdBuildInfoPath = paths?.cwdPath ?? path.resolve(process.cwd(), "dist/build-info.json");
13
+ const resolvedPath = existsSync(fallbackPath) ? fallbackPath : cwdBuildInfoPath;
14
14
  if (!existsSync(resolvedPath)) {
15
15
  return fallbackBuildInfo;
16
16
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "service": "patchrelay",
3
- "version": "0.6.0",
4
- "commit": "3dd80e1684b4",
5
- "builtAt": "2026-03-13T13:20:38.482Z"
3
+ "version": "0.7.0",
4
+ "commit": "450840270db1",
5
+ "builtAt": "2026-03-13T18:08:50.316Z"
6
6
  }
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 Error(`Unknown command: ${requestedCommand}. Run \`patchrelay help\`.`);
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 Error(`Unknown flag${unknownFlags.length === 1 ? "" : "s"} for ${command}: ${unknownFlags.map((flag) => `--${flag}`).join(", ")}`);
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 { runPreflight } from "../../preflight.js";
4
- import { parseCsvFlag } from "../args.js";
3
+ import { hasHelpFlag, parseCsvFlag } from "../args.js";
5
4
  import { runConnectFlow, parseTimeoutSeconds } from "../connect-flow.js";
6
- import { CliDataAccess } from "../data.js";
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
- const subcommand = params.commandArgs[0];
13
- if (subcommand !== "apply") {
14
- throw new Error("Usage: patchrelay project apply <id> <repo-path> [--issue-prefix <prefixes>] [--team-id <ids>] [--no-connect] [--timeout <seconds>]");
15
- }
16
- const projectId = params.commandArgs[1];
17
- const repoPath = params.commandArgs[2];
18
- if (!projectId || !repoPath) {
19
- throw new Error("Usage: patchrelay project apply <id> <repo-path> [--issue-prefix <prefixes>] [--team-id <ids>] [--no-connect] [--timeout <seconds>]");
20
- }
21
- const result = await upsertProjectInConfig({
22
- id: projectId,
23
- repoPath,
24
- issueKeyPrefixes: parseCsvFlag(params.parsed.flags.get("issue-prefix")),
25
- linearTeamIds: parseCsvFlag(params.parsed.flags.get("team-id")),
26
- });
27
- const serviceUnits = await installUserServiceUnits();
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
- const report = await runPreflight(fullConfig);
63
- const failedChecks = report.checks.filter((check) => check.status === "fail");
64
- if (failedChecks.length > 0) {
65
- if (params.json) {
66
- writeOutput(params.stdout, formatJson({
67
- ...result,
68
- serviceUnits,
69
- readiness: report,
70
- connect: {
71
- attempted: false,
72
- skipped: "preflight_failed",
73
- },
74
- }));
75
- return 0;
76
- }
77
- lines.push("Linear connect was skipped because PatchRelay is not ready yet:");
78
- lines.push(...failedChecks.map((check) => `- [${check.scope}] ${check.message}`));
79
- lines.push("Fix the failures above and rerun `patchrelay project apply`.");
80
- writeOutput(params.stdout, `${lines.join("\n")}\n`);
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
- const serviceState = await tryManageService(params.runInteractive, installServiceCommands());
84
- if (!serviceState.ok) {
85
- throw new Error(`Project was saved, but PatchRelay could not be reloaded: ${serviceState.error}`);
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
- const cliData = params.options?.data ?? new CliDataAccess(fullConfig);
88
- try {
89
- if (params.json) {
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 await runConnectFlow({
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
- catch (error) {
137
- writeOutput(params.stderr, `${error instanceof Error ? error.message : String(error)}\n`);
138
- return 1;
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
+ }
@@ -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 { runPreflight } from "../preflight.js";
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 { CliDataAccess } from "./data.js";
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
- writeOutput(stdout, `${helpText()}\n`);
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
- return await handleProjectCommand({
220
- commandArgs,
221
- parsed,
222
- json,
223
- stdout,
224
- stderr,
225
- runInteractive,
226
- ...(options ? { options } : {}),
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 ??= new CliDataAccess(config);
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
+ }
@@ -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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "patchrelay",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "repository": {