@muhammad_zihad/prless 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Muhammad AR Zihad
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,97 @@
1
+ # PRless
2
+
3
+ A local, GitHub-style code review tool with an agent-agnostic AI handoff. Review a local
4
+ git diff in the browser, leave inline comments anchored to specific lines, then export the
5
+ comments to a file that any CLI coding agent (Claude Code, Codex, …) can read and act on.
6
+
7
+ ## Install
8
+
9
+ Requires **Node.js 18+**. One command on every platform — macOS, Linux, and Windows:
10
+
11
+ ```bash
12
+ npm install -g @muhammad_zihad/prless
13
+ ```
14
+
15
+ That puts a `prless` command on your PATH (on Windows, npm generates the `.cmd`/PowerShell
16
+ shims automatically). Prefer not to install globally? Run it on demand with `npx`:
17
+
18
+ ```bash
19
+ npx @muhammad_zihad/prless open .
20
+ ```
21
+
22
+ ### macOS / Linux via Homebrew
23
+
24
+ ```bash
25
+ brew install muhammadZihad/tap/prless
26
+ ```
27
+
28
+ `brew` installs Node for you. See [packaging/README.md](packaging/README.md) for publishing
29
+ the npm package and setting up the tap.
30
+
31
+ ### From source
32
+
33
+ ```bash
34
+ git clone https://github.com/muhammadZihad/prless.git && cd prless
35
+ npm install # builds automatically via the prepare script
36
+ npm link # makes `prless` available globally
37
+ ```
38
+
39
+ ## Use
40
+
41
+ ```bash
42
+ prless open . # review the git repo in the current directory
43
+ prless open ~/projects/my-app # review a repo by path
44
+ prless open ./my-app --port 4200
45
+ prless help # show usage
46
+ ```
47
+
48
+ Works on macOS, Linux, and Windows. This starts a local server and opens the UI in your
49
+ browser. You can:
50
+
51
+ - **Pick a diff source** — working tree vs HEAD, staged vs HEAD, or compare two branches.
52
+ - **Comment inline** — click a line's gutter to leave a comment (⌘/Ctrl+Enter to submit).
53
+ - **Resolve / delete** comments; toggle split / unified view.
54
+ - **Switch the app theme** — light or dark (defaults to your OS setting, remembered per browser).
55
+ - **Choose a syntax theme** — Auto, GitHub Light/Dark, One Dark, Dracula, Nord, Monokai,
56
+ Solarized Light. The diff renders as a self-contained editor surface, so any code theme
57
+ looks right regardless of the app theme.
58
+ - **Export for AI** — writes open comments to `.prless/review.md`.
59
+
60
+ Typography uses Geist (UI) and JetBrains Mono (code), bundled locally so it works offline.
61
+
62
+ Comments persist in `.prless/comments.json` in the repo (gitignore the `.prless/` folder).
63
+
64
+ ## Hand off to an agent
65
+
66
+ After exporting, run your agent in the repo and point it at the file:
67
+
68
+ ```bash
69
+ claude # then: "address the comments in .prless/review.md"
70
+ # or
71
+ codex "address the comments in .prless/review.md"
72
+ ```
73
+
74
+ `review.md` lists each open comment grouped by file, with the target line, side, and the
75
+ requested change — no API keys or MCP required.
76
+
77
+ ## Develop
78
+
79
+ ```bash
80
+ npm run dev # Vite UI on :5174 (proxying /api) + API on :4100 with reload
81
+ npm test # vitest (git, comments, export)
82
+ npm run typecheck
83
+ ```
84
+
85
+ ## Layout
86
+
87
+ ```
88
+ src/
89
+ shared/types.ts # types shared by server + web
90
+ server/ # Fastify API + CLI (git diff, comment store, review.md export)
91
+ web/ # React + Vite UI (react-diff-view)
92
+ ```
93
+
94
+ ## Scope
95
+
96
+ Local, single-user. No GitHub PR ingestion, no auth/DB, no MCP — handoff is the exported
97
+ file. These are deliberate non-goals and can be layered on later.
@@ -0,0 +1,101 @@
1
+ #!/usr/bin/env node
2
+ import path from 'node:path';
3
+ import open from 'open';
4
+ import { buildServer } from './index.js';
5
+ import { assertGitRepo, createGit, GitError } from './git.js';
6
+ const USAGE = `prless — local code review with agent-agnostic AI handoff
7
+
8
+ Usage:
9
+ prless open <repository> Review a git repository at the given path
10
+ prless open . Review the git repository in the current directory
11
+ prless help Show this help
12
+
13
+ Options (for "open"):
14
+ --port <n> Port to serve on (default 4100, or $PRLESS_PORT)
15
+ --no-open Do not launch a browser automatically
16
+ --dev Internal: run the API only (UI served by the Vite dev server)
17
+
18
+ Examples:
19
+ prless open .
20
+ prless open ~/projects/my-app
21
+ prless open ./my-app --port 4200
22
+ `;
23
+ function parseOpenArgs(rest) {
24
+ let repoArg;
25
+ let dev = false;
26
+ let noOpen = false;
27
+ let port = Number(process.env.PRLESS_PORT ?? 4100);
28
+ for (let i = 0; i < rest.length; i++) {
29
+ const a = rest[i];
30
+ if (a === '--dev')
31
+ dev = true;
32
+ else if (a === '--no-open')
33
+ noOpen = true;
34
+ else if (a === '--port')
35
+ port = Number(rest[++i]);
36
+ else if (!a.startsWith('-') && repoArg === undefined)
37
+ repoArg = a;
38
+ }
39
+ if (repoArg === undefined) {
40
+ console.error('prless: "open" requires a repository path (use "." for the current directory).\n');
41
+ console.error(USAGE);
42
+ process.exit(1);
43
+ }
44
+ return {
45
+ // path.resolve normalises against cwd on every platform (mac/linux/windows).
46
+ repoRoot: path.resolve(repoArg),
47
+ dev,
48
+ noOpen,
49
+ port,
50
+ };
51
+ }
52
+ async function runOpen(opts) {
53
+ const git = createGit(opts.repoRoot);
54
+ try {
55
+ await assertGitRepo(git);
56
+ }
57
+ catch (err) {
58
+ if (err instanceof GitError) {
59
+ console.error(`prless: ${opts.repoRoot} is not a git repository.`);
60
+ process.exit(1);
61
+ }
62
+ throw err;
63
+ }
64
+ const app = await buildServer({ repoRoot: opts.repoRoot, dev: opts.dev });
65
+ await app.listen({ port: opts.port, host: '127.0.0.1' });
66
+ const url = opts.dev ? `http://localhost:5174` : `http://localhost:${opts.port}`;
67
+ console.log(`prless: reviewing ${opts.repoRoot}`);
68
+ console.log(`prless: serving ${opts.dev ? 'API' : 'UI'} on http://localhost:${opts.port}`);
69
+ if (opts.dev)
70
+ console.log(`prless: open the Vite dev server at ${url}`);
71
+ if (!opts.dev && !opts.noOpen) {
72
+ await open(url).catch(() => {
73
+ // Launching a browser is best-effort and varies by platform.
74
+ console.log(`prless: open ${url} in your browser`);
75
+ });
76
+ }
77
+ }
78
+ async function main() {
79
+ const argv = process.argv.slice(2);
80
+ const command = argv[0];
81
+ const rest = argv.slice(1);
82
+ switch (command) {
83
+ case 'open':
84
+ await runOpen(parseOpenArgs(rest));
85
+ break;
86
+ case 'help':
87
+ case '--help':
88
+ case '-h':
89
+ case undefined:
90
+ console.log(USAGE);
91
+ break;
92
+ default:
93
+ console.error(`prless: unknown command "${command}"\n`);
94
+ console.error(USAGE);
95
+ process.exit(1);
96
+ }
97
+ }
98
+ main().catch((err) => {
99
+ console.error(err);
100
+ process.exit(1);
101
+ });
@@ -0,0 +1,73 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ export class CommentStore {
5
+ dir;
6
+ file;
7
+ constructor(repoRoot) {
8
+ this.dir = path.join(repoRoot, '.prless');
9
+ this.file = path.join(this.dir, 'comments.json');
10
+ }
11
+ get filePath() {
12
+ return this.file;
13
+ }
14
+ async list() {
15
+ try {
16
+ const raw = await readFile(this.file, 'utf8');
17
+ const parsed = JSON.parse(raw);
18
+ return Array.isArray(parsed) ? parsed : [];
19
+ }
20
+ catch (err) {
21
+ if (err.code === 'ENOENT') {
22
+ return [];
23
+ }
24
+ throw err;
25
+ }
26
+ }
27
+ async add(input) {
28
+ const comments = await this.list();
29
+ const now = new Date().toISOString();
30
+ const comment = {
31
+ id: randomUUID(),
32
+ file: input.file,
33
+ line: input.line,
34
+ side: input.side,
35
+ snippet: input.snippet ?? '',
36
+ body: input.body,
37
+ status: 'open',
38
+ createdAt: now,
39
+ updatedAt: now,
40
+ };
41
+ comments.push(comment);
42
+ await this.save(comments);
43
+ return comment;
44
+ }
45
+ async patch(id, patch) {
46
+ const comments = await this.list();
47
+ const idx = comments.findIndex((c) => c.id === id);
48
+ if (idx === -1)
49
+ return null;
50
+ const existing = comments[idx];
51
+ const updated = {
52
+ ...existing,
53
+ body: patch.body ?? existing.body,
54
+ status: patch.status ?? existing.status,
55
+ updatedAt: new Date().toISOString(),
56
+ };
57
+ comments[idx] = updated;
58
+ await this.save(comments);
59
+ return updated;
60
+ }
61
+ async remove(id) {
62
+ const comments = await this.list();
63
+ const next = comments.filter((c) => c.id !== id);
64
+ if (next.length === comments.length)
65
+ return false;
66
+ await this.save(next);
67
+ return true;
68
+ }
69
+ async save(comments) {
70
+ await mkdir(this.dir, { recursive: true });
71
+ await writeFile(this.file, JSON.stringify(comments, null, 2) + '\n', 'utf8');
72
+ }
73
+ }
@@ -0,0 +1,42 @@
1
+ import { mkdtemp, rm } from 'node:fs/promises';
2
+ import { tmpdir } from 'node:os';
3
+ import path from 'node:path';
4
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
5
+ import { CommentStore } from './comments.js';
6
+ describe('CommentStore', () => {
7
+ let dir;
8
+ let store;
9
+ beforeEach(async () => {
10
+ dir = await mkdtemp(path.join(tmpdir(), 'prless-'));
11
+ store = new CommentStore(dir);
12
+ });
13
+ afterEach(async () => {
14
+ await rm(dir, { recursive: true, force: true });
15
+ });
16
+ it('returns an empty list before anything is written', async () => {
17
+ expect(await store.list()).toEqual([]);
18
+ });
19
+ it('adds, persists and round-trips a comment', async () => {
20
+ const created = await store.add({ file: 'a.ts', line: 3, side: 'new', body: 'hi', snippet: 'x' });
21
+ expect(created.id).toBeTruthy();
22
+ expect(created.status).toBe('open');
23
+ const reread = await new CommentStore(dir).list();
24
+ expect(reread).toHaveLength(1);
25
+ expect(reread[0].body).toBe('hi');
26
+ });
27
+ it('patches status and body', async () => {
28
+ const created = await store.add({ file: 'a.ts', line: 1, side: 'new', body: 'orig' });
29
+ const patched = await store.patch(created.id, { status: 'resolved', body: 'edited' });
30
+ expect(patched?.status).toBe('resolved');
31
+ expect(patched?.body).toBe('edited');
32
+ });
33
+ it('returns null when patching a missing id', async () => {
34
+ expect(await store.patch('nope', { status: 'resolved' })).toBeNull();
35
+ });
36
+ it('removes a comment', async () => {
37
+ const created = await store.add({ file: 'a.ts', line: 1, side: 'new', body: 'x' });
38
+ expect(await store.remove(created.id)).toBe(true);
39
+ expect(await store.list()).toEqual([]);
40
+ expect(await store.remove(created.id)).toBe(false);
41
+ });
42
+ });
@@ -0,0 +1,47 @@
1
+ import { mkdir, writeFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ const HEADER = `# Code Review — action required
4
+
5
+ You are addressing review comments on this repository. For each comment below,
6
+ make the requested change in the referenced file, then briefly note what you changed.
7
+ Line numbers refer to the indicated side of the diff ("new" = current file contents,
8
+ "old" = the version before the change).
9
+ `;
10
+ /**
11
+ * Render open comments into a deterministic, agent-friendly markdown document,
12
+ * grouped by file and ordered by line. Pure function for easy testing.
13
+ */
14
+ export function renderReviewMarkdown(comments) {
15
+ const open = comments.filter((c) => c.status === 'open');
16
+ if (open.length === 0) {
17
+ return `${HEADER}\n_No open comments._\n`;
18
+ }
19
+ const byFile = new Map();
20
+ for (const c of open) {
21
+ const bucket = byFile.get(c.file) ?? [];
22
+ bucket.push(c);
23
+ byFile.set(c.file, bucket);
24
+ }
25
+ const sections = [];
26
+ for (const file of [...byFile.keys()].sort()) {
27
+ const lines = byFile
28
+ .get(file)
29
+ .slice()
30
+ .sort((a, b) => a.line - b.line || a.createdAt.localeCompare(b.createdAt));
31
+ const items = lines.map((c) => {
32
+ const snippet = c.snippet.trim();
33
+ const context = snippet ? ` \`${snippet}\`` : '';
34
+ const body = c.body.trim().replace(/\n/g, '\n ');
35
+ return `- **Line ${c.line} (${c.side}):**${context}\n → ${body}`;
36
+ });
37
+ sections.push(`## ${file}\n\n${items.join('\n\n')}`);
38
+ }
39
+ return `${HEADER}\n${sections.join('\n\n')}\n`;
40
+ }
41
+ export async function exportReview(repoRoot, comments) {
42
+ const dir = path.join(repoRoot, '.prless');
43
+ const file = path.join(dir, 'review.md');
44
+ await mkdir(dir, { recursive: true });
45
+ await writeFile(file, renderReviewMarkdown(comments), 'utf8');
46
+ return { path: file, count: comments.filter((c) => c.status === 'open').length };
47
+ }
@@ -0,0 +1,43 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { renderReviewMarkdown } from './export.js';
3
+ function comment(overrides) {
4
+ return {
5
+ id: 'id',
6
+ file: 'src/a.ts',
7
+ line: 1,
8
+ side: 'new',
9
+ snippet: '',
10
+ body: 'do something',
11
+ status: 'open',
12
+ createdAt: '2026-01-01T00:00:00.000Z',
13
+ updatedAt: '2026-01-01T00:00:00.000Z',
14
+ ...overrides,
15
+ };
16
+ }
17
+ describe('renderReviewMarkdown', () => {
18
+ it('renders an empty placeholder when there are no open comments', () => {
19
+ const md = renderReviewMarkdown([comment({ status: 'resolved' })]);
20
+ expect(md).toContain('_No open comments._');
21
+ });
22
+ it('groups open comments by file, sorted by line, with line context', () => {
23
+ const md = renderReviewMarkdown([
24
+ comment({ id: '1', file: 'src/b.ts', line: 5, side: 'new', snippet: 'return x', body: 'guard null' }),
25
+ comment({ id: '2', file: 'src/a.ts', line: 10, side: 'old', snippet: 'let u', body: 'rename u' }),
26
+ comment({ id: '3', file: 'src/a.ts', line: 2, side: 'new', snippet: 'const z=1', body: 'inline this' }),
27
+ ]);
28
+ // Files alphabetical: a.ts before b.ts
29
+ expect(md.indexOf('## src/a.ts')).toBeLessThan(md.indexOf('## src/b.ts'));
30
+ // Within a.ts, line 2 before line 10
31
+ expect(md.indexOf('Line 2 (new)')).toBeLessThan(md.indexOf('Line 10 (old)'));
32
+ expect(md).toContain('`return x`');
33
+ expect(md).toContain('→ guard null');
34
+ });
35
+ it('excludes resolved comments', () => {
36
+ const md = renderReviewMarkdown([
37
+ comment({ id: '1', body: 'keep me', status: 'open' }),
38
+ comment({ id: '2', body: 'drop me', status: 'resolved' }),
39
+ ]);
40
+ expect(md).toContain('keep me');
41
+ expect(md).not.toContain('drop me');
42
+ });
43
+ });
@@ -0,0 +1,46 @@
1
+ import { simpleGit } from 'simple-git';
2
+ export class GitError extends Error {
3
+ }
4
+ export function createGit(repoRoot) {
5
+ return simpleGit({ baseDir: repoRoot });
6
+ }
7
+ export async function assertGitRepo(git) {
8
+ const isRepo = await git.checkIsRepo();
9
+ if (!isRepo) {
10
+ throw new GitError('Not a git repository');
11
+ }
12
+ }
13
+ export async function getRefs(git) {
14
+ const branchSummary = await git.branchLocal();
15
+ return {
16
+ current: branchSummary.current,
17
+ branches: branchSummary.all,
18
+ };
19
+ }
20
+ /**
21
+ * Produce a unified diff for the requested mode.
22
+ * - working: all uncommitted changes vs HEAD (staged + unstaged)
23
+ * - staged: staged changes only
24
+ * - compare: `git diff <base> <head>` (two-dot)
25
+ */
26
+ export async function getDiff(git, mode, base, head) {
27
+ let args;
28
+ switch (mode) {
29
+ case 'staged':
30
+ args = ['--staged'];
31
+ break;
32
+ case 'compare':
33
+ if (!base || !head) {
34
+ throw new GitError('compare mode requires both base and head');
35
+ }
36
+ args = [base, head];
37
+ break;
38
+ case 'working':
39
+ default:
40
+ args = ['HEAD'];
41
+ break;
42
+ }
43
+ // Stable, machine-friendly diff output.
44
+ const raw = await git.diff(['--no-color', ...args]);
45
+ return { mode, base, head, raw };
46
+ }
@@ -0,0 +1,36 @@
1
+ import { mkdtemp, rm, writeFile } from 'node:fs/promises';
2
+ import { tmpdir } from 'node:os';
3
+ import path from 'node:path';
4
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
5
+ import { assertGitRepo, createGit, getDiff, getRefs } from './git.js';
6
+ describe('git layer', () => {
7
+ let dir;
8
+ beforeEach(async () => {
9
+ dir = await mkdtemp(path.join(tmpdir(), 'prless-git-'));
10
+ const git = createGit(dir);
11
+ await git.init();
12
+ await git.addConfig('user.email', 'test@example.com');
13
+ await git.addConfig('user.name', 'Test');
14
+ await writeFile(path.join(dir, 'a.ts'), 'const a = 1;\n');
15
+ await git.add('.');
16
+ await git.commit('init');
17
+ });
18
+ afterEach(async () => {
19
+ await rm(dir, { recursive: true, force: true });
20
+ });
21
+ it('detects a git repo', async () => {
22
+ await expect(assertGitRepo(createGit(dir))).resolves.toBeUndefined();
23
+ });
24
+ it('lists refs including the current branch', async () => {
25
+ const refs = await getRefs(createGit(dir));
26
+ expect(refs.current).toBeTruthy();
27
+ expect(refs.branches).toContain(refs.current);
28
+ });
29
+ it('produces a working-tree diff for uncommitted changes', async () => {
30
+ await writeFile(path.join(dir, 'a.ts'), 'const a = 2;\n');
31
+ const res = await getDiff(createGit(dir), 'working');
32
+ expect(res.raw).toContain('a.ts');
33
+ expect(res.raw).toContain('-const a = 1;');
34
+ expect(res.raw).toContain('+const a = 2;');
35
+ });
36
+ });
@@ -0,0 +1,31 @@
1
+ import { fileURLToPath } from 'node:url';
2
+ import path from 'node:path';
3
+ import { existsSync } from 'node:fs';
4
+ import Fastify from 'fastify';
5
+ import fastifyStatic from '@fastify/static';
6
+ import { CommentStore } from './comments.js';
7
+ import { createGit } from './git.js';
8
+ import { registerApiRoutes } from './routes.js';
9
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
10
+ export async function buildServer(opts) {
11
+ const app = Fastify({ logger: false });
12
+ const git = createGit(opts.repoRoot);
13
+ const store = new CommentStore(opts.repoRoot);
14
+ await registerApiRoutes(app, { repoRoot: opts.repoRoot, git, store });
15
+ // In dev, Vite serves the UI and proxies /api here. In a built install we
16
+ // serve the compiled web assets ourselves so the CLI is a single process.
17
+ if (!opts.dev) {
18
+ const webRoot = path.join(__dirname, '..', 'web');
19
+ if (existsSync(webRoot)) {
20
+ await app.register(fastifyStatic, { root: webRoot });
21
+ // SPA fallback for any non-API route.
22
+ app.setNotFoundHandler((request, reply) => {
23
+ if (request.url.startsWith('/api')) {
24
+ return reply.code(404).send({ error: 'not found' });
25
+ }
26
+ return reply.sendFile('index.html');
27
+ });
28
+ }
29
+ }
30
+ return app;
31
+ }
@@ -0,0 +1,59 @@
1
+ import { exportReview } from './export.js';
2
+ import { getDiff, getRefs, GitError } from './git.js';
3
+ export async function registerApiRoutes(app, ctx) {
4
+ app.get('/api/refs', async () => {
5
+ return getRefs(ctx.git);
6
+ });
7
+ app.get('/api/diff', async (request, reply) => {
8
+ const q = request.query;
9
+ const mode = (q.mode ?? 'working');
10
+ try {
11
+ return await getDiff(ctx.git, mode, q.base, q.head);
12
+ }
13
+ catch (err) {
14
+ if (err instanceof GitError) {
15
+ return reply.code(400).send({ error: err.message });
16
+ }
17
+ throw err;
18
+ }
19
+ });
20
+ app.get('/api/comments', async () => {
21
+ return ctx.store.list();
22
+ });
23
+ app.post('/api/comments', async (request, reply) => {
24
+ const body = request.body;
25
+ if (!body || !body.file || typeof body.line !== 'number' || !body.side || !body.body) {
26
+ return reply.code(400).send({ error: 'file, line, side and body are required' });
27
+ }
28
+ const created = await ctx.store.add({
29
+ file: body.file,
30
+ line: body.line,
31
+ side: body.side,
32
+ body: body.body,
33
+ snippet: body.snippet,
34
+ });
35
+ return reply.code(201).send(created);
36
+ });
37
+ app.patch('/api/comments/:id', async (request, reply) => {
38
+ const { id } = request.params;
39
+ const patch = request.body;
40
+ const updated = await ctx.store.patch(id, patch);
41
+ if (!updated) {
42
+ return reply.code(404).send({ error: 'comment not found' });
43
+ }
44
+ return updated;
45
+ });
46
+ app.delete('/api/comments/:id', async (request, reply) => {
47
+ const { id } = request.params;
48
+ const ok = await ctx.store.remove(id);
49
+ if (!ok) {
50
+ return reply.code(404).send({ error: 'comment not found' });
51
+ }
52
+ return reply.code(204).send();
53
+ });
54
+ app.post('/api/export', async () => {
55
+ const comments = await ctx.store.list();
56
+ const result = await exportReview(ctx.repoRoot, comments);
57
+ return result;
58
+ });
59
+ }
@@ -0,0 +1 @@
1
+ export {};