@open-cr-agent/cli 0.5.0 → 0.6.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.
Files changed (139) hide show
  1. package/dist/cloud/account-salt.d.ts +6 -0
  2. package/dist/cloud/account-salt.js +33 -0
  3. package/dist/{review → cloud}/account-settings.js +5 -4
  4. package/dist/cloud/browser.d.ts +17 -0
  5. package/dist/cloud/browser.js +37 -0
  6. package/dist/cloud/client.d.ts +93 -0
  7. package/dist/cloud/client.js +221 -0
  8. package/dist/cloud/credentials.d.ts +18 -0
  9. package/dist/cloud/credentials.js +52 -0
  10. package/dist/cloud/deps.d.ts +14 -0
  11. package/dist/cloud/deps.js +34 -0
  12. package/dist/cloud/file-lock.d.ts +14 -0
  13. package/dist/cloud/file-lock.js +77 -0
  14. package/dist/cloud/findings.d.ts +8 -0
  15. package/dist/{review/cloud-findings.js → cloud/findings.js} +10 -32
  16. package/dist/cloud/gateway-token.d.ts +18 -0
  17. package/dist/cloud/gateway-token.js +63 -0
  18. package/dist/cloud/memory.d.ts +8 -0
  19. package/dist/cloud/memory.js +68 -0
  20. package/dist/cloud/private-file.d.ts +8 -0
  21. package/dist/cloud/private-file.js +46 -0
  22. package/dist/{review/cloud-providers.d.ts → cloud/providers.d.ts} +12 -6
  23. package/dist/cloud/providers.js +132 -0
  24. package/dist/cloud/review.d.ts +28 -0
  25. package/dist/cloud/review.js +76 -0
  26. package/dist/cloud/settings.d.ts +22 -0
  27. package/dist/cloud/settings.js +64 -0
  28. package/dist/cloud/upload.d.ts +20 -0
  29. package/dist/{review/cloud-upload.js → cloud/upload.js} +74 -71
  30. package/dist/commands/login.d.ts +5 -0
  31. package/dist/commands/login.js +164 -0
  32. package/dist/commands/memory.d.ts +3 -0
  33. package/dist/{memory.js → commands/memory.js} +21 -20
  34. package/dist/commands/metrics.d.ts +45 -0
  35. package/dist/{metrics.js → commands/metrics.js} +32 -45
  36. package/dist/{plugins-command.d.ts → commands/plugins.d.ts} +3 -9
  37. package/dist/{plugins-command.js → commands/plugins.js} +29 -53
  38. package/dist/{review → commands/review}/args.d.ts +7 -10
  39. package/dist/{review → commands/review}/args.js +11 -16
  40. package/dist/commands/review/deliver.d.ts +7 -0
  41. package/dist/commands/review/deliver.js +56 -0
  42. package/dist/commands/review/deps.d.ts +18 -0
  43. package/dist/commands/review/deps.js +2 -0
  44. package/dist/commands/review/execute-run.d.ts +13 -0
  45. package/dist/commands/review/execute-run.js +97 -0
  46. package/dist/commands/review/exit-code.d.ts +4 -0
  47. package/dist/commands/review/exit-code.js +29 -0
  48. package/dist/commands/review/plan-prices.d.ts +3 -0
  49. package/dist/commands/review/plan-prices.js +18 -0
  50. package/dist/{review → commands/review}/plan-render.js +22 -2
  51. package/dist/commands/review/plan-run.d.ts +5 -0
  52. package/dist/commands/review/plan-run.js +38 -0
  53. package/dist/commands/review/plan-schema.d.ts +97 -0
  54. package/dist/commands/review/plan-schema.js +34 -0
  55. package/dist/{review → commands/review}/progress.d.ts +3 -5
  56. package/dist/{review → commands/review}/progress.js +3 -3
  57. package/dist/{review → commands/review}/provenance.d.ts +4 -2
  58. package/dist/{review → commands/review}/provenance.js +2 -3
  59. package/dist/{review → commands/review}/render.js +1 -1
  60. package/dist/commands/review/resolve-run.d.ts +29 -0
  61. package/dist/commands/review/resolve-run.js +125 -0
  62. package/dist/commands/review/review-options.d.ts +6 -0
  63. package/dist/commands/review/review-options.js +35 -0
  64. package/dist/{review → commands/review}/runtimes.js +1 -1
  65. package/dist/{review → commands/review}/sarif-input.d.ts +0 -1
  66. package/dist/{review → commands/review}/sarif-input.js +5 -5
  67. package/dist/commands/review/sarif.d.ts +96 -0
  68. package/dist/{review → commands/review}/sarif.js +29 -7
  69. package/dist/commands/review/settings-sources.d.ts +17 -0
  70. package/dist/commands/review/settings-sources.js +30 -0
  71. package/dist/commands/review/target.d.ts +38 -0
  72. package/dist/commands/review/target.js +126 -0
  73. package/dist/commands/review.d.ts +7 -0
  74. package/dist/commands/review.js +34 -0
  75. package/dist/config/cli-config.d.ts +41 -0
  76. package/dist/config/cli-config.js +199 -0
  77. package/dist/{review/remote-config.d.ts → config/remote.d.ts} +3 -3
  78. package/dist/{review/remote-config.js → config/remote.js} +5 -23
  79. package/dist/{review/config.d.ts → config/schema.d.ts} +8 -31
  80. package/dist/config/schema.js +138 -0
  81. package/dist/config/settings.d.ts +37 -0
  82. package/dist/config/settings.js +133 -0
  83. package/dist/config/user-dir.d.ts +3 -0
  84. package/dist/config/user-dir.js +10 -0
  85. package/dist/index.d.ts +1 -1
  86. package/dist/index.js +10 -1
  87. package/dist/internal.d.ts +6 -0
  88. package/dist/internal.js +7 -0
  89. package/dist/io/exit.d.ts +9 -0
  90. package/dist/io/exit.js +12 -0
  91. package/dist/io/output.d.ts +4 -0
  92. package/dist/io/output.js +2 -0
  93. package/dist/io/usage-error.d.ts +5 -0
  94. package/dist/io/usage-error.js +8 -0
  95. package/dist/main.js +2 -2
  96. package/dist/{review/account-plugins.d.ts → plugins/account.d.ts} +2 -2
  97. package/dist/{review/account-plugins.js → plugins/account.js} +7 -7
  98. package/dist/{review/plugins.d.ts → plugins/load.d.ts} +2 -2
  99. package/dist/{review/plugins.js → plugins/load.js} +6 -5
  100. package/dist/plugins/npm.d.ts +8 -0
  101. package/dist/plugins/npm.js +24 -0
  102. package/dist/{plugin-store.d.ts → plugins/store.d.ts} +1 -1
  103. package/dist/{plugin-store.js → plugins/store.js} +5 -4
  104. package/dist/repository-id.d.ts +12 -0
  105. package/dist/repository-id.js +72 -0
  106. package/dist/run.d.ts +2 -2
  107. package/dist/run.js +15 -12
  108. package/dist/session/store.d.ts +9 -0
  109. package/dist/session/store.js +31 -0
  110. package/package.json +17 -7
  111. package/dist/account-salt.d.ts +0 -7
  112. package/dist/account-salt.js +0 -43
  113. package/dist/cloud.d.ts +0 -34
  114. package/dist/cloud.js +0 -272
  115. package/dist/memory.d.ts +0 -4
  116. package/dist/metrics.d.ts +0 -47
  117. package/dist/review/cloud-findings.d.ts +0 -31
  118. package/dist/review/cloud-memory.d.ts +0 -6
  119. package/dist/review/cloud-memory.js +0 -58
  120. package/dist/review/cloud-providers.js +0 -60
  121. package/dist/review/cloud-review.d.ts +0 -19
  122. package/dist/review/cloud-review.js +0 -51
  123. package/dist/review/cloud-settings.d.ts +0 -16
  124. package/dist/review/cloud-settings.js +0 -118
  125. package/dist/review/cloud-upload.d.ts +0 -60
  126. package/dist/review/command.d.ts +0 -33
  127. package/dist/review/command.js +0 -293
  128. package/dist/review/config.js +0 -304
  129. package/dist/review/sarif.d.ts +0 -4
  130. package/dist/review/settings-sources.d.ts +0 -11
  131. package/dist/review/settings-sources.js +0 -58
  132. package/dist/review/target.d.ts +0 -18
  133. package/dist/review/target.js +0 -210
  134. /package/dist/{review → cloud}/account-settings.d.ts +0 -0
  135. /package/dist/{review → commands/review}/plan-render.d.ts +0 -0
  136. /package/dist/{review → commands/review}/render.d.ts +0 -0
  137. /package/dist/{review → commands/review}/runtimes.d.ts +0 -0
  138. /package/dist/{review → io}/terminal.d.ts +0 -0
  139. /package/dist/{review → io}/terminal.js +0 -0
@@ -1,13 +1,12 @@
1
- import { readdir, readFile } from "node:fs/promises";
2
1
  import { join, resolve } from "node:path";
3
2
  import { parseArgs } from "node:util";
4
- import { reportOutputSchema, } from "@open-cr-agent/core";
5
3
  import { findRepositoryRoot } from "@open-cr-agent/vcs-local/internal";
6
- import { UsageError } from "./review/args.js";
7
- import { SESSIONS_DIR } from "./review/command.js";
8
- import { forTerminal } from "./review/terminal.js";
9
- export const METRICS_VERSION = 1;
10
- export const METRICS_USAGE = `Usage: ocra metrics [options]
4
+ import { EXIT } from "../io/exit.js";
5
+ import { forTerminal } from "../io/terminal.js";
6
+ import { UsageError } from "../io/usage-error.js";
7
+ import { listSessions, readSessionReport, SESSIONS_DIR, sessionStart, sessionsDir, } from "../session/store.js";
8
+ const METRICS_VERSION = 1;
9
+ const METRICS_USAGE = `Usage: ocra metrics [options]
11
10
 
12
11
  Counts over the finished reviews in ${SESSIONS_DIR}: runs, cost, findings,
13
12
  what reviewers later fixed or dismissed, per reviewer. Reads each session's
@@ -33,18 +32,18 @@ export async function metricsCommand(argv, out, cwd) {
33
32
  });
34
33
  if (values.help) {
35
34
  out.write(METRICS_USAGE);
36
- return 0;
35
+ return EXIT.ok;
37
36
  }
38
37
  const format = values.format ?? "text";
39
38
  if (format !== "text" && format !== "json")
40
39
  throw new UsageError("--format must be text or json");
41
40
  const since = values.since === undefined ? undefined : parseSince(values.since);
42
- const sessionsDir = values.sessions
41
+ const dir = values.sessions
43
42
  ? resolve(cwd, values.sessions)
44
- : join(await findRepositoryRoot(cwd), SESSIONS_DIR);
45
- const metrics = await collectMetrics(sessionsDir, since);
43
+ : sessionsDir(await findRepositoryRoot(cwd));
44
+ const metrics = await collectMetrics(dir, since);
46
45
  out.write(format === "json" ? `${JSON.stringify(metrics, null, 2)}\n` : renderMetrics(metrics));
47
- return 0;
46
+ return EXIT.ok;
48
47
  }
49
48
  function parseSince(text) {
50
49
  const date = new Date(text);
@@ -52,55 +51,34 @@ function parseSince(text) {
52
51
  throw new UsageError(`--since needs an ISO 8601 date, not "${text}"`);
53
52
  return date;
54
53
  }
55
- // A session directory is named by its run id, whose first part is the start
56
- // time (newRunId in core); that is what --since compares against.
57
- export function sessionStart(id) {
58
- const m = /^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})Z/.exec(id);
59
- if (!m)
60
- return undefined;
61
- const [, y, mo, d, h, mi, s] = m;
62
- return new Date(`${y}-${mo}-${d}T${h}:${mi}:${s}Z`);
63
- }
64
- export async function collectMetrics(sessionsDir, since) {
65
- const names = (await readdir(sessionsDir).catch(() => []))
66
- .filter((n) => !n.startsWith("."))
67
- .filter((n) => {
54
+ export async function collectMetrics(dir, since) {
55
+ // --since compares the start time in each session's id.
56
+ const names = (await listSessions(dir)).filter((n) => {
68
57
  const start = sessionStart(n);
69
58
  return since === undefined || (start !== undefined && start >= since);
70
- })
71
- .sort();
59
+ });
72
60
  const reports = [];
73
61
  let unreadable = 0;
74
62
  for (const name of names) {
75
- const report = await readReport(join(sessionsDir, name, "report.json"));
63
+ const report = await readSessionReport(join(dir, name));
76
64
  if (report)
77
65
  reports.push(report);
78
66
  else
79
67
  unreadable += 1;
80
68
  }
81
69
  return aggregate(reports, {
82
- sessionsDir,
70
+ sessionsDir: dir,
83
71
  unreadable,
84
72
  ...(since ? { since: since.toISOString() } : {}),
85
73
  });
86
74
  }
87
- async function readReport(path) {
88
- let data;
89
- try {
90
- data = JSON.parse(await readFile(path, "utf8"));
91
- }
92
- catch {
93
- return undefined;
94
- }
95
- const parsed = reportOutputSchema.safeParse(data);
96
- return parsed.success ? parsed.data : undefined;
97
- }
98
75
  function aggregate(reports, meta) {
99
76
  const byVerdict = {};
100
77
  const bySeverity = { critical: 0, warning: 0, suggestion: 0 };
101
78
  const byVerification = { confirmed: 0, uncertain: 0, unchecked: 0 };
102
79
  const reviewers = new Map();
103
80
  const reviewerOf = new Map();
81
+ const recorded = new Map();
104
82
  const unique = new Set();
105
83
  const fixed = new Set();
106
84
  const dismissed = new Set();
@@ -148,22 +126,31 @@ function aggregate(reports, meta) {
148
126
  if (!reviewerOf.has(finding.fingerprint))
149
127
  reviewerOf.set(finding.fingerprint, finding.reviewer);
150
128
  }
151
- for (const f of report.rereview?.fixed ?? [])
129
+ for (const f of report.rereview?.fixed ?? []) {
152
130
  fixed.add(f.fingerprint);
153
- for (const f of report.rereview?.dismissed ?? [])
131
+ if (f.reviewer && !recorded.has(f.fingerprint))
132
+ recorded.set(f.fingerprint, f.reviewer);
133
+ }
134
+ for (const f of report.rereview?.dismissed ?? []) {
154
135
  dismissed.add(f.fingerprint);
136
+ if (f.reviewer && !recorded.has(f.fingerprint))
137
+ recorded.set(f.fingerprint, f.reviewer);
138
+ }
155
139
  }
156
140
  // A fingerprint can be dismissed in one review and gone in a later one:
157
141
  // the reviewer's decision stands.
158
142
  for (const fp of dismissed)
159
143
  fixed.delete(fp);
144
+ // The reviewer the earlier review recorded with the finding; reports from
145
+ // before it was recorded fall back to the first that reported the fingerprint.
146
+ const creditedTo = (fp) => recorded.get(fp) ?? reviewerOf.get(fp);
160
147
  for (const fp of fixed) {
161
- const reviewer = reviewerOf.get(fp);
148
+ const reviewer = creditedTo(fp);
162
149
  if (reviewer)
163
150
  forReviewer(reviewer).fixed += 1;
164
151
  }
165
152
  for (const fp of dismissed) {
166
- const reviewer = reviewerOf.get(fp);
153
+ const reviewer = creditedTo(fp);
167
154
  if (reviewer)
168
155
  forReviewer(reviewer).dismissed += 1;
169
156
  }
@@ -194,7 +181,7 @@ function rate(fixed, dismissed) {
194
181
  }
195
182
  const percent = (value) => (value === null ? "n/a" : `${Math.round(value * 100)}%`);
196
183
  const dollars = (value) => (value === null ? "n/a" : `$${value.toFixed(2)}`);
197
- export function renderMetrics(m) {
184
+ function renderMetrics(m) {
198
185
  const lines = [
199
186
  `Runs: ${m.runs.total}${m.since ? ` since ${m.since}` : ""} (${m.runs.incomplete} incomplete, ${m.runs.unreadable} unreadable)`,
200
187
  ` by verdict: ${entries(m.runs.byVerdict)}`,
@@ -1,15 +1,9 @@
1
- import type { Output } from "./review/progress.js";
1
+ import type { Output } from "../io/output.js";
2
+ import type { NpmRunner } from "../plugins/npm.js";
2
3
  export declare const PLUGINS_USAGE = "Usage: ocra plugins allow <name>@<version>\n ocra plugins deny <name>\n ocra plugins list\n\nYour ocra Cloud settings may name plugins; a review loads one only if this\nmachine allows it. allow shows the package and who published it, installs that\nexact version with install scripts off into a directory of your own, and\nrecords its integrity; deny removes it.\n";
3
- /** Runs npm with these arguments, never through a shell; resolves with its standard output. */
4
- export type NpmRunner = (args: readonly string[]) => Promise<string>;
5
4
  export interface PluginsDeps {
6
5
  dir: string;
7
6
  npm: NpmRunner;
8
7
  }
9
8
  export declare function pluginsCommand(argv: readonly string[], out: Output, deps: PluginsDeps): Promise<number>;
10
- export declare function parseSpec(spec: string): {
11
- name: string;
12
- version: string;
13
- };
14
- export declare function defaultNpm(env?: NodeJS.ProcessEnv): NpmRunner;
15
- //# sourceMappingURL=plugins-command.d.ts.map
9
+ //# sourceMappingURL=plugins.d.ts.map
@@ -1,10 +1,10 @@
1
- import { execFile } from "node:child_process";
2
1
  import { access, rm, writeFile } from "node:fs/promises";
3
- import { dirname, join } from "node:path";
4
- import { ensureDir, installedEntry, isExactVersion, isPackageName, readAllowed, writeAllowed, } from "./plugin-store.js";
5
- import { UsageError } from "./review/args.js";
6
- import { ConfigError } from "./review/config.js";
7
- import { forTerminal } from "./review/terminal.js";
2
+ import { join } from "node:path";
3
+ import { ConfigError } from "../config/cli-config.js";
4
+ import { EXIT } from "../io/exit.js";
5
+ import { forTerminal } from "../io/terminal.js";
6
+ import { UsageError } from "../io/usage-error.js";
7
+ import { ensureDir, installedEntry, isExactVersion, isPackageName, readAllowed, writeAllowed, } from "../plugins/store.js";
8
8
  // `ocra plugins` (ADR-0027): the user's own decision, on each machine, to
9
9
  // let their ocra Cloud account load a third-party plugin, like adding a
10
10
  // dependency. The account names packages; only what is allowed here loads.
@@ -21,7 +21,7 @@ export async function pluginsCommand(argv, out, deps) {
21
21
  const [action, target, ...extra] = argv;
22
22
  if (action === "--help" || action === "-h" || action === "help") {
23
23
  out.write(PLUGINS_USAGE);
24
- return 0;
24
+ return EXIT.ok;
25
25
  }
26
26
  if (extra.length > 0)
27
27
  throw new UsageError(`Unexpected argument: ${extra[0]}`);
@@ -37,14 +37,14 @@ async function list(out, dir) {
37
37
  const allowed = await readAllowed(dir);
38
38
  if (allowed.length === 0) {
39
39
  out.write("No plugins allowed. Allow one with: ocra plugins allow <name>@<version>\n");
40
- return 0;
40
+ return EXIT.ok;
41
41
  }
42
42
  for (const p of allowed)
43
43
  out.write(`${p.name}@${p.version} ${p.integrity}\n`);
44
44
  out.write(`(installed in ${dir})\n`);
45
- return 0;
45
+ return EXIT.ok;
46
46
  }
47
- export function parseSpec(spec) {
47
+ function parseSpec(spec) {
48
48
  const at = spec.lastIndexOf("@");
49
49
  const name = at > 0 ? spec.slice(0, at) : spec;
50
50
  const version = at > 0 ? spec.slice(at + 1) : "";
@@ -57,25 +57,28 @@ export function parseSpec(spec) {
57
57
  }
58
58
  async function allow(spec, out, deps) {
59
59
  const { name, version } = parseSpec(spec);
60
- const view = describe(await deps.npm(["view", `${name}@${version}`, "--json"]));
60
+ await ensureDir(deps.dir);
61
+ // npm installs into the nearest package.json; one of the directory's own
62
+ // keeps it from walking up to a project.
63
+ const manifest = join(deps.dir, "package.json");
64
+ if (!(await exists(manifest))) {
65
+ await writeFile(manifest, `${JSON.stringify({ private: true }, null, 2)}\n`, { mode: 0o600 });
66
+ }
67
+ const view = describe(await deps.npm(["view", `${name}@${version}`, "--json"], deps.dir));
61
68
  if (view.name !== name || view.version !== version) {
62
69
  throw new ConfigError(`npm did not resolve ${name}@${version} to that exact version`);
63
70
  }
71
+ if (!view.integrity) {
72
+ throw new ConfigError(`npm view lists no integrity for ${name}@${version}; not allowed`);
73
+ }
64
74
  out.write(forTerminal([
65
75
  `Package: ${view.name}@${view.version}`,
66
76
  `Published by: ${view.publisher ?? "unknown"}`,
67
77
  `Maintainers: ${view.maintainers.join(", ") || "unknown"}`,
68
- `Integrity: ${view.integrity ?? "unknown"}`,
78
+ `Integrity: ${view.integrity}`,
69
79
  `Installing into ${deps.dir} with install scripts off`,
70
80
  "",
71
81
  ].join("\n")));
72
- await ensureDir(deps.dir);
73
- // npm installs into the nearest package.json; one of the directory's own
74
- // keeps it from walking up to a project.
75
- const manifest = join(deps.dir, "package.json");
76
- if (!(await exists(manifest))) {
77
- await writeFile(manifest, `${JSON.stringify({ private: true }, null, 2)}\n`, { mode: 0o600 });
78
- }
79
82
  await deps.npm([
80
83
  "install",
81
84
  "--ignore-scripts",
@@ -85,19 +88,19 @@ async function allow(spec, out, deps) {
85
88
  "--prefix",
86
89
  deps.dir,
87
90
  `${name}@${version}`,
88
- ]);
91
+ ], deps.dir);
89
92
  const installed = await installedEntry(deps.dir, name);
90
93
  if (installed?.version !== version || !installed.integrity) {
91
94
  throw new ConfigError(`npm did not install ${name}@${version} into ${deps.dir}`);
92
95
  }
93
- if (view.integrity && view.integrity !== installed.integrity) {
96
+ if (view.integrity !== installed.integrity) {
94
97
  throw new ConfigError(`${name}@${version} installed with another integrity than the registry lists; not allowed`);
95
98
  }
96
99
  const entry = { name, version, integrity: installed.integrity };
97
100
  const allowed = (await readAllowed(deps.dir)).filter((p) => p.name !== name);
98
101
  await writeAllowed(deps.dir, [...allowed, entry]);
99
102
  out.write(`Allowed ${name}@${version}: your ocra Cloud settings can now load it here.\n`);
100
- return 0;
103
+ return EXIT.ok;
101
104
  }
102
105
  async function deny(name, out, deps) {
103
106
  if (!isPackageName(name))
@@ -106,26 +109,18 @@ async function deny(name, out, deps) {
106
109
  const kept = allowed.filter((p) => p.name !== name);
107
110
  if (kept.length === allowed.length) {
108
111
  out.write(`${name} was not allowed.\n`);
109
- return 0;
112
+ return EXIT.ok;
110
113
  }
111
114
  // The record is what loading checks; removing the files is housekeeping.
112
115
  await writeAllowed(deps.dir, kept);
113
116
  try {
114
- await deps.npm([
115
- "uninstall",
116
- "--ignore-scripts",
117
- "--no-audit",
118
- "--no-fund",
119
- "--prefix",
120
- deps.dir,
121
- name,
122
- ]);
117
+ await deps.npm(["uninstall", "--ignore-scripts", "--no-audit", "--no-fund", "--prefix", deps.dir, name], deps.dir);
123
118
  }
124
119
  catch {
125
120
  await rm(join(deps.dir, "node_modules", name), { recursive: true, force: true });
126
121
  }
127
122
  out.write(`Denied ${name}: it no longer loads.\n`);
128
- return 0;
123
+ return EXIT.ok;
129
124
  }
130
125
  function describe(stdout) {
131
126
  let data;
@@ -161,23 +156,4 @@ function describe(stdout) {
161
156
  async function exists(path) {
162
157
  return access(path).then(() => true, () => false);
163
158
  }
164
- // npm on Windows is npm.cmd, which execFile cannot run without a shell; run
165
- // the npm that ships with this Node directly instead.
166
- export function defaultNpm(env = process.env) {
167
- const [file, prefix] = process.platform === "win32"
168
- ? [
169
- process.execPath,
170
- [join(dirname(process.execPath), "node_modules", "npm", "bin", "npm-cli.js")],
171
- ]
172
- : ["npm", []];
173
- return (args) => new Promise((resolve, reject) => {
174
- execFile(file, [...prefix, ...args], { env, timeout: 5 * 60_000, maxBuffer: 16 * 1024 * 1024, shell: false }, (error, stdout, stderr) => {
175
- if (error) {
176
- reject(new ConfigError(`npm ${args[0]} failed: ${forTerminal(String(stderr).trim().split("\n").slice(-3).join(" ") || error.message)}`));
177
- }
178
- else
179
- resolve(String(stdout));
180
- });
181
- });
182
- }
183
- //# sourceMappingURL=plugins-command.js.map
159
+ //# sourceMappingURL=plugins.js.map
@@ -1,20 +1,19 @@
1
- import { OcraError } from "@open-cr-agent/core";
2
1
  import type { LocalTarget } from "@open-cr-agent/vcs-local";
3
- export type OutputFormat = "text" | "json" | "sarif";
4
- export interface PullRequestTarget {
2
+ type OutputFormat = "text" | "json" | "sarif";
3
+ interface PullRequestArgs {
5
4
  number: number;
6
5
  repo?: string;
7
6
  publish: boolean;
8
7
  }
9
- export interface MergeRequestTarget {
8
+ interface MergeRequestArgs {
10
9
  iid: number;
11
10
  project?: string;
12
11
  publish: boolean;
13
12
  }
14
13
  export interface ReviewArgs {
15
14
  target: LocalTarget;
16
- pullRequest?: PullRequestTarget;
17
- mergeRequest?: MergeRequestTarget;
15
+ pullRequest?: PullRequestArgs;
16
+ mergeRequest?: MergeRequestArgs;
18
17
  format: OutputFormat;
19
18
  output?: string;
20
19
  ignoreRepoConfig?: true;
@@ -29,9 +28,7 @@ export interface ReviewArgs {
29
28
  temperature?: number;
30
29
  seed?: number;
31
30
  }
32
- export declare class UsageError extends OcraError {
33
- constructor(message: string);
34
- }
35
- export declare const REVIEW_USAGE = "Usage: ocra review [options]\n\nReviews uncommitted changes by default.\n\nOptions:\n --from <ref> Review changes on --to since it diverged from <ref>\n --to <ref> End of the range (default: HEAD)\n --commit <sha> Review a single commit\n --pr <number> Review a GitHub pull request (needs GITHUB_TOKEN)\n --repo <owner/name> Repository of --pr (default: GITHUB_REPOSITORY or origin)\n --mr <iid> Review a GitLab merge request (needs GITLAB_TOKEN)\n --project <id|path> Project of --mr (default: CI_PROJECT_ID or origin)\n --publish With --pr or --mr: post the review to it\n --full With --pr or --mr: review every file, not only what\n changed since the previous review\n --format <format> text (default), json, or sarif (SARIF 2.1.0; not with --plan)\n --output <file> Write the result to a file instead of stdout\n --reviewers <ids> Run only these reviewers (comma-separated)\n --max-cost-usd <n> Spend limit: reviews use 80%, verification and judging the rest\n --import-sarif <file> Add the results of a SARIF 2.1.0 log (Semgrep, CodeQL, \u2026)\n that fall on the change; repeatable; not with --plan\n --plan Show files, bundles, review tasks and prompt sizes; call no model\n --ultra Favor recall: all reviewers at every tier, two samples each (about 2x cost)\n --temperature <n> Sampling temperature, 0 to 2 (default: the provider's, or sampling in config)\n --seed <n> Sampling seed, where the runtime and provider support one\n --config <file> Read this configuration file instead of the repository's\n .ocra/config.json; it applies with --no-repo-config too\n --no-repo-config Ignore .ocra/config.json and its plugins (for untrusted\n code); models come from OCRA_MODEL_* variables or --config\n --no-upload When signed in to ocra Cloud, do not send this review's\n counts (verdict, numbers of findings, files, tokens)\n -h, --help Show help\n";
31
+ export declare const REVIEW_USAGE = "Usage: ocra review [options]\n\nReviews uncommitted changes by default.\n\nOptions:\n --from <ref> Review changes on --to since it diverged from <ref>\n --to <ref> End of the range (default: HEAD)\n --commit <sha> Review a single commit\n --pr <number> Review a GitHub pull request (needs GITHUB_TOKEN)\n --repo <owner/name> Repository of --pr (default: GITHUB_REPOSITORY or origin)\n --mr <iid> Review a GitLab merge request (needs GITLAB_TOKEN)\n --project <id|path> Project of --mr (default: CI_PROJECT_ID or origin)\n --publish With --pr or --mr: post the review to it\n --full With --pr or --mr: review every file, not only what\n changed since the previous review\n --format <format> text (default), json, or sarif (SARIF 2.1.0; not with --plan)\n --output <file> Write the result to a file instead of stdout\n --reviewers <ids> Run only these reviewers (comma-separated)\n --max-cost-usd <n> Spend limit: reviews use 80%, verification and judging the rest\n --import-sarif <file> Add the results of a SARIF 2.1.0 log (Semgrep, CodeQL, \u2026)\n that fall on the change; repeatable; not with --plan\n --plan Show files, bundles, review tasks and prompt sizes; call no model\n --ultra Favor recall: all reviewers at every tier, two samples each (about 2x cost)\n --temperature <n> Sampling temperature, 0 to 2 (default: the provider's, or sampling in config)\n --seed <n> Sampling seed, where the runtime and provider support one\n --config <file> Read this configuration file instead of the repository's\n .ocra/config.json; it applies with --no-repo-config too\n --no-repo-config Ignore .ocra/config.json and its plugins (for untrusted\n code); models come from OCRA_MODEL_* variables or --config\n --no-upload When signed in to ocra Cloud, send nothing about this\n review: no counts, per-reviewer counts or findings\n -h, --help Show help\n";
36
32
  export declare function parseReviewArgs(argv: string[]): ReviewArgs | "help";
33
+ export {};
37
34
  //# sourceMappingURL=args.d.ts.map
@@ -1,11 +1,6 @@
1
1
  import { parseArgs } from "node:util";
2
- import { OcraError } from "@open-cr-agent/core";
3
- export class UsageError extends OcraError {
4
- constructor(message) {
5
- super("INPUT_USAGE", message);
6
- this.name = "UsageError";
7
- }
8
- }
2
+ import { errorMessage } from "@open-cr-agent/core";
3
+ import { UsageError } from "../../io/usage-error.js";
9
4
  export const REVIEW_USAGE = `Usage: ocra review [options]
10
5
 
11
6
  Reviews uncommitted changes by default.
@@ -35,8 +30,8 @@ Options:
35
30
  .ocra/config.json; it applies with --no-repo-config too
36
31
  --no-repo-config Ignore .ocra/config.json and its plugins (for untrusted
37
32
  code); models come from OCRA_MODEL_* variables or --config
38
- --no-upload When signed in to ocra Cloud, do not send this review's
39
- counts (verdict, numbers of findings, files, tokens)
33
+ --no-upload When signed in to ocra Cloud, send nothing about this
34
+ review: no counts, per-reviewer counts or findings
40
35
  -h, --help Show help
41
36
  `;
42
37
  export function parseReviewArgs(argv) {
@@ -45,7 +40,7 @@ export function parseReviewArgs(argv) {
45
40
  parsed = parse(argv);
46
41
  }
47
42
  catch (error) {
48
- throw new UsageError(error.message);
43
+ throw new UsageError(errorMessage(error));
49
44
  }
50
45
  const { values, positionals } = parsed;
51
46
  if (values.help)
@@ -56,9 +51,9 @@ export function parseReviewArgs(argv) {
56
51
  if (format !== "text" && format !== "json" && format !== "sarif") {
57
52
  throw new UsageError(`--format must be text, json or sarif, got ${format}`);
58
53
  }
59
- const args = { target: target(values), format };
60
- const pullRequest = pullRequestTarget(values);
61
- const mergeRequest = mergeRequestTarget(values);
54
+ const args = { target: parseLocalTarget(values), format };
55
+ const pullRequest = parsePullRequest(values);
56
+ const mergeRequest = parseMergeRequest(values);
62
57
  if (pullRequest && mergeRequest)
63
58
  throw new UsageError("--pr cannot be combined with --mr");
64
59
  if (!pullRequest && !mergeRequest && values.publish) {
@@ -160,7 +155,7 @@ function parse(argv) {
160
155
  },
161
156
  });
162
157
  }
163
- function pullRequestTarget(values) {
158
+ function parsePullRequest(values) {
164
159
  if (values.pr === undefined) {
165
160
  if (values.repo !== undefined)
166
161
  throw new UsageError("--repo requires --pr");
@@ -183,7 +178,7 @@ function pullRequestTarget(values) {
183
178
  }
184
179
  // A GitLab project is a numeric id or a full path, "group/sub/project".
185
180
  const PROJECT = /^(\d+|[\w.-]+(\/[\w.-]+)+)$/;
186
- function mergeRequestTarget(values) {
181
+ function parseMergeRequest(values) {
187
182
  if (values.mr === undefined) {
188
183
  if (values.project !== undefined)
189
184
  throw new UsageError("--project requires --mr");
@@ -204,7 +199,7 @@ function mergeRequestTarget(values) {
204
199
  target.project = values.project;
205
200
  return target;
206
201
  }
207
- function target(values) {
202
+ function parseLocalTarget(values) {
208
203
  if (values.commit !== undefined) {
209
204
  if (values.from !== undefined || values.to !== undefined) {
210
205
  throw new UsageError("--commit cannot be combined with --from or --to");
@@ -0,0 +1,7 @@
1
+ import type { ReviewArgs } from "./args.js";
2
+ import type { ReviewDeps } from "./deps.js";
3
+ import type { ExecutedRun } from "./execute-run.js";
4
+ import type { ResolvedRun, ReviewIo } from "./resolve-run.js";
5
+ export declare function writeReport(run: ResolvedRun, { report }: ExecutedRun, args: ReviewArgs, io: ReviewIo, deps: ReviewDeps): Promise<void>;
6
+ export declare function deliver(run: ResolvedRun, executed: ExecutedRun, args: ReviewArgs, io: ReviewIo, deps: ReviewDeps): Promise<void>;
7
+ //# sourceMappingURL=deliver.d.ts.map
@@ -0,0 +1,56 @@
1
+ import { join, relative, resolve } from "node:path";
2
+ import { sendToCloud } from "../../cloud/review.js";
3
+ import { VERSION } from "../../version.js";
4
+ import { renderJson, renderText } from "./render.js";
5
+ import { renderSarif } from "./sarif.js";
6
+ import { CHANGE_REQUEST } from "./target.js";
7
+ // The report in the format asked for, to stdout or --output.
8
+ export async function writeReport(run, { report }, args, io, deps) {
9
+ const sessionDir = relative(deps.cwd, join(run.session.dir, run.session.id)) || ".";
10
+ const rendered = args.format === "json"
11
+ ? renderJson(report)
12
+ : args.format === "sarif"
13
+ ? renderSarif(report, VERSION)
14
+ : renderText(report, sessionDir);
15
+ if (args.output === undefined) {
16
+ io.out.write(rendered);
17
+ }
18
+ else {
19
+ await deps.writeFile(resolve(deps.cwd, args.output), rendered);
20
+ io.err.write(`[ocra] Wrote ${args.output}\n`);
21
+ }
22
+ const hidden = report.remembered.filter((e) => e.source === "account").length;
23
+ if (hidden > 0)
24
+ io.err.write(`[ocra] ${hidden} finding(s) hidden by your ocra Cloud memory\n`);
25
+ }
26
+ // Publishes to the pull or merge request when asked, and uploads to ocra
27
+ // Cloud when signed in.
28
+ export async function deliver(run, executed, args, io, deps) {
29
+ const { target } = run;
30
+ if (target.publish && target.platform !== "local") {
31
+ const changeRequest = CHANGE_REQUEST[target.platform];
32
+ // Stopping halfway could post the inline comments without the summary
33
+ // that remembers them; a first Ctrl-C only warns.
34
+ const stopHolding = deps.onInterrupt?.(() => {
35
+ io.err.write(`[ocra] Publishing; Ctrl-C again quits now and may leave the ${changeRequest} half updated\n`);
36
+ });
37
+ try {
38
+ const published = await run.vcs.publish(executed.report);
39
+ for (const warning of published.warnings)
40
+ run.warn(warning);
41
+ }
42
+ finally {
43
+ stopHolding?.();
44
+ }
45
+ io.err.write(`[ocra] Published the review to the ${changeRequest}\n`);
46
+ }
47
+ if (executed.cloudReview && run.signedInCloud && !args.noUpload) {
48
+ await sendToCloud(executed.report, executed.cloudReview, run.signedInCloud, {
49
+ source: target.platform,
50
+ durationMs: deps.now() - executed.started,
51
+ err: io.err,
52
+ warn: run.warn,
53
+ });
54
+ }
55
+ }
56
+ //# sourceMappingURL=deliver.js.map
@@ -0,0 +1,18 @@
1
+ import type { OcraPlugin } from "@open-cr-agent/core";
2
+ import type { CloudDeps } from "../../cloud/deps.js";
3
+ import type { NpmRunner } from "../../plugins/npm.js";
4
+ import type { RuntimeLoaders } from "./runtimes.js";
5
+ export interface ReviewDeps {
6
+ cwd: string;
7
+ env: Readonly<Record<string, string | undefined>>;
8
+ builtinPlugins: readonly OcraPlugin[];
9
+ runtimes: RuntimeLoaders;
10
+ writeFile(path: string, content: string): Promise<void>;
11
+ now(): number;
12
+ heartbeatMs: number;
13
+ fetch?: typeof fetch;
14
+ cloud?: CloudDeps;
15
+ npm?: NpmRunner;
16
+ onInterrupt?(handler: () => void): () => void;
17
+ }
18
+ //# sourceMappingURL=deps.d.ts.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=deps.js.map
@@ -0,0 +1,13 @@
1
+ import { type ReviewReport } from "@open-cr-agent/core";
2
+ import { type CloudReview } from "../../cloud/review.js";
3
+ import type { ReviewArgs } from "./args.js";
4
+ import type { ReviewDeps } from "./deps.js";
5
+ import type { ResolvedRun, ReviewIo } from "./resolve-run.js";
6
+ export interface ExecutedRun {
7
+ report: ReviewReport;
8
+ interrupted: boolean;
9
+ cloudReview?: CloudReview;
10
+ started: number;
11
+ }
12
+ export declare function executeRun(run: ResolvedRun, args: ReviewArgs, io: ReviewIo, deps: ReviewDeps): Promise<ExecutedRun>;
13
+ //# sourceMappingURL=execute-run.d.ts.map
@@ -0,0 +1,97 @@
1
+ import { review, } from "@open-cr-agent/core";
2
+ import { REVIEW_DEFAULTS } from "@open-cr-agent/core/internal";
3
+ import { withCloudProviders } from "../../cloud/providers.js";
4
+ import { prepareCloudReview } from "../../cloud/review.js";
5
+ import { agentChains } from "../../config/cli-config.js";
6
+ import { VERSION } from "../../version.js";
7
+ import { ProgressPrinter } from "./progress.js";
8
+ import { configHash, requestedSampling } from "./provenance.js";
9
+ import { configuredOptions } from "./review-options.js";
10
+ import { loadSarifLogs } from "./sarif-input.js";
11
+ import { accountOf } from "./settings-sources.js";
12
+ export async function executeRun(run, args, io, deps) {
13
+ const sarif = await loadSarifLogs(args.importSarif ?? [], deps.cwd);
14
+ const cloudReview = run.signedInCloud
15
+ ? await prepareCloudReview(run.root, run.signedInCloud, run.warn, run.target.repository)
16
+ : undefined;
17
+ const sampling = requestedSampling(run.config, args);
18
+ const { runtime, stopCloud } = await startRuntime(run, deps, sampling);
19
+ const progress = new ProgressPrinter(io.err, { heartbeatMs: deps.heartbeatMs, now: deps.now });
20
+ const interrupt = new AbortController();
21
+ const stopListening = deps.onInterrupt?.(() => {
22
+ io.err.write("[ocra] Interrupted: stopping and writing a partial report (Ctrl-C again quits now)\n");
23
+ interrupt.abort();
24
+ });
25
+ const started = deps.now();
26
+ try {
27
+ const report = await review({
28
+ ...prepareReview(run, args, sampling, sarif, cloudReview),
29
+ signal: interrupt.signal,
30
+ vcs: run.vcs,
31
+ runtime,
32
+ onEvent: (event) => {
33
+ run.registry.emit(event);
34
+ progress.onEvent(event);
35
+ },
36
+ });
37
+ return {
38
+ report,
39
+ interrupted: interrupt.signal.aborted,
40
+ ...(cloudReview ? { cloudReview } : {}),
41
+ started,
42
+ };
43
+ }
44
+ finally {
45
+ stopListening?.();
46
+ progress.stop();
47
+ stopCloud();
48
+ await runtime.dispose?.();
49
+ }
50
+ }
51
+ // The runtime the configuration names, with ocra Cloud's providers when signed in.
52
+ async function startRuntime(run, deps, sampling) {
53
+ const { config } = run;
54
+ const models = config.models;
55
+ const agentModels = agentChains({ reviewers: run.overrides, roles: config.roles });
56
+ const cloud = await withCloudProviders([...Object.values(models), ...Object.values(agentModels)], config.providers, deps.env,
57
+ // Even when the session was lost: an ocra- model then fails with why.
58
+ deps.cloud, run.warn, {
59
+ timeoutMs: config.runTimeoutMinutes
60
+ ? config.runTimeoutMinutes * 60_000
61
+ : REVIEW_DEFAULTS.runTimeoutMs,
62
+ // The direct runtime reads a provider's key at each call (DirectRuntime.target).
63
+ keyReadPerCall: config.runtime === "direct",
64
+ });
65
+ try {
66
+ const runtime = run.registry.createRuntime(config.runtime, {
67
+ models,
68
+ ...(Object.keys(agentModels).length > 0 ? { agentModels } : {}),
69
+ env: cloud.env,
70
+ providers: cloud.providers,
71
+ ...(Object.keys(sampling).length > 0 ? { sampling } : {}),
72
+ });
73
+ return { runtime, stopCloud: () => cloud.stop() };
74
+ }
75
+ catch (error) {
76
+ cloud.stop();
77
+ throw error;
78
+ }
79
+ }
80
+ function prepareReview(run, args, sampling, sarif, cloudReview) {
81
+ const { config, ultra, accountSettings } = run;
82
+ return {
83
+ ...configuredOptions(run, args),
84
+ identity: {
85
+ runId: run.session.id,
86
+ provenance: {
87
+ ocraVersion: VERSION,
88
+ configHash: configHash(config, ultra ? { ...args, ultra: true } : args, accountSettings),
89
+ sampling,
90
+ ...accountOf(accountSettings),
91
+ },
92
+ },
93
+ ...(sarif.length > 0 ? { sarif } : {}),
94
+ ...(cloudReview?.memory.length ? { accountMemory: cloudReview.memory } : {}),
95
+ };
96
+ }
97
+ //# sourceMappingURL=execute-run.js.map
@@ -0,0 +1,4 @@
1
+ import { type ReviewReport } from "@open-cr-agent/core";
2
+ import type { Output } from "../../io/output.js";
3
+ export declare function exitCode(report: ReviewReport, err: Output): number;
4
+ //# sourceMappingURL=exit-code.d.ts.map