@open-cr-agent/cli 0.4.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 (132) hide show
  1. package/dist/cloud/account-salt.d.ts +6 -0
  2. package/dist/cloud/account-salt.js +33 -0
  3. package/dist/cloud/account-settings.d.ts +100 -0
  4. package/dist/cloud/account-settings.js +107 -0
  5. package/dist/cloud/browser.d.ts +17 -0
  6. package/dist/cloud/browser.js +37 -0
  7. package/dist/cloud/client.d.ts +93 -0
  8. package/dist/cloud/client.js +221 -0
  9. package/dist/cloud/credentials.d.ts +18 -0
  10. package/dist/cloud/credentials.js +52 -0
  11. package/dist/cloud/deps.d.ts +14 -0
  12. package/dist/cloud/deps.js +34 -0
  13. package/dist/cloud/file-lock.d.ts +14 -0
  14. package/dist/cloud/file-lock.js +77 -0
  15. package/dist/cloud/findings.d.ts +8 -0
  16. package/dist/cloud/findings.js +58 -0
  17. package/dist/cloud/gateway-token.d.ts +18 -0
  18. package/dist/cloud/gateway-token.js +63 -0
  19. package/dist/cloud/memory.d.ts +8 -0
  20. package/dist/cloud/memory.js +68 -0
  21. package/dist/cloud/private-file.d.ts +8 -0
  22. package/dist/cloud/private-file.js +46 -0
  23. package/dist/cloud/providers.d.ts +18 -0
  24. package/dist/cloud/providers.js +132 -0
  25. package/dist/cloud/review.d.ts +28 -0
  26. package/dist/cloud/review.js +76 -0
  27. package/dist/cloud/settings.d.ts +22 -0
  28. package/dist/cloud/settings.js +64 -0
  29. package/dist/cloud/upload.d.ts +20 -0
  30. package/dist/cloud/upload.js +183 -0
  31. package/dist/commands/login.d.ts +5 -0
  32. package/dist/commands/login.js +164 -0
  33. package/dist/commands/memory.d.ts +3 -0
  34. package/dist/{memory.js → commands/memory.js} +21 -20
  35. package/dist/commands/metrics.d.ts +45 -0
  36. package/dist/{metrics.js → commands/metrics.js} +32 -45
  37. package/dist/commands/plugins.d.ts +9 -0
  38. package/dist/commands/plugins.js +159 -0
  39. package/dist/{review → commands/review}/args.d.ts +7 -10
  40. package/dist/{review → commands/review}/args.js +11 -16
  41. package/dist/commands/review/deliver.d.ts +7 -0
  42. package/dist/commands/review/deliver.js +56 -0
  43. package/dist/commands/review/deps.d.ts +18 -0
  44. package/dist/commands/review/deps.js +2 -0
  45. package/dist/commands/review/execute-run.d.ts +13 -0
  46. package/dist/commands/review/execute-run.js +97 -0
  47. package/dist/commands/review/exit-code.d.ts +4 -0
  48. package/dist/commands/review/exit-code.js +29 -0
  49. package/dist/commands/review/plan-prices.d.ts +3 -0
  50. package/dist/commands/review/plan-prices.js +18 -0
  51. package/dist/{review → commands/review}/plan-render.js +22 -2
  52. package/dist/commands/review/plan-run.d.ts +5 -0
  53. package/dist/commands/review/plan-run.js +38 -0
  54. package/dist/commands/review/plan-schema.d.ts +97 -0
  55. package/dist/commands/review/plan-schema.js +34 -0
  56. package/dist/{review → commands/review}/progress.d.ts +3 -5
  57. package/dist/{review → commands/review}/progress.js +3 -3
  58. package/dist/{review → commands/review}/provenance.d.ts +4 -2
  59. package/dist/{review → commands/review}/provenance.js +11 -2
  60. package/dist/{review → commands/review}/render.js +8 -3
  61. package/dist/commands/review/resolve-run.d.ts +29 -0
  62. package/dist/commands/review/resolve-run.js +125 -0
  63. package/dist/commands/review/review-options.d.ts +6 -0
  64. package/dist/commands/review/review-options.js +35 -0
  65. package/dist/{review → commands/review}/runtimes.js +1 -1
  66. package/dist/{review → commands/review}/sarif-input.d.ts +0 -1
  67. package/dist/{review → commands/review}/sarif-input.js +5 -5
  68. package/dist/commands/review/sarif.d.ts +96 -0
  69. package/dist/{review → commands/review}/sarif.js +29 -7
  70. package/dist/commands/review/settings-sources.d.ts +17 -0
  71. package/dist/commands/review/settings-sources.js +30 -0
  72. package/dist/commands/review/target.d.ts +38 -0
  73. package/dist/commands/review/target.js +126 -0
  74. package/dist/commands/review.d.ts +7 -0
  75. package/dist/commands/review.js +34 -0
  76. package/dist/config/cli-config.d.ts +41 -0
  77. package/dist/config/cli-config.js +199 -0
  78. package/dist/{review/remote-config.d.ts → config/remote.d.ts} +3 -3
  79. package/dist/{review/remote-config.js → config/remote.js} +5 -23
  80. package/dist/{review/config.d.ts → config/schema.d.ts} +13 -25
  81. package/dist/config/schema.js +138 -0
  82. package/dist/config/settings.d.ts +37 -0
  83. package/dist/config/settings.js +133 -0
  84. package/dist/config/user-dir.d.ts +3 -0
  85. package/dist/config/user-dir.js +10 -0
  86. package/dist/index.d.ts +1 -1
  87. package/dist/index.js +10 -1
  88. package/dist/internal.d.ts +6 -0
  89. package/dist/internal.js +7 -0
  90. package/dist/io/exit.d.ts +9 -0
  91. package/dist/io/exit.js +12 -0
  92. package/dist/io/output.d.ts +4 -0
  93. package/dist/io/output.js +2 -0
  94. package/dist/io/usage-error.d.ts +5 -0
  95. package/dist/io/usage-error.js +8 -0
  96. package/dist/main.js +2 -2
  97. package/dist/plugins/account.d.ts +23 -0
  98. package/dist/plugins/account.js +54 -0
  99. package/dist/plugins/load.d.ts +13 -0
  100. package/dist/plugins/load.js +78 -0
  101. package/dist/plugins/npm.d.ts +8 -0
  102. package/dist/plugins/npm.js +24 -0
  103. package/dist/plugins/store.d.ts +20 -0
  104. package/dist/plugins/store.js +85 -0
  105. package/dist/repository-id.d.ts +12 -0
  106. package/dist/repository-id.js +72 -0
  107. package/dist/run.d.ts +2 -2
  108. package/dist/run.js +32 -10
  109. package/dist/session/store.d.ts +9 -0
  110. package/dist/session/store.js +31 -0
  111. package/package.json +17 -7
  112. package/dist/cloud.d.ts +0 -32
  113. package/dist/cloud.js +0 -262
  114. package/dist/memory.d.ts +0 -4
  115. package/dist/metrics.d.ts +0 -47
  116. package/dist/review/cloud-providers.d.ts +0 -12
  117. package/dist/review/cloud-providers.js +0 -59
  118. package/dist/review/cloud-upload.d.ts +0 -40
  119. package/dist/review/cloud-upload.js +0 -135
  120. package/dist/review/command.d.ts +0 -31
  121. package/dist/review/command.js +0 -240
  122. package/dist/review/config.js +0 -280
  123. package/dist/review/plugins.d.ts +0 -3
  124. package/dist/review/plugins.js +0 -32
  125. package/dist/review/sarif.d.ts +0 -4
  126. package/dist/review/target.d.ts +0 -17
  127. package/dist/review/target.js +0 -207
  128. /package/dist/{review → commands/review}/plan-render.d.ts +0 -0
  129. /package/dist/{review → commands/review}/render.d.ts +0 -0
  130. /package/dist/{review → commands/review}/runtimes.d.ts +0 -0
  131. /package/dist/{review → io}/terminal.d.ts +0 -0
  132. /package/dist/{review → io}/terminal.js +0 -0
@@ -0,0 +1,45 @@
1
+ import type { Severity, Verification } from "@open-cr-agent/core";
2
+ import type { Output } from "../io/output.js";
3
+ declare const METRICS_VERSION = 1;
4
+ interface ReviewerMetrics {
5
+ tasks: number;
6
+ failedTasks: number;
7
+ findings: number;
8
+ costUsd: number;
9
+ fixed: number;
10
+ dismissed: number;
11
+ acceptanceRate: number | null;
12
+ }
13
+ export interface Metrics {
14
+ version: typeof METRICS_VERSION;
15
+ sessionsDir: string;
16
+ since?: string;
17
+ runs: {
18
+ total: number;
19
+ byVerdict: Record<string, number>;
20
+ incomplete: number;
21
+ unreadable: number;
22
+ };
23
+ cost: {
24
+ usd: number;
25
+ inputTokens: number;
26
+ outputTokens: number;
27
+ usdPerRun: number | null;
28
+ };
29
+ findings: {
30
+ reported: number;
31
+ unique: number;
32
+ bySeverity: Record<Severity, number>;
33
+ byVerification: Record<Verification, number>;
34
+ };
35
+ outcomes: {
36
+ fixed: number;
37
+ dismissed: number;
38
+ acceptanceRate: number | null;
39
+ };
40
+ reviewers: Record<string, ReviewerMetrics>;
41
+ }
42
+ export declare function metricsCommand(argv: string[], out: Output, cwd: string): Promise<number>;
43
+ export declare function collectMetrics(dir: string, since?: Date): Promise<Metrics>;
44
+ export {};
45
+ //# sourceMappingURL=metrics.d.ts.map
@@ -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)}`,
@@ -0,0 +1,9 @@
1
+ import type { Output } from "../io/output.js";
2
+ import type { NpmRunner } from "../plugins/npm.js";
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";
4
+ export interface PluginsDeps {
5
+ dir: string;
6
+ npm: NpmRunner;
7
+ }
8
+ export declare function pluginsCommand(argv: readonly string[], out: Output, deps: PluginsDeps): Promise<number>;
9
+ //# sourceMappingURL=plugins.d.ts.map
@@ -0,0 +1,159 @@
1
+ import { access, rm, writeFile } from "node:fs/promises";
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
+ // `ocra plugins` (ADR-0027): the user's own decision, on each machine, to
9
+ // let their ocra Cloud account load a third-party plugin, like adding a
10
+ // dependency. The account names packages; only what is allowed here loads.
11
+ export const PLUGINS_USAGE = `Usage: ocra plugins allow <name>@<version>
12
+ ocra plugins deny <name>
13
+ ocra plugins list
14
+
15
+ Your ocra Cloud settings may name plugins; a review loads one only if this
16
+ machine allows it. allow shows the package and who published it, installs that
17
+ exact version with install scripts off into a directory of your own, and
18
+ records its integrity; deny removes it.
19
+ `;
20
+ export async function pluginsCommand(argv, out, deps) {
21
+ const [action, target, ...extra] = argv;
22
+ if (action === "--help" || action === "-h" || action === "help") {
23
+ out.write(PLUGINS_USAGE);
24
+ return EXIT.ok;
25
+ }
26
+ if (extra.length > 0)
27
+ throw new UsageError(`Unexpected argument: ${extra[0]}`);
28
+ if (action === "list" && target === undefined)
29
+ return list(out, deps.dir);
30
+ if (action === "allow" && target !== undefined)
31
+ return allow(target, out, deps);
32
+ if (action === "deny" && target !== undefined)
33
+ return deny(target, out, deps);
34
+ throw new UsageError(action ? `Unknown or incomplete command: plugins ${action}` : "");
35
+ }
36
+ async function list(out, dir) {
37
+ const allowed = await readAllowed(dir);
38
+ if (allowed.length === 0) {
39
+ out.write("No plugins allowed. Allow one with: ocra plugins allow <name>@<version>\n");
40
+ return EXIT.ok;
41
+ }
42
+ for (const p of allowed)
43
+ out.write(`${p.name}@${p.version} ${p.integrity}\n`);
44
+ out.write(`(installed in ${dir})\n`);
45
+ return EXIT.ok;
46
+ }
47
+ function parseSpec(spec) {
48
+ const at = spec.lastIndexOf("@");
49
+ const name = at > 0 ? spec.slice(0, at) : spec;
50
+ const version = at > 0 ? spec.slice(at + 1) : "";
51
+ if (!isPackageName(name))
52
+ throw new UsageError(`Not an npm package name: ${forTerminal(name)}`);
53
+ if (!isExactVersion(version)) {
54
+ throw new UsageError(`Name an exact version, such as ${name}@1.2.3: a range or tag could install something else later`);
55
+ }
56
+ return { name, version };
57
+ }
58
+ async function allow(spec, out, deps) {
59
+ const { name, version } = parseSpec(spec);
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));
68
+ if (view.name !== name || view.version !== version) {
69
+ throw new ConfigError(`npm did not resolve ${name}@${version} to that exact version`);
70
+ }
71
+ if (!view.integrity) {
72
+ throw new ConfigError(`npm view lists no integrity for ${name}@${version}; not allowed`);
73
+ }
74
+ out.write(forTerminal([
75
+ `Package: ${view.name}@${view.version}`,
76
+ `Published by: ${view.publisher ?? "unknown"}`,
77
+ `Maintainers: ${view.maintainers.join(", ") || "unknown"}`,
78
+ `Integrity: ${view.integrity}`,
79
+ `Installing into ${deps.dir} with install scripts off`,
80
+ "",
81
+ ].join("\n")));
82
+ await deps.npm([
83
+ "install",
84
+ "--ignore-scripts",
85
+ "--no-audit",
86
+ "--no-fund",
87
+ "--save-exact",
88
+ "--prefix",
89
+ deps.dir,
90
+ `${name}@${version}`,
91
+ ], deps.dir);
92
+ const installed = await installedEntry(deps.dir, name);
93
+ if (installed?.version !== version || !installed.integrity) {
94
+ throw new ConfigError(`npm did not install ${name}@${version} into ${deps.dir}`);
95
+ }
96
+ if (view.integrity !== installed.integrity) {
97
+ throw new ConfigError(`${name}@${version} installed with another integrity than the registry lists; not allowed`);
98
+ }
99
+ const entry = { name, version, integrity: installed.integrity };
100
+ const allowed = (await readAllowed(deps.dir)).filter((p) => p.name !== name);
101
+ await writeAllowed(deps.dir, [...allowed, entry]);
102
+ out.write(`Allowed ${name}@${version}: your ocra Cloud settings can now load it here.\n`);
103
+ return EXIT.ok;
104
+ }
105
+ async function deny(name, out, deps) {
106
+ if (!isPackageName(name))
107
+ throw new UsageError(`Not an npm package name: ${forTerminal(name)}`);
108
+ const allowed = await readAllowed(deps.dir);
109
+ const kept = allowed.filter((p) => p.name !== name);
110
+ if (kept.length === allowed.length) {
111
+ out.write(`${name} was not allowed.\n`);
112
+ return EXIT.ok;
113
+ }
114
+ // The record is what loading checks; removing the files is housekeeping.
115
+ await writeAllowed(deps.dir, kept);
116
+ try {
117
+ await deps.npm(["uninstall", "--ignore-scripts", "--no-audit", "--no-fund", "--prefix", deps.dir, name], deps.dir);
118
+ }
119
+ catch {
120
+ await rm(join(deps.dir, "node_modules", name), { recursive: true, force: true });
121
+ }
122
+ out.write(`Denied ${name}: it no longer loads.\n`);
123
+ return EXIT.ok;
124
+ }
125
+ function describe(stdout) {
126
+ let data;
127
+ try {
128
+ data = JSON.parse(stdout);
129
+ }
130
+ catch {
131
+ throw new ConfigError("npm view did not answer with JSON");
132
+ }
133
+ if (Array.isArray(data))
134
+ data = data.length === 1 ? data[0] : undefined;
135
+ if (typeof data !== "object" || data === null) {
136
+ throw new ConfigError("npm view did not describe one version");
137
+ }
138
+ const v = data;
139
+ const person = (p) => typeof p === "string"
140
+ ? p
141
+ : typeof p === "object" && p !== null && typeof p.name === "string"
142
+ ? p.name
143
+ : undefined;
144
+ const integrity = v.dist?.integrity ?? v["dist.integrity"];
145
+ const publisher = person(v._npmUser);
146
+ return {
147
+ ...(typeof v.name === "string" ? { name: v.name } : {}),
148
+ ...(typeof v.version === "string" ? { version: v.version } : {}),
149
+ ...(publisher ? { publisher } : {}),
150
+ maintainers: (Array.isArray(v.maintainers) ? v.maintainers : [])
151
+ .map(person)
152
+ .filter((m) => m !== undefined),
153
+ ...(typeof integrity === "string" ? { integrity } : {}),
154
+ };
155
+ }
156
+ async function exists(path) {
157
+ return access(path).then(() => true, () => false);
158
+ }
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