@peaske7/readit 0.1.7 → 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 +0 -3
- package/biome.json +1 -1
- package/bun.lock +43 -185
- package/docs/perf-baseline.md +75 -0
- package/docs/superpowers/plans/2026-03-26-surgical-pruning.md +1176 -0
- package/e2e/perf/add-comment.spec.ts +118 -0
- package/e2e/perf/fixtures/generate.ts +331 -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/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 +286 -0
- package/e2e/perf/utils/perf-cli.ts +86 -0
- package/package.json +9 -18
- package/playwright.config.ts +12 -0
- package/src/App.tsx +133 -178
- package/src/{cli/index.ts → cli.ts} +211 -107
- package/src/components/ActionsMenu.tsx +6 -27
- package/src/components/DocumentViewer/DocumentViewer.tsx +78 -105
- package/src/components/DocumentViewer/MermaidDiagram.tsx +6 -7
- package/src/components/Header.tsx +9 -20
- package/src/components/InlineEditor.tsx +5 -5
- package/src/components/MarginNote.tsx +71 -93
- package/src/components/MarginNotes.tsx +7 -34
- package/src/components/RawModal.tsx +9 -8
- package/src/components/ReanchorConfirm.tsx +2 -2
- package/src/components/SettingsModal.tsx +11 -89
- package/src/components/TabBar.tsx +4 -4
- package/src/components/TableOfContents.tsx +5 -5
- package/src/components/comments/CommentInput.tsx +7 -35
- package/src/components/comments/CommentListItem.tsx +9 -11
- package/src/components/comments/CommentManager.tsx +53 -37
- package/src/components/comments/CommentNav.tsx +14 -14
- package/src/components/ui/ActionLink.tsx +14 -18
- package/src/components/ui/Button.tsx +42 -43
- package/src/components/ui/Dialog.tsx +73 -113
- package/src/components/ui/DropdownMenu.tsx +113 -69
- package/src/components/ui/Text.tsx +30 -37
- package/src/contexts/CommentContext.tsx +75 -106
- package/src/contexts/LocaleContext.tsx +45 -4
- package/src/contexts/PositionsContext.tsx +16 -0
- package/src/contexts/SettingsContext.tsx +133 -0
- package/src/hooks/useClickOutside.ts +0 -4
- package/src/hooks/useCommentNavigation.ts +6 -29
- package/src/hooks/useComments.ts +6 -18
- package/src/hooks/useDocument.ts +35 -34
- package/src/hooks/useHeadings.test.ts +8 -50
- package/src/hooks/useHeadings.ts +5 -88
- package/src/hooks/useScrollSpy.ts +10 -14
- package/src/hooks/useTextSelection.ts +1 -38
- package/src/lib/__fixtures__/bench-data.ts +1 -41
- package/src/lib/anchor.bench.ts +57 -67
- package/src/lib/anchor.test.ts +5 -1
- package/src/lib/anchor.ts +13 -93
- package/src/lib/comment-storage.test.ts +4 -4
- package/src/lib/comment-storage.ts +2 -46
- package/src/lib/export.ts +7 -13
- package/src/lib/highlight/core.test.ts +1 -1
- package/src/lib/highlight/dom.ts +5 -68
- package/src/lib/highlight/highlighter.ts +102 -262
- package/src/lib/highlight/resolver.ts +112 -0
- package/src/lib/highlight/types.ts +0 -35
- package/src/lib/highlight/worker.ts +45 -0
- package/src/lib/i18n/en.ts +1 -50
- package/src/lib/i18n/ja.ts +1 -50
- package/src/lib/i18n/types.ts +1 -49
- package/src/lib/margin-layout.ts +5 -27
- package/src/lib/positions.ts +150 -0
- package/src/lib/utils.ts +2 -19
- package/src/schema.ts +81 -0
- package/src/{server/index.ts → server.ts} +111 -81
- package/src/{store/index.ts → store.ts} +14 -46
- package/vite.config.ts +8 -0
- package/src/components/DocumentViewer/IframeContainer.tsx +0 -251
- package/src/components/DocumentViewer/InlineCode.tsx +0 -60
- package/src/components/DocumentViewer/index.ts +0 -1
- package/src/components/FloatingTOC.tsx +0 -61
- package/src/components/ShortcutCapture.tsx +0 -48
- package/src/components/ShortcutList.tsx +0 -198
- package/src/components/comments/CommentMinimap.tsx +0 -62
- package/src/components/ui/ActionBar.tsx +0 -16
- package/src/components/ui/SeparatorDot.tsx +0 -9
- package/src/contexts/LayoutContext.tsx +0 -88
- package/src/hooks/useClipboard.ts +0 -82
- package/src/hooks/useEditorScheme.ts +0 -51
- package/src/hooks/useFontPreference.ts +0 -59
- 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/useThemePreference.ts +0 -66
- package/src/lib/comment-storage.bench.ts +0 -63
- 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/export.bench.ts +0 -35
- package/src/lib/highlight/colors.ts +0 -37
- package/src/lib/highlight/core.ts +0 -54
- 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/margin-layout.bench.ts +0 -28
- 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/shortcut-registry.ts +0 -209
- package/src/lib/utils.test.ts +0 -110
- package/src/store/index.test.ts +0 -242
- 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
|
|
|
@@ -34,17 +33,113 @@ interface ServerInfo {
|
|
|
34
33
|
pid: number;
|
|
35
34
|
}
|
|
36
35
|
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
interface ServerTarget {
|
|
37
|
+
kind: "existing" | "started";
|
|
38
|
+
port: number;
|
|
39
|
+
url: string;
|
|
40
|
+
server?: { stop(): void };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const READIT_DIR = join(os.homedir(), ".readit");
|
|
44
|
+
const SERVER_INFO_PATH = join(READIT_DIR, "server.json");
|
|
45
|
+
const SERVER_LOCK_PATH = join(READIT_DIR, "server.lock");
|
|
46
|
+
const SERVER_LOCK_MAX_AGE_MS = 30_000;
|
|
47
|
+
const SERVER_LOCK_TIMEOUT_MS = 10_000;
|
|
48
|
+
const SERVER_LOCK_WAIT_MS = 100;
|
|
49
|
+
|
|
50
|
+
function isAlive(pid: number): boolean {
|
|
51
|
+
try {
|
|
52
|
+
process.kill(pid, 0);
|
|
53
|
+
return true;
|
|
54
|
+
} catch {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function getErrnoCode(err: unknown): string | undefined {
|
|
60
|
+
return err instanceof Error && "code" in err
|
|
61
|
+
? (err as NodeJS.ErrnoException).code
|
|
62
|
+
: undefined;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function sleep(ms: number): Promise<void> {
|
|
66
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function clearStaleServerLock(): Promise<void> {
|
|
70
|
+
try {
|
|
71
|
+
const [stats, content] = await Promise.all([
|
|
72
|
+
fs.stat(SERVER_LOCK_PATH),
|
|
73
|
+
fs.readFile(SERVER_LOCK_PATH, "utf-8").catch(() => ""),
|
|
74
|
+
]);
|
|
75
|
+
|
|
76
|
+
const age = Date.now() - stats.mtimeMs;
|
|
77
|
+
let pid: number | undefined;
|
|
78
|
+
|
|
79
|
+
if (content) {
|
|
80
|
+
try {
|
|
81
|
+
const lock = JSON.parse(content) as { pid?: number };
|
|
82
|
+
pid = lock.pid;
|
|
83
|
+
} catch {
|
|
84
|
+
// Ignore malformed lock files and fall back to age-based cleanup.
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (age > SERVER_LOCK_MAX_AGE_MS || (pid !== undefined && !isAlive(pid))) {
|
|
89
|
+
await fs.unlink(SERVER_LOCK_PATH).catch(() => {});
|
|
90
|
+
}
|
|
91
|
+
} catch (err) {
|
|
92
|
+
if (getErrnoCode(err) !== "ENOENT") throw err;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function withServerLock<T>(run: () => Promise<T>): Promise<T> {
|
|
97
|
+
await fs.mkdir(READIT_DIR, { recursive: true });
|
|
98
|
+
const start = Date.now();
|
|
99
|
+
|
|
100
|
+
while (true) {
|
|
101
|
+
let handle: fs.FileHandle | undefined;
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
handle = await fs.open(SERVER_LOCK_PATH, "wx");
|
|
105
|
+
await handle.writeFile(
|
|
106
|
+
JSON.stringify({ pid: process.pid, createdAt: Date.now() }),
|
|
107
|
+
"utf-8",
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
return await run();
|
|
112
|
+
} finally {
|
|
113
|
+
await handle.close().catch(() => {});
|
|
114
|
+
await fs.unlink(SERVER_LOCK_PATH).catch(() => {});
|
|
115
|
+
}
|
|
116
|
+
} catch (err) {
|
|
117
|
+
if (handle) {
|
|
118
|
+
await handle.close().catch(() => {});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (getErrnoCode(err) !== "EEXIST") {
|
|
122
|
+
throw err;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
await clearStaleServerLock();
|
|
126
|
+
|
|
127
|
+
if (Date.now() - start >= SERVER_LOCK_TIMEOUT_MS) {
|
|
128
|
+
throw new Error("Timed out waiting for readit server lock");
|
|
129
|
+
}
|
|
39
130
|
|
|
131
|
+
await sleep(SERVER_LOCK_WAIT_MS);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function discoverServer(): Promise<ServerInfo | null> {
|
|
40
137
|
try {
|
|
41
|
-
const content = readFileSync(
|
|
138
|
+
const content = readFileSync(SERVER_INFO_PATH, "utf-8");
|
|
42
139
|
const info: ServerInfo = JSON.parse(content);
|
|
43
140
|
|
|
44
141
|
// Verify the process is alive
|
|
45
|
-
|
|
46
|
-
process.kill(info.pid, 0);
|
|
47
|
-
} catch {
|
|
142
|
+
if (!isAlive(info.pid)) {
|
|
48
143
|
return null;
|
|
49
144
|
}
|
|
50
145
|
|
|
@@ -62,9 +157,65 @@ async function discoverServer(): Promise<ServerInfo | null> {
|
|
|
62
157
|
}
|
|
63
158
|
}
|
|
64
159
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
160
|
+
async function attachFiles(
|
|
161
|
+
server: ServerInfo,
|
|
162
|
+
files: { path: string }[],
|
|
163
|
+
): Promise<void> {
|
|
164
|
+
for (const file of files) {
|
|
165
|
+
try {
|
|
166
|
+
const res = await fetch(`http://127.0.0.1:${server.port}/api/documents`, {
|
|
167
|
+
method: "POST",
|
|
168
|
+
headers: { "Content-Type": "application/json" },
|
|
169
|
+
body: JSON.stringify({ path: file.path }),
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
if (!res.ok) {
|
|
173
|
+
const data = await res.json();
|
|
174
|
+
console.error(`error: failed to add ${file.path}: ${data.error}`);
|
|
175
|
+
process.exit(1);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const data = await res.json();
|
|
179
|
+
if (data.status === "added") {
|
|
180
|
+
console.log(`Added: ${data.fileName}`);
|
|
181
|
+
} else {
|
|
182
|
+
console.log(`Present: ${data.fileName}`);
|
|
183
|
+
}
|
|
184
|
+
} catch (err) {
|
|
185
|
+
console.error(
|
|
186
|
+
"error: failed to connect to server:",
|
|
187
|
+
err instanceof Error ? err.message : err,
|
|
188
|
+
);
|
|
189
|
+
process.exit(1);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function getServerTarget(
|
|
195
|
+
files: FileEntry[],
|
|
196
|
+
port: number,
|
|
197
|
+
host: string,
|
|
198
|
+
): Promise<ServerTarget> {
|
|
199
|
+
return withServerLock(async () => {
|
|
200
|
+
const server = await discoverServer();
|
|
201
|
+
if (server) {
|
|
202
|
+
return {
|
|
203
|
+
kind: "existing",
|
|
204
|
+
port: server.port,
|
|
205
|
+
url: `http://127.0.0.1:${server.port}`,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const started = await startServer({ files, port, host });
|
|
210
|
+
return {
|
|
211
|
+
kind: "started",
|
|
212
|
+
port: started.port,
|
|
213
|
+
url: started.url,
|
|
214
|
+
server: started.server,
|
|
215
|
+
};
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
68
219
|
function findCommentFiles(dir: string): string[] {
|
|
69
220
|
const results: string[] = [];
|
|
70
221
|
|
|
@@ -95,9 +246,6 @@ function findCommentFiles(dir: string): string[] {
|
|
|
95
246
|
return results;
|
|
96
247
|
}
|
|
97
248
|
|
|
98
|
-
/**
|
|
99
|
-
* Recursively find reviewable files (.md, .markdown, .html, .htm) in a directory.
|
|
100
|
-
*/
|
|
101
249
|
function findReviewableFiles(dir: string): FileEntry[] {
|
|
102
250
|
const results: FileEntry[] = [];
|
|
103
251
|
|
|
@@ -113,14 +261,8 @@ function findReviewableFiles(dir: string): FileEntry[] {
|
|
|
113
261
|
if (lstat.isSymbolicLink()) continue;
|
|
114
262
|
if (lstat.isDirectory()) {
|
|
115
263
|
results.push(...findReviewableFiles(fullPath));
|
|
116
|
-
} else {
|
|
117
|
-
|
|
118
|
-
if (type) {
|
|
119
|
-
results.push({
|
|
120
|
-
type,
|
|
121
|
-
filePath: fullPath,
|
|
122
|
-
});
|
|
123
|
-
}
|
|
264
|
+
} else if (isMarkdownFile(entry)) {
|
|
265
|
+
results.push({ filePath: fullPath });
|
|
124
266
|
}
|
|
125
267
|
} catch (err) {
|
|
126
268
|
if (isPermissionError(err)) {
|
|
@@ -137,9 +279,6 @@ function findReviewableFiles(dir: string): FileEntry[] {
|
|
|
137
279
|
return results;
|
|
138
280
|
}
|
|
139
281
|
|
|
140
|
-
/**
|
|
141
|
-
* Resolve CLI arguments into a deduplicated list of FileEntry objects.
|
|
142
|
-
*/
|
|
143
282
|
function resolveFiles(args: string[]): FileEntry[] {
|
|
144
283
|
const seen = new Set<string>();
|
|
145
284
|
const files: FileEntry[] = [];
|
|
@@ -167,27 +306,21 @@ function resolveFiles(args: string[]): FileEntry[] {
|
|
|
167
306
|
} else {
|
|
168
307
|
if (seen.has(filePath)) continue;
|
|
169
308
|
|
|
170
|
-
|
|
171
|
-
if (!type) {
|
|
309
|
+
if (!isMarkdownFile(filePath)) {
|
|
172
310
|
console.error(
|
|
173
|
-
`error: unsupported file type: ${arg} (expected .md
|
|
311
|
+
`error: unsupported file type: ${arg} (expected .md or .markdown)`,
|
|
174
312
|
);
|
|
175
313
|
process.exit(1);
|
|
176
314
|
}
|
|
177
315
|
|
|
178
316
|
seen.add(filePath);
|
|
179
|
-
files.push({
|
|
180
|
-
type,
|
|
181
|
-
filePath,
|
|
182
|
-
});
|
|
317
|
+
files.push({ filePath });
|
|
183
318
|
}
|
|
184
319
|
}
|
|
185
320
|
|
|
186
321
|
return files;
|
|
187
322
|
}
|
|
188
323
|
|
|
189
|
-
// ─── Onboarding ──────────────────────────────────────────────────────
|
|
190
|
-
|
|
191
324
|
const SETTINGS_PATH = join(os.homedir(), ".readit", "settings.json");
|
|
192
325
|
|
|
193
326
|
function isOnboarded(): boolean {
|
|
@@ -292,14 +425,11 @@ Go ahead and add a few comments to this document. When you're done, export them
|
|
|
292
425
|
|
|
293
426
|
const WELCOME_PATH = join(os.homedir(), ".readit", "welcome.md");
|
|
294
427
|
|
|
295
|
-
// ─── Program ─────────────────────────────────────────────────────────
|
|
296
|
-
|
|
297
428
|
program
|
|
298
429
|
.name("readit")
|
|
299
|
-
.description("Review Markdown
|
|
430
|
+
.description("Review Markdown documents with inline comments")
|
|
300
431
|
.version("0.1.3");
|
|
301
432
|
|
|
302
|
-
// List command: show all commented files
|
|
303
433
|
program
|
|
304
434
|
.command("list")
|
|
305
435
|
.description("List all files with comments")
|
|
@@ -338,7 +468,6 @@ program
|
|
|
338
468
|
}
|
|
339
469
|
});
|
|
340
470
|
|
|
341
|
-
// Show command: display comments for a file
|
|
342
471
|
program
|
|
343
472
|
.command("show <file>")
|
|
344
473
|
.description("Show comments for a file")
|
|
@@ -382,9 +511,8 @@ program
|
|
|
382
511
|
}
|
|
383
512
|
});
|
|
384
513
|
|
|
385
|
-
// Main review command (default) — accepts zero or more files/directories
|
|
386
514
|
program
|
|
387
|
-
.argument("[files...]", "Markdown
|
|
515
|
+
.argument("[files...]", "Markdown files/directories to review")
|
|
388
516
|
.option("-p, --port <number>", "Port to run server on", "4567")
|
|
389
517
|
.option("--host <address>", "Host address to bind to", "127.0.0.1")
|
|
390
518
|
.option("--no-open", "Don't automatically open browser")
|
|
@@ -408,7 +536,6 @@ program
|
|
|
408
536
|
files = [
|
|
409
537
|
{
|
|
410
538
|
content: WELCOME_CONTENT,
|
|
411
|
-
type: "markdown" as DocumentType,
|
|
412
539
|
filePath: WELCOME_PATH,
|
|
413
540
|
},
|
|
414
541
|
];
|
|
@@ -433,6 +560,17 @@ program
|
|
|
433
560
|
process.exit(1);
|
|
434
561
|
}
|
|
435
562
|
|
|
563
|
+
// Snapshot previous session before startServer() overwrites server.json
|
|
564
|
+
let previousPort: number | undefined;
|
|
565
|
+
try {
|
|
566
|
+
const info = JSON.parse(readFileSync(SERVER_INFO_PATH, "utf-8"));
|
|
567
|
+
if (!isAlive(info.pid)) {
|
|
568
|
+
previousPort = info.port;
|
|
569
|
+
}
|
|
570
|
+
} catch {
|
|
571
|
+
// No previous session — will open browser normally
|
|
572
|
+
}
|
|
573
|
+
|
|
436
574
|
try {
|
|
437
575
|
const { url, server } = await startServer({
|
|
438
576
|
files,
|
|
@@ -453,7 +591,7 @@ readit - Document Review Tool
|
|
|
453
591
|
Server running. Press Ctrl+C to stop.
|
|
454
592
|
`);
|
|
455
593
|
} else {
|
|
456
|
-
const fileList = files.map((f) => ` ${f.filePath}
|
|
594
|
+
const fileList = files.map((f) => ` ${f.filePath}`);
|
|
457
595
|
|
|
458
596
|
console.log(`
|
|
459
597
|
readit - Document Review Tool
|
|
@@ -467,7 +605,11 @@ ${fileList.join("\n")}
|
|
|
467
605
|
`);
|
|
468
606
|
}
|
|
469
607
|
|
|
470
|
-
|
|
608
|
+
const browserLikelyOpen =
|
|
609
|
+
previousPort === preferredPort ||
|
|
610
|
+
process.env.NODE_ENV === "development";
|
|
611
|
+
|
|
612
|
+
if (options.open && !browserLikelyOpen) {
|
|
471
613
|
open(url);
|
|
472
614
|
}
|
|
473
615
|
|
|
@@ -493,17 +635,16 @@ ${fileList.join("\n")}
|
|
|
493
635
|
},
|
|
494
636
|
);
|
|
495
637
|
|
|
496
|
-
// Open command: add files to running server or start new one
|
|
497
638
|
program
|
|
498
639
|
.command("open")
|
|
499
|
-
.argument("<files...>", "Markdown
|
|
640
|
+
.argument("<files...>", "Markdown files to add to running server")
|
|
500
641
|
.description("Add files to a running readit server, or start a new one")
|
|
501
642
|
.option("-p, --port <number>", "Port for new server (if starting)", "4567")
|
|
502
643
|
.option("--host <address>", "Host for new server (if starting)", "127.0.0.1")
|
|
503
644
|
.action(
|
|
504
645
|
async (fileArgs: string[], options: { port: string; host: string }) => {
|
|
505
646
|
// Resolve and validate files
|
|
506
|
-
const resolvedFiles: { path: string
|
|
647
|
+
const resolvedFiles: { path: string }[] = [];
|
|
507
648
|
for (const arg of fileArgs) {
|
|
508
649
|
const inputPath = resolve(process.cwd(), arg);
|
|
509
650
|
|
|
@@ -514,91 +655,54 @@ program
|
|
|
514
655
|
|
|
515
656
|
const filePath = realpathSync(inputPath);
|
|
516
657
|
|
|
517
|
-
|
|
518
|
-
if (!type) {
|
|
658
|
+
if (!isMarkdownFile(filePath)) {
|
|
519
659
|
console.error(
|
|
520
|
-
`error: unsupported file type: ${arg} (expected .md
|
|
660
|
+
`error: unsupported file type: ${arg} (expected .md or .markdown)`,
|
|
521
661
|
);
|
|
522
662
|
process.exit(1);
|
|
523
663
|
}
|
|
524
664
|
|
|
525
|
-
resolvedFiles.push({ path: filePath
|
|
665
|
+
resolvedFiles.push({ path: filePath });
|
|
526
666
|
}
|
|
527
667
|
|
|
528
|
-
// Try to find running server
|
|
529
|
-
const server = await discoverServer();
|
|
530
|
-
|
|
531
|
-
if (server) {
|
|
532
|
-
// Send files to running server
|
|
533
|
-
for (const file of resolvedFiles) {
|
|
534
|
-
try {
|
|
535
|
-
const res = await fetch(
|
|
536
|
-
`http://127.0.0.1:${server.port}/api/documents`,
|
|
537
|
-
{
|
|
538
|
-
method: "POST",
|
|
539
|
-
headers: { "Content-Type": "application/json" },
|
|
540
|
-
body: JSON.stringify({ path: file.path }),
|
|
541
|
-
},
|
|
542
|
-
);
|
|
543
|
-
|
|
544
|
-
if (!res.ok) {
|
|
545
|
-
const data = await res.json();
|
|
546
|
-
console.error(`error: failed to add ${file.path}: ${data.error}`);
|
|
547
|
-
process.exit(1);
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
const data = await res.json();
|
|
551
|
-
if (data.status === "added") {
|
|
552
|
-
console.log(`Added: ${data.fileName} (${data.type})`);
|
|
553
|
-
} else {
|
|
554
|
-
console.log(`Present: ${data.fileName} (${data.type})`);
|
|
555
|
-
}
|
|
556
|
-
} catch (err) {
|
|
557
|
-
console.error(
|
|
558
|
-
"error: failed to connect to server:",
|
|
559
|
-
err instanceof Error ? err.message : err,
|
|
560
|
-
);
|
|
561
|
-
process.exit(1);
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
console.log(`\nServer: http://127.0.0.1:${server.port}`);
|
|
566
|
-
return;
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
// No running server — start one
|
|
570
|
-
console.log("No running server found, starting new one...\n");
|
|
571
|
-
|
|
572
668
|
const files = resolvedFiles.map((f) => ({
|
|
573
|
-
type: f.type,
|
|
574
669
|
filePath: f.path,
|
|
575
670
|
}));
|
|
576
671
|
|
|
577
672
|
const preferredPort = Number.parseInt(options.port, 10);
|
|
578
673
|
try {
|
|
579
|
-
const
|
|
674
|
+
const target = await getServerTarget(
|
|
580
675
|
files,
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
676
|
+
preferredPort,
|
|
677
|
+
options.host,
|
|
678
|
+
);
|
|
584
679
|
|
|
585
|
-
|
|
680
|
+
if (target.kind === "existing") {
|
|
681
|
+
await attachFiles(
|
|
682
|
+
{ port: target.port, pid: process.pid },
|
|
683
|
+
resolvedFiles,
|
|
684
|
+
);
|
|
685
|
+
console.log(`\nServer: ${target.url}`);
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
const fileList = files.map((f) => ` ${f.filePath}`);
|
|
586
690
|
console.log(`
|
|
587
691
|
readit - Document Review Tool
|
|
588
692
|
|
|
589
693
|
${files.length === 1 ? "File:" : "Files:"}
|
|
590
694
|
${fileList.join("\n")}
|
|
591
|
-
URL: ${url}
|
|
695
|
+
URL: ${target.url}
|
|
592
696
|
|
|
593
697
|
Server running. Close browser tab to stop.
|
|
594
698
|
Press Ctrl+C to force stop.
|
|
595
699
|
`);
|
|
596
700
|
|
|
597
|
-
open(url);
|
|
701
|
+
open(target.url);
|
|
598
702
|
|
|
599
703
|
process.on("SIGINT", async () => {
|
|
600
704
|
console.log("\n\nShutting down...");
|
|
601
|
-
|
|
705
|
+
target.server?.stop();
|
|
602
706
|
await removeServerInfo();
|
|
603
707
|
process.exit(0);
|
|
604
708
|
});
|
|
@@ -1,17 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
2
|
+
ClipboardCopy,
|
|
3
3
|
FileDown,
|
|
4
4
|
FileText,
|
|
5
|
-
Maximize2,
|
|
6
|
-
Minimize2,
|
|
7
5
|
MoreHorizontal,
|
|
8
6
|
RefreshCw,
|
|
9
7
|
Settings,
|
|
10
|
-
TextQuote,
|
|
11
8
|
} from "lucide-react";
|
|
12
9
|
import { useState } from "react";
|
|
13
|
-
import {
|
|
14
|
-
import { useLayoutContext } from "../contexts/LayoutContext";
|
|
10
|
+
import { useCommentData } from "../contexts/CommentContext";
|
|
15
11
|
import { useLocale } from "../contexts/LocaleContext";
|
|
16
12
|
import { RawModal } from "./RawModal";
|
|
17
13
|
import { SettingsModal } from "./SettingsModal";
|
|
@@ -26,19 +22,16 @@ import {
|
|
|
26
22
|
|
|
27
23
|
interface ActionsMenuProps {
|
|
28
24
|
onCopyAll: () => void;
|
|
29
|
-
onCopyAllRaw: () => void;
|
|
30
25
|
onExportJson: () => void;
|
|
31
26
|
onReload: () => void;
|
|
32
27
|
}
|
|
33
28
|
|
|
34
29
|
export function ActionsMenu({
|
|
35
30
|
onCopyAll,
|
|
36
|
-
onCopyAllRaw,
|
|
37
31
|
onExportJson,
|
|
38
32
|
onReload,
|
|
39
33
|
}: ActionsMenuProps) {
|
|
40
|
-
const { commentCount } =
|
|
41
|
-
const { isFullscreen, toggleLayoutMode } = useLayoutContext();
|
|
34
|
+
const { commentCount } = useCommentData();
|
|
42
35
|
const { t } = useLocale();
|
|
43
36
|
|
|
44
37
|
const [menuOpen, setMenuOpen] = useState(false);
|
|
@@ -59,10 +52,6 @@ export function ActionsMenu({
|
|
|
59
52
|
</Button>
|
|
60
53
|
</DropdownMenuTrigger>
|
|
61
54
|
<DropdownMenuContent align="end" className="min-w-[160px]">
|
|
62
|
-
<DropdownMenuItem onSelect={() => toggleLayoutMode()}>
|
|
63
|
-
{isFullscreen ? <Minimize2 /> : <Maximize2 />}
|
|
64
|
-
{isFullscreen ? t("actions.centered") : t("actions.fullscreen")}
|
|
65
|
-
</DropdownMenuItem>
|
|
66
55
|
<DropdownMenuItem onSelect={() => setSettingsOpen(true)}>
|
|
67
56
|
<Settings />
|
|
68
57
|
{t("actions.settings")}
|
|
@@ -74,19 +63,9 @@ export function ActionsMenu({
|
|
|
74
63
|
</DropdownMenuItem>
|
|
75
64
|
{commentCount > 0 && (
|
|
76
65
|
<>
|
|
77
|
-
<DropdownMenuItem
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
>
|
|
81
|
-
<BotMessageSquare />
|
|
82
|
-
{t("actions.copyAllAI")}
|
|
83
|
-
</DropdownMenuItem>
|
|
84
|
-
<DropdownMenuItem
|
|
85
|
-
onSelect={() => onCopyAllRaw()}
|
|
86
|
-
title={t("actions.copyAllRawTitle")}
|
|
87
|
-
>
|
|
88
|
-
<TextQuote />
|
|
89
|
-
{t("actions.copyAllRaw")}
|
|
66
|
+
<DropdownMenuItem onSelect={() => onCopyAll()}>
|
|
67
|
+
<ClipboardCopy />
|
|
68
|
+
{t("actions.copyAll")}
|
|
90
69
|
</DropdownMenuItem>
|
|
91
70
|
<DropdownMenuItem onSelect={() => onExportJson()}>
|
|
92
71
|
<FileDown />
|