@peaske7/readit 0.3.3-rc.1 → 0.3.3-rc.2
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 +35 -0
- package/dist/index.js +421 -58
- package/package.json +1 -1
- package/shell/_readit +13 -0
package/README.md
CHANGED
|
@@ -42,6 +42,7 @@ readit <file> --clean # Clear existing comments
|
|
|
42
42
|
readit list # List all files with comments
|
|
43
43
|
readit show <file> # Show comments for a file
|
|
44
44
|
readit open <files...> # Add files to running server
|
|
45
|
+
readit zed-open <file> # Open from a Zed task or extension
|
|
45
46
|
readit completion zsh # Output shell integration script
|
|
46
47
|
```
|
|
47
48
|
|
|
@@ -129,6 +130,40 @@ require("readit").setup({
|
|
|
129
130
|
|
|
130
131
|
Run `:checkhealth readit` to verify your setup.
|
|
131
132
|
|
|
133
|
+
## Zed
|
|
134
|
+
|
|
135
|
+
Zed can open the current Markdown file in readit through a task. The preview opens in your browser because Zed extensions do not currently expose arbitrary preview webviews like the VS Code extension API.
|
|
136
|
+
|
|
137
|
+
Add this to `.zed/tasks.json` in a project:
|
|
138
|
+
|
|
139
|
+
```json
|
|
140
|
+
[
|
|
141
|
+
{
|
|
142
|
+
"label": "readit: open preview",
|
|
143
|
+
"command": "readit",
|
|
144
|
+
"args": ["zed-open", "$ZED_FILE"],
|
|
145
|
+
"save": "current",
|
|
146
|
+
"reveal": "never",
|
|
147
|
+
"hide": "on_success"
|
|
148
|
+
}
|
|
149
|
+
]
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Then add a keybinding in `~/.config/zed/keymap.json`:
|
|
153
|
+
|
|
154
|
+
```json
|
|
155
|
+
[
|
|
156
|
+
{
|
|
157
|
+
"context": "Workspace",
|
|
158
|
+
"bindings": {
|
|
159
|
+
"cmd-shift-r": ["task::Spawn", { "task_name": "readit: open preview" }]
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
]
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Run `readit zed-open README.md` manually if the task fails. Check that `readit` is on `PATH`, Bun is installed, and `~/.readit/server.json` points to a healthy server process.
|
|
166
|
+
|
|
132
167
|
## Live Reload
|
|
133
168
|
|
|
134
169
|
readit watches open documents for changes and automatically refreshes the browser. This works with any editor:
|
package/dist/index.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
var __require = import.meta.require;
|
|
4
4
|
|
|
5
5
|
// src/cli.ts
|
|
6
|
+
import { spawn as spawn2 } from "child_process";
|
|
6
7
|
import {
|
|
7
8
|
existsSync,
|
|
8
9
|
lstatSync,
|
|
@@ -19,7 +20,7 @@ import open2 from "open";
|
|
|
19
20
|
// package.json
|
|
20
21
|
var package_default = {
|
|
21
22
|
name: "@peaske7/readit",
|
|
22
|
-
version: "0.3.3-rc.
|
|
23
|
+
version: "0.3.3-rc.2",
|
|
23
24
|
description: "A CLI tool to review Markdown documents with inline comments",
|
|
24
25
|
author: "Jay Shimada <peaske@pm.me>",
|
|
25
26
|
license: "MIT",
|
|
@@ -1764,31 +1765,36 @@ Browser disconnected, shutting down...`);
|
|
|
1764
1765
|
return null;
|
|
1765
1766
|
}
|
|
1766
1767
|
}
|
|
1767
|
-
|
|
1768
|
-
|
|
1768
|
+
const pageCache = new Map;
|
|
1769
|
+
const pageCacheGz = new Map;
|
|
1769
1770
|
function invalidatePageCache() {
|
|
1770
|
-
pageCache
|
|
1771
|
-
pageCacheGz
|
|
1771
|
+
pageCache.clear();
|
|
1772
|
+
pageCacheGz.clear();
|
|
1772
1773
|
}
|
|
1773
1774
|
async function serveAppPage(req) {
|
|
1774
1775
|
const acceptGzip = req.headers.get("accept-encoding")?.includes("gzip") ?? false;
|
|
1776
|
+
const url = new URL(req.url);
|
|
1777
|
+
const requestedPath = url.searchParams.get("path");
|
|
1778
|
+
const activePath = requestedPath && fileMap.has(requestedPath) ? requestedPath : defaultPath;
|
|
1775
1779
|
try {
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1780
|
+
const cachedPage = pageCache.get(activePath);
|
|
1781
|
+
if (cachedPage) {
|
|
1782
|
+
const cachedPageGz = pageCacheGz.get(activePath);
|
|
1783
|
+
if (acceptGzip && cachedPageGz) {
|
|
1784
|
+
return new Response(cachedPageGz, {
|
|
1779
1785
|
headers: {
|
|
1780
1786
|
"Content-Type": "text/html; charset=utf-8",
|
|
1781
1787
|
"Content-Encoding": "gzip"
|
|
1782
1788
|
}
|
|
1783
1789
|
});
|
|
1784
1790
|
}
|
|
1785
|
-
return new Response(
|
|
1791
|
+
return new Response(cachedPage, {
|
|
1786
1792
|
headers: { "Content-Type": "text/html; charset=utf-8" }
|
|
1787
1793
|
});
|
|
1788
1794
|
}
|
|
1789
|
-
const { html, headings } = await ensureRenderedHtml(
|
|
1790
|
-
const content = await ensureFileContent(
|
|
1791
|
-
const comments = await readCommentsFromFile(
|
|
1795
|
+
const { html, headings } = await ensureRenderedHtml(activePath);
|
|
1796
|
+
const content = await ensureFileContent(activePath);
|
|
1797
|
+
const comments = await readCommentsFromFile(activePath, content, html);
|
|
1792
1798
|
const settings = await readSettings();
|
|
1793
1799
|
const files = fileOrder.map((fp) => ({
|
|
1794
1800
|
path: fp,
|
|
@@ -1796,10 +1802,10 @@ Browser disconnected, shutting down...`);
|
|
|
1796
1802
|
}));
|
|
1797
1803
|
const inlineData = {
|
|
1798
1804
|
files,
|
|
1799
|
-
activeFile:
|
|
1805
|
+
activeFile: activePath,
|
|
1800
1806
|
settings,
|
|
1801
1807
|
documents: {
|
|
1802
|
-
[
|
|
1808
|
+
[activePath]: {
|
|
1803
1809
|
headings,
|
|
1804
1810
|
comments
|
|
1805
1811
|
}
|
|
@@ -1820,7 +1826,7 @@ Browser disconnected, shutting down...`);
|
|
|
1820
1826
|
}
|
|
1821
1827
|
}
|
|
1822
1828
|
const body = renderTemplate({
|
|
1823
|
-
title: basename(
|
|
1829
|
+
title: basename(activePath),
|
|
1824
1830
|
cssPath,
|
|
1825
1831
|
jsPath,
|
|
1826
1832
|
documentHtml: html,
|
|
@@ -1829,11 +1835,12 @@ Browser disconnected, shutting down...`);
|
|
|
1829
1835
|
fontFamily: settings.fontFamily
|
|
1830
1836
|
});
|
|
1831
1837
|
if (!isDev) {
|
|
1832
|
-
|
|
1833
|
-
|
|
1838
|
+
const gz = Bun.gzipSync(new TextEncoder().encode(body));
|
|
1839
|
+
pageCache.set(activePath, body);
|
|
1840
|
+
pageCacheGz.set(activePath, gz);
|
|
1834
1841
|
}
|
|
1835
1842
|
if (acceptGzip) {
|
|
1836
|
-
const gz = pageCacheGz ?? Bun.gzipSync(new TextEncoder().encode(body));
|
|
1843
|
+
const gz = pageCacheGz.get(activePath) ?? Bun.gzipSync(new TextEncoder().encode(body));
|
|
1837
1844
|
return new Response(gz, {
|
|
1838
1845
|
headers: {
|
|
1839
1846
|
"Content-Type": "text/html; charset=utf-8",
|
|
@@ -2164,6 +2171,231 @@ async function startServer(options) {
|
|
|
2164
2171
|
throw new Error(`No available port found starting from ${options.port}`);
|
|
2165
2172
|
}
|
|
2166
2173
|
|
|
2174
|
+
// src/zed-lsp.ts
|
|
2175
|
+
import { spawn } from "child_process";
|
|
2176
|
+
import { fileURLToPath } from "url";
|
|
2177
|
+
var OPEN_PREVIEW_COMMAND = "readit.openPreview";
|
|
2178
|
+
var OPEN_PREVIEW_IN_BROWSER_COMMAND = "readit.openPreviewInBrowser";
|
|
2179
|
+
var METHOD_NOT_FOUND = -32601;
|
|
2180
|
+
var INVALID_PARAMS = -32602;
|
|
2181
|
+
var INTERNAL_ERROR = -32603;
|
|
2182
|
+
function filePathFromUri(uri) {
|
|
2183
|
+
if (!uri.startsWith("file://"))
|
|
2184
|
+
return null;
|
|
2185
|
+
try {
|
|
2186
|
+
return fileURLToPath(uri);
|
|
2187
|
+
} catch {
|
|
2188
|
+
return null;
|
|
2189
|
+
}
|
|
2190
|
+
}
|
|
2191
|
+
function isMarkdownUri(uri) {
|
|
2192
|
+
const filePath = filePathFromUri(uri);
|
|
2193
|
+
return filePath !== null && isMarkdownFile(filePath);
|
|
2194
|
+
}
|
|
2195
|
+
function hasId(message) {
|
|
2196
|
+
return Object.hasOwn(message, "id");
|
|
2197
|
+
}
|
|
2198
|
+
function send(message) {
|
|
2199
|
+
const body = JSON.stringify(message);
|
|
2200
|
+
process.stdout.write(`Content-Length: ${Buffer.byteLength(body, "utf8")}\r
|
|
2201
|
+
\r
|
|
2202
|
+
${body}`);
|
|
2203
|
+
}
|
|
2204
|
+
function sendResponse(id, result) {
|
|
2205
|
+
send({ jsonrpc: "2.0", id, result });
|
|
2206
|
+
}
|
|
2207
|
+
function sendError(id, code, message) {
|
|
2208
|
+
send({
|
|
2209
|
+
jsonrpc: "2.0",
|
|
2210
|
+
id,
|
|
2211
|
+
error: { code, message }
|
|
2212
|
+
});
|
|
2213
|
+
}
|
|
2214
|
+
function getTextDocumentUri(params) {
|
|
2215
|
+
const p = params;
|
|
2216
|
+
const uri = p?.textDocument?.uri;
|
|
2217
|
+
return typeof uri === "string" ? uri : null;
|
|
2218
|
+
}
|
|
2219
|
+
function codeActionsFor(uri) {
|
|
2220
|
+
if (!isMarkdownUri(uri))
|
|
2221
|
+
return [];
|
|
2222
|
+
return [
|
|
2223
|
+
{
|
|
2224
|
+
title: "readit: Open Preview",
|
|
2225
|
+
kind: "source",
|
|
2226
|
+
command: {
|
|
2227
|
+
title: "readit: Open Preview",
|
|
2228
|
+
command: OPEN_PREVIEW_COMMAND,
|
|
2229
|
+
arguments: [uri]
|
|
2230
|
+
}
|
|
2231
|
+
},
|
|
2232
|
+
{
|
|
2233
|
+
title: "readit: Open Preview in Browser",
|
|
2234
|
+
kind: "source",
|
|
2235
|
+
command: {
|
|
2236
|
+
title: "readit: Open Preview in Browser",
|
|
2237
|
+
command: OPEN_PREVIEW_IN_BROWSER_COMMAND,
|
|
2238
|
+
arguments: [uri]
|
|
2239
|
+
}
|
|
2240
|
+
}
|
|
2241
|
+
];
|
|
2242
|
+
}
|
|
2243
|
+
function cliInvocation(args) {
|
|
2244
|
+
if (!process.versions.bun) {
|
|
2245
|
+
throw new Error("Bun is required to run readit. Install Bun and retry.");
|
|
2246
|
+
}
|
|
2247
|
+
const entrypoint = process.argv[1];
|
|
2248
|
+
if (!entrypoint) {
|
|
2249
|
+
throw new Error("readit CLI entrypoint is unavailable");
|
|
2250
|
+
}
|
|
2251
|
+
return {
|
|
2252
|
+
command: process.execPath,
|
|
2253
|
+
args: [entrypoint, ...args]
|
|
2254
|
+
};
|
|
2255
|
+
}
|
|
2256
|
+
async function runReaditZedOpen(filePath) {
|
|
2257
|
+
const invocation = cliInvocation(["zed-open", filePath]);
|
|
2258
|
+
const child = spawn(invocation.command, invocation.args, {
|
|
2259
|
+
cwd: process.cwd(),
|
|
2260
|
+
env: { ...process.env },
|
|
2261
|
+
stdio: ["ignore", "ignore", "pipe"]
|
|
2262
|
+
});
|
|
2263
|
+
let stderr = "";
|
|
2264
|
+
child.stderr?.on("data", (chunk) => {
|
|
2265
|
+
stderr += chunk.toString("utf8");
|
|
2266
|
+
});
|
|
2267
|
+
const code = await new Promise((resolve3, reject) => {
|
|
2268
|
+
child.on("error", reject);
|
|
2269
|
+
child.on("exit", resolve3);
|
|
2270
|
+
});
|
|
2271
|
+
if (code !== 0) {
|
|
2272
|
+
throw new Error(stderr.trim() || `readit zed-open exited with status ${code}`);
|
|
2273
|
+
}
|
|
2274
|
+
}
|
|
2275
|
+
async function executeCommand(params) {
|
|
2276
|
+
const p = params;
|
|
2277
|
+
if (p.command !== OPEN_PREVIEW_COMMAND && p.command !== OPEN_PREVIEW_IN_BROWSER_COMMAND) {
|
|
2278
|
+
throw new Error(`unsupported command: ${String(p.command)}`);
|
|
2279
|
+
}
|
|
2280
|
+
const uri = p.arguments?.[0];
|
|
2281
|
+
if (typeof uri !== "string") {
|
|
2282
|
+
throw new Error("missing document URI");
|
|
2283
|
+
}
|
|
2284
|
+
const filePath = filePathFromUri(uri);
|
|
2285
|
+
if (!filePath) {
|
|
2286
|
+
throw new Error(`unsupported URI: ${uri}`);
|
|
2287
|
+
}
|
|
2288
|
+
if (!isMarkdownFile(filePath)) {
|
|
2289
|
+
throw new Error(`unsupported file type: ${filePath}`);
|
|
2290
|
+
}
|
|
2291
|
+
await runReaditZedOpen(filePath);
|
|
2292
|
+
}
|
|
2293
|
+
async function handleMessage(message, state) {
|
|
2294
|
+
if (typeof message.method !== "string") {
|
|
2295
|
+
if (hasId(message)) {
|
|
2296
|
+
sendError(message.id, INVALID_PARAMS, "missing method");
|
|
2297
|
+
}
|
|
2298
|
+
return;
|
|
2299
|
+
}
|
|
2300
|
+
const requestHasId = hasId(message);
|
|
2301
|
+
try {
|
|
2302
|
+
switch (message.method) {
|
|
2303
|
+
case "initialize":
|
|
2304
|
+
if (requestHasId) {
|
|
2305
|
+
sendResponse(message.id, {
|
|
2306
|
+
capabilities: {
|
|
2307
|
+
codeActionProvider: true,
|
|
2308
|
+
executeCommandProvider: {
|
|
2309
|
+
commands: [
|
|
2310
|
+
OPEN_PREVIEW_COMMAND,
|
|
2311
|
+
OPEN_PREVIEW_IN_BROWSER_COMMAND
|
|
2312
|
+
]
|
|
2313
|
+
}
|
|
2314
|
+
},
|
|
2315
|
+
serverInfo: {
|
|
2316
|
+
name: "readit-zed-lsp",
|
|
2317
|
+
version: package_default.version
|
|
2318
|
+
}
|
|
2319
|
+
});
|
|
2320
|
+
}
|
|
2321
|
+
return;
|
|
2322
|
+
case "initialized":
|
|
2323
|
+
return;
|
|
2324
|
+
case "textDocument/codeAction": {
|
|
2325
|
+
const uri = getTextDocumentUri(message.params);
|
|
2326
|
+
if (requestHasId) {
|
|
2327
|
+
sendResponse(message.id, uri ? codeActionsFor(uri) : []);
|
|
2328
|
+
}
|
|
2329
|
+
return;
|
|
2330
|
+
}
|
|
2331
|
+
case "workspace/executeCommand":
|
|
2332
|
+
await executeCommand(message.params);
|
|
2333
|
+
if (requestHasId) {
|
|
2334
|
+
sendResponse(message.id, null);
|
|
2335
|
+
}
|
|
2336
|
+
return;
|
|
2337
|
+
case "shutdown":
|
|
2338
|
+
state.shuttingDown = true;
|
|
2339
|
+
if (requestHasId) {
|
|
2340
|
+
sendResponse(message.id, null);
|
|
2341
|
+
}
|
|
2342
|
+
return;
|
|
2343
|
+
case "exit":
|
|
2344
|
+
process.exit(state.shuttingDown ? 0 : 1);
|
|
2345
|
+
return;
|
|
2346
|
+
default:
|
|
2347
|
+
if (requestHasId) {
|
|
2348
|
+
sendError(message.id, METHOD_NOT_FOUND, `method not found: ${message.method}`);
|
|
2349
|
+
}
|
|
2350
|
+
}
|
|
2351
|
+
} catch (err) {
|
|
2352
|
+
if (requestHasId) {
|
|
2353
|
+
sendError(message.id, message.method === "workspace/executeCommand" ? INVALID_PARAMS : INTERNAL_ERROR, err instanceof Error ? err.message : String(err));
|
|
2354
|
+
}
|
|
2355
|
+
}
|
|
2356
|
+
}
|
|
2357
|
+
function processBuffer(buffer, onMessage) {
|
|
2358
|
+
let remaining = buffer;
|
|
2359
|
+
while (true) {
|
|
2360
|
+
const headerEnd = remaining.indexOf(`\r
|
|
2361
|
+
\r
|
|
2362
|
+
`);
|
|
2363
|
+
if (headerEnd === -1)
|
|
2364
|
+
return remaining;
|
|
2365
|
+
const header = remaining.subarray(0, headerEnd).toString("ascii");
|
|
2366
|
+
const lengthMatch = header.match(/content-length:\s*(\d+)/i);
|
|
2367
|
+
if (!lengthMatch) {
|
|
2368
|
+
remaining = remaining.subarray(headerEnd + 4);
|
|
2369
|
+
continue;
|
|
2370
|
+
}
|
|
2371
|
+
const contentLength = Number.parseInt(lengthMatch[1], 10);
|
|
2372
|
+
const bodyStart = headerEnd + 4;
|
|
2373
|
+
const bodyEnd = bodyStart + contentLength;
|
|
2374
|
+
if (remaining.length < bodyEnd)
|
|
2375
|
+
return remaining;
|
|
2376
|
+
const body = remaining.subarray(bodyStart, bodyEnd).toString("utf8");
|
|
2377
|
+
remaining = remaining.subarray(bodyEnd);
|
|
2378
|
+
try {
|
|
2379
|
+
onMessage(JSON.parse(body));
|
|
2380
|
+
} catch {
|
|
2381
|
+
sendError(null, INVALID_PARAMS, "invalid JSON-RPC payload");
|
|
2382
|
+
}
|
|
2383
|
+
}
|
|
2384
|
+
}
|
|
2385
|
+
function startZedLspServer() {
|
|
2386
|
+
let buffer = Buffer.alloc(0);
|
|
2387
|
+
const state = { shuttingDown: false };
|
|
2388
|
+
return new Promise((resolve3) => {
|
|
2389
|
+
process.stdin.on("data", (chunk) => {
|
|
2390
|
+
buffer = processBuffer(Buffer.concat([buffer, chunk]), (message) => {
|
|
2391
|
+
handleMessage(message, state);
|
|
2392
|
+
});
|
|
2393
|
+
});
|
|
2394
|
+
process.stdin.on("end", resolve3);
|
|
2395
|
+
process.stdin.resume();
|
|
2396
|
+
});
|
|
2397
|
+
}
|
|
2398
|
+
|
|
2167
2399
|
// src/cli.ts
|
|
2168
2400
|
var program = new Command;
|
|
2169
2401
|
function isPermissionError(err) {
|
|
@@ -2175,6 +2407,8 @@ var SERVER_LOCK_PATH = join4(READIT_DIR, "server.lock");
|
|
|
2175
2407
|
var SERVER_LOCK_MAX_AGE_MS = 30000;
|
|
2176
2408
|
var SERVER_LOCK_TIMEOUT_MS = 1e4;
|
|
2177
2409
|
var SERVER_LOCK_WAIT_MS = 100;
|
|
2410
|
+
var BACKGROUND_SERVER_TIMEOUT_MS = 1e4;
|
|
2411
|
+
var BACKGROUND_SERVER_WAIT_MS = 100;
|
|
2178
2412
|
function isAlive(pid) {
|
|
2179
2413
|
try {
|
|
2180
2414
|
process.kill(pid, 0);
|
|
@@ -2189,6 +2423,52 @@ function getErrnoCode(err) {
|
|
|
2189
2423
|
function sleep(ms) {
|
|
2190
2424
|
return new Promise((resolve4) => setTimeout(resolve4, ms));
|
|
2191
2425
|
}
|
|
2426
|
+
function parsePort(value) {
|
|
2427
|
+
const port = Number.parseInt(value, 10);
|
|
2428
|
+
if (Number.isNaN(port) || port < 1 || port > 65535) {
|
|
2429
|
+
throw new Error(`invalid port number: ${value}`);
|
|
2430
|
+
}
|
|
2431
|
+
return port;
|
|
2432
|
+
}
|
|
2433
|
+
function readRawOption(name, shortName) {
|
|
2434
|
+
const args = process.argv.slice(2);
|
|
2435
|
+
const longName = `--${name}`;
|
|
2436
|
+
for (let i = 0;i < args.length; i++) {
|
|
2437
|
+
const arg = args[i];
|
|
2438
|
+
if (arg === longName || shortName && arg === shortName) {
|
|
2439
|
+
const value = args[i + 1];
|
|
2440
|
+
return value && !value.startsWith("-") ? value : undefined;
|
|
2441
|
+
}
|
|
2442
|
+
if (arg.startsWith(`${longName}=`)) {
|
|
2443
|
+
return arg.slice(longName.length + 1);
|
|
2444
|
+
}
|
|
2445
|
+
}
|
|
2446
|
+
return;
|
|
2447
|
+
}
|
|
2448
|
+
function hasRawFlag(name) {
|
|
2449
|
+
return process.argv.slice(2).includes(`--${name}`);
|
|
2450
|
+
}
|
|
2451
|
+
function resolveExistingPath(arg) {
|
|
2452
|
+
const inputPath = resolve3(process.cwd(), arg);
|
|
2453
|
+
if (!existsSync(inputPath)) {
|
|
2454
|
+
console.error(`error: not found: ${inputPath}`);
|
|
2455
|
+
process.exit(1);
|
|
2456
|
+
}
|
|
2457
|
+
return realpathSync(inputPath);
|
|
2458
|
+
}
|
|
2459
|
+
function resolveMarkdownFile(arg) {
|
|
2460
|
+
const filePath = resolveExistingPath(arg);
|
|
2461
|
+
const stat3 = statSync(filePath);
|
|
2462
|
+
if (stat3.isDirectory()) {
|
|
2463
|
+
console.error(`error: expected a Markdown file, got directory: ${arg}`);
|
|
2464
|
+
process.exit(1);
|
|
2465
|
+
}
|
|
2466
|
+
if (!isMarkdownFile(filePath)) {
|
|
2467
|
+
console.error(`error: unsupported file type: ${arg} (expected .md or .markdown)`);
|
|
2468
|
+
process.exit(1);
|
|
2469
|
+
}
|
|
2470
|
+
return filePath;
|
|
2471
|
+
}
|
|
2192
2472
|
async function clearStaleServerLock() {
|
|
2193
2473
|
try {
|
|
2194
2474
|
const [stats, content] = await Promise.all([
|
|
@@ -2259,31 +2539,82 @@ async function discoverServer() {
|
|
|
2259
2539
|
return null;
|
|
2260
2540
|
}
|
|
2261
2541
|
}
|
|
2262
|
-
async function
|
|
2542
|
+
async function addDocumentToServer(server, file) {
|
|
2543
|
+
const res = await fetch(`http://127.0.0.1:${server.port}/api/documents`, {
|
|
2544
|
+
method: "POST",
|
|
2545
|
+
headers: { "Content-Type": "application/json" },
|
|
2546
|
+
body: JSON.stringify({ path: file.path })
|
|
2547
|
+
});
|
|
2548
|
+
const data = await res.json();
|
|
2549
|
+
if (!res.ok) {
|
|
2550
|
+
throw new Error(`failed to add ${file.path}: ${data.error ?? res.statusText}`);
|
|
2551
|
+
}
|
|
2552
|
+
return data;
|
|
2553
|
+
}
|
|
2554
|
+
async function attachFiles(server, files, opts = {}) {
|
|
2263
2555
|
for (const file of files) {
|
|
2264
2556
|
try {
|
|
2265
|
-
const
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
console.error(`error: failed to add ${file.path}: ${data2.error}`);
|
|
2273
|
-
process.exit(1);
|
|
2274
|
-
}
|
|
2275
|
-
const data = await res.json();
|
|
2276
|
-
if (data.status === "added") {
|
|
2277
|
-
console.log(`Added: ${data.fileName}`);
|
|
2278
|
-
} else {
|
|
2279
|
-
console.log(`Present: ${data.fileName}`);
|
|
2557
|
+
const data = await addDocumentToServer(server, file);
|
|
2558
|
+
if (!opts.quiet) {
|
|
2559
|
+
if (data.status === "added") {
|
|
2560
|
+
console.log(`Added: ${data.fileName}`);
|
|
2561
|
+
} else {
|
|
2562
|
+
console.log(`Present: ${data.fileName}`);
|
|
2563
|
+
}
|
|
2280
2564
|
}
|
|
2281
2565
|
} catch (err) {
|
|
2282
|
-
console.error("error: failed to
|
|
2566
|
+
console.error("error: failed to add document:", err instanceof Error ? err.message : err);
|
|
2283
2567
|
process.exit(1);
|
|
2284
2568
|
}
|
|
2285
2569
|
}
|
|
2286
2570
|
}
|
|
2571
|
+
function readitUrlForFile(port, filePath) {
|
|
2572
|
+
return `http://127.0.0.1:${port}/?path=${encodeURIComponent(filePath)}`;
|
|
2573
|
+
}
|
|
2574
|
+
function getCurrentCliInvocation(args) {
|
|
2575
|
+
if (!process.versions.bun) {
|
|
2576
|
+
throw new Error("Bun is required to start readit. Install Bun and retry.");
|
|
2577
|
+
}
|
|
2578
|
+
const entrypoint = process.argv[1];
|
|
2579
|
+
if (!entrypoint) {
|
|
2580
|
+
throw new Error("failed to locate the readit CLI entrypoint");
|
|
2581
|
+
}
|
|
2582
|
+
return {
|
|
2583
|
+
command: process.execPath,
|
|
2584
|
+
args: [entrypoint, ...args]
|
|
2585
|
+
};
|
|
2586
|
+
}
|
|
2587
|
+
async function waitForBackgroundServer(pid) {
|
|
2588
|
+
const startedAt = Date.now();
|
|
2589
|
+
while (Date.now() - startedAt < BACKGROUND_SERVER_TIMEOUT_MS) {
|
|
2590
|
+
const server = await discoverServer();
|
|
2591
|
+
if (server)
|
|
2592
|
+
return server;
|
|
2593
|
+
if (pid !== undefined && !isAlive(pid)) {
|
|
2594
|
+
throw new Error("readit server process exited before becoming healthy");
|
|
2595
|
+
}
|
|
2596
|
+
await sleep(BACKGROUND_SERVER_WAIT_MS);
|
|
2597
|
+
}
|
|
2598
|
+
throw new Error("timed out waiting for readit server to start");
|
|
2599
|
+
}
|
|
2600
|
+
async function startBackgroundServer(files, port, host) {
|
|
2601
|
+
const invocation = getCurrentCliInvocation([
|
|
2602
|
+
"--port",
|
|
2603
|
+
String(port),
|
|
2604
|
+
"--host",
|
|
2605
|
+
host,
|
|
2606
|
+
"--no-open",
|
|
2607
|
+
...files.map((file) => file.filePath)
|
|
2608
|
+
]);
|
|
2609
|
+
const child = spawn2(invocation.command, invocation.args, {
|
|
2610
|
+
cwd: process.cwd(),
|
|
2611
|
+
detached: true,
|
|
2612
|
+
env: { ...process.env },
|
|
2613
|
+
stdio: "ignore"
|
|
2614
|
+
});
|
|
2615
|
+
child.unref();
|
|
2616
|
+
return waitForBackgroundServer(child.pid);
|
|
2617
|
+
}
|
|
2287
2618
|
async function getServerTarget(files, port, host) {
|
|
2288
2619
|
return withServerLock(async () => {
|
|
2289
2620
|
const server = await discoverServer();
|
|
@@ -2365,12 +2696,7 @@ function resolveFiles(args) {
|
|
|
2365
2696
|
const seen = new Set;
|
|
2366
2697
|
const files = [];
|
|
2367
2698
|
for (const arg of args) {
|
|
2368
|
-
const
|
|
2369
|
-
if (!existsSync(inputPath)) {
|
|
2370
|
-
console.error(`error: not found: ${inputPath}`);
|
|
2371
|
-
process.exit(1);
|
|
2372
|
-
}
|
|
2373
|
-
const filePath = realpathSync(inputPath);
|
|
2699
|
+
const filePath = resolveExistingPath(arg);
|
|
2374
2700
|
const stat3 = statSync(filePath);
|
|
2375
2701
|
if (stat3.isDirectory()) {
|
|
2376
2702
|
const found = findReviewableFiles(filePath);
|
|
@@ -2567,9 +2893,11 @@ program.argument("[files...]", "Markdown files/directories to review").option("-
|
|
|
2567
2893
|
process.exit(1);
|
|
2568
2894
|
}
|
|
2569
2895
|
}
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2896
|
+
let preferredPort;
|
|
2897
|
+
try {
|
|
2898
|
+
preferredPort = parsePort(options.port);
|
|
2899
|
+
} catch (err) {
|
|
2900
|
+
console.error("error:", err instanceof Error ? err.message : String(err));
|
|
2573
2901
|
process.exit(1);
|
|
2574
2902
|
}
|
|
2575
2903
|
let previousPort;
|
|
@@ -2631,25 +2959,55 @@ Shutting down...`);
|
|
|
2631
2959
|
process.exit(1);
|
|
2632
2960
|
}
|
|
2633
2961
|
});
|
|
2962
|
+
program.command("zed-open").argument("<file>", "Markdown file to open in readit from Zed").description("Open a Markdown file in readit and return after launch").option("-p, --port <number>", "Port for new server (if starting)", "4567").option("--host <address>", "Host for new server (if starting)", "127.0.0.1").option("--no-open", "Don't automatically open browser").action(async (fileArg, options) => {
|
|
2963
|
+
const filePath = resolveMarkdownFile(fileArg);
|
|
2964
|
+
const host = readRawOption("host") ?? options.host;
|
|
2965
|
+
const shouldOpen = options.open && !hasRawFlag("no-open");
|
|
2966
|
+
let preferredPort;
|
|
2967
|
+
try {
|
|
2968
|
+
preferredPort = parsePort(readRawOption("port", "-p") ?? options.port);
|
|
2969
|
+
} catch (err) {
|
|
2970
|
+
console.error("error:", err instanceof Error ? err.message : String(err));
|
|
2971
|
+
process.exit(1);
|
|
2972
|
+
}
|
|
2973
|
+
try {
|
|
2974
|
+
const file = { path: filePath };
|
|
2975
|
+
const server = await withServerLock(async () => {
|
|
2976
|
+
const existing = await discoverServer();
|
|
2977
|
+
if (existing)
|
|
2978
|
+
return existing;
|
|
2979
|
+
return startBackgroundServer([{ filePath }], preferredPort, host);
|
|
2980
|
+
});
|
|
2981
|
+
await attachFiles(server, [file], { quiet: true });
|
|
2982
|
+
const url = readitUrlForFile(server.port, filePath);
|
|
2983
|
+
if (shouldOpen) {
|
|
2984
|
+
await open2(url);
|
|
2985
|
+
}
|
|
2986
|
+
console.log(`Opened: ${url}`);
|
|
2987
|
+
} catch (err) {
|
|
2988
|
+
console.error("error: failed to open readit from Zed:", err instanceof Error ? err.message : err);
|
|
2989
|
+
process.exit(1);
|
|
2990
|
+
}
|
|
2991
|
+
});
|
|
2992
|
+
program.command("zed-lsp").description("Run the readit Zed LSP bridge").action(async () => {
|
|
2993
|
+
await startZedLspServer();
|
|
2994
|
+
});
|
|
2634
2995
|
program.command("open").argument("<files...>", "Markdown files to add to running server").description("Add files to a running readit server, or start a new one").option("-p, --port <number>", "Port for new server (if starting)", "4567").option("--host <address>", "Host for new server (if starting)", "127.0.0.1").action(async (fileArgs, options) => {
|
|
2635
2996
|
const resolvedFiles = [];
|
|
2636
2997
|
for (const arg of fileArgs) {
|
|
2637
|
-
const
|
|
2638
|
-
if (!existsSync(inputPath)) {
|
|
2639
|
-
console.error(`error: not found: ${inputPath}`);
|
|
2640
|
-
process.exit(1);
|
|
2641
|
-
}
|
|
2642
|
-
const filePath = realpathSync(inputPath);
|
|
2643
|
-
if (!isMarkdownFile(filePath)) {
|
|
2644
|
-
console.error(`error: unsupported file type: ${arg} (expected .md or .markdown)`);
|
|
2645
|
-
process.exit(1);
|
|
2646
|
-
}
|
|
2998
|
+
const filePath = resolveMarkdownFile(arg);
|
|
2647
2999
|
resolvedFiles.push({ path: filePath });
|
|
2648
3000
|
}
|
|
2649
3001
|
const files = resolvedFiles.map((f) => ({
|
|
2650
3002
|
filePath: f.path
|
|
2651
3003
|
}));
|
|
2652
|
-
|
|
3004
|
+
let preferredPort;
|
|
3005
|
+
try {
|
|
3006
|
+
preferredPort = parsePort(options.port);
|
|
3007
|
+
} catch (err) {
|
|
3008
|
+
console.error("error:", err instanceof Error ? err.message : String(err));
|
|
3009
|
+
process.exit(1);
|
|
3010
|
+
}
|
|
2653
3011
|
try {
|
|
2654
3012
|
const target = await getServerTarget(files, preferredPort, options.host);
|
|
2655
3013
|
if (target.kind === "existing") {
|
|
@@ -2749,6 +3107,8 @@ _readit() {
|
|
|
2749
3107
|
cmd_or_files)
|
|
2750
3108
|
local -a commands=(
|
|
2751
3109
|
'open:Add files to running server'
|
|
3110
|
+
'zed-open:Open a Markdown file from Zed'
|
|
3111
|
+
'zed-lsp:Run the Zed LSP bridge'
|
|
2752
3112
|
'list:List files with comments'
|
|
2753
3113
|
'show:Show comments for a file'
|
|
2754
3114
|
'completion:Output shell completion script'
|
|
@@ -2757,7 +3117,7 @@ _readit() {
|
|
|
2757
3117
|
;;
|
|
2758
3118
|
args)
|
|
2759
3119
|
case "\${line[1]}" in
|
|
2760
|
-
open) _arguments '*:file:_readit_markdown_files' ;;
|
|
3120
|
+
open|zed-open) _arguments '*:file:_readit_markdown_files' ;;
|
|
2761
3121
|
show) _arguments '1:file:_files -g "*.md *.markdown"' ;;
|
|
2762
3122
|
*) _arguments '*:file:_readit_markdown_files' ;;
|
|
2763
3123
|
esac
|
|
@@ -2777,7 +3137,7 @@ _readit_completions() {
|
|
|
2777
3137
|
COMPREPLY=()
|
|
2778
3138
|
cur="\${COMP_WORDS[COMP_CWORD]}"
|
|
2779
3139
|
prev="\${COMP_WORDS[COMP_CWORD-1]}"
|
|
2780
|
-
commands="open list show completion"
|
|
3140
|
+
commands="open zed-open zed-lsp list show completion"
|
|
2781
3141
|
|
|
2782
3142
|
if [[ \${COMP_CWORD} -eq 1 ]]; then
|
|
2783
3143
|
COMPREPLY=( $(compgen -W "\${commands}" -- "\${cur}") )
|
|
@@ -2789,7 +3149,7 @@ _readit_completions() {
|
|
|
2789
3149
|
fi
|
|
2790
3150
|
|
|
2791
3151
|
case "\${COMP_WORDS[1]}" in
|
|
2792
|
-
open|show)
|
|
3152
|
+
open|zed-open|show)
|
|
2793
3153
|
local files=$(find . -type f \\( -name '*.md' -o -name '*.markdown' \\) \\
|
|
2794
3154
|
-not -path '*/.*' -not -path '*/node_modules/*' 2>/dev/null | sed 's|^\\./||')
|
|
2795
3155
|
COMPREPLY=( $(compgen -W "\${files}" -- "\${cur}") )
|
|
@@ -2814,6 +3174,8 @@ complete -c readit -f
|
|
|
2814
3174
|
|
|
2815
3175
|
# Subcommands
|
|
2816
3176
|
complete -c readit -n '__fish_use_subcommand' -a 'open' -d 'Add files to running server'
|
|
3177
|
+
complete -c readit -n '__fish_use_subcommand' -a 'zed-open' -d 'Open a Markdown file from Zed'
|
|
3178
|
+
complete -c readit -n '__fish_use_subcommand' -a 'zed-lsp' -d 'Run the Zed LSP bridge'
|
|
2817
3179
|
complete -c readit -n '__fish_use_subcommand' -a 'list' -d 'List files with comments'
|
|
2818
3180
|
complete -c readit -n '__fish_use_subcommand' -a 'show' -d 'Show comments for a file'
|
|
2819
3181
|
complete -c readit -n '__fish_use_subcommand' -a 'completion' -d 'Output shell completion script'
|
|
@@ -2827,6 +3189,7 @@ complete -c readit -l clean -d 'Clear existing comments'
|
|
|
2827
3189
|
# File arguments for default command and open
|
|
2828
3190
|
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|^\\./||")'
|
|
2829
3191
|
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|^\\./||")'
|
|
3192
|
+
complete -c readit -n '__fish_seen_subcommand_from zed-open' -F -a '(find . -type f \\( -name "*.md" -o -name "*.markdown" \\) -not -path "*/.*" -not -path "*/node_modules/*" 2>/dev/null | sed "s|^\\./||")'
|
|
2830
3193
|
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|^\\./||")'
|
|
2831
3194
|
|
|
2832
3195
|
# Shell completions for completion subcommand
|
package/package.json
CHANGED
package/shell/_readit
CHANGED
|
@@ -111,6 +111,8 @@ _readit() {
|
|
|
111
111
|
local -a commands
|
|
112
112
|
commands=(
|
|
113
113
|
'open:Add files to a running server or start a new one'
|
|
114
|
+
'zed-open:Open a Markdown file from Zed'
|
|
115
|
+
'zed-lsp:Run the Zed LSP bridge'
|
|
114
116
|
'list:List all files with comments'
|
|
115
117
|
'show:Show comments for a file'
|
|
116
118
|
'completion:Output shell completion script'
|
|
@@ -128,6 +130,17 @@ _readit() {
|
|
|
128
130
|
'--host[Host for new server]:host:' \
|
|
129
131
|
'*:file:_readit_markdown_files'
|
|
130
132
|
;;
|
|
133
|
+
zed-open)
|
|
134
|
+
_arguments \
|
|
135
|
+
'-p[Port for new server]:port:' \
|
|
136
|
+
'--port[Port for new server]:port:' \
|
|
137
|
+
'--host[Host for new server]:host:' \
|
|
138
|
+
'--no-open[Do not automatically open browser]' \
|
|
139
|
+
'1:file:_readit_markdown_files'
|
|
140
|
+
;;
|
|
141
|
+
zed-lsp)
|
|
142
|
+
# Internal command used by the Zed extension.
|
|
143
|
+
;;
|
|
131
144
|
show)
|
|
132
145
|
_arguments \
|
|
133
146
|
'1:file:_readit_commented_files'
|