pulls.review 0.1.0 → 0.2.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/README.md ADDED
@@ -0,0 +1,16 @@
1
+ # pulls.review
2
+
3
+ Review local git changes in the browser, grouped and summarized like a pull request on [pulls.review](https://pulls.review).
4
+
5
+ ```sh
6
+ npx pulls.review # the current branch against the default branch (or a ref picker on main)
7
+ npx pulls.review main...feat # what feat adds since it forked from main
8
+ npx pulls.review v1..v2 # the tree diff between two revisions
9
+ npx pulls.review feat # a branch against the default branch
10
+ npx pulls.review HEAD # one commit against its parent
11
+ npx pulls.review --worktree # uncommitted changes against HEAD, untracked files included
12
+ ```
13
+
14
+ Run it inside a git repository. It serves on `localhost` through [devframe](https://github.com/devframes/devframe) and opens the page for the argument; any other branch, range or commit can be picked from `/`. The tab is trusted with a one-time code printed in the terminal. Reviewed marks and AI results are cached in `.git/pulls-review`.
15
+
16
+ To analyze a GitHub pull request in CI, use [`@pulls.review/actions`](../actions).
package/bin.mjs ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import './dist/index.mjs'
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "pulls.review",
3
3
  "type": "module",
4
- "version": "0.1.0",
5
- "description": "Analyze a GitHub pull request with pulls.review and post the grouped summary as a PR comment.",
4
+ "version": "0.2.0",
5
+ "description": "Review local git changes - the working tree, a commit or any range - grouped and summarized in the browser.",
6
6
  "author": "Anthony Fu <hi@antfu.me>",
7
7
  "license": "MIT",
8
8
  "funding": "https://github.com/sponsors/antfu",
@@ -16,30 +16,38 @@
16
16
  "keywords": [
17
17
  "pull-request",
18
18
  "code-review",
19
- "github-actions",
20
- "llm"
19
+ "git",
20
+ "devframe"
21
21
  ],
22
+ "exports": {
23
+ "./devframe": "./dist/devframe.mjs"
24
+ },
22
25
  "bin": {
23
- "pulls.review": "./dist/index.mjs",
24
- "pulls-review": "./dist/index.mjs"
26
+ "pulls.review": "./bin.mjs",
27
+ "pulls-review": "./bin.mjs"
25
28
  },
26
29
  "files": [
30
+ "bin.mjs",
27
31
  "dist"
28
32
  ],
29
33
  "engines": {
30
34
  "node": ">=20"
31
35
  },
32
36
  "dependencies": {
33
- "@pulls.review/core": "^0.1.0",
34
- "nostics": "^1.3.0"
37
+ "@pulls.review/core": "^0.2.0",
38
+ "devframe": "^1.2.0",
39
+ "h3": "^2.0.1-rc.32",
40
+ "unstorage": "^1.17.5"
35
41
  },
36
42
  "devDependencies": {
37
43
  "@types/node": "^26.6.4",
44
+ "tsdown": "^0.23.0",
38
45
  "typescript": "^6.0.3",
39
46
  "vitest": "^5.0.3"
40
47
  },
41
48
  "scripts": {
42
- "build": "tsdown",
49
+ "build": "tsdown && pnpm -F @pulls.review/app run build:local",
50
+ "dev": "node bin.mjs",
43
51
  "typecheck": "tsc --noEmit",
44
52
  "test": "vitest run"
45
53
  }
package/dist/index.mjs DELETED
@@ -1,149 +0,0 @@
1
- #!/usr/bin/env node
2
- import { readFileSync } from "node:fs";
3
- import process from "node:process";
4
- import { parseArgs } from "node:util";
5
- import { githubTokenFromEnv, llmSettingsFromEnv } from "@pulls.review/core/env";
6
- import { runLlmAnalysis } from "@pulls.review/core/llm";
7
- import { Diagnostic, formatDiagnostic } from "nostics";
8
- import { DEFAULT_LOCALE, isLocale } from "@pulls.review/core/locales";
9
- import { resolveModel } from "@pulls.review/core/analyze";
10
- import { diagnostics } from "@pulls.review/core/diagnostics";
11
- import { createGithubPullRequestSource } from "@pulls.review/core/github";
12
- import { staticCredentials } from "@pulls.review/core/types";
13
- //#region src/config.ts
14
- /** The first value that is set and not blank: an Action passes unset inputs as `''`. */
15
- function first(...values) {
16
- return values.find((value) => value);
17
- }
18
- function resolveGithubToken(env) {
19
- const token = githubTokenFromEnv(env);
20
- if (!token) throw new Error("A GitHub token is required: set GITHUB_TOKEN (or PULLS_REVIEW_GITHUB_TOKEN).");
21
- return token;
22
- }
23
- function resolveLocale(env, flags = {}) {
24
- const requested = first(flags.locale, env.PULLS_REVIEW_LOCALE);
25
- if (requested === void 0) return DEFAULT_LOCALE;
26
- if (!isLocale(requested)) throw new Error(`Unsupported locale "${requested}".`);
27
- return requested;
28
- }
29
- /**
30
- * `owner/repo#123` or a github.com PR URL; in a workflow run, the PR the event is about.
31
- */
32
- function resolveTarget(arg, env, event) {
33
- if (arg) {
34
- const match = arg.match(/^(?:https:\/\/github\.com\/)?([^/\s]+)\/([^/#\s]+)(?:#|\/pull\/)(\d+)\/?$/);
35
- if (!match) throw new Error(`Cannot read a pull request from "${arg}": expected owner/repo#123 or a github.com pull request URL.`);
36
- return {
37
- owner: match[1],
38
- repo: match[2],
39
- number: match[3]
40
- };
41
- }
42
- const [owner, repo] = env.GITHUB_REPOSITORY?.split("/") ?? [];
43
- const number = event?.pull_request?.number;
44
- if (!owner || !repo || number === void 0) throw new Error("No pull request given. Pass owner/repo#123, or run on a pull_request event in GitHub Actions.");
45
- return {
46
- owner,
47
- repo,
48
- number: String(number)
49
- };
50
- }
51
- //#endregion
52
- //#region src/run.ts
53
- /** The author GitHub shows for a workflow's `GITHUB_TOKEN`, which cannot look itself up at `/user`. */
54
- const ACTIONS_BOT_LOGIN = "github-actions[bot]";
55
- function describeProgress(progress) {
56
- switch (progress.kind) {
57
- case "thinking": return `[${progress.step}] thinking`;
58
- case "reading": return `[${progress.step}] reading ${progress.paths.length} file${progress.paths.length === 1 ? "" : "s"}`;
59
- case "organizing": return `[${progress.step}] organizing groups`;
60
- }
61
- }
62
- /**
63
- * Fetches the PR, analyzes it and upserts the shared-analysis comment, exactly like the
64
- * site's "share as comment" - one comment per author, found by its marker. A comment
65
- * already carrying the PR's current head sha is left alone, so re-runs on an unchanged
66
- * PR cost no model call.
67
- */
68
- async function run({ pr, githubToken, llm, locale, log }, analyze) {
69
- const resolved = resolveModel(llm);
70
- if (!resolved) throw diagnostics.llmNotConfigured();
71
- const source = createGithubPullRequestSource(pr, staticCredentials(githubToken));
72
- const [diff, login, comments] = await Promise.all([
73
- source.fetch(),
74
- source.viewer().then((viewer) => viewer?.login ?? ACTIONS_BOT_LOGIN, () => ACTIONS_BOT_LOGIN),
75
- source.sharing.list()
76
- ]);
77
- const headSha = diff.head?.sha ?? "";
78
- const existing = comments.find((comment) => comment.login === login);
79
- if (existing && existing.analysis.headSha === headSha) return {
80
- status: "up-to-date",
81
- url: existing.url
82
- };
83
- log(`Analyzing ${pr.owner}/${pr.repo}#${pr.number} (${diff.files.length} files) with ${resolved.model.provider}/${resolved.model.id}`);
84
- const { result } = await analyze(diff, resolved, locale, { onProgress: (progress) => log(describeProgress(progress)) });
85
- return {
86
- status: "posted",
87
- url: (await source.sharing.upsert(login, {
88
- headSha,
89
- result
90
- }, existing)).url
91
- };
92
- }
93
- //#endregion
94
- //#region src/index.ts
95
- const HELP = `Usage: pulls.review [owner/repo#123 | https://github.com/owner/repo/pull/123] [options]
96
-
97
- Analyzes the pull request with an LLM and posts (or updates) the grouped summary as
98
- a PR comment that pulls.review picks up. Without a target, reads the pull_request
99
- event of the running GitHub Actions job.
100
-
101
- Options:
102
- --provider <name> gateway | anthropic | openai-compatible (PULLS_REVIEW_PROVIDER)
103
- --model <id> model id for the provider (PULLS_REVIEW_MODEL)
104
- --locale <tag> language of the summaries, default en (PULLS_REVIEW_LOCALE)
105
- -h, --help
106
-
107
- Environment:
108
- GITHUB_TOKEN token with pull-requests: write (PULLS_REVIEW_GITHUB_TOKEN)
109
- AI_GATEWAY_API_KEY Vercel AI Gateway key (PULLS_REVIEW_API_KEY)
110
- ANTHROPIC_API_KEY Anthropic key (PULLS_REVIEW_API_KEY)
111
- OPENAI_API_KEY OpenAI-compatible key (PULLS_REVIEW_API_KEY)
112
- OPENAI_BASE_URL OpenAI-compatible endpoint (PULLS_REVIEW_BASE_URL)
113
- `;
114
- function readEvent(path) {
115
- return path ? JSON.parse(readFileSync(path, "utf-8")) : void 0;
116
- }
117
- async function main() {
118
- const { values: flags, positionals } = parseArgs({
119
- options: {
120
- provider: { type: "string" },
121
- model: { type: "string" },
122
- locale: { type: "string" },
123
- help: {
124
- type: "boolean",
125
- short: "h"
126
- }
127
- },
128
- allowPositionals: true
129
- });
130
- if (flags.help) {
131
- console.log(HELP);
132
- return;
133
- }
134
- const env = process.env;
135
- const outcome = await run({
136
- pr: resolveTarget(positionals[0], env, readEvent(env.GITHUB_EVENT_PATH)),
137
- githubToken: resolveGithubToken(env),
138
- llm: llmSettingsFromEnv(env, flags),
139
- locale: resolveLocale(env, flags),
140
- log: (line) => console.log(line)
141
- }, runLlmAnalysis);
142
- console.log(outcome.status === "up-to-date" ? `Comment already covers the current head: ${outcome.url}` : `Posted ${outcome.url}`);
143
- }
144
- main().catch((error) => {
145
- console.error(error instanceof Diagnostic ? formatDiagnostic(error) : error instanceof Error ? error.message : String(error));
146
- process.exitCode = 1;
147
- });
148
- //#endregion
149
- export {};