@open-cr-agent/cli 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cloud/account-salt.d.ts +6 -0
- package/dist/cloud/account-salt.js +33 -0
- package/dist/{review → cloud}/account-settings.js +5 -4
- 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/{review/cloud-findings.js → cloud/findings.js} +10 -32
- 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/{review/cloud-providers.d.ts → cloud/providers.d.ts} +12 -6
- 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/{review/cloud-upload.js → cloud/upload.js} +74 -71
- 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/{plugins-command.d.ts → commands/plugins.d.ts} +3 -9
- package/dist/{plugins-command.js → commands/plugins.js} +29 -53
- 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 +2 -3
- package/dist/{review → commands/review}/render.js +1 -1
- 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} +8 -31
- 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/{review/account-plugins.d.ts → plugins/account.d.ts} +2 -2
- package/dist/{review/account-plugins.js → plugins/account.js} +7 -7
- package/dist/{review/plugins.d.ts → plugins/load.d.ts} +2 -2
- package/dist/{review/plugins.js → plugins/load.js} +6 -5
- package/dist/plugins/npm.d.ts +8 -0
- package/dist/plugins/npm.js +24 -0
- package/dist/{plugin-store.d.ts → plugins/store.d.ts} +1 -1
- package/dist/{plugin-store.js → plugins/store.js} +5 -4
- 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 +15 -12
- package/dist/session/store.d.ts +9 -0
- package/dist/session/store.js +31 -0
- package/package.json +17 -7
- package/dist/account-salt.d.ts +0 -7
- package/dist/account-salt.js +0 -43
- package/dist/cloud.d.ts +0 -34
- package/dist/cloud.js +0 -272
- package/dist/memory.d.ts +0 -4
- package/dist/metrics.d.ts +0 -47
- package/dist/review/cloud-findings.d.ts +0 -31
- package/dist/review/cloud-memory.d.ts +0 -6
- package/dist/review/cloud-memory.js +0 -58
- package/dist/review/cloud-providers.js +0 -60
- package/dist/review/cloud-review.d.ts +0 -19
- package/dist/review/cloud-review.js +0 -51
- package/dist/review/cloud-settings.d.ts +0 -16
- package/dist/review/cloud-settings.js +0 -118
- package/dist/review/cloud-upload.d.ts +0 -60
- package/dist/review/command.d.ts +0 -33
- package/dist/review/command.js +0 -293
- package/dist/review/config.js +0 -304
- package/dist/review/sarif.d.ts +0 -4
- package/dist/review/settings-sources.d.ts +0 -11
- package/dist/review/settings-sources.js +0 -58
- package/dist/review/target.d.ts +0 -18
- package/dist/review/target.js +0 -210
- /package/dist/{review → cloud}/account-settings.d.ts +0 -0
- /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
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
+
import { isNotFound } from "@open-cr-agent/core/internal";
|
|
3
4
|
import { z } from "zod";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
5
|
+
import { ConfigError } from "../config/cli-config.js";
|
|
6
|
+
import { ocraConfigDir } from "../config/user-dir.js";
|
|
6
7
|
// The plugins this machine allows its ocra Cloud account to name (ADR-0027):
|
|
7
8
|
// installed by `ocra plugins allow` into a directory of the user's own, never
|
|
8
9
|
// the reviewed checkout, and recorded with the integrity npm installed.
|
|
@@ -34,7 +35,7 @@ export async function readAllowed(dir) {
|
|
|
34
35
|
text = await readFile(join(dir, "plugins.json"), "utf8");
|
|
35
36
|
}
|
|
36
37
|
catch (error) {
|
|
37
|
-
if (error
|
|
38
|
+
if (isNotFound(error))
|
|
38
39
|
return [];
|
|
39
40
|
throw error;
|
|
40
41
|
}
|
|
@@ -81,4 +82,4 @@ export async function installedEntry(dir, name) {
|
|
|
81
82
|
...(typeof integrity === "string" ? { integrity } : {}),
|
|
82
83
|
};
|
|
83
84
|
}
|
|
84
|
-
//# sourceMappingURL=
|
|
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,16 +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 {
|
|
11
|
-
import { BUILTIN_PLUGINS,
|
|
12
|
-
import {
|
|
13
|
-
import { forTerminal } from "./
|
|
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";
|
|
14
17
|
import { VERSION } from "./version.js";
|
|
15
18
|
const USAGE = `Usage: ocra <command> [options]
|
|
16
19
|
|
|
@@ -87,7 +90,7 @@ export async function run(argv, out, err, deps = defaultDeps()) {
|
|
|
87
90
|
}
|
|
88
91
|
if (command === "login" || command === "logout" || command === "whoami") {
|
|
89
92
|
try {
|
|
90
|
-
return await
|
|
93
|
+
return await loginCommand(command, rest, out, err, defaultCloudDeps(deps.env));
|
|
91
94
|
}
|
|
92
95
|
catch (error) {
|
|
93
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
|
}
|
package/dist/account-salt.d.ts
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
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
|
package/dist/account-salt.js
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import type { Output } from "./review/progress.js";
|
|
2
|
-
/** ocra Cloud's address; OCRA_CLOUD_URL names another server. */
|
|
3
|
-
export declare const DEFAULT_CLOUD_URL = "https://app.ocracloud.com";
|
|
4
|
-
export declare const LOGIN_USAGE: string;
|
|
5
|
-
export type Credentials = {
|
|
6
|
-
server: string;
|
|
7
|
-
login: string;
|
|
8
|
-
access_token: string;
|
|
9
|
-
refresh_token: string;
|
|
10
|
-
/** Epoch milliseconds when the access token expires. */
|
|
11
|
-
expires_at: number;
|
|
12
|
-
};
|
|
13
|
-
export type CloudDeps = {
|
|
14
|
-
env: Readonly<Record<string, string | undefined>>;
|
|
15
|
-
fetch: typeof fetch;
|
|
16
|
-
now: () => number;
|
|
17
|
-
sleep: (ms: number) => Promise<void>;
|
|
18
|
-
openBrowser: (url: string) => void;
|
|
19
|
-
credentialsPath: string;
|
|
20
|
-
clientName: string;
|
|
21
|
-
};
|
|
22
|
-
export declare function defaultCloudDeps(env?: CloudDeps["env"]): CloudDeps;
|
|
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;
|
|
26
|
-
export declare function readCredentials(path: string): Promise<Credentials | undefined>;
|
|
27
|
-
/**
|
|
28
|
-
* The saved session with a live access token, refreshed (and saved) when
|
|
29
|
-
* it has expired or is about to; undefined when not signed in or the
|
|
30
|
-
* session was revoked.
|
|
31
|
-
*/
|
|
32
|
-
export declare function cloudSession(deps: CloudDeps, minValidityMs?: number): Promise<Credentials | undefined>;
|
|
33
|
-
export declare function cloudCommand(command: "login" | "logout" | "whoami", argv: string[], out: Output, err: Output, deps: CloudDeps): Promise<number>;
|
|
34
|
-
//# sourceMappingURL=cloud.d.ts.map
|
package/dist/cloud.js
DELETED
|
@@ -1,272 +0,0 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
|
-
import { chmod, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
|
-
import { homedir, hostname } from "node:os";
|
|
4
|
-
import { dirname, join } from "node:path";
|
|
5
|
-
import { parseArgs } from "node:util";
|
|
6
|
-
import { errorMessage } from "@open-cr-agent/core/internal";
|
|
7
|
-
import { fetchAccountSalt, saveAccountSalt } from "./account-salt.js";
|
|
8
|
-
import { UsageError } from "./review/args.js";
|
|
9
|
-
import { forTerminal } from "./review/terminal.js";
|
|
10
|
-
import { VERSION } from "./version.js";
|
|
11
|
-
// ocra Cloud sign-in (ADR-0024): `ocra login` runs the OAuth device flow
|
|
12
|
-
// (RFC 8628) against ocra Cloud, `ocra whoami` shows the account, and
|
|
13
|
-
// `ocra logout` ends the session. Nothing here runs unless one of these
|
|
14
|
-
// commands is used: a self-hosted user never sees a login prompt.
|
|
15
|
-
/** ocra Cloud's address; OCRA_CLOUD_URL names another server. */
|
|
16
|
-
export const DEFAULT_CLOUD_URL = "https://app.ocracloud.com";
|
|
17
|
-
export const LOGIN_USAGE = `Usage: ocra login [--no-browser]
|
|
18
|
-
ocra logout
|
|
19
|
-
ocra whoami
|
|
20
|
-
|
|
21
|
-
Sign in to ocra Cloud with GitHub, so reviews can use the model keys stored
|
|
22
|
-
there. ocra login shows a code, opens the browser to confirm it, and saves the
|
|
23
|
-
session to ${credentialsHint()} (readable only by you).
|
|
24
|
-
|
|
25
|
-
Environment:
|
|
26
|
-
OCRA_CLOUD=off Disable ocra Cloud: these commands refuse to run
|
|
27
|
-
OCRA_CLOUD_URL Another ocra Cloud server (default ${DEFAULT_CLOUD_URL})
|
|
28
|
-
`;
|
|
29
|
-
export function defaultCloudDeps(env = process.env) {
|
|
30
|
-
return {
|
|
31
|
-
env,
|
|
32
|
-
fetch: globalThis.fetch,
|
|
33
|
-
now: Date.now,
|
|
34
|
-
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
35
|
-
openBrowser,
|
|
36
|
-
credentialsPath: credentialsPath(env),
|
|
37
|
-
clientName: `${hostname()} (ocra ${VERSION})`,
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
function credentialsHint() {
|
|
41
|
-
return process.platform === "win32"
|
|
42
|
-
? "%APPDATA%\\ocra\\credentials.json"
|
|
43
|
-
: "~/.config/ocra/credentials.json";
|
|
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) {
|
|
50
|
-
const base = process.platform === "win32"
|
|
51
|
-
? (env.APPDATA ?? join(homedir(), "AppData", "Roaming"))
|
|
52
|
-
: (env.XDG_CONFIG_HOME ?? join(homedir(), ".config"));
|
|
53
|
-
return join(base, "ocra");
|
|
54
|
-
}
|
|
55
|
-
function cloudUrl(env) {
|
|
56
|
-
const named = env.OCRA_CLOUD_URL || DEFAULT_CLOUD_URL;
|
|
57
|
-
let url;
|
|
58
|
-
try {
|
|
59
|
-
url = new URL(named);
|
|
60
|
-
}
|
|
61
|
-
catch {
|
|
62
|
-
throw new UsageError(`OCRA_CLOUD_URL is not a URL: ${named}`);
|
|
63
|
-
}
|
|
64
|
-
if (url.protocol !== "https:" && url.hostname !== "localhost" && url.hostname !== "127.0.0.1") {
|
|
65
|
-
throw new UsageError(`OCRA_CLOUD_URL must use https: ${url}`);
|
|
66
|
-
}
|
|
67
|
-
return url.origin;
|
|
68
|
-
}
|
|
69
|
-
function openBrowser(url) {
|
|
70
|
-
const [cmd, args] = process.platform === "darwin"
|
|
71
|
-
? ["open", [url]]
|
|
72
|
-
: process.platform === "win32"
|
|
73
|
-
? ["cmd", ["/c", "start", "", url]]
|
|
74
|
-
: ["xdg-open", [url]];
|
|
75
|
-
try {
|
|
76
|
-
const child = spawn(cmd, args, { stdio: "ignore", detached: true });
|
|
77
|
-
child.on("error", () => { });
|
|
78
|
-
child.unref();
|
|
79
|
-
}
|
|
80
|
-
catch {
|
|
81
|
-
// The URL is printed as well; a missing opener is not an error.
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
export async function readCredentials(path) {
|
|
85
|
-
let text;
|
|
86
|
-
try {
|
|
87
|
-
text = await readFile(path, "utf8");
|
|
88
|
-
}
|
|
89
|
-
catch {
|
|
90
|
-
return undefined;
|
|
91
|
-
}
|
|
92
|
-
const c = JSON.parse(text);
|
|
93
|
-
if (!c.server || !c.access_token || !c.refresh_token || typeof c.expires_at !== "number") {
|
|
94
|
-
return undefined;
|
|
95
|
-
}
|
|
96
|
-
return c;
|
|
97
|
-
}
|
|
98
|
-
async function writeCredentials(path, c) {
|
|
99
|
-
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
100
|
-
await writeFile(path, `${JSON.stringify(c, null, 2)}\n`, { mode: 0o600 });
|
|
101
|
-
// writeFile keeps the mode of a file that already exists.
|
|
102
|
-
await chmod(path, 0o600);
|
|
103
|
-
}
|
|
104
|
-
async function postJson(deps, url, body, token) {
|
|
105
|
-
const res = await deps.fetch(url, {
|
|
106
|
-
method: "POST",
|
|
107
|
-
headers: {
|
|
108
|
-
"content-type": "application/json",
|
|
109
|
-
"user-agent": `ocra/${VERSION}`,
|
|
110
|
-
...(token ? { authorization: `Bearer ${token}` } : {}),
|
|
111
|
-
},
|
|
112
|
-
body: JSON.stringify(body),
|
|
113
|
-
signal: AbortSignal.timeout(30_000),
|
|
114
|
-
});
|
|
115
|
-
return {
|
|
116
|
-
status: res.status,
|
|
117
|
-
body: (await res.json().catch(() => ({}))),
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
async function fetchMe(deps, server, token) {
|
|
121
|
-
const res = await deps.fetch(`${server}/api/me`, {
|
|
122
|
-
headers: { authorization: `Bearer ${token}`, "user-agent": `ocra/${VERSION}` },
|
|
123
|
-
signal: AbortSignal.timeout(30_000),
|
|
124
|
-
});
|
|
125
|
-
if (res.status === 401)
|
|
126
|
-
return undefined;
|
|
127
|
-
if (!res.ok)
|
|
128
|
-
throw new Error(`ocra Cloud answered ${res.status}`);
|
|
129
|
-
return (await res.json());
|
|
130
|
-
}
|
|
131
|
-
/**
|
|
132
|
-
* The saved session with a live access token, refreshed (and saved) when
|
|
133
|
-
* it has expired or is about to; undefined when not signed in or the
|
|
134
|
-
* session was revoked.
|
|
135
|
-
*/
|
|
136
|
-
export async function cloudSession(deps, minValidityMs = 60_000) {
|
|
137
|
-
const saved = await readCredentials(deps.credentialsPath);
|
|
138
|
-
if (!saved)
|
|
139
|
-
return undefined;
|
|
140
|
-
if (saved.expires_at - deps.now() > minValidityMs)
|
|
141
|
-
return saved;
|
|
142
|
-
const { status, body } = await postJson(deps, `${saved.server}/api/device/refresh`, {
|
|
143
|
-
refresh_token: saved.refresh_token,
|
|
144
|
-
});
|
|
145
|
-
const t = body;
|
|
146
|
-
if (status !== 200 || !t.access_token || !t.refresh_token || !t.expires_in)
|
|
147
|
-
return undefined;
|
|
148
|
-
const next = {
|
|
149
|
-
...saved,
|
|
150
|
-
access_token: t.access_token,
|
|
151
|
-
refresh_token: t.refresh_token,
|
|
152
|
-
expires_at: deps.now() + t.expires_in * 1000,
|
|
153
|
-
};
|
|
154
|
-
await writeCredentials(deps.credentialsPath, next);
|
|
155
|
-
return next;
|
|
156
|
-
}
|
|
157
|
-
export async function cloudCommand(command, argv, out, err, deps) {
|
|
158
|
-
const { values } = parseArgs({
|
|
159
|
-
args: argv,
|
|
160
|
-
strict: true,
|
|
161
|
-
options: { "no-browser": { type: "boolean" }, help: { type: "boolean", short: "h" } },
|
|
162
|
-
});
|
|
163
|
-
if (values.help) {
|
|
164
|
-
out.write(LOGIN_USAGE);
|
|
165
|
-
return 0;
|
|
166
|
-
}
|
|
167
|
-
if (deps.env.OCRA_CLOUD === "off") {
|
|
168
|
-
err.write("ocra Cloud is off (OCRA_CLOUD=off).\n");
|
|
169
|
-
return 2;
|
|
170
|
-
}
|
|
171
|
-
if (command === "whoami")
|
|
172
|
-
return whoami(out, err, deps);
|
|
173
|
-
if (command === "logout")
|
|
174
|
-
return logout(out, deps);
|
|
175
|
-
return login(out, err, deps, !values["no-browser"]);
|
|
176
|
-
}
|
|
177
|
-
async function whoami(out, err, deps) {
|
|
178
|
-
const session = await cloudSession(deps);
|
|
179
|
-
if (!session) {
|
|
180
|
-
err.write("Not signed in. Run ocra login.\n");
|
|
181
|
-
return 1;
|
|
182
|
-
}
|
|
183
|
-
const me = await fetchMe(deps, session.server, session.access_token);
|
|
184
|
-
if (!me) {
|
|
185
|
-
err.write("The saved session was revoked. Run ocra login.\n");
|
|
186
|
-
return 1;
|
|
187
|
-
}
|
|
188
|
-
out.write(forTerminal(`${me.login} on ${session.server}\n`));
|
|
189
|
-
return 0;
|
|
190
|
-
}
|
|
191
|
-
async function logout(out, deps) {
|
|
192
|
-
const saved = await readCredentials(deps.credentialsPath);
|
|
193
|
-
if (saved) {
|
|
194
|
-
// Ends the session on the server too; the local file goes either way.
|
|
195
|
-
const session = await cloudSession(deps).catch(() => undefined);
|
|
196
|
-
if (session) {
|
|
197
|
-
await postJson(deps, `${session.server}/api/auth/logout`, {}, session.access_token).catch(() => { });
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
await rm(deps.credentialsPath, { force: true });
|
|
201
|
-
await saveAccountSalt(deps.credentialsPath, null);
|
|
202
|
-
out.write(saved ? "Signed out.\n" : "Not signed in.\n");
|
|
203
|
-
return 0;
|
|
204
|
-
}
|
|
205
|
-
async function login(out, err, deps, browser) {
|
|
206
|
-
const server = cloudUrl(deps.env);
|
|
207
|
-
const start = await postJson(deps, `${server}/api/device/code`, { client_name: deps.clientName });
|
|
208
|
-
const code = start.body;
|
|
209
|
-
if (start.status !== 200 ||
|
|
210
|
-
!code.device_code ||
|
|
211
|
-
!code.user_code ||
|
|
212
|
-
!code.verification_uri_complete) {
|
|
213
|
-
err.write(`ocra Cloud did not start a login (HTTP ${start.status}). Try again in a minute.\n`);
|
|
214
|
-
return 2;
|
|
215
|
-
}
|
|
216
|
-
out.write(forTerminal(`Confirm this code in your browser: ${code.user_code}\n` +
|
|
217
|
-
` ${code.verification_uri_complete}\n` +
|
|
218
|
-
`(or open ${code.verification_uri} and type it)\n\nWaiting for approval...\n`));
|
|
219
|
-
if (browser)
|
|
220
|
-
deps.openBrowser(code.verification_uri_complete);
|
|
221
|
-
let interval = (code.interval ?? 5) * 1000;
|
|
222
|
-
const deadline = deps.now() + (code.expires_in ?? 600) * 1000;
|
|
223
|
-
while (deps.now() < deadline) {
|
|
224
|
-
await deps.sleep(interval);
|
|
225
|
-
let answer;
|
|
226
|
-
try {
|
|
227
|
-
answer = await postJson(deps, `${server}/api/device/token`, {
|
|
228
|
-
device_code: code.device_code,
|
|
229
|
-
});
|
|
230
|
-
}
|
|
231
|
-
catch (error) {
|
|
232
|
-
// A dropped connection is retried on the next tick.
|
|
233
|
-
err.write(forTerminal(`(${errorMessage(error)}; retrying)\n`));
|
|
234
|
-
continue;
|
|
235
|
-
}
|
|
236
|
-
const t = answer.body;
|
|
237
|
-
if (answer.status === 200 && t.access_token && t.refresh_token && t.expires_in) {
|
|
238
|
-
const me = await fetchMe(deps, server, t.access_token);
|
|
239
|
-
const credentials = {
|
|
240
|
-
server,
|
|
241
|
-
login: me?.login ?? "",
|
|
242
|
-
access_token: t.access_token,
|
|
243
|
-
refresh_token: t.refresh_token,
|
|
244
|
-
expires_at: deps.now() + t.expires_in * 1000,
|
|
245
|
-
};
|
|
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(() => { });
|
|
251
|
-
out.write(forTerminal(`Signed in as ${credentials.login}.\n`));
|
|
252
|
-
return 0;
|
|
253
|
-
}
|
|
254
|
-
if (t.error === "authorization_pending")
|
|
255
|
-
continue;
|
|
256
|
-
if (t.error === "slow_down") {
|
|
257
|
-
interval = typeof t.interval === "number" ? t.interval * 1000 : interval + 5000;
|
|
258
|
-
continue;
|
|
259
|
-
}
|
|
260
|
-
if (t.error === "access_denied") {
|
|
261
|
-
err.write("Login denied in the browser.\n");
|
|
262
|
-
return 1;
|
|
263
|
-
}
|
|
264
|
-
if (t.error === "expired_token")
|
|
265
|
-
break;
|
|
266
|
-
err.write(forTerminal(`Login failed: ${t.error ?? `HTTP ${answer.status}`}\n`));
|
|
267
|
-
return 2;
|
|
268
|
-
}
|
|
269
|
-
err.write("The code expired before it was confirmed. Run ocra login again.\n");
|
|
270
|
-
return 1;
|
|
271
|
-
}
|
|
272
|
-
//# sourceMappingURL=cloud.js.map
|
package/dist/memory.d.ts
DELETED
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
import type { Output } from "./review/progress.js";
|
|
2
|
-
export declare const MEMORY_USAGE = "Usage: ocra memory <command>\n\nRemember findings the team accepts, so ocra stops reporting them.\nEntries live in .ocra/memory.json; commit it to share them.\n\nCommands:\n list Show remembered findings\n add <id> --reason <text> Remember a finding from the last review\n [--session <dir>] (default: the newest session in .ocra/sessions)\n";
|
|
3
|
-
export declare function memoryCommand(argv: string[], out: Output, cwd: string, now?: Date): Promise<number>;
|
|
4
|
-
//# sourceMappingURL=memory.d.ts.map
|