@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.
- package/dist/cloud/account-salt.d.ts +6 -0
- package/dist/cloud/account-salt.js +33 -0
- package/dist/cloud/account-settings.d.ts +100 -0
- package/dist/cloud/account-settings.js +107 -0
- package/dist/cloud/browser.d.ts +17 -0
- package/dist/cloud/browser.js +37 -0
- package/dist/cloud/client.d.ts +93 -0
- package/dist/cloud/client.js +221 -0
- package/dist/cloud/credentials.d.ts +18 -0
- package/dist/cloud/credentials.js +52 -0
- package/dist/cloud/deps.d.ts +14 -0
- package/dist/cloud/deps.js +34 -0
- package/dist/cloud/file-lock.d.ts +14 -0
- package/dist/cloud/file-lock.js +77 -0
- package/dist/cloud/findings.d.ts +8 -0
- package/dist/cloud/findings.js +58 -0
- package/dist/cloud/gateway-token.d.ts +18 -0
- package/dist/cloud/gateway-token.js +63 -0
- package/dist/cloud/memory.d.ts +8 -0
- package/dist/cloud/memory.js +68 -0
- package/dist/cloud/private-file.d.ts +8 -0
- package/dist/cloud/private-file.js +46 -0
- package/dist/cloud/providers.d.ts +18 -0
- package/dist/cloud/providers.js +132 -0
- package/dist/cloud/review.d.ts +28 -0
- package/dist/cloud/review.js +76 -0
- package/dist/cloud/settings.d.ts +22 -0
- package/dist/cloud/settings.js +64 -0
- package/dist/cloud/upload.d.ts +20 -0
- package/dist/cloud/upload.js +183 -0
- package/dist/commands/login.d.ts +5 -0
- package/dist/commands/login.js +164 -0
- package/dist/commands/memory.d.ts +3 -0
- package/dist/{memory.js → commands/memory.js} +21 -20
- package/dist/commands/metrics.d.ts +45 -0
- package/dist/{metrics.js → commands/metrics.js} +32 -45
- package/dist/commands/plugins.d.ts +9 -0
- package/dist/commands/plugins.js +159 -0
- package/dist/{review → commands/review}/args.d.ts +7 -10
- package/dist/{review → commands/review}/args.js +11 -16
- package/dist/commands/review/deliver.d.ts +7 -0
- package/dist/commands/review/deliver.js +56 -0
- package/dist/commands/review/deps.d.ts +18 -0
- package/dist/commands/review/deps.js +2 -0
- package/dist/commands/review/execute-run.d.ts +13 -0
- package/dist/commands/review/execute-run.js +97 -0
- package/dist/commands/review/exit-code.d.ts +4 -0
- package/dist/commands/review/exit-code.js +29 -0
- package/dist/commands/review/plan-prices.d.ts +3 -0
- package/dist/commands/review/plan-prices.js +18 -0
- package/dist/{review → commands/review}/plan-render.js +22 -2
- package/dist/commands/review/plan-run.d.ts +5 -0
- package/dist/commands/review/plan-run.js +38 -0
- package/dist/commands/review/plan-schema.d.ts +97 -0
- package/dist/commands/review/plan-schema.js +34 -0
- package/dist/{review → commands/review}/progress.d.ts +3 -5
- package/dist/{review → commands/review}/progress.js +3 -3
- package/dist/{review → commands/review}/provenance.d.ts +4 -2
- package/dist/{review → commands/review}/provenance.js +11 -2
- package/dist/{review → commands/review}/render.js +8 -3
- package/dist/commands/review/resolve-run.d.ts +29 -0
- package/dist/commands/review/resolve-run.js +125 -0
- package/dist/commands/review/review-options.d.ts +6 -0
- package/dist/commands/review/review-options.js +35 -0
- package/dist/{review → commands/review}/runtimes.js +1 -1
- package/dist/{review → commands/review}/sarif-input.d.ts +0 -1
- package/dist/{review → commands/review}/sarif-input.js +5 -5
- package/dist/commands/review/sarif.d.ts +96 -0
- package/dist/{review → commands/review}/sarif.js +29 -7
- package/dist/commands/review/settings-sources.d.ts +17 -0
- package/dist/commands/review/settings-sources.js +30 -0
- package/dist/commands/review/target.d.ts +38 -0
- package/dist/commands/review/target.js +126 -0
- package/dist/commands/review.d.ts +7 -0
- package/dist/commands/review.js +34 -0
- package/dist/config/cli-config.d.ts +41 -0
- package/dist/config/cli-config.js +199 -0
- package/dist/{review/remote-config.d.ts → config/remote.d.ts} +3 -3
- package/dist/{review/remote-config.js → config/remote.js} +5 -23
- package/dist/{review/config.d.ts → config/schema.d.ts} +13 -25
- package/dist/config/schema.js +138 -0
- package/dist/config/settings.d.ts +37 -0
- package/dist/config/settings.js +133 -0
- package/dist/config/user-dir.d.ts +3 -0
- package/dist/config/user-dir.js +10 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +10 -1
- package/dist/internal.d.ts +6 -0
- package/dist/internal.js +7 -0
- package/dist/io/exit.d.ts +9 -0
- package/dist/io/exit.js +12 -0
- package/dist/io/output.d.ts +4 -0
- package/dist/io/output.js +2 -0
- package/dist/io/usage-error.d.ts +5 -0
- package/dist/io/usage-error.js +8 -0
- package/dist/main.js +2 -2
- package/dist/plugins/account.d.ts +23 -0
- package/dist/plugins/account.js +54 -0
- package/dist/plugins/load.d.ts +13 -0
- package/dist/plugins/load.js +78 -0
- package/dist/plugins/npm.d.ts +8 -0
- package/dist/plugins/npm.js +24 -0
- package/dist/plugins/store.d.ts +20 -0
- package/dist/plugins/store.js +85 -0
- package/dist/repository-id.d.ts +12 -0
- package/dist/repository-id.js +72 -0
- package/dist/run.d.ts +2 -2
- package/dist/run.js +32 -10
- package/dist/session/store.d.ts +9 -0
- package/dist/session/store.js +31 -0
- package/package.json +17 -7
- package/dist/cloud.d.ts +0 -32
- package/dist/cloud.js +0 -262
- package/dist/memory.d.ts +0 -4
- package/dist/metrics.d.ts +0 -47
- package/dist/review/cloud-providers.d.ts +0 -12
- package/dist/review/cloud-providers.js +0 -59
- package/dist/review/cloud-upload.d.ts +0 -40
- package/dist/review/cloud-upload.js +0 -135
- package/dist/review/command.d.ts +0 -31
- package/dist/review/command.js +0 -240
- package/dist/review/config.js +0 -280
- package/dist/review/plugins.d.ts +0 -3
- package/dist/review/plugins.js +0 -32
- package/dist/review/sarif.d.ts +0 -4
- package/dist/review/target.d.ts +0 -17
- package/dist/review/target.js +0 -207
- /package/dist/{review → commands/review}/plan-render.d.ts +0 -0
- /package/dist/{review → commands/review}/render.d.ts +0 -0
- /package/dist/{review → commands/review}/runtimes.d.ts +0 -0
- /package/dist/{review → io}/terminal.d.ts +0 -0
- /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 {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
|
|
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
|
|
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
|
|
41
|
+
const dir = values.sessions
|
|
43
42
|
? resolve(cwd, values.sessions)
|
|
44
|
-
:
|
|
45
|
-
const metrics = await collectMetrics(
|
|
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
|
|
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
|
-
|
|
56
|
-
//
|
|
57
|
-
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
4
|
-
|
|
2
|
+
type OutputFormat = "text" | "json" | "sarif";
|
|
3
|
+
interface PullRequestArgs {
|
|
5
4
|
number: number;
|
|
6
5
|
repo?: string;
|
|
7
6
|
publish: boolean;
|
|
8
7
|
}
|
|
9
|
-
|
|
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?:
|
|
17
|
-
mergeRequest?:
|
|
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
|
|
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 {
|
|
3
|
-
|
|
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,
|
|
39
|
-
counts
|
|
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
|
|
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:
|
|
60
|
-
const pullRequest =
|
|
61
|
-
const mergeRequest =
|
|
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
|
|
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
|
|
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
|
|
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,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
|