@open-cr-agent/cli 0.4.0 → 0.5.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 (39) hide show
  1. package/dist/account-salt.d.ts +7 -0
  2. package/dist/account-salt.js +43 -0
  3. package/dist/cloud.d.ts +2 -0
  4. package/dist/cloud.js +11 -1
  5. package/dist/plugin-store.d.ts +20 -0
  6. package/dist/plugin-store.js +84 -0
  7. package/dist/plugins-command.d.ts +15 -0
  8. package/dist/plugins-command.js +183 -0
  9. package/dist/review/account-plugins.d.ts +23 -0
  10. package/dist/review/account-plugins.js +54 -0
  11. package/dist/review/account-settings.d.ts +100 -0
  12. package/dist/review/account-settings.js +106 -0
  13. package/dist/review/cloud-findings.d.ts +31 -0
  14. package/dist/review/cloud-findings.js +80 -0
  15. package/dist/review/cloud-memory.d.ts +6 -0
  16. package/dist/review/cloud-memory.js +58 -0
  17. package/dist/review/cloud-providers.d.ts +2 -2
  18. package/dist/review/cloud-providers.js +3 -2
  19. package/dist/review/cloud-review.d.ts +19 -0
  20. package/dist/review/cloud-review.js +51 -0
  21. package/dist/review/cloud-settings.d.ts +16 -0
  22. package/dist/review/cloud-settings.js +118 -0
  23. package/dist/review/cloud-upload.d.ts +26 -6
  24. package/dist/review/cloud-upload.js +82 -37
  25. package/dist/review/command.d.ts +2 -0
  26. package/dist/review/command.js +81 -28
  27. package/dist/review/config.d.ts +17 -6
  28. package/dist/review/config.js +31 -7
  29. package/dist/review/plan-render.js +1 -1
  30. package/dist/review/plugins.d.ts +10 -0
  31. package/dist/review/plugins.js +46 -1
  32. package/dist/review/provenance.js +11 -1
  33. package/dist/review/render.js +7 -2
  34. package/dist/review/settings-sources.d.ts +11 -0
  35. package/dist/review/settings-sources.js +58 -0
  36. package/dist/review/target.d.ts +1 -0
  37. package/dist/review/target.js +3 -0
  38. package/dist/run.js +19 -0
  39. package/package.json +7 -7
@@ -0,0 +1,7 @@
1
+ import type { CloudDeps } from "./cloud.js";
2
+ export declare function accountSaltPath(credentialsPath: string): string;
3
+ /** The account's salt, or null; throws when the server cannot be asked. */
4
+ export declare function fetchAccountSalt(deps: Pick<CloudDeps, "fetch">, server: string, token: string): Promise<string | null>;
5
+ /** Keeps the salt beside the credentials (0600), or removes it when the account has none. */
6
+ export declare function saveAccountSalt(credentialsPath: string, salt: string | null): Promise<void>;
7
+ //# sourceMappingURL=account-salt.d.ts.map
@@ -0,0 +1,43 @@
1
+ import { chmod, mkdir, rm, writeFile } from "node:fs/promises";
2
+ import { dirname, join } from "node:path";
3
+ import { VERSION } from "./version.js";
4
+ // The account's repository-hash salt (ADR-0028, 4). The server answers one
5
+ // only while the account shares findings, so that a repository hashes alike
6
+ // on every machine of the account; null means the per-machine salt stays.
7
+ // It is kept beside the credentials, readable only by this user, and
8
+ // refreshed at login and by each signed-in review.
9
+ const SALT = /^[0-9a-f]{64}$/;
10
+ export function accountSaltPath(credentialsPath) {
11
+ return join(dirname(credentialsPath), "account-salt");
12
+ }
13
+ /** The account's salt, or null; throws when the server cannot be asked. */
14
+ export async function fetchAccountSalt(deps, server, token) {
15
+ const res = await deps.fetch(`${server}/api/account/salt`, {
16
+ headers: { authorization: `Bearer ${token}`, "user-agent": `ocra/${VERSION}` },
17
+ signal: AbortSignal.timeout(15_000),
18
+ });
19
+ // A server without account salts shares no findings.
20
+ if (res.status === 404)
21
+ return null;
22
+ if (!res.ok)
23
+ throw new Error(`HTTP ${res.status}`);
24
+ const body = (await res.json());
25
+ if (body.salt === null)
26
+ return null;
27
+ if (typeof body.salt === "string" && SALT.test(body.salt))
28
+ return body.salt;
29
+ throw new Error("the answer is not a salt");
30
+ }
31
+ /** Keeps the salt beside the credentials (0600), or removes it when the account has none. */
32
+ export async function saveAccountSalt(credentialsPath, salt) {
33
+ const path = accountSaltPath(credentialsPath);
34
+ if (salt === null) {
35
+ await rm(path, { force: true });
36
+ return;
37
+ }
38
+ await mkdir(dirname(path), { recursive: true, mode: 0o700 });
39
+ await writeFile(path, `${salt}\n`, { mode: 0o600 });
40
+ // writeFile keeps the mode of a file that already exists.
41
+ await chmod(path, 0o600);
42
+ }
43
+ //# sourceMappingURL=account-salt.js.map
package/dist/cloud.d.ts CHANGED
@@ -21,6 +21,8 @@ export type CloudDeps = {
21
21
  };
22
22
  export declare function defaultCloudDeps(env?: CloudDeps["env"]): CloudDeps;
23
23
  export declare function credentialsPath(env: CloudDeps["env"]): string;
24
+ /** ~/.config/ocra ($XDG_CONFIG_HOME/ocra), or %APPDATA%\ocra on Windows: this machine's own. */
25
+ export declare function ocraConfigDir(env: CloudDeps["env"]): string;
24
26
  export declare function readCredentials(path: string): Promise<Credentials | undefined>;
25
27
  /**
26
28
  * The saved session with a live access token, refreshed (and saved) when
package/dist/cloud.js CHANGED
@@ -4,6 +4,7 @@ import { homedir, hostname } from "node:os";
4
4
  import { dirname, join } from "node:path";
5
5
  import { parseArgs } from "node:util";
6
6
  import { errorMessage } from "@open-cr-agent/core/internal";
7
+ import { fetchAccountSalt, saveAccountSalt } from "./account-salt.js";
7
8
  import { UsageError } from "./review/args.js";
8
9
  import { forTerminal } from "./review/terminal.js";
9
10
  import { VERSION } from "./version.js";
@@ -42,10 +43,14 @@ function credentialsHint() {
42
43
  : "~/.config/ocra/credentials.json";
43
44
  }
44
45
  export function credentialsPath(env) {
46
+ return join(ocraConfigDir(env), "credentials.json");
47
+ }
48
+ /** ~/.config/ocra ($XDG_CONFIG_HOME/ocra), or %APPDATA%\ocra on Windows: this machine's own. */
49
+ export function ocraConfigDir(env) {
45
50
  const base = process.platform === "win32"
46
51
  ? (env.APPDATA ?? join(homedir(), "AppData", "Roaming"))
47
52
  : (env.XDG_CONFIG_HOME ?? join(homedir(), ".config"));
48
- return join(base, "ocra", "credentials.json");
53
+ return join(base, "ocra");
49
54
  }
50
55
  function cloudUrl(env) {
51
56
  const named = env.OCRA_CLOUD_URL || DEFAULT_CLOUD_URL;
@@ -193,6 +198,7 @@ async function logout(out, deps) {
193
198
  }
194
199
  }
195
200
  await rm(deps.credentialsPath, { force: true });
201
+ await saveAccountSalt(deps.credentialsPath, null);
196
202
  out.write(saved ? "Signed out.\n" : "Not signed in.\n");
197
203
  return 0;
198
204
  }
@@ -238,6 +244,10 @@ async function login(out, err, deps, browser) {
238
244
  expires_at: deps.now() + t.expires_in * 1000,
239
245
  };
240
246
  await writeCredentials(deps.credentialsPath, credentials);
247
+ // Best effort: each signed-in review asks again.
248
+ await fetchAccountSalt(deps, server, t.access_token)
249
+ .then((salt) => saveAccountSalt(deps.credentialsPath, salt))
250
+ .catch(() => { });
241
251
  out.write(forTerminal(`Signed in as ${credentials.login}.\n`));
242
252
  return 0;
243
253
  }
@@ -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=plugin-store.d.ts.map
@@ -0,0 +1,84 @@
1
+ import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { z } from "zod";
4
+ import { ocraConfigDir } from "./cloud.js";
5
+ import { ConfigError } from "./review/config.js";
6
+ // The plugins this machine allows its ocra Cloud account to name (ADR-0027):
7
+ // installed by `ocra plugins allow` into a directory of the user's own, never
8
+ // the reviewed checkout, and recorded with the integrity npm installed.
9
+ // npm's package-name rules, less a leading "-" or "." that npm could read as
10
+ // an option or a path; at most 214 characters.
11
+ const PACKAGE_NAME = /^(?:@[a-z0-9~][a-z0-9._~-]*\/)?[a-z0-9~][a-z0-9._~-]*$/;
12
+ // An exact version: no range, tag or URL.
13
+ const EXACT_VERSION = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
14
+ export function isPackageName(name) {
15
+ return name.length <= 214 && PACKAGE_NAME.test(name);
16
+ }
17
+ export function isExactVersion(version) {
18
+ return version.length <= 256 && EXACT_VERSION.test(version);
19
+ }
20
+ const allowedSchema = z
21
+ .object({
22
+ name: z.string().refine(isPackageName),
23
+ version: z.string().refine(isExactVersion),
24
+ integrity: z.string().min(1),
25
+ })
26
+ .strict();
27
+ const fileSchema = z.object({ plugins: z.array(allowedSchema) }).strict();
28
+ export function pluginsDir(env) {
29
+ return join(ocraConfigDir(env), "plugins");
30
+ }
31
+ export async function readAllowed(dir) {
32
+ let text;
33
+ try {
34
+ text = await readFile(join(dir, "plugins.json"), "utf8");
35
+ }
36
+ catch (error) {
37
+ if (error.code === "ENOENT")
38
+ return [];
39
+ throw error;
40
+ }
41
+ let data;
42
+ try {
43
+ data = JSON.parse(text);
44
+ }
45
+ catch {
46
+ data = undefined;
47
+ }
48
+ const parsed = fileSchema.safeParse(data);
49
+ if (!parsed.success) {
50
+ throw new ConfigError(`${join(dir, "plugins.json")} is invalid; allow the plugins again or remove it`);
51
+ }
52
+ return parsed.data.plugins;
53
+ }
54
+ export async function writeAllowed(dir, plugins) {
55
+ await ensureDir(dir);
56
+ const path = join(dir, "plugins.json");
57
+ const sorted = [...plugins].sort((a, b) => (a.name < b.name ? -1 : 1));
58
+ await writeFile(path, `${JSON.stringify({ plugins: sorted }, null, 2)}\n`, { mode: 0o600 });
59
+ // writeFile keeps the mode of a file that already exists.
60
+ await chmod(path, 0o600);
61
+ }
62
+ export async function ensureDir(dir) {
63
+ await mkdir(dir, { recursive: true, mode: 0o700 });
64
+ await chmod(dir, 0o700);
65
+ }
66
+ /** What npm's lockfile in the directory says it installed for a package. */
67
+ export async function installedEntry(dir, name) {
68
+ let lock;
69
+ try {
70
+ lock = JSON.parse(await readFile(join(dir, "package-lock.json"), "utf8"));
71
+ }
72
+ catch {
73
+ return undefined;
74
+ }
75
+ const entry = lock.packages?.[`node_modules/${name}`];
76
+ if (typeof entry !== "object" || entry === null)
77
+ return undefined;
78
+ const { version, integrity } = entry;
79
+ return {
80
+ ...(typeof version === "string" ? { version } : {}),
81
+ ...(typeof integrity === "string" ? { integrity } : {}),
82
+ };
83
+ }
84
+ //# sourceMappingURL=plugin-store.js.map
@@ -0,0 +1,15 @@
1
+ import type { Output } from "./review/progress.js";
2
+ export declare const PLUGINS_USAGE = "Usage: ocra plugins allow <name>@<version>\n ocra plugins deny <name>\n ocra plugins list\n\nYour ocra Cloud settings may name plugins; a review loads one only if this\nmachine allows it. allow shows the package and who published it, installs that\nexact version with install scripts off into a directory of your own, and\nrecords its integrity; deny removes it.\n";
3
+ /** Runs npm with these arguments, never through a shell; resolves with its standard output. */
4
+ export type NpmRunner = (args: readonly string[]) => Promise<string>;
5
+ export interface PluginsDeps {
6
+ dir: string;
7
+ npm: NpmRunner;
8
+ }
9
+ export declare function pluginsCommand(argv: readonly string[], out: Output, deps: PluginsDeps): Promise<number>;
10
+ export declare function parseSpec(spec: string): {
11
+ name: string;
12
+ version: string;
13
+ };
14
+ export declare function defaultNpm(env?: NodeJS.ProcessEnv): NpmRunner;
15
+ //# sourceMappingURL=plugins-command.d.ts.map
@@ -0,0 +1,183 @@
1
+ import { execFile } from "node:child_process";
2
+ import { access, rm, writeFile } from "node:fs/promises";
3
+ import { dirname, join } from "node:path";
4
+ import { ensureDir, installedEntry, isExactVersion, isPackageName, readAllowed, writeAllowed, } from "./plugin-store.js";
5
+ import { UsageError } from "./review/args.js";
6
+ import { ConfigError } from "./review/config.js";
7
+ import { forTerminal } from "./review/terminal.js";
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 0;
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 0;
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 0;
46
+ }
47
+ export 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
+ const view = describe(await deps.npm(["view", `${name}@${version}`, "--json"]));
61
+ if (view.name !== name || view.version !== version) {
62
+ throw new ConfigError(`npm did not resolve ${name}@${version} to that exact version`);
63
+ }
64
+ out.write(forTerminal([
65
+ `Package: ${view.name}@${view.version}`,
66
+ `Published by: ${view.publisher ?? "unknown"}`,
67
+ `Maintainers: ${view.maintainers.join(", ") || "unknown"}`,
68
+ `Integrity: ${view.integrity ?? "unknown"}`,
69
+ `Installing into ${deps.dir} with install scripts off`,
70
+ "",
71
+ ].join("\n")));
72
+ await ensureDir(deps.dir);
73
+ // npm installs into the nearest package.json; one of the directory's own
74
+ // keeps it from walking up to a project.
75
+ const manifest = join(deps.dir, "package.json");
76
+ if (!(await exists(manifest))) {
77
+ await writeFile(manifest, `${JSON.stringify({ private: true }, null, 2)}\n`, { mode: 0o600 });
78
+ }
79
+ await deps.npm([
80
+ "install",
81
+ "--ignore-scripts",
82
+ "--no-audit",
83
+ "--no-fund",
84
+ "--save-exact",
85
+ "--prefix",
86
+ deps.dir,
87
+ `${name}@${version}`,
88
+ ]);
89
+ const installed = await installedEntry(deps.dir, name);
90
+ if (installed?.version !== version || !installed.integrity) {
91
+ throw new ConfigError(`npm did not install ${name}@${version} into ${deps.dir}`);
92
+ }
93
+ if (view.integrity && view.integrity !== installed.integrity) {
94
+ throw new ConfigError(`${name}@${version} installed with another integrity than the registry lists; not allowed`);
95
+ }
96
+ const entry = { name, version, integrity: installed.integrity };
97
+ const allowed = (await readAllowed(deps.dir)).filter((p) => p.name !== name);
98
+ await writeAllowed(deps.dir, [...allowed, entry]);
99
+ out.write(`Allowed ${name}@${version}: your ocra Cloud settings can now load it here.\n`);
100
+ return 0;
101
+ }
102
+ async function deny(name, out, deps) {
103
+ if (!isPackageName(name))
104
+ throw new UsageError(`Not an npm package name: ${forTerminal(name)}`);
105
+ const allowed = await readAllowed(deps.dir);
106
+ const kept = allowed.filter((p) => p.name !== name);
107
+ if (kept.length === allowed.length) {
108
+ out.write(`${name} was not allowed.\n`);
109
+ return 0;
110
+ }
111
+ // The record is what loading checks; removing the files is housekeeping.
112
+ await writeAllowed(deps.dir, kept);
113
+ try {
114
+ await deps.npm([
115
+ "uninstall",
116
+ "--ignore-scripts",
117
+ "--no-audit",
118
+ "--no-fund",
119
+ "--prefix",
120
+ deps.dir,
121
+ name,
122
+ ]);
123
+ }
124
+ catch {
125
+ await rm(join(deps.dir, "node_modules", name), { recursive: true, force: true });
126
+ }
127
+ out.write(`Denied ${name}: it no longer loads.\n`);
128
+ return 0;
129
+ }
130
+ function describe(stdout) {
131
+ let data;
132
+ try {
133
+ data = JSON.parse(stdout);
134
+ }
135
+ catch {
136
+ throw new ConfigError("npm view did not answer with JSON");
137
+ }
138
+ if (Array.isArray(data))
139
+ data = data.length === 1 ? data[0] : undefined;
140
+ if (typeof data !== "object" || data === null) {
141
+ throw new ConfigError("npm view did not describe one version");
142
+ }
143
+ const v = data;
144
+ const person = (p) => typeof p === "string"
145
+ ? p
146
+ : typeof p === "object" && p !== null && typeof p.name === "string"
147
+ ? p.name
148
+ : undefined;
149
+ const integrity = v.dist?.integrity ?? v["dist.integrity"];
150
+ const publisher = person(v._npmUser);
151
+ return {
152
+ ...(typeof v.name === "string" ? { name: v.name } : {}),
153
+ ...(typeof v.version === "string" ? { version: v.version } : {}),
154
+ ...(publisher ? { publisher } : {}),
155
+ maintainers: (Array.isArray(v.maintainers) ? v.maintainers : [])
156
+ .map(person)
157
+ .filter((m) => m !== undefined),
158
+ ...(typeof integrity === "string" ? { integrity } : {}),
159
+ };
160
+ }
161
+ async function exists(path) {
162
+ return access(path).then(() => true, () => false);
163
+ }
164
+ // npm on Windows is npm.cmd, which execFile cannot run without a shell; run
165
+ // the npm that ships with this Node directly instead.
166
+ export function defaultNpm(env = process.env) {
167
+ const [file, prefix] = process.platform === "win32"
168
+ ? [
169
+ process.execPath,
170
+ [join(dirname(process.execPath), "node_modules", "npm", "bin", "npm-cli.js")],
171
+ ]
172
+ : ["npm", []];
173
+ return (args) => new Promise((resolve, reject) => {
174
+ execFile(file, [...prefix, ...args], { env, timeout: 5 * 60_000, maxBuffer: 16 * 1024 * 1024, shell: false }, (error, stdout, stderr) => {
175
+ if (error) {
176
+ reject(new ConfigError(`npm ${args[0]} failed: ${forTerminal(String(stderr).trim().split("\n").slice(-3).join(" ") || error.message)}`));
177
+ }
178
+ else
179
+ resolve(String(stdout));
180
+ });
181
+ });
182
+ }
183
+ //# sourceMappingURL=plugins-command.js.map
@@ -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 accountPlugins(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-plugins.d.ts.map
@@ -0,0 +1,54 @@
1
+ import { isPackageName } from "../plugin-store.js";
2
+ import { loadAccountPlugins } from "./plugins.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 accountPlugins(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 loadAccountPlugins(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, plugin.name)) {
44
+ settings[plugin.name] = account.pluginSettings[plugin.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-plugins.js.map
@@ -0,0 +1,100 @@
1
+ import { z } from "zod";
2
+ declare const SETTINGS: {
3
+ readonly concurrency: z.ZodOptional<z.ZodNumber>;
4
+ readonly taskTimeoutMinutes: z.ZodOptional<z.ZodNumber>;
5
+ readonly runTimeoutMinutes: z.ZodOptional<z.ZodNumber>;
6
+ readonly maxCostUsd: z.ZodOptional<z.ZodNumber>;
7
+ readonly maxTasks: z.ZodOptional<z.ZodNumber>;
8
+ readonly verify: z.ZodOptional<z.ZodBoolean>;
9
+ readonly judge: z.ZodOptional<z.ZodBoolean>;
10
+ readonly sampling: z.ZodOptional<z.ZodObject<{
11
+ temperature: z.ZodOptional<z.ZodNumber>;
12
+ seed: z.ZodOptional<z.ZodNumber>;
13
+ }, z.core.$strict>>;
14
+ readonly include: z.ZodDefault<z.ZodArray<z.ZodString>>;
15
+ readonly exclude: z.ZodDefault<z.ZodArray<z.ZodString>>;
16
+ readonly rules: z.ZodArray<z.ZodObject<{
17
+ path: z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>;
18
+ rule: z.ZodString;
19
+ }, z.core.$strict>>;
20
+ readonly ultra: z.ZodBoolean;
21
+ };
22
+ declare const AGENTS: {
23
+ readonly effort: z.ZodDefault<z.ZodObject<{
24
+ top: z.ZodOptional<z.ZodEnum<{
25
+ high: "high";
26
+ low: "low";
27
+ medium: "medium";
28
+ minimal: "minimal";
29
+ none: "none";
30
+ }>>;
31
+ standard: z.ZodOptional<z.ZodEnum<{
32
+ high: "high";
33
+ low: "low";
34
+ medium: "medium";
35
+ minimal: "minimal";
36
+ none: "none";
37
+ }>>;
38
+ light: z.ZodOptional<z.ZodEnum<{
39
+ high: "high";
40
+ low: "low";
41
+ medium: "medium";
42
+ minimal: "minimal";
43
+ none: "none";
44
+ }>>;
45
+ }, z.core.$strict>>;
46
+ readonly reviewers: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
47
+ enabled: z.ZodOptional<z.ZodBoolean>;
48
+ minTier: z.ZodOptional<z.ZodEnum<{
49
+ full: "full";
50
+ lite: "lite";
51
+ trivial: "trivial";
52
+ }>>;
53
+ models: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>, z.ZodTransform<string[], string | string[]>>>;
54
+ effort: z.ZodOptional<z.ZodEnum<{
55
+ high: "high";
56
+ low: "low";
57
+ medium: "medium";
58
+ minimal: "minimal";
59
+ none: "none";
60
+ }>>;
61
+ }, z.core.$strict>>>;
62
+ readonly roles: z.ZodDefault<z.ZodRecord<z.ZodEnum<{
63
+ helper: "helper";
64
+ judge: "judge";
65
+ verifier: "verifier";
66
+ }> & z.core.$partial, z.ZodObject<{
67
+ models: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>, z.ZodTransform<string[], string | string[]>>>;
68
+ effort: z.ZodOptional<z.ZodEnum<{
69
+ high: "high";
70
+ low: "low";
71
+ medium: "medium";
72
+ minimal: "minimal";
73
+ none: "none";
74
+ }>>;
75
+ }, z.core.$strict>>>;
76
+ };
77
+ declare const TOP: {
78
+ readonly models: z.ZodDefault<z.ZodObject<{
79
+ top: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>, z.ZodTransform<string[], string | string[]>>>;
80
+ standard: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>, z.ZodTransform<string[], string | string[]>>>;
81
+ light: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>, z.ZodTransform<string[], string | string[]>>>;
82
+ }, z.core.$strict>>;
83
+ readonly runtime: z.ZodEnum<{
84
+ direct: "direct";
85
+ opencode: "opencode";
86
+ }>;
87
+ };
88
+ type Parsed<T extends Record<string, z.ZodType>> = {
89
+ [K in keyof T]?: z.output<T[K]>;
90
+ };
91
+ export type AccountSettings = Parsed<typeof TOP> & Parsed<typeof AGENTS> & Parsed<typeof SETTINGS> & {
92
+ version: string | null;
93
+ };
94
+ /** The account's settings, and warnings naming what was refused or ignored. */
95
+ export declare function parseAccountSettings(data: unknown): {
96
+ settings: AccountSettings;
97
+ warnings: string[];
98
+ };
99
+ export {};
100
+ //# sourceMappingURL=account-settings.d.ts.map