@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,6 @@
|
|
|
1
|
+
export type { ReviewDeps } from "./commands/review/deps.js";
|
|
2
|
+
export { type CliPlanOutput, cliPlanOutputSchema, planJsonSchema, } from "./commands/review/plan-schema.js";
|
|
3
|
+
export { configJsonSchema } from "./config/schema.js";
|
|
4
|
+
export type { Output } from "./io/output.js";
|
|
5
|
+
export { run } from "./run.js";
|
|
6
|
+
//# sourceMappingURL=internal.d.ts.map
|
package/dist/internal.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// "@open-cr-agent/cli/internal": what ocra's own packages share beyond the
|
|
2
|
+
// public API (index.ts), such as the command with its dependencies
|
|
3
|
+
// injected. Not a contract: any release may change or remove any of it.
|
|
4
|
+
export { cliPlanOutputSchema, planJsonSchema, } from "./commands/review/plan-schema.js";
|
|
5
|
+
export { configJsonSchema } from "./config/schema.js";
|
|
6
|
+
export { run } from "./run.js";
|
|
7
|
+
//# sourceMappingURL=internal.js.map
|
package/dist/io/exit.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Exit codes are a contract (docs/manual, "Exit codes"): every command
|
|
2
|
+
// returns one of these, never a bare number.
|
|
3
|
+
export const EXIT = {
|
|
4
|
+
ok: 0,
|
|
5
|
+
blocking: 1,
|
|
6
|
+
// ocra login, logout and whoami: no usable ocra Cloud session.
|
|
7
|
+
notSignedIn: 1,
|
|
8
|
+
error: 2,
|
|
9
|
+
incomplete: 3,
|
|
10
|
+
interrupted: 130,
|
|
11
|
+
};
|
|
12
|
+
//# sourceMappingURL=exit.js.map
|
package/dist/main.js
CHANGED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { OcraPlugin } from "@open-cr-agent/core";
|
|
2
|
+
export interface AccountPlugins {
|
|
3
|
+
plugins: string[];
|
|
4
|
+
pluginSettings: Record<string, unknown>;
|
|
5
|
+
}
|
|
6
|
+
export declare function parseAccountPlugins(body: unknown, warn: (message: string) => void): AccountPlugins;
|
|
7
|
+
/**
|
|
8
|
+
* The account's plugins this review loads, and their settings. Only where a
|
|
9
|
+
* repository's plugins could load (allowed: never for pull or merge requests,
|
|
10
|
+
* never with --no-repo-config); a package the configuration already lists
|
|
11
|
+
* loads as the configuration's, and a plugin whose name is taken is skipped.
|
|
12
|
+
*/
|
|
13
|
+
export declare function loadAccountPlugins(account: AccountPlugins, options: {
|
|
14
|
+
allowed: boolean;
|
|
15
|
+
configured: readonly string[];
|
|
16
|
+
taken: readonly OcraPlugin[];
|
|
17
|
+
dir: string;
|
|
18
|
+
warn: (message: string) => void;
|
|
19
|
+
}): Promise<{
|
|
20
|
+
plugins: OcraPlugin[];
|
|
21
|
+
settings: Record<string, unknown>;
|
|
22
|
+
}>;
|
|
23
|
+
//# sourceMappingURL=account.d.ts.map
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { loadAllowedPlugins } from "./load.js";
|
|
2
|
+
import { isPackageName } from "./store.js";
|
|
3
|
+
const MAX_PLUGINS = 50;
|
|
4
|
+
export function parseAccountPlugins(body, warn) {
|
|
5
|
+
const settings = record(record(body).settings);
|
|
6
|
+
const listed = Array.isArray(settings.plugins) ? settings.plugins : [];
|
|
7
|
+
const plugins = [
|
|
8
|
+
...new Set(listed.filter((p) => typeof p === "string" && isPackageName(p))),
|
|
9
|
+
].slice(0, MAX_PLUGINS);
|
|
10
|
+
const refused = listed.filter((p) => typeof p !== "string" || !isPackageName(p));
|
|
11
|
+
if (refused.length > 0 || (settings.plugins !== undefined && !Array.isArray(settings.plugins))) {
|
|
12
|
+
warn(`ignoring plugins in your ocra Cloud settings that are not npm package names: ${refused.map((p) => JSON.stringify(p)).join(", ") || JSON.stringify(settings.plugins)}`);
|
|
13
|
+
}
|
|
14
|
+
if (listed.length > MAX_PLUGINS) {
|
|
15
|
+
warn(`your ocra Cloud settings name more than ${MAX_PLUGINS} plugins; the rest are ignored`);
|
|
16
|
+
}
|
|
17
|
+
return { plugins, pluginSettings: record(settings.pluginSettings) };
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The account's plugins this review loads, and their settings. Only where a
|
|
21
|
+
* repository's plugins could load (allowed: never for pull or merge requests,
|
|
22
|
+
* never with --no-repo-config); a package the configuration already lists
|
|
23
|
+
* loads as the configuration's, and a plugin whose name is taken is skipped.
|
|
24
|
+
*/
|
|
25
|
+
export async function loadAccountPlugins(account, options) {
|
|
26
|
+
const names = account.plugins.filter((name) => !options.configured.includes(name));
|
|
27
|
+
if (names.length === 0)
|
|
28
|
+
return { plugins: [], settings: {} };
|
|
29
|
+
if (!options.allowed) {
|
|
30
|
+
options.warn(`plugins from your ocra Cloud settings do not load for pull or merge requests or with --no-repo-config: ${names.join(", ")}`);
|
|
31
|
+
return { plugins: [], settings: {} };
|
|
32
|
+
}
|
|
33
|
+
const taken = new Set(options.taken.map((p) => p.name));
|
|
34
|
+
const plugins = [];
|
|
35
|
+
const settings = {};
|
|
36
|
+
for (const { name, plugin } of await loadAllowedPlugins(names, options.dir, options.warn)) {
|
|
37
|
+
if (taken.has(plugin.name)) {
|
|
38
|
+
options.warn(`skipping plugin ${name} from your ocra Cloud settings: a plugin named "${plugin.name}" is already loaded`);
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
taken.add(plugin.name);
|
|
42
|
+
plugins.push(plugin);
|
|
43
|
+
if (Object.hasOwn(account.pluginSettings, name)) {
|
|
44
|
+
settings[plugin.name] = account.pluginSettings[name];
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return { plugins, settings };
|
|
48
|
+
}
|
|
49
|
+
function record(value) {
|
|
50
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
51
|
+
? value
|
|
52
|
+
: {};
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=account.js.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { OcraPlugin } from "@open-cr-agent/core";
|
|
2
|
+
export declare function loadExternalPlugins(specifiers: readonly string[], root: string): Promise<OcraPlugin[]>;
|
|
3
|
+
/**
|
|
4
|
+
* The plugins an ocra Cloud account names that this machine allowed: each
|
|
5
|
+
* from the allowed directory alone, at the version and integrity recorded
|
|
6
|
+
* when it was allowed. Anything else is skipped with a warning; an account
|
|
7
|
+
* plugin never fails the review.
|
|
8
|
+
*/
|
|
9
|
+
export declare function loadAllowedPlugins(names: readonly string[], dir: string, warn: (message: string) => void): Promise<{
|
|
10
|
+
name: string;
|
|
11
|
+
plugin: OcraPlugin;
|
|
12
|
+
}[]>;
|
|
13
|
+
//# sourceMappingURL=load.d.ts.map
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { realpath } from "node:fs/promises";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { isAbsolute, join, resolve, sep } from "node:path";
|
|
4
|
+
import { pathToFileURL } from "node:url";
|
|
5
|
+
import { errorMessage } from "@open-cr-agent/core";
|
|
6
|
+
import { ConfigError } from "../config/cli-config.js";
|
|
7
|
+
import { installedEntry, readAllowed } from "./store.js";
|
|
8
|
+
// Plugins listed in the repository config execute code, so they are resolved
|
|
9
|
+
// only from the repository's own dependencies or relative paths inside it.
|
|
10
|
+
export async function loadExternalPlugins(specifiers, root) {
|
|
11
|
+
const plugins = [];
|
|
12
|
+
for (const specifier of specifiers) {
|
|
13
|
+
const module = await import(pathToFileURL(resolvePlugin(specifier, root)).href);
|
|
14
|
+
const plugin = module.default ?? module.plugin;
|
|
15
|
+
if (!isPlugin(plugin)) {
|
|
16
|
+
throw new ConfigError(`Plugin "${specifier}" must export an ocra plugin as default or "plugin"`);
|
|
17
|
+
}
|
|
18
|
+
plugins.push(plugin);
|
|
19
|
+
}
|
|
20
|
+
return plugins;
|
|
21
|
+
}
|
|
22
|
+
function resolvePlugin(specifier, root) {
|
|
23
|
+
if (specifier.startsWith(".") || isAbsolute(specifier))
|
|
24
|
+
return resolve(root, specifier);
|
|
25
|
+
try {
|
|
26
|
+
return createRequire(join(root, "package.json")).resolve(specifier);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
throw new ConfigError(`Cannot find plugin "${specifier}" from ${root}; install it as a dependency`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function isPlugin(value) {
|
|
33
|
+
return (typeof value === "object" && value !== null && typeof value.name === "string");
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The plugins an ocra Cloud account names that this machine allowed: each
|
|
37
|
+
* from the allowed directory alone, at the version and integrity recorded
|
|
38
|
+
* when it was allowed. Anything else is skipped with a warning; an account
|
|
39
|
+
* plugin never fails the review.
|
|
40
|
+
*/
|
|
41
|
+
export async function loadAllowedPlugins(names, dir, warn) {
|
|
42
|
+
const allowed = new Map((await readAllowed(dir)).map((p) => [p.name, p]));
|
|
43
|
+
const missing = names.filter((name) => !allowed.has(name));
|
|
44
|
+
if (missing.length > 0) {
|
|
45
|
+
warn(`your ocra Cloud settings name plugins this machine has not allowed, so they do not load: ${missing.join(", ")}; allow each with ocra plugins allow <name>@<version>`);
|
|
46
|
+
}
|
|
47
|
+
const loaded = [];
|
|
48
|
+
for (const name of names) {
|
|
49
|
+
const record = allowed.get(name);
|
|
50
|
+
if (!record)
|
|
51
|
+
continue;
|
|
52
|
+
try {
|
|
53
|
+
const installed = await installedEntry(dir, name);
|
|
54
|
+
if (installed?.version !== record.version || installed.integrity !== record.integrity) {
|
|
55
|
+
throw new Error(`it is not the ${record.version} allowed here; run ocra plugins allow ${name}@<version> again`);
|
|
56
|
+
}
|
|
57
|
+
const module = await import(pathToFileURL(await allowedEntry(dir, name)).href);
|
|
58
|
+
const plugin = module.default ?? module.plugin;
|
|
59
|
+
if (!isPlugin(plugin))
|
|
60
|
+
throw new Error('it exports no ocra plugin as default or "plugin"');
|
|
61
|
+
loaded.push({ name, plugin });
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
warn(`skipping plugin ${name} from your ocra Cloud settings: ${errorMessage(error)}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return loaded;
|
|
68
|
+
}
|
|
69
|
+
// Node's resolution walks up past the directory to any node_modules above
|
|
70
|
+
// it; the entry must be inside the allowed package itself.
|
|
71
|
+
async function allowedEntry(dir, name) {
|
|
72
|
+
const entry = await realpath(createRequire(join(dir, "package.json")).resolve(name));
|
|
73
|
+
const root = await realpath(join(dir, "node_modules", name));
|
|
74
|
+
if (!entry.startsWith(`${root}${sep}`))
|
|
75
|
+
throw new Error(`it resolves outside ${dir}`);
|
|
76
|
+
return entry;
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=load.js.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runs npm with these arguments in `cwd`, never through a shell; resolves
|
|
3
|
+
* with its standard output. Every call runs in the plugin directory, so the
|
|
4
|
+
* npm configuration of whatever project ocra was started in does not apply.
|
|
5
|
+
*/
|
|
6
|
+
export type NpmRunner = (args: readonly string[], cwd: string) => Promise<string>;
|
|
7
|
+
export declare function defaultNpm(env?: NodeJS.ProcessEnv): NpmRunner;
|
|
8
|
+
//# sourceMappingURL=npm.d.ts.map
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { ConfigError } from "../config/cli-config.js";
|
|
4
|
+
import { forTerminal } from "../io/terminal.js";
|
|
5
|
+
// npm on Windows is npm.cmd, which execFile cannot run without a shell; run
|
|
6
|
+
// the npm that ships with this Node directly instead.
|
|
7
|
+
export function defaultNpm(env = process.env) {
|
|
8
|
+
const [file, prefix] = process.platform === "win32"
|
|
9
|
+
? [
|
|
10
|
+
process.execPath,
|
|
11
|
+
[join(dirname(process.execPath), "node_modules", "npm", "bin", "npm-cli.js")],
|
|
12
|
+
]
|
|
13
|
+
: ["npm", []];
|
|
14
|
+
return (args, cwd) => new Promise((resolve, reject) => {
|
|
15
|
+
execFile(file, [...prefix, ...args], { cwd, env, timeout: 5 * 60_000, maxBuffer: 16 * 1024 * 1024, shell: false }, (error, stdout, stderr) => {
|
|
16
|
+
if (error) {
|
|
17
|
+
reject(new ConfigError(`npm ${args[0]} failed: ${forTerminal(String(stderr).trim().split("\n").slice(-3).join(" ") || error.message)}`));
|
|
18
|
+
}
|
|
19
|
+
else
|
|
20
|
+
resolve(String(stdout));
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=npm.js.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare function isPackageName(name: string): boolean;
|
|
3
|
+
export declare function isExactVersion(version: string): boolean;
|
|
4
|
+
declare const allowedSchema: z.ZodObject<{
|
|
5
|
+
name: z.ZodString;
|
|
6
|
+
version: z.ZodString;
|
|
7
|
+
integrity: z.ZodString;
|
|
8
|
+
}, z.core.$strict>;
|
|
9
|
+
export type AllowedPlugin = z.infer<typeof allowedSchema>;
|
|
10
|
+
export declare function pluginsDir(env: Readonly<Record<string, string | undefined>>): string;
|
|
11
|
+
export declare function readAllowed(dir: string): Promise<AllowedPlugin[]>;
|
|
12
|
+
export declare function writeAllowed(dir: string, plugins: readonly AllowedPlugin[]): Promise<void>;
|
|
13
|
+
export declare function ensureDir(dir: string): Promise<void>;
|
|
14
|
+
/** What npm's lockfile in the directory says it installed for a package. */
|
|
15
|
+
export declare function installedEntry(dir: string, name: string): Promise<{
|
|
16
|
+
version?: string;
|
|
17
|
+
integrity?: string;
|
|
18
|
+
} | undefined>;
|
|
19
|
+
export {};
|
|
20
|
+
//# sourceMappingURL=store.d.ts.map
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { isNotFound } from "@open-cr-agent/core/internal";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { ConfigError } from "../config/cli-config.js";
|
|
6
|
+
import { ocraConfigDir } from "../config/user-dir.js";
|
|
7
|
+
// The plugins this machine allows its ocra Cloud account to name (ADR-0027):
|
|
8
|
+
// installed by `ocra plugins allow` into a directory of the user's own, never
|
|
9
|
+
// the reviewed checkout, and recorded with the integrity npm installed.
|
|
10
|
+
// npm's package-name rules, less a leading "-" or "." that npm could read as
|
|
11
|
+
// an option or a path; at most 214 characters.
|
|
12
|
+
const PACKAGE_NAME = /^(?:@[a-z0-9~][a-z0-9._~-]*\/)?[a-z0-9~][a-z0-9._~-]*$/;
|
|
13
|
+
// An exact version: no range, tag or URL.
|
|
14
|
+
const EXACT_VERSION = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
15
|
+
export function isPackageName(name) {
|
|
16
|
+
return name.length <= 214 && PACKAGE_NAME.test(name);
|
|
17
|
+
}
|
|
18
|
+
export function isExactVersion(version) {
|
|
19
|
+
return version.length <= 256 && EXACT_VERSION.test(version);
|
|
20
|
+
}
|
|
21
|
+
const allowedSchema = z
|
|
22
|
+
.object({
|
|
23
|
+
name: z.string().refine(isPackageName),
|
|
24
|
+
version: z.string().refine(isExactVersion),
|
|
25
|
+
integrity: z.string().min(1),
|
|
26
|
+
})
|
|
27
|
+
.strict();
|
|
28
|
+
const fileSchema = z.object({ plugins: z.array(allowedSchema) }).strict();
|
|
29
|
+
export function pluginsDir(env) {
|
|
30
|
+
return join(ocraConfigDir(env), "plugins");
|
|
31
|
+
}
|
|
32
|
+
export async function readAllowed(dir) {
|
|
33
|
+
let text;
|
|
34
|
+
try {
|
|
35
|
+
text = await readFile(join(dir, "plugins.json"), "utf8");
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
if (isNotFound(error))
|
|
39
|
+
return [];
|
|
40
|
+
throw error;
|
|
41
|
+
}
|
|
42
|
+
let data;
|
|
43
|
+
try {
|
|
44
|
+
data = JSON.parse(text);
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
data = undefined;
|
|
48
|
+
}
|
|
49
|
+
const parsed = fileSchema.safeParse(data);
|
|
50
|
+
if (!parsed.success) {
|
|
51
|
+
throw new ConfigError(`${join(dir, "plugins.json")} is invalid; allow the plugins again or remove it`);
|
|
52
|
+
}
|
|
53
|
+
return parsed.data.plugins;
|
|
54
|
+
}
|
|
55
|
+
export async function writeAllowed(dir, plugins) {
|
|
56
|
+
await ensureDir(dir);
|
|
57
|
+
const path = join(dir, "plugins.json");
|
|
58
|
+
const sorted = [...plugins].sort((a, b) => (a.name < b.name ? -1 : 1));
|
|
59
|
+
await writeFile(path, `${JSON.stringify({ plugins: sorted }, null, 2)}\n`, { mode: 0o600 });
|
|
60
|
+
// writeFile keeps the mode of a file that already exists.
|
|
61
|
+
await chmod(path, 0o600);
|
|
62
|
+
}
|
|
63
|
+
export async function ensureDir(dir) {
|
|
64
|
+
await mkdir(dir, { recursive: true, mode: 0o700 });
|
|
65
|
+
await chmod(dir, 0o700);
|
|
66
|
+
}
|
|
67
|
+
/** What npm's lockfile in the directory says it installed for a package. */
|
|
68
|
+
export async function installedEntry(dir, name) {
|
|
69
|
+
let lock;
|
|
70
|
+
try {
|
|
71
|
+
lock = JSON.parse(await readFile(join(dir, "package-lock.json"), "utf8"));
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
const entry = lock.packages?.[`node_modules/${name}`];
|
|
77
|
+
if (typeof entry !== "object" || entry === null)
|
|
78
|
+
return undefined;
|
|
79
|
+
const { version, integrity } = entry;
|
|
80
|
+
return {
|
|
81
|
+
...(typeof version === "string" ? { version } : {}),
|
|
82
|
+
...(typeof integrity === "string" ? { integrity } : {}),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=store.js.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `https://host/owner/repo` for a remote in URL form (`https://`,
|
|
3
|
+
* `ssh://`, `git://`) or scp form (`user@host:owner/repo`), without user,
|
|
4
|
+
* password, port, trailing `/` or `.git`, case-folded; undefined for a
|
|
5
|
+
* local path.
|
|
6
|
+
*/
|
|
7
|
+
export declare function normaliseRemote(url: string): string | undefined;
|
|
8
|
+
/** The repository behind a pull or merge request's web page, such as https://host/o/r/pull/7. */
|
|
9
|
+
export declare function repositoryOfWebUrl(url: string): string | undefined;
|
|
10
|
+
/** The repository behind origin; its address as written when it is a path, the root without one. */
|
|
11
|
+
export declare function originRepository(root: string): Promise<string>;
|
|
12
|
+
//# sourceMappingURL=repository-id.d.ts.map
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
// What a repository is called before it is salted and hashed for ocra Cloud
|
|
4
|
+
// (ADR-0028): `https://host/owner/repo`, the same for every way of writing
|
|
5
|
+
// its address, so a laptop cloning over SSH and CI cloning over HTTPS group
|
|
6
|
+
// their reviews and match the account's memory. It is the https form
|
|
7
|
+
// because 0.5.0 and earlier hashed the remote URL as written: an HTTPS
|
|
8
|
+
// clone, the common case and CI's, keeps its hash across the upgrade.
|
|
9
|
+
const run = promisify(execFile);
|
|
10
|
+
// Hosts that serve SSH for another host's repositories (port 443 fallbacks).
|
|
11
|
+
const SSH_ALIASES = {
|
|
12
|
+
"ssh.github.com": "github.com",
|
|
13
|
+
"altssh.gitlab.com": "gitlab.com",
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* `https://host/owner/repo` for a remote in URL form (`https://`,
|
|
17
|
+
* `ssh://`, `git://`) or scp form (`user@host:owner/repo`), without user,
|
|
18
|
+
* password, port, trailing `/` or `.git`, case-folded; undefined for a
|
|
19
|
+
* local path.
|
|
20
|
+
*/
|
|
21
|
+
export function normaliseRemote(url) {
|
|
22
|
+
const parsed = hostAndPath(url.trim());
|
|
23
|
+
if (!parsed)
|
|
24
|
+
return undefined;
|
|
25
|
+
const host = parsed.host.toLowerCase();
|
|
26
|
+
const path = parsed.path
|
|
27
|
+
.replace(/^\/+|\/+$/g, "")
|
|
28
|
+
.replace(/\.git$/i, "")
|
|
29
|
+
.replace(/\/+$/, "");
|
|
30
|
+
if (!host || !path)
|
|
31
|
+
return undefined;
|
|
32
|
+
return `https://${SSH_ALIASES[host] ?? host}/${path}`.toLowerCase();
|
|
33
|
+
}
|
|
34
|
+
/** The repository behind a pull or merge request's web page, such as https://host/o/r/pull/7. */
|
|
35
|
+
export function repositoryOfWebUrl(url) {
|
|
36
|
+
const page = /^(.+?)\/(?:-\/)?(?:pull|merge_requests)\/\d+\/?$/.exec(url.trim());
|
|
37
|
+
return page?.[1] ? normaliseRemote(page[1]) : undefined;
|
|
38
|
+
}
|
|
39
|
+
/** The repository behind origin; its address as written when it is a path, the root without one. */
|
|
40
|
+
export async function originRepository(root) {
|
|
41
|
+
let url;
|
|
42
|
+
try {
|
|
43
|
+
const { stdout } = await run("git", ["-C", root, "remote", "get-url", "origin"], {
|
|
44
|
+
timeout: 10_000,
|
|
45
|
+
});
|
|
46
|
+
url = stdout.trim();
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return root;
|
|
50
|
+
}
|
|
51
|
+
return normaliseRemote(url) ?? url.toLowerCase();
|
|
52
|
+
}
|
|
53
|
+
function hostAndPath(url) {
|
|
54
|
+
if (/^[a-z][\w+.-]*:\/\//i.test(url)) {
|
|
55
|
+
try {
|
|
56
|
+
const parsed = new URL(url);
|
|
57
|
+
if (parsed.protocol === "file:")
|
|
58
|
+
return undefined;
|
|
59
|
+
return { host: parsed.hostname, path: decodeURIComponent(parsed.pathname) };
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// scp form; a Windows path such as C:\repo or C:/repo, whose "host" is
|
|
66
|
+
// one letter, is not one.
|
|
67
|
+
const scp = /^(?:[^@\s/]+@)?([^:/\s\\]+):(.+)$/.exec(url);
|
|
68
|
+
if (!scp?.[1] || !scp[2] || scp[1].length === 1)
|
|
69
|
+
return undefined;
|
|
70
|
+
return { host: scp[1], path: scp[2] };
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=repository-id.js.map
|
package/dist/run.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type { Output } from "./
|
|
1
|
+
import type { ReviewDeps } from "./commands/review/deps.js";
|
|
2
|
+
import type { Output } from "./io/output.js";
|
|
3
3
|
export declare function defaultDeps(): ReviewDeps;
|
|
4
4
|
export declare function run(argv: string[], out: Output, err: Output, deps?: ReviewDeps): Promise<number>;
|
|
5
5
|
//# sourceMappingURL=run.d.ts.map
|
package/dist/run.js
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
import { writeFile } from "node:fs/promises";
|
|
2
2
|
import { parseArgs } from "node:util";
|
|
3
|
-
import { isOcraError } from "@open-cr-agent/core";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { memoryCommand } from "./memory.js";
|
|
7
|
-
import { metricsCommand } from "./metrics.js";
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import { BUILTIN_RUNTIMES } from "./review/runtimes.js";
|
|
11
|
-
import {
|
|
3
|
+
import { errorMessage, isOcraError } from "@open-cr-agent/core";
|
|
4
|
+
import { defaultCloudDeps } from "./cloud/deps.js";
|
|
5
|
+
import { LOGIN_USAGE, loginCommand } from "./commands/login.js";
|
|
6
|
+
import { memoryCommand } from "./commands/memory.js";
|
|
7
|
+
import { metricsCommand } from "./commands/metrics.js";
|
|
8
|
+
import { PLUGINS_USAGE, pluginsCommand } from "./commands/plugins.js";
|
|
9
|
+
import { parseReviewArgs, REVIEW_USAGE } from "./commands/review/args.js";
|
|
10
|
+
import { BUILTIN_RUNTIMES } from "./commands/review/runtimes.js";
|
|
11
|
+
import { BUILTIN_PLUGINS, reviewCommand } from "./commands/review.js";
|
|
12
|
+
import { EXIT } from "./io/exit.js";
|
|
13
|
+
import { forTerminal } from "./io/terminal.js";
|
|
14
|
+
import { UsageError } from "./io/usage-error.js";
|
|
15
|
+
import { defaultNpm } from "./plugins/npm.js";
|
|
16
|
+
import { pluginsDir } from "./plugins/store.js";
|
|
12
17
|
import { VERSION } from "./version.js";
|
|
13
18
|
const USAGE = `Usage: ocra <command> [options]
|
|
14
19
|
|
|
@@ -16,6 +21,7 @@ Commands:
|
|
|
16
21
|
review Review code changes (run "ocra review --help" for options)
|
|
17
22
|
memory Remember findings the team accepts (run "ocra memory --help")
|
|
18
23
|
metrics Counts over past reviews: runs, cost, findings, per reviewer (run "ocra metrics --help")
|
|
24
|
+
plugins Allow plugins your ocra Cloud settings name on this machine (run "ocra plugins --help")
|
|
19
25
|
login Sign in to ocra Cloud (in development; run "ocra login --help")
|
|
20
26
|
logout Sign out of ocra Cloud
|
|
21
27
|
whoami Show the ocra Cloud account
|
|
@@ -66,9 +72,25 @@ export async function run(argv, out, err, deps = defaultDeps()) {
|
|
|
66
72
|
return EXIT.error;
|
|
67
73
|
}
|
|
68
74
|
}
|
|
75
|
+
if (command === "plugins") {
|
|
76
|
+
try {
|
|
77
|
+
return await pluginsCommand(rest, out, {
|
|
78
|
+
dir: pluginsDir(deps.env),
|
|
79
|
+
npm: deps.npm ?? defaultNpm(),
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
if (error instanceof UsageError) {
|
|
84
|
+
err.write(`${error.message ? `${error.message}\n\n` : ""}${PLUGINS_USAGE}`);
|
|
85
|
+
}
|
|
86
|
+
else
|
|
87
|
+
err.write(failure(error));
|
|
88
|
+
return EXIT.error;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
69
91
|
if (command === "login" || command === "logout" || command === "whoami") {
|
|
70
92
|
try {
|
|
71
|
-
return await
|
|
93
|
+
return await loginCommand(command, rest, out, err, defaultCloudDeps(deps.env));
|
|
72
94
|
}
|
|
73
95
|
catch (error) {
|
|
74
96
|
if (error instanceof UsageError)
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ReportOutput } from "@open-cr-agent/core";
|
|
2
|
+
export declare const SESSIONS_DIR = ".ocra/sessions";
|
|
3
|
+
export declare function sessionsDir(root: string): string;
|
|
4
|
+
export declare function reportPath(sessionDir: string): string;
|
|
5
|
+
/** The session ids in a sessions directory, oldest first; none when it does not exist. */
|
|
6
|
+
export declare function listSessions(dir: string): Promise<string[]>;
|
|
7
|
+
export declare function sessionStart(id: string): Date | undefined;
|
|
8
|
+
export declare function readSessionReport(sessionDir: string): Promise<ReportOutput | undefined>;
|
|
9
|
+
//# sourceMappingURL=store.d.ts.map
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { readdir } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { REPORT_FILE, readReport } from "@open-cr-agent/core/internal";
|
|
4
|
+
// Where a repository keeps its review sessions: one directory per run, named
|
|
5
|
+
// by the run id, written by core's session log (sessionJsonlPlugin).
|
|
6
|
+
export const SESSIONS_DIR = ".ocra/sessions";
|
|
7
|
+
export function sessionsDir(root) {
|
|
8
|
+
return join(root, SESSIONS_DIR);
|
|
9
|
+
}
|
|
10
|
+
export function reportPath(sessionDir) {
|
|
11
|
+
return join(sessionDir, REPORT_FILE);
|
|
12
|
+
}
|
|
13
|
+
/** The session ids in a sessions directory, oldest first; none when it does not exist. */
|
|
14
|
+
export async function listSessions(dir) {
|
|
15
|
+
return (await readdir(dir).catch(() => [])).filter((n) => !n.startsWith(".")).sort();
|
|
16
|
+
}
|
|
17
|
+
// A session directory is named by its run id, whose first part is the start
|
|
18
|
+
// time (newRunId in core).
|
|
19
|
+
export function sessionStart(id) {
|
|
20
|
+
const m = /^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})Z/.exec(id);
|
|
21
|
+
if (!m)
|
|
22
|
+
return undefined;
|
|
23
|
+
const [, y, mo, d, h, mi, s] = m;
|
|
24
|
+
return new Date(`${y}-${mo}-${d}T${h}:${mi}:${s}Z`);
|
|
25
|
+
}
|
|
26
|
+
// A session's version 1 report; undefined when it is missing, unreadable or
|
|
27
|
+
// of another version, which `ocra metrics` counts as unreadable.
|
|
28
|
+
export async function readSessionReport(sessionDir) {
|
|
29
|
+
return readReport(reportPath(sessionDir)).catch(() => undefined);
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=store.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-cr-agent/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "ocra: open-source multi-agent code review for local changes and GitHub pull requests",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"code-review",
|
|
@@ -27,6 +27,11 @@
|
|
|
27
27
|
"types": "./dist/index.d.ts",
|
|
28
28
|
"default": "./dist/index.js"
|
|
29
29
|
},
|
|
30
|
+
"./internal": {
|
|
31
|
+
"@open-cr-agent/source": "./src/internal.ts",
|
|
32
|
+
"types": "./dist/internal.d.ts",
|
|
33
|
+
"default": "./dist/internal.js"
|
|
34
|
+
},
|
|
30
35
|
"./package.json": "./package.json"
|
|
31
36
|
},
|
|
32
37
|
"bin": {
|
|
@@ -37,17 +42,22 @@
|
|
|
37
42
|
"!dist/**/*.map"
|
|
38
43
|
],
|
|
39
44
|
"dependencies": {
|
|
40
|
-
"@open-cr-agent/
|
|
41
|
-
"@open-cr-agent/
|
|
42
|
-
"@open-cr-agent/
|
|
43
|
-
"@open-cr-agent/vcs-
|
|
44
|
-
"@open-cr-agent/vcs-
|
|
45
|
+
"@open-cr-agent/cloud-contract": "0.6.0",
|
|
46
|
+
"@open-cr-agent/core": "0.6.0",
|
|
47
|
+
"@open-cr-agent/runtime-direct": "0.6.0",
|
|
48
|
+
"@open-cr-agent/vcs-github": "0.6.0",
|
|
49
|
+
"@open-cr-agent/vcs-gitlab": "0.6.0",
|
|
50
|
+
"@open-cr-agent/vcs-local": "0.6.0",
|
|
51
|
+
"@open-cr-agent/vcs-platform": "0.6.0",
|
|
45
52
|
"zod": "^4.6.5"
|
|
46
53
|
},
|
|
47
54
|
"optionalDependencies": {
|
|
48
|
-
"@open-cr-agent/runtime-opencode": "0.
|
|
55
|
+
"@open-cr-agent/runtime-opencode": "0.6.0"
|
|
49
56
|
},
|
|
50
57
|
"publishConfig": {
|
|
51
58
|
"access": "public"
|
|
59
|
+
},
|
|
60
|
+
"devDependencies": {
|
|
61
|
+
"@open-cr-agent/test-support": "0.0.0"
|
|
52
62
|
}
|
|
53
63
|
}
|