@peaske7/readit 0.1.8 → 0.2.1
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/.claude/CLAUDE.md +118 -76
- package/.claude/commands/review.md +1 -1
- package/.claude/roadmap.md +32 -9
- package/.claude/user-stories.md +100 -15
- package/AGENTS.md +30 -26
- package/Makefile +32 -0
- package/README.md +90 -5
- package/biome.json +18 -8
- package/bun.lock +426 -710
- package/bunfig.toml +2 -0
- package/docs/perf-baseline.md +130 -0
- package/docs/superpowers/plans/2026-03-26-surgical-pruning.md +1176 -0
- package/docs/superpowers/specs/2026-03-27-go-server-rewrite-design.md +284 -0
- package/e2e/comments.spec.ts +14 -58
- package/e2e/document-load.spec.ts +1 -23
- package/e2e/export.spec.ts +4 -4
- package/e2e/perf/add-comment.spec.ts +116 -0
- package/e2e/perf/fixtures/generate.ts +327 -0
- package/e2e/perf/initial-load.spec.ts +49 -0
- package/e2e/perf/perf.setup.ts +23 -0
- package/e2e/perf/perf.teardown.ts +9 -0
- package/e2e/perf/screenshot-final.png +0 -0
- package/e2e/perf/scroll.spec.ts +39 -0
- package/e2e/perf/tab-switch.spec.ts +69 -0
- package/e2e/perf/text-selection.spec.ts +119 -0
- package/e2e/perf/utils/metrics.ts +350 -0
- package/e2e/perf/utils/perf-cli.ts +86 -0
- package/e2e/persistence-file.spec.ts +41 -26
- package/e2e/utils/selection.ts +17 -73
- package/go/cmd/readit/main.go +416 -0
- package/go/go.mod +20 -0
- package/go/go.sum +41 -0
- package/go/internal/server/anchor.go +302 -0
- package/go/internal/server/anchor_test.go +111 -0
- package/go/internal/server/comments.go +390 -0
- package/go/internal/server/documents.go +113 -0
- package/go/internal/server/embed.go +17 -0
- package/go/internal/server/headings.go +33 -0
- package/go/internal/server/headings_test.go +75 -0
- package/go/internal/server/htmltext.go +123 -0
- package/go/internal/server/markdown.go +157 -0
- package/go/internal/server/markdown_bench_test.go +42 -0
- package/go/internal/server/markdown_test.go +79 -0
- package/go/internal/server/server.go +453 -0
- package/go/internal/server/server_bench_test.go +122 -0
- package/go/internal/server/settings.go +110 -0
- package/go/internal/server/sse.go +140 -0
- package/go/internal/server/storage.go +275 -0
- package/go/internal/server/storage_test.go +118 -0
- package/go/internal/server/template.go +66 -0
- package/go/internal/server/types.go +101 -0
- package/go/internal/server/watcher.go +74 -0
- package/index.html +4 -14
- package/nvim-readit/lua/readit/health.lua +64 -0
- package/nvim-readit/lua/readit/init.lua +463 -0
- package/nvim-readit/plugin/readit.lua +19 -0
- package/package.json +24 -41
- package/playwright.config.ts +12 -0
- package/shell/_readit +158 -0
- package/shell/readit.zsh +87 -0
- package/src/App.svelte +881 -0
- package/src/{cli/index.ts → cli.ts} +216 -70
- package/src/components/ActionsMenu.svelte +95 -0
- package/src/components/CommentBadge.svelte +67 -0
- package/src/components/CommentErrorBanner.svelte +33 -0
- package/src/components/CommentInput.svelte +75 -0
- package/src/components/CommentListItem.svelte +95 -0
- package/src/components/CommentManager.svelte +129 -0
- package/src/components/CommentNav.svelte +109 -0
- package/src/components/DocumentViewer.svelte +218 -0
- package/src/components/FloatingComment.svelte +107 -0
- package/src/components/Header.svelte +76 -0
- package/src/components/InlineEditor.svelte +72 -0
- package/src/components/MarginNote.svelte +167 -0
- package/src/components/MarginNotesContainer.svelte +33 -0
- package/src/components/RawModal.svelte +126 -0
- package/src/components/ReanchorConfirm.svelte +30 -0
- package/src/components/SettingsModal.svelte +220 -0
- package/src/components/ShortcutCapture.svelte +82 -0
- package/src/components/ShortcutList.svelte +145 -0
- package/src/components/TabBar.svelte +52 -0
- package/src/components/TableOfContents.svelte +125 -0
- package/src/components/ui/ActionLink.svelte +40 -0
- package/src/components/ui/Button.svelte +53 -0
- package/src/components/ui/Dialog.svelte +97 -0
- package/src/components/ui/DropdownMenu.svelte +85 -0
- package/src/components/ui/DropdownMenuItem.svelte +38 -0
- package/src/components/ui/DropdownMenuSeparator.svelte +11 -0
- package/src/components/ui/Text.svelte +42 -0
- package/src/env.d.ts +6 -0
- package/src/index.css +36 -166
- package/src/lib/__fixtures__/bench-data.ts +1 -54
- package/src/lib/anchor.bench.ts +47 -68
- package/src/lib/anchor.test.ts +5 -9
- package/src/lib/anchor.ts +9 -93
- package/src/lib/comment-storage.bench.ts +6 -20
- package/src/lib/comment-storage.test.ts +45 -37
- package/src/lib/comment-storage.ts +23 -64
- package/src/lib/export.bench.ts +9 -23
- package/src/lib/export.ts +7 -14
- package/src/lib/headings.test.ts +103 -0
- package/src/lib/headings.ts +44 -0
- package/src/lib/highlight/core.test.ts +1 -6
- package/src/lib/highlight/dom.ts +53 -280
- package/src/lib/highlight/highlight-registry.ts +221 -0
- package/src/lib/highlight/highlight.bench.ts +92 -0
- package/src/lib/highlight/highlighter.ts +122 -302
- package/src/lib/highlight/{core.ts → resolver.ts} +3 -19
- package/src/lib/highlight/types.ts +0 -40
- package/src/lib/html-text.test.ts +162 -0
- package/src/lib/html-text.ts +161 -0
- package/src/lib/i18n/en.ts +13 -36
- package/src/lib/i18n/ja.ts +14 -37
- package/src/lib/i18n/types.ts +13 -36
- package/src/lib/margin-layout.bench.ts +48 -15
- package/src/lib/margin-layout.ts +2 -31
- package/src/lib/markdown-renderer.test.ts +154 -0
- package/src/lib/markdown-renderer.ts +177 -0
- package/src/lib/mermaid-config.ts +38 -0
- package/src/lib/mermaid-renderer.ts +162 -0
- package/src/lib/mermaid-worker.ts +60 -0
- package/src/lib/positions.ts +157 -0
- package/src/lib/shortcut-registry.ts +138 -103
- package/src/lib/utils.ts +2 -48
- package/src/main.ts +16 -0
- package/src/schema.ts +92 -0
- package/src/{server/index.ts → server.ts} +427 -163
- package/src/stores/app.svelte.ts +231 -0
- package/src/stores/locale.svelte.ts +46 -0
- package/src/stores/settings.svelte.ts +90 -0
- package/src/stores/shortcuts.svelte.ts +104 -0
- package/src/stores/ui.svelte.ts +12 -0
- package/src/template.ts +104 -0
- package/src/test-setup.ts +47 -0
- package/svelte.config.js +5 -0
- package/tsconfig.json +2 -2
- package/vite.config.ts +31 -3
- package/vscode-readit/.mcp.json +7 -0
- package/vscode-readit/.vscodeignore +7 -0
- package/vscode-readit/bun.lock +78 -0
- package/vscode-readit/icon.svg +10 -0
- package/vscode-readit/package.json +110 -0
- package/vscode-readit/src/extension.ts +117 -0
- package/vscode-readit/src/server-manager.ts +272 -0
- package/vscode-readit/src/webview-provider.ts +204 -0
- package/vscode-readit/tsconfig.json +20 -0
- package/e2e/fixtures/sample.html +0 -13
- package/src/App.tsx +0 -416
- package/src/components/ActionsMenu.tsx +0 -112
- package/src/components/DocumentViewer/CodeBlock.tsx +0 -160
- package/src/components/DocumentViewer/DocumentViewer.tsx +0 -259
- package/src/components/DocumentViewer/IframeContainer.tsx +0 -251
- package/src/components/DocumentViewer/InlineCode.tsx +0 -60
- package/src/components/DocumentViewer/MermaidDiagram.tsx +0 -137
- package/src/components/DocumentViewer/index.ts +0 -1
- package/src/components/FloatingTOC.tsx +0 -61
- package/src/components/Header.tsx +0 -65
- package/src/components/InlineEditor.tsx +0 -74
- package/src/components/MarginNote.tsx +0 -207
- package/src/components/MarginNotes.tsx +0 -50
- package/src/components/RawModal.tsx +0 -143
- package/src/components/ReanchorConfirm.tsx +0 -36
- package/src/components/SettingsModal.tsx +0 -310
- package/src/components/ShortcutCapture.tsx +0 -48
- package/src/components/ShortcutList.tsx +0 -198
- package/src/components/TabBar.tsx +0 -60
- package/src/components/TableOfContents.tsx +0 -108
- package/src/components/comments/CommentBadge.tsx +0 -49
- package/src/components/comments/CommentInput.tsx +0 -114
- package/src/components/comments/CommentListItem.tsx +0 -92
- package/src/components/comments/CommentManager.tsx +0 -113
- package/src/components/comments/CommentMinimap.tsx +0 -62
- package/src/components/comments/CommentNav.tsx +0 -109
- package/src/components/ui/ActionBar.tsx +0 -16
- package/src/components/ui/ActionLink.tsx +0 -32
- package/src/components/ui/Button.tsx +0 -55
- package/src/components/ui/Dialog.tsx +0 -156
- package/src/components/ui/DropdownMenu.tsx +0 -114
- package/src/components/ui/SeparatorDot.tsx +0 -9
- package/src/components/ui/Text.tsx +0 -54
- package/src/contexts/CommentContext.tsx +0 -229
- package/src/contexts/LayoutContext.tsx +0 -88
- package/src/contexts/LocaleContext.tsx +0 -35
- package/src/hooks/useClickOutside.ts +0 -35
- package/src/hooks/useClipboard.ts +0 -82
- package/src/hooks/useCommentNavigation.ts +0 -130
- package/src/hooks/useComments.ts +0 -323
- package/src/hooks/useDocument.ts +0 -156
- package/src/hooks/useEditorScheme.ts +0 -51
- package/src/hooks/useFontPreference.ts +0 -59
- package/src/hooks/useHeadings.test.ts +0 -159
- package/src/hooks/useHeadings.ts +0 -129
- package/src/hooks/useKeybindings.ts +0 -108
- package/src/hooks/useKeyboardShortcuts.ts +0 -63
- package/src/hooks/useLayoutMode.ts +0 -44
- package/src/hooks/useLocalePreference.ts +0 -42
- package/src/hooks/useReanchorMode.ts +0 -33
- package/src/hooks/useScrollMetrics.ts +0 -56
- package/src/hooks/useScrollSpy.ts +0 -81
- package/src/hooks/useTextSelection.ts +0 -123
- package/src/hooks/useThemePreference.ts +0 -66
- package/src/lib/context.bench.ts +0 -41
- package/src/lib/context.test.ts +0 -224
- package/src/lib/context.ts +0 -193
- package/src/lib/editor-links.ts +0 -59
- package/src/lib/highlight/colors.ts +0 -37
- package/src/lib/highlight/index.ts +0 -23
- package/src/lib/highlight/script-builder.ts +0 -485
- package/src/lib/html-processor.test.tsx +0 -170
- package/src/lib/html-processor.tsx +0 -95
- package/src/lib/i18n/completeness.test.ts +0 -51
- package/src/lib/i18n/translations.test.ts +0 -39
- package/src/lib/layout-constants.ts +0 -12
- package/src/lib/scroll.test.ts +0 -118
- package/src/lib/scroll.ts +0 -47
- package/src/lib/shortcut-registry.test.ts +0 -173
- package/src/lib/utils.test.ts +0 -110
- package/src/main.tsx +0 -13
- package/src/store/index.test.ts +0 -242
- package/src/store/index.ts +0 -254
- package/src/types/index.ts +0 -127
|
@@ -13,11 +13,10 @@ import * as os from "node:os";
|
|
|
13
13
|
import { join, resolve } from "node:path";
|
|
14
14
|
import { Command } from "commander";
|
|
15
15
|
import open from "open";
|
|
16
|
-
import { getCommentPath, parseCommentFile } from "
|
|
17
|
-
import {
|
|
18
|
-
import type { FileEntry } from "
|
|
19
|
-
import { removeServerInfo, startServer } from "
|
|
20
|
-
import type { DocumentType } from "../types/index.js";
|
|
16
|
+
import { getCommentPath, parseCommentFile } from "./lib/comment-storage.js";
|
|
17
|
+
import { isMarkdownFile } from "./lib/utils.js";
|
|
18
|
+
import type { FileEntry } from "./server.js";
|
|
19
|
+
import { removeServerInfo, startServer } from "./server.js";
|
|
21
20
|
|
|
22
21
|
const program = new Command();
|
|
23
22
|
|
|
@@ -81,9 +80,7 @@ async function clearStaleServerLock(): Promise<void> {
|
|
|
81
80
|
try {
|
|
82
81
|
const lock = JSON.parse(content) as { pid?: number };
|
|
83
82
|
pid = lock.pid;
|
|
84
|
-
} catch {
|
|
85
|
-
// Ignore malformed lock files and fall back to age-based cleanup.
|
|
86
|
-
}
|
|
83
|
+
} catch {}
|
|
87
84
|
}
|
|
88
85
|
|
|
89
86
|
if (age > SERVER_LOCK_MAX_AGE_MS || (pid !== undefined && !isAlive(pid))) {
|
|
@@ -139,12 +136,10 @@ async function discoverServer(): Promise<ServerInfo | null> {
|
|
|
139
136
|
const content = readFileSync(SERVER_INFO_PATH, "utf-8");
|
|
140
137
|
const info: ServerInfo = JSON.parse(content);
|
|
141
138
|
|
|
142
|
-
// Verify the process is alive
|
|
143
139
|
if (!isAlive(info.pid)) {
|
|
144
140
|
return null;
|
|
145
141
|
}
|
|
146
142
|
|
|
147
|
-
// Verify health endpoint responds
|
|
148
143
|
try {
|
|
149
144
|
const res = await fetch(`http://127.0.0.1:${info.port}/api/health`);
|
|
150
145
|
if (!res.ok) return null;
|
|
@@ -160,7 +155,7 @@ async function discoverServer(): Promise<ServerInfo | null> {
|
|
|
160
155
|
|
|
161
156
|
async function attachFiles(
|
|
162
157
|
server: ServerInfo,
|
|
163
|
-
files: { path: string
|
|
158
|
+
files: { path: string }[],
|
|
164
159
|
): Promise<void> {
|
|
165
160
|
for (const file of files) {
|
|
166
161
|
try {
|
|
@@ -178,9 +173,9 @@ async function attachFiles(
|
|
|
178
173
|
|
|
179
174
|
const data = await res.json();
|
|
180
175
|
if (data.status === "added") {
|
|
181
|
-
console.log(`Added: ${data.fileName}
|
|
176
|
+
console.log(`Added: ${data.fileName}`);
|
|
182
177
|
} else {
|
|
183
|
-
console.log(`Present: ${data.fileName}
|
|
178
|
+
console.log(`Present: ${data.fileName}`);
|
|
184
179
|
}
|
|
185
180
|
} catch (err) {
|
|
186
181
|
console.error(
|
|
@@ -217,9 +212,6 @@ async function getServerTarget(
|
|
|
217
212
|
});
|
|
218
213
|
}
|
|
219
214
|
|
|
220
|
-
/**
|
|
221
|
-
* Recursively find all .comments.md files in a directory.
|
|
222
|
-
*/
|
|
223
215
|
function findCommentFiles(dir: string): string[] {
|
|
224
216
|
const results: string[] = [];
|
|
225
217
|
|
|
@@ -250,16 +242,12 @@ function findCommentFiles(dir: string): string[] {
|
|
|
250
242
|
return results;
|
|
251
243
|
}
|
|
252
244
|
|
|
253
|
-
/**
|
|
254
|
-
* Recursively find reviewable files (.md, .markdown, .html, .htm) in a directory.
|
|
255
|
-
*/
|
|
256
245
|
function findReviewableFiles(dir: string): FileEntry[] {
|
|
257
246
|
const results: FileEntry[] = [];
|
|
258
247
|
|
|
259
248
|
try {
|
|
260
249
|
const entries = readdirSync(dir);
|
|
261
250
|
for (const entry of entries) {
|
|
262
|
-
// Skip hidden directories and node_modules
|
|
263
251
|
if (entry.startsWith(".") || entry === "node_modules") continue;
|
|
264
252
|
|
|
265
253
|
const fullPath = join(dir, entry);
|
|
@@ -268,14 +256,8 @@ function findReviewableFiles(dir: string): FileEntry[] {
|
|
|
268
256
|
if (lstat.isSymbolicLink()) continue;
|
|
269
257
|
if (lstat.isDirectory()) {
|
|
270
258
|
results.push(...findReviewableFiles(fullPath));
|
|
271
|
-
} else {
|
|
272
|
-
|
|
273
|
-
if (type) {
|
|
274
|
-
results.push({
|
|
275
|
-
type,
|
|
276
|
-
filePath: fullPath,
|
|
277
|
-
});
|
|
278
|
-
}
|
|
259
|
+
} else if (isMarkdownFile(entry)) {
|
|
260
|
+
results.push({ filePath: fullPath });
|
|
279
261
|
}
|
|
280
262
|
} catch (err) {
|
|
281
263
|
if (isPermissionError(err)) {
|
|
@@ -292,9 +274,6 @@ function findReviewableFiles(dir: string): FileEntry[] {
|
|
|
292
274
|
return results;
|
|
293
275
|
}
|
|
294
276
|
|
|
295
|
-
/**
|
|
296
|
-
* Resolve CLI arguments into a deduplicated list of FileEntry objects.
|
|
297
|
-
*/
|
|
298
277
|
function resolveFiles(args: string[]): FileEntry[] {
|
|
299
278
|
const seen = new Set<string>();
|
|
300
279
|
const files: FileEntry[] = [];
|
|
@@ -322,27 +301,21 @@ function resolveFiles(args: string[]): FileEntry[] {
|
|
|
322
301
|
} else {
|
|
323
302
|
if (seen.has(filePath)) continue;
|
|
324
303
|
|
|
325
|
-
|
|
326
|
-
if (!type) {
|
|
304
|
+
if (!isMarkdownFile(filePath)) {
|
|
327
305
|
console.error(
|
|
328
|
-
`error: unsupported file type: ${arg} (expected .md
|
|
306
|
+
`error: unsupported file type: ${arg} (expected .md or .markdown)`,
|
|
329
307
|
);
|
|
330
308
|
process.exit(1);
|
|
331
309
|
}
|
|
332
310
|
|
|
333
311
|
seen.add(filePath);
|
|
334
|
-
files.push({
|
|
335
|
-
type,
|
|
336
|
-
filePath,
|
|
337
|
-
});
|
|
312
|
+
files.push({ filePath });
|
|
338
313
|
}
|
|
339
314
|
}
|
|
340
315
|
|
|
341
316
|
return files;
|
|
342
317
|
}
|
|
343
318
|
|
|
344
|
-
// ─── Onboarding ──────────────────────────────────────────────────────
|
|
345
|
-
|
|
346
319
|
const SETTINGS_PATH = join(os.homedir(), ".readit", "settings.json");
|
|
347
320
|
|
|
348
321
|
function isOnboarded(): boolean {
|
|
@@ -360,9 +333,7 @@ async function markOnboarded(): Promise<void> {
|
|
|
360
333
|
try {
|
|
361
334
|
const content = readFileSync(SETTINGS_PATH, "utf-8");
|
|
362
335
|
settings = JSON.parse(content);
|
|
363
|
-
} catch {
|
|
364
|
-
// No existing settings
|
|
365
|
-
}
|
|
336
|
+
} catch {}
|
|
366
337
|
settings.onboarded = true;
|
|
367
338
|
const dir = join(os.homedir(), ".readit");
|
|
368
339
|
await fs.mkdir(dir, { recursive: true });
|
|
@@ -447,14 +418,11 @@ Go ahead and add a few comments to this document. When you're done, export them
|
|
|
447
418
|
|
|
448
419
|
const WELCOME_PATH = join(os.homedir(), ".readit", "welcome.md");
|
|
449
420
|
|
|
450
|
-
// ─── Program ─────────────────────────────────────────────────────────
|
|
451
|
-
|
|
452
421
|
program
|
|
453
422
|
.name("readit")
|
|
454
|
-
.description("Review Markdown
|
|
455
|
-
.version("0.
|
|
423
|
+
.description("Review Markdown documents with inline comments")
|
|
424
|
+
.version("0.2.0");
|
|
456
425
|
|
|
457
|
-
// List command: show all commented files
|
|
458
426
|
program
|
|
459
427
|
.command("list")
|
|
460
428
|
.description("List all files with comments")
|
|
@@ -487,13 +455,10 @@ program
|
|
|
487
455
|
` ${commentCount} comment${commentCount !== 1 ? "s" : ""}`,
|
|
488
456
|
);
|
|
489
457
|
console.log();
|
|
490
|
-
} catch {
|
|
491
|
-
// Skip unreadable files
|
|
492
|
-
}
|
|
458
|
+
} catch {}
|
|
493
459
|
}
|
|
494
460
|
});
|
|
495
461
|
|
|
496
|
-
// Show command: display comments for a file
|
|
497
462
|
program
|
|
498
463
|
.command("show <file>")
|
|
499
464
|
.description("Show comments for a file")
|
|
@@ -525,7 +490,6 @@ program
|
|
|
525
490
|
`Selected: "${comment.selectedText.slice(0, 80)}${comment.selectedText.length > 80 ? "..." : ""}"`,
|
|
526
491
|
);
|
|
527
492
|
console.log(`Comment: ${comment.comment}`);
|
|
528
|
-
console.log(`Created: ${comment.createdAt}`);
|
|
529
493
|
console.log();
|
|
530
494
|
}
|
|
531
495
|
} catch (err) {
|
|
@@ -537,9 +501,8 @@ program
|
|
|
537
501
|
}
|
|
538
502
|
});
|
|
539
503
|
|
|
540
|
-
// Main review command (default) — accepts zero or more files/directories
|
|
541
504
|
program
|
|
542
|
-
.argument("[files...]", "Markdown
|
|
505
|
+
.argument("[files...]", "Markdown files/directories to review")
|
|
543
506
|
.option("-p, --port <number>", "Port to run server on", "4567")
|
|
544
507
|
.option("--host <address>", "Host address to bind to", "127.0.0.1")
|
|
545
508
|
.option("--no-open", "Don't automatically open browser")
|
|
@@ -563,7 +526,6 @@ program
|
|
|
563
526
|
files = [
|
|
564
527
|
{
|
|
565
528
|
content: WELCOME_CONTENT,
|
|
566
|
-
type: "markdown" as DocumentType,
|
|
567
529
|
filePath: WELCOME_PATH,
|
|
568
530
|
},
|
|
569
531
|
];
|
|
@@ -588,6 +550,14 @@ program
|
|
|
588
550
|
process.exit(1);
|
|
589
551
|
}
|
|
590
552
|
|
|
553
|
+
let previousPort: number | undefined;
|
|
554
|
+
try {
|
|
555
|
+
const info = JSON.parse(readFileSync(SERVER_INFO_PATH, "utf-8"));
|
|
556
|
+
if (!isAlive(info.pid)) {
|
|
557
|
+
previousPort = info.port;
|
|
558
|
+
}
|
|
559
|
+
} catch {}
|
|
560
|
+
|
|
591
561
|
try {
|
|
592
562
|
const { url, server } = await startServer({
|
|
593
563
|
files,
|
|
@@ -608,7 +578,7 @@ readit - Document Review Tool
|
|
|
608
578
|
Server running. Press Ctrl+C to stop.
|
|
609
579
|
`);
|
|
610
580
|
} else {
|
|
611
|
-
const fileList = files.map((f) => ` ${f.filePath}
|
|
581
|
+
const fileList = files.map((f) => ` ${f.filePath}`);
|
|
612
582
|
|
|
613
583
|
console.log(`
|
|
614
584
|
readit - Document Review Tool
|
|
@@ -622,16 +592,18 @@ ${fileList.join("\n")}
|
|
|
622
592
|
`);
|
|
623
593
|
}
|
|
624
594
|
|
|
625
|
-
|
|
595
|
+
const browserLikelyOpen =
|
|
596
|
+
previousPort === preferredPort ||
|
|
597
|
+
process.env.NODE_ENV === "development";
|
|
598
|
+
|
|
599
|
+
if (options.open && !browserLikelyOpen) {
|
|
626
600
|
open(url);
|
|
627
601
|
}
|
|
628
602
|
|
|
629
|
-
// Mark onboarding complete on first server start
|
|
630
603
|
if (fileArgs.length === 0) {
|
|
631
604
|
await markOnboarded();
|
|
632
605
|
}
|
|
633
606
|
|
|
634
|
-
// Graceful shutdown on Ctrl+C
|
|
635
607
|
process.on("SIGINT", async () => {
|
|
636
608
|
console.log("\n\nShutting down...");
|
|
637
609
|
server.stop();
|
|
@@ -648,17 +620,15 @@ ${fileList.join("\n")}
|
|
|
648
620
|
},
|
|
649
621
|
);
|
|
650
622
|
|
|
651
|
-
// Open command: add files to running server or start new one
|
|
652
623
|
program
|
|
653
624
|
.command("open")
|
|
654
|
-
.argument("<files...>", "Markdown
|
|
625
|
+
.argument("<files...>", "Markdown files to add to running server")
|
|
655
626
|
.description("Add files to a running readit server, or start a new one")
|
|
656
627
|
.option("-p, --port <number>", "Port for new server (if starting)", "4567")
|
|
657
628
|
.option("--host <address>", "Host for new server (if starting)", "127.0.0.1")
|
|
658
629
|
.action(
|
|
659
630
|
async (fileArgs: string[], options: { port: string; host: string }) => {
|
|
660
|
-
|
|
661
|
-
const resolvedFiles: { path: string; type: DocumentType }[] = [];
|
|
631
|
+
const resolvedFiles: { path: string }[] = [];
|
|
662
632
|
for (const arg of fileArgs) {
|
|
663
633
|
const inputPath = resolve(process.cwd(), arg);
|
|
664
634
|
|
|
@@ -669,19 +639,17 @@ program
|
|
|
669
639
|
|
|
670
640
|
const filePath = realpathSync(inputPath);
|
|
671
641
|
|
|
672
|
-
|
|
673
|
-
if (!type) {
|
|
642
|
+
if (!isMarkdownFile(filePath)) {
|
|
674
643
|
console.error(
|
|
675
|
-
`error: unsupported file type: ${arg} (expected .md
|
|
644
|
+
`error: unsupported file type: ${arg} (expected .md or .markdown)`,
|
|
676
645
|
);
|
|
677
646
|
process.exit(1);
|
|
678
647
|
}
|
|
679
648
|
|
|
680
|
-
resolvedFiles.push({ path: filePath
|
|
649
|
+
resolvedFiles.push({ path: filePath });
|
|
681
650
|
}
|
|
682
651
|
|
|
683
652
|
const files = resolvedFiles.map((f) => ({
|
|
684
|
-
type: f.type,
|
|
685
653
|
filePath: f.path,
|
|
686
654
|
}));
|
|
687
655
|
|
|
@@ -702,7 +670,7 @@ program
|
|
|
702
670
|
return;
|
|
703
671
|
}
|
|
704
672
|
|
|
705
|
-
const fileList = files.map((f) => ` ${f.filePath}
|
|
673
|
+
const fileList = files.map((f) => ` ${f.filePath}`);
|
|
706
674
|
console.log(`
|
|
707
675
|
readit - Document Review Tool
|
|
708
676
|
|
|
@@ -732,4 +700,182 @@ ${fileList.join("\n")}
|
|
|
732
700
|
},
|
|
733
701
|
);
|
|
734
702
|
|
|
703
|
+
program
|
|
704
|
+
.command("completion")
|
|
705
|
+
.argument("[shell]", "Shell type (zsh, bash, fish)", "zsh")
|
|
706
|
+
.description("Output shell completion and integration script")
|
|
707
|
+
.action((shell: string) => {
|
|
708
|
+
const shellDir = join(import.meta.dir, "..", "shell");
|
|
709
|
+
|
|
710
|
+
switch (shell) {
|
|
711
|
+
case "zsh": {
|
|
712
|
+
// Output the full zsh integration:
|
|
713
|
+
// 1. _readit compdef (loaded into fpath via autoload) - handles @ prefix
|
|
714
|
+
// 2. readit.zsh widget (accept-line bracket stripping + syntax highlighting)
|
|
715
|
+
const widgetPath = join(shellDir, "readit.zsh");
|
|
716
|
+
const compPath = join(shellDir, "_readit");
|
|
717
|
+
|
|
718
|
+
if (!existsSync(widgetPath) || !existsSync(compPath)) {
|
|
719
|
+
console.log(generateInlineZshCompletion());
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// Wrap the compdef in an autoload function so eval works cleanly
|
|
724
|
+
const compdefContent = readFileSync(compPath, "utf-8");
|
|
725
|
+
const widgetContent = readFileSync(widgetPath, "utf-8");
|
|
726
|
+
|
|
727
|
+
const lines: string[] = [];
|
|
728
|
+
lines.push("# readit shell integration for zsh");
|
|
729
|
+
lines.push('# Add to your .zshrc: eval "$(readit completion zsh)"');
|
|
730
|
+
lines.push("");
|
|
731
|
+
lines.push("# ── _readit compdef (autoloaded) ──");
|
|
732
|
+
lines.push(
|
|
733
|
+
"# This handles: subcommand/option completion + @ file autocomplete",
|
|
734
|
+
);
|
|
735
|
+
lines.push(
|
|
736
|
+
"# Renders [file.md] in a native multi-column grid via compadd",
|
|
737
|
+
);
|
|
738
|
+
lines.push("");
|
|
739
|
+
// Replace #compdef with autoload -Uz _readit; _readit() { ... }
|
|
740
|
+
lines.push(
|
|
741
|
+
compdefContent
|
|
742
|
+
.replace(
|
|
743
|
+
/^#compdef readit\n/,
|
|
744
|
+
"autoload -Uz _readit\n_readit() {\n",
|
|
745
|
+
)
|
|
746
|
+
.replace(/\n_readit "\$@"\n?$/, "\n}\n"),
|
|
747
|
+
);
|
|
748
|
+
lines.push("");
|
|
749
|
+
lines.push("# ── readit.zsh (sourced) ──");
|
|
750
|
+
lines.push(
|
|
751
|
+
"# This handles: @[...] bracket stripping on Enter + syntax highlighting",
|
|
752
|
+
);
|
|
753
|
+
lines.push("");
|
|
754
|
+
// Strip the shebang and guard from the widget since it's being eval'd
|
|
755
|
+
lines.push(
|
|
756
|
+
widgetContent
|
|
757
|
+
.replace(/^#!/, "#")
|
|
758
|
+
.replace(/\n\(\( \$\+ functions\[_readit_plugin_loaded\] \)\)/, ""),
|
|
759
|
+
);
|
|
760
|
+
console.log(lines.join("\n"));
|
|
761
|
+
break;
|
|
762
|
+
}
|
|
763
|
+
case "bash":
|
|
764
|
+
console.log(generateBashCompletion());
|
|
765
|
+
break;
|
|
766
|
+
case "fish":
|
|
767
|
+
console.log(generateFishCompletion());
|
|
768
|
+
break;
|
|
769
|
+
default:
|
|
770
|
+
console.error(`error: unsupported shell: ${shell}`);
|
|
771
|
+
console.error("Supported shells: zsh, bash, fish");
|
|
772
|
+
process.exit(1);
|
|
773
|
+
}
|
|
774
|
+
});
|
|
775
|
+
|
|
735
776
|
program.parse();
|
|
777
|
+
|
|
778
|
+
function generateInlineZshCompletion(): string {
|
|
779
|
+
return `
|
|
780
|
+
#compdef readit
|
|
781
|
+
|
|
782
|
+
_readit_markdown_files() {
|
|
783
|
+
local -a files
|
|
784
|
+
files=( \${(f)"$(find . -type f \\( -name '*.md' -o -name '*.markdown' \\) -not -path '*/\\.*' -not -path '*/node_modules/*' 2>/dev/null | sed 's|^\\./||')"} )
|
|
785
|
+
_describe -t files 'markdown files' files
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
_readit() {
|
|
789
|
+
local context state state_descr line
|
|
790
|
+
typeset -A opt_args
|
|
791
|
+
_arguments -C '1:command:->cmd_or_files' '*::arg:->args'
|
|
792
|
+
case "$state" in
|
|
793
|
+
cmd_or_files)
|
|
794
|
+
local -a commands=(
|
|
795
|
+
'open:Add files to running server'
|
|
796
|
+
'list:List files with comments'
|
|
797
|
+
'show:Show comments for a file'
|
|
798
|
+
'completion:Output shell completion script'
|
|
799
|
+
)
|
|
800
|
+
_alternative 'commands:command:compadd -a commands' 'files:markdown file:_readit_markdown_files'
|
|
801
|
+
;;
|
|
802
|
+
args)
|
|
803
|
+
case "\${line[1]}" in
|
|
804
|
+
open) _arguments '*:file:_readit_markdown_files' ;;
|
|
805
|
+
show) _arguments '1:file:_files -g "*.md *.markdown"' ;;
|
|
806
|
+
*) _arguments '*:file:_readit_markdown_files' ;;
|
|
807
|
+
esac
|
|
808
|
+
;;
|
|
809
|
+
esac
|
|
810
|
+
}
|
|
811
|
+
_readit "$@"
|
|
812
|
+
`.trim();
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
function generateBashCompletion(): string {
|
|
816
|
+
return `
|
|
817
|
+
# readit bash completion
|
|
818
|
+
# Add to .bashrc: eval "$(readit completion bash)"
|
|
819
|
+
|
|
820
|
+
_readit_completions() {
|
|
821
|
+
local cur prev commands
|
|
822
|
+
COMPREPLY=()
|
|
823
|
+
cur="\${COMP_WORDS[COMP_CWORD]}"
|
|
824
|
+
prev="\${COMP_WORDS[COMP_CWORD-1]}"
|
|
825
|
+
commands="open list show completion"
|
|
826
|
+
|
|
827
|
+
if [[ \${COMP_CWORD} -eq 1 ]]; then
|
|
828
|
+
COMPREPLY=( $(compgen -W "\${commands}" -- "\${cur}") )
|
|
829
|
+
# Also complete markdown files
|
|
830
|
+
local files=$(find . -type f \\( -name '*.md' -o -name '*.markdown' \\) \\
|
|
831
|
+
-not -path '*/.*' -not -path '*/node_modules/*' 2>/dev/null | sed 's|^\\./||')
|
|
832
|
+
COMPREPLY+=( $(compgen -W "\${files}" -- "\${cur}") )
|
|
833
|
+
return 0
|
|
834
|
+
fi
|
|
835
|
+
|
|
836
|
+
case "\${COMP_WORDS[1]}" in
|
|
837
|
+
open|show)
|
|
838
|
+
local files=$(find . -type f \\( -name '*.md' -o -name '*.markdown' \\) \\
|
|
839
|
+
-not -path '*/.*' -not -path '*/node_modules/*' 2>/dev/null | sed 's|^\\./||')
|
|
840
|
+
COMPREPLY=( $(compgen -W "\${files}" -- "\${cur}") )
|
|
841
|
+
;;
|
|
842
|
+
completion)
|
|
843
|
+
COMPREPLY=( $(compgen -W "zsh bash fish" -- "\${cur}") )
|
|
844
|
+
;;
|
|
845
|
+
esac
|
|
846
|
+
return 0
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
complete -F _readit_completions readit
|
|
850
|
+
`.trim();
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
function generateFishCompletion(): string {
|
|
854
|
+
return `
|
|
855
|
+
# readit fish completion
|
|
856
|
+
# Add to config.fish: readit completion fish | source
|
|
857
|
+
|
|
858
|
+
# Disable file completions by default
|
|
859
|
+
complete -c readit -f
|
|
860
|
+
|
|
861
|
+
# Subcommands
|
|
862
|
+
complete -c readit -n '__fish_use_subcommand' -a 'open' -d 'Add files to running server'
|
|
863
|
+
complete -c readit -n '__fish_use_subcommand' -a 'list' -d 'List files with comments'
|
|
864
|
+
complete -c readit -n '__fish_use_subcommand' -a 'show' -d 'Show comments for a file'
|
|
865
|
+
complete -c readit -n '__fish_use_subcommand' -a 'completion' -d 'Output shell completion script'
|
|
866
|
+
|
|
867
|
+
# Options
|
|
868
|
+
complete -c readit -s p -l port -d 'Port to run server on'
|
|
869
|
+
complete -c readit -l host -d 'Host address to bind to'
|
|
870
|
+
complete -c readit -l no-open -d "Don't automatically open browser"
|
|
871
|
+
complete -c readit -l clean -d 'Clear existing comments'
|
|
872
|
+
|
|
873
|
+
# File arguments for default command and open
|
|
874
|
+
complete -c readit -n '__fish_use_subcommand' -F -a '(find . -type f \\( -name "*.md" -o -name "*.markdown" \\) -not -path "*/.*" -not -path "*/node_modules/*" 2>/dev/null | sed "s|^\\./||")'
|
|
875
|
+
complete -c readit -n '__fish_seen_subcommand_from open' -F -a '(find . -type f \\( -name "*.md" -o -name "*.markdown" \\) -not -path "*/.*" -not -path "*/node_modules/*" 2>/dev/null | sed "s|^\\./||")'
|
|
876
|
+
complete -c readit -n '__fish_seen_subcommand_from show' -F -a '(find . -type f \\( -name "*.md" -o -name "*.markdown" \\) -not -path "*/.*" -not -path "*/node_modules/*" 2>/dev/null | sed "s|^\\./||")'
|
|
877
|
+
|
|
878
|
+
# Shell completions for completion subcommand
|
|
879
|
+
complete -c readit -n '__fish_seen_subcommand_from completion' -a 'zsh bash fish'
|
|
880
|
+
`.trim();
|
|
881
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import {
|
|
3
|
+
ClipboardCopy,
|
|
4
|
+
FileDown,
|
|
5
|
+
FileText,
|
|
6
|
+
MoreHorizontal,
|
|
7
|
+
RefreshCw,
|
|
8
|
+
Settings,
|
|
9
|
+
} from "lucide-svelte";
|
|
10
|
+
import { t } from "../stores/locale.svelte";
|
|
11
|
+
import RawModal from "./RawModal.svelte";
|
|
12
|
+
import SettingsModal from "./SettingsModal.svelte";
|
|
13
|
+
import Button from "./ui/Button.svelte";
|
|
14
|
+
import DropdownMenu from "./ui/DropdownMenu.svelte";
|
|
15
|
+
import DropdownMenuItem from "./ui/DropdownMenuItem.svelte";
|
|
16
|
+
import DropdownMenuSeparator from "./ui/DropdownMenuSeparator.svelte";
|
|
17
|
+
|
|
18
|
+
interface Props {
|
|
19
|
+
commentCount: number;
|
|
20
|
+
oncopyall: () => void;
|
|
21
|
+
onexportjson: () => void;
|
|
22
|
+
onreload: () => void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let { commentCount, oncopyall, onexportjson, onreload }: Props = $props();
|
|
26
|
+
|
|
27
|
+
let menuOpen = $state(false);
|
|
28
|
+
let rawModalOpen = $state(false);
|
|
29
|
+
let settingsOpen = $state(false);
|
|
30
|
+
</script>
|
|
31
|
+
|
|
32
|
+
<DropdownMenu bind:open={menuOpen} align="end" contentClass="min-w-[160px]">
|
|
33
|
+
{#snippet trigger()}
|
|
34
|
+
<Button
|
|
35
|
+
variant="ghost"
|
|
36
|
+
size="icon"
|
|
37
|
+
class="size-7"
|
|
38
|
+
title={t("actions.ariaLabel")}
|
|
39
|
+
>
|
|
40
|
+
<MoreHorizontal class="w-4 h-4" />
|
|
41
|
+
</Button>
|
|
42
|
+
{/snippet}
|
|
43
|
+
|
|
44
|
+
<DropdownMenuItem
|
|
45
|
+
onselect={() => {
|
|
46
|
+
settingsOpen = true;
|
|
47
|
+
menuOpen = false;
|
|
48
|
+
}}
|
|
49
|
+
>
|
|
50
|
+
<Settings />
|
|
51
|
+
{t("actions.settings")}
|
|
52
|
+
</DropdownMenuItem>
|
|
53
|
+
<DropdownMenuSeparator />
|
|
54
|
+
<DropdownMenuItem
|
|
55
|
+
onselect={() => {
|
|
56
|
+
onreload();
|
|
57
|
+
menuOpen = false;
|
|
58
|
+
}}
|
|
59
|
+
>
|
|
60
|
+
<RefreshCw />
|
|
61
|
+
{t("actions.reload")}
|
|
62
|
+
</DropdownMenuItem>
|
|
63
|
+
{#if commentCount > 0}
|
|
64
|
+
<DropdownMenuItem
|
|
65
|
+
onselect={() => {
|
|
66
|
+
oncopyall();
|
|
67
|
+
menuOpen = false;
|
|
68
|
+
}}
|
|
69
|
+
>
|
|
70
|
+
<ClipboardCopy />
|
|
71
|
+
{t("actions.copyAll")}
|
|
72
|
+
</DropdownMenuItem>
|
|
73
|
+
<DropdownMenuItem
|
|
74
|
+
onselect={() => {
|
|
75
|
+
onexportjson();
|
|
76
|
+
menuOpen = false;
|
|
77
|
+
}}
|
|
78
|
+
>
|
|
79
|
+
<FileDown />
|
|
80
|
+
{t("actions.exportJson")}
|
|
81
|
+
</DropdownMenuItem>
|
|
82
|
+
<DropdownMenuItem
|
|
83
|
+
onselect={() => {
|
|
84
|
+
rawModalOpen = true;
|
|
85
|
+
menuOpen = false;
|
|
86
|
+
}}
|
|
87
|
+
>
|
|
88
|
+
<FileText />
|
|
89
|
+
{t("actions.viewRaw")}
|
|
90
|
+
</DropdownMenuItem>
|
|
91
|
+
{/if}
|
|
92
|
+
</DropdownMenu>
|
|
93
|
+
|
|
94
|
+
<RawModal bind:open={rawModalOpen} onclose={() => (rawModalOpen = false)} />
|
|
95
|
+
<SettingsModal bind:open={settingsOpen} onclose={() => (settingsOpen = false)} />
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { cn } from "../lib/utils";
|
|
3
|
+
import type { Comment } from "../schema";
|
|
4
|
+
import { t } from "../stores/locale.svelte";
|
|
5
|
+
import CommentManager from "./CommentManager.svelte";
|
|
6
|
+
import DropdownMenu from "./ui/DropdownMenu.svelte";
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
comments: Comment[];
|
|
10
|
+
fileName: string;
|
|
11
|
+
onedit: (id: string, newText: string) => void;
|
|
12
|
+
ondelete: (id: string) => void;
|
|
13
|
+
ondeleteall: () => void;
|
|
14
|
+
onnavigate: (id: string) => void;
|
|
15
|
+
onstartreanchor: (id: string) => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
let {
|
|
19
|
+
comments,
|
|
20
|
+
fileName,
|
|
21
|
+
onedit,
|
|
22
|
+
ondelete,
|
|
23
|
+
ondeleteall,
|
|
24
|
+
onnavigate,
|
|
25
|
+
onstartreanchor,
|
|
26
|
+
}: Props = $props();
|
|
27
|
+
|
|
28
|
+
let commentsOpen = $state(false);
|
|
29
|
+
let commentCount = $derived(comments.length);
|
|
30
|
+
</script>
|
|
31
|
+
|
|
32
|
+
{#if commentCount > 0}
|
|
33
|
+
<DropdownMenu
|
|
34
|
+
bind:open={commentsOpen}
|
|
35
|
+
align="end"
|
|
36
|
+
contentClass="w-80 max-h-96 overflow-hidden p-0"
|
|
37
|
+
>
|
|
38
|
+
{#snippet trigger()}
|
|
39
|
+
<button
|
|
40
|
+
type="button"
|
|
41
|
+
class={cn(
|
|
42
|
+
"inline-flex items-center gap-1 text-xs tabular-nums select-none transition-colors",
|
|
43
|
+
commentsOpen
|
|
44
|
+
? "text-zinc-600"
|
|
45
|
+
: "text-zinc-400 hover:text-zinc-600",
|
|
46
|
+
)}
|
|
47
|
+
title={commentCount === 1
|
|
48
|
+
? t("commentBadge.title", { count: commentCount })
|
|
49
|
+
: t("commentBadge.titlePlural", { count: commentCount })}
|
|
50
|
+
>
|
|
51
|
+
<span class="text-zinc-300">·</span>
|
|
52
|
+
{commentCount}
|
|
53
|
+
</button>
|
|
54
|
+
{/snippet}
|
|
55
|
+
|
|
56
|
+
<CommentManager
|
|
57
|
+
{comments}
|
|
58
|
+
{fileName}
|
|
59
|
+
onclose={() => (commentsOpen = false)}
|
|
60
|
+
{onedit}
|
|
61
|
+
{ondelete}
|
|
62
|
+
{ondeleteall}
|
|
63
|
+
{onnavigate}
|
|
64
|
+
{onstartreanchor}
|
|
65
|
+
/>
|
|
66
|
+
</DropdownMenu>
|
|
67
|
+
{/if}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { AlertCircle, X } from "lucide-svelte";
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
error: string | null;
|
|
6
|
+
ondismiss: () => void;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
let { error, ondismiss }: Props = $props();
|
|
10
|
+
</script>
|
|
11
|
+
|
|
12
|
+
{#if error}
|
|
13
|
+
<div
|
|
14
|
+
role="alert"
|
|
15
|
+
aria-live="polite"
|
|
16
|
+
class="fixed top-24 left-1/2 -translate-x-1/2 z-40 w-full max-w-3xl px-4 pointer-events-none"
|
|
17
|
+
>
|
|
18
|
+
<div
|
|
19
|
+
class="flex items-start gap-3 px-4 py-2 text-sm bg-red-50 dark:bg-red-950 border border-red-200 dark:border-red-900 text-red-900 dark:text-red-200 rounded-lg shadow-lg pointer-events-auto"
|
|
20
|
+
>
|
|
21
|
+
<AlertCircle class="size-4 shrink-0 mt-0.5" />
|
|
22
|
+
<p class="flex-1 select-text break-words">{error}</p>
|
|
23
|
+
<button
|
|
24
|
+
type="button"
|
|
25
|
+
onclick={ondismiss}
|
|
26
|
+
aria-label="Dismiss"
|
|
27
|
+
class="shrink-0 rounded p-1 -mr-1 hover:bg-red-100 dark:hover:bg-red-900/40 transition-colors"
|
|
28
|
+
>
|
|
29
|
+
<X class="size-4" />
|
|
30
|
+
</button>
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
{/if}
|