mdzilla 0.0.6 → 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 +47 -60
- package/dist/_chunks/exporter.mjs +174 -62
- package/dist/_chunks/server.mjs +394 -370
- package/dist/cli/main.mjs +167 -596
- package/dist/index.d.mts +105 -64
- package/dist/index.mjs +2 -2
- package/package.json +10 -11
package/dist/cli/main.mjs
CHANGED
|
@@ -1,43 +1,23 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { l as extractSnippets, n as writeCollection, r as resolveSource, u as Collection } from "../_chunks/exporter.mjs";
|
|
3
|
+
import { parseMeta, renderToAnsi, renderToText } from "md4x";
|
|
3
4
|
import { readFile } from "node:fs/promises";
|
|
4
5
|
import { basename } from "node:path";
|
|
5
|
-
import { parseMeta, renderToAnsi, renderToText } from "md4x";
|
|
6
6
|
import { parseArgs } from "node:util";
|
|
7
7
|
import { isAgent } from "std-env";
|
|
8
8
|
import { highlightText } from "@speed-highlight/core/terminal";
|
|
9
|
-
import { exec,
|
|
9
|
+
import { exec, execFile } from "node:child_process";
|
|
10
10
|
//#region src/cli/_ansi.ts
|
|
11
11
|
const noColor = !!(process.env.NO_COLOR || process.env.TERM === "dumb" || !process.stdout.isTTY || isAgent);
|
|
12
12
|
const ESC = "\x1B[";
|
|
13
13
|
const ANSI_RE = /\x1B(?:\[[0-9;]*[a-zA-Z]|\][^\x07\x1B]*(?:\x07|\x1B\\))/g;
|
|
14
|
-
const clear = () => process.stdout.write(`${ESC}2J${ESC}3J${ESC}H`);
|
|
15
|
-
const enterAltScreen = () => process.stdout.write(`${ESC}?1049h`);
|
|
16
|
-
const leaveAltScreen = () => process.stdout.write(`${ESC}?1049l`);
|
|
17
|
-
const hideCursor = () => process.stdout.write(`${ESC}?25l`);
|
|
18
|
-
const showCursor = () => process.stdout.write(`${ESC}?25h`);
|
|
19
14
|
const bold = (s) => noColor ? s : `${ESC}1m${s}${ESC}0m`;
|
|
20
15
|
const dim = (s) => noColor ? s : `${ESC}2m${s}${ESC}0m`;
|
|
21
16
|
const cyan = (s) => noColor ? s : `${ESC}36m${s}${ESC}0m`;
|
|
22
17
|
const yellow = (s) => noColor ? s : `${ESC}33m${s}${ESC}0m`;
|
|
23
|
-
const bgGray = (s) => {
|
|
24
|
-
if (noColor) return s;
|
|
25
|
-
const bg = `${ESC}48;5;237m`;
|
|
26
|
-
return `${bg}${s.replaceAll(`${ESC}0m`, `${ESC}0m${bg}`)}${ESC}0m`;
|
|
27
|
-
};
|
|
28
18
|
const stripAnsi = (s) => s.replace(ANSI_RE, "");
|
|
29
|
-
const visibleLength = (s) => stripAnsi(s).length;
|
|
30
|
-
function padTo(s, width) {
|
|
31
|
-
const visible = visibleLength(s);
|
|
32
|
-
if (visible >= width) return truncateTo(s, width);
|
|
33
|
-
return s + " ".repeat(width - visible);
|
|
34
|
-
}
|
|
35
|
-
function truncateTo(s, width) {
|
|
36
|
-
if (visibleLength(s) <= width) return s;
|
|
37
|
-
return wrapAnsi(s, width)[0] || "";
|
|
38
|
-
}
|
|
39
19
|
function wrapAnsi(s, width) {
|
|
40
|
-
if (width <= 0 ||
|
|
20
|
+
if (width <= 0 || stripAnsi(s).length <= width) return [s];
|
|
41
21
|
const tokens = [];
|
|
42
22
|
let last = 0;
|
|
43
23
|
const re = new RegExp(ANSI_RE.source, "g");
|
|
@@ -83,74 +63,18 @@ function wrapAnsi(s, width) {
|
|
|
83
63
|
}
|
|
84
64
|
return lines;
|
|
85
65
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
const re = new RegExp(ANSI_RE.source, "g");
|
|
90
|
-
const visibleChars = [];
|
|
91
|
-
let last = 0;
|
|
92
|
-
let match;
|
|
93
|
-
while ((match = re.exec(s)) !== null) {
|
|
94
|
-
for (let i = last; i < match.index; i++) visibleChars.push({
|
|
95
|
-
char: s[i],
|
|
96
|
-
rawIdx: i
|
|
97
|
-
});
|
|
98
|
-
last = match.index + match[0].length;
|
|
99
|
-
}
|
|
100
|
-
for (let i = last; i < s.length; i++) visibleChars.push({
|
|
101
|
-
char: s[i],
|
|
102
|
-
rawIdx: i
|
|
103
|
-
});
|
|
104
|
-
const lowerVisible = visibleChars.map((c) => c.char).join("").toLowerCase();
|
|
66
|
+
function highlight(text, query) {
|
|
67
|
+
if (!query) return text;
|
|
68
|
+
const lowerText = text.toLowerCase();
|
|
105
69
|
const lowerQuery = query.toLowerCase();
|
|
106
|
-
const matches = [];
|
|
107
|
-
let pos = 0;
|
|
108
|
-
while ((pos = lowerVisible.indexOf(lowerQuery, pos)) >= 0) {
|
|
109
|
-
matches.push([pos, pos + query.length]);
|
|
110
|
-
pos += 1;
|
|
111
|
-
}
|
|
112
|
-
if (matches.length === 0) return s;
|
|
113
|
-
const hlRanges = [];
|
|
114
|
-
for (const [start, end] of matches) {
|
|
115
|
-
const rawStart = visibleChars[start].rawIdx;
|
|
116
|
-
const rawEnd = end < visibleChars.length ? visibleChars[end].rawIdx : s.length;
|
|
117
|
-
hlRanges.push([rawStart, rawEnd]);
|
|
118
|
-
}
|
|
119
|
-
const hlOn = `${ESC}7m`;
|
|
120
|
-
const hlOff = `${ESC}27m`;
|
|
121
|
-
const resetSeq = `${ESC}0m`;
|
|
122
70
|
let result = "";
|
|
123
|
-
let
|
|
124
|
-
let
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
while ((escMatch = escRe.exec(s)) !== null) escapes.push({
|
|
129
|
-
idx: escMatch.index,
|
|
130
|
-
seq: escMatch[0]
|
|
131
|
-
});
|
|
132
|
-
let escIdx = 0;
|
|
133
|
-
for (rawIdx = 0; rawIdx <= s.length; rawIdx++) {
|
|
134
|
-
const wasIn = inHighlight;
|
|
135
|
-
inHighlight = hlRanges.some(([a, b]) => rawIdx >= a && rawIdx < b);
|
|
136
|
-
if (inHighlight && !wasIn) result += hlOn;
|
|
137
|
-
if (!inHighlight && wasIn) result += hlOff;
|
|
138
|
-
if (rawIdx >= s.length) break;
|
|
139
|
-
if (escIdx < escapes.length && escapes[escIdx].idx === rawIdx) {
|
|
140
|
-
const esc = escapes[escIdx];
|
|
141
|
-
result += esc.seq;
|
|
142
|
-
if (inHighlight && esc.seq === resetSeq) result += hlOn;
|
|
143
|
-
rawIdx += esc.seq.length - 1;
|
|
144
|
-
escIdx++;
|
|
145
|
-
} else result += s[rawIdx];
|
|
71
|
+
let last = 0;
|
|
72
|
+
let pos = 0;
|
|
73
|
+
while ((pos = lowerText.indexOf(lowerQuery, last)) >= 0) {
|
|
74
|
+
result += text.slice(last, pos) + bold(yellow(text.slice(pos, pos + query.length)));
|
|
75
|
+
last = pos + query.length;
|
|
146
76
|
}
|
|
147
|
-
return result;
|
|
148
|
-
}
|
|
149
|
-
function highlight(text, query) {
|
|
150
|
-
if (!query) return text;
|
|
151
|
-
const idx = text.toLowerCase().indexOf(query.toLowerCase());
|
|
152
|
-
if (idx < 0) return text;
|
|
153
|
-
return text.slice(0, idx) + bold(yellow(text.slice(idx, idx + query.length))) + text.slice(idx + query.length);
|
|
77
|
+
return last === 0 ? text : result + text.slice(last);
|
|
154
78
|
}
|
|
155
79
|
//#endregion
|
|
156
80
|
//#region src/cli/_usage.ts
|
|
@@ -175,31 +99,34 @@ function printUsage(hasInput) {
|
|
|
175
99
|
` ${bold("mdzilla")} ${dim("— Markdown browser for humans and agents")}`,
|
|
176
100
|
"",
|
|
177
101
|
`${bold("Usage:")}`,
|
|
178
|
-
` ${bin} ${cyan("<
|
|
179
|
-
` ${bin} ${cyan("<
|
|
180
|
-
` ${bin} ${cyan("
|
|
181
|
-
` ${bin} ${cyan("
|
|
182
|
-
|
|
102
|
+
` ${bin} ${cyan("<source>")} ${dim("Open docs in browser")}`,
|
|
103
|
+
` ${bin} ${cyan("<source>")} ${cyan("<path>")} ${dim("Render a specific page")}`,
|
|
104
|
+
` ${bin} ${cyan("<source>")} ${cyan("<query>")} ${dim("Search docs")}`,
|
|
105
|
+
` ${bin} ${cyan("<file.md>")} ${dim("Render a single markdown file")}`,
|
|
106
|
+
"",
|
|
107
|
+
`${bold("Sources:")}`,
|
|
108
|
+
` ${cyan("./docs")} Local directory`,
|
|
109
|
+
` ${cyan("gh:owner/repo")} GitHub repo (looks for docs/ directory)`,
|
|
110
|
+
` ${cyan("npm:package-name")} npm package docs`,
|
|
111
|
+
` ${cyan("https://example.com")} Remote docs via HTTP / llms.txt`,
|
|
183
112
|
"",
|
|
184
113
|
`${bold("Options:")}`,
|
|
185
114
|
` ${cyan("--export")} ${dim("<dir>")} Export docs to flat .md files`,
|
|
186
|
-
` ${cyan("--
|
|
187
|
-
` ${cyan("--plain")} Plain text output (no TUI)`,
|
|
188
|
-
` ${cyan("--headless")} Alias for --plain`,
|
|
115
|
+
` ${cyan("--plain")} Plain text output (auto-enabled for agents / non-TTY)`,
|
|
189
116
|
` ${cyan("-h, --help")} Show this help message`,
|
|
190
117
|
"",
|
|
191
|
-
`${bold("
|
|
192
|
-
` ${dim("
|
|
193
|
-
` ${
|
|
194
|
-
` ${
|
|
195
|
-
` ${
|
|
118
|
+
`${bold("Examples:")}`,
|
|
119
|
+
` ${bin} ${cyan("gh:unjs/h3")} ${dim("Open H3 docs in browser")}`,
|
|
120
|
+
` ${bin} ${cyan("gh:unjs/h3 /guide/basics")} ${dim("Render a specific page")}`,
|
|
121
|
+
` ${bin} ${cyan("gh:unjs/h3 router")} ${dim("Search for 'router'")}`,
|
|
122
|
+
` ${bin} ${cyan("gh:unjs/h3 --export ./h3-docs")} ${dim("Export to flat files")}`
|
|
196
123
|
].join("\n"));
|
|
197
124
|
process.exit(hasInput ? 0 : 1);
|
|
198
125
|
}
|
|
199
126
|
//#endregion
|
|
200
127
|
//#region src/cli/content.ts
|
|
201
|
-
async function renderContent(content, entry
|
|
202
|
-
const
|
|
128
|
+
async function renderContent(content, entry) {
|
|
129
|
+
const cols = process.stdout.columns || 80;
|
|
203
130
|
const codeBlocks = [];
|
|
204
131
|
const codeRe = /```(\w+)[^\n]*\n([\s\S]*?)```/g;
|
|
205
132
|
let m;
|
|
@@ -230,18 +157,27 @@ async function renderContent(content, entry, navWidth) {
|
|
|
230
157
|
if (hl) {
|
|
231
158
|
const hlLines = hl.split("\n");
|
|
232
159
|
if (hlLines.length > 0 && hlLines[hlLines.length - 1] === "") hlLines.pop();
|
|
233
|
-
for (const hlLine of hlLines) for (const w of wrapAnsi(" " + hlLine,
|
|
234
|
-
} else for (const dl of dimLines) for (const w of wrapAnsi(dl,
|
|
160
|
+
for (const hlLine of hlLines) for (const w of wrapAnsi(" " + hlLine, cols)) lines.push(w + "\x1B[0m");
|
|
161
|
+
} else for (const dl of dimLines) for (const w of wrapAnsi(dl, cols)) lines.push(w + "\x1B[0m");
|
|
235
162
|
blockIdx++;
|
|
236
163
|
lines.push("");
|
|
237
164
|
} else dimLines.push(rawLine);
|
|
238
165
|
continue;
|
|
239
166
|
}
|
|
240
|
-
for (const w of wrapAnsi(rawLine,
|
|
167
|
+
for (const w of wrapAnsi(rawLine, cols)) lines.push(w + "\x1B[0m");
|
|
241
168
|
}
|
|
242
169
|
return lines;
|
|
243
170
|
}
|
|
244
171
|
//#endregion
|
|
172
|
+
//#region src/cli/_utils.ts
|
|
173
|
+
function openInBrowser(url) {
|
|
174
|
+
const parsed = new URL(url);
|
|
175
|
+
if (parsed.hostname === "[::]" || parsed.hostname === "[::1]" || parsed.hostname === "127.0.0.1") parsed.hostname = "localhost";
|
|
176
|
+
url = parsed.href;
|
|
177
|
+
if (process.platform === "win32") exec(`start "" ${JSON.stringify(url)}`, () => {});
|
|
178
|
+
else execFile(process.platform === "darwin" ? "open" : "xdg-open", [url], () => {});
|
|
179
|
+
}
|
|
180
|
+
//#endregion
|
|
245
181
|
//#region src/cli/render.ts
|
|
246
182
|
async function singleFileMode(filePath, plain, isURL) {
|
|
247
183
|
const raw = isURL ? await fetch(filePath, { headers: { accept: "text/markdown, text/plain;q=0.9, text/html;q=0.8" } }).then((r) => r.text()) : await readFile(filePath, "utf8");
|
|
@@ -256,16 +192,16 @@ async function singleFileMode(filePath, plain, isURL) {
|
|
|
256
192
|
path: "/" + slug,
|
|
257
193
|
title: meta.title || slug,
|
|
258
194
|
order: 0
|
|
259
|
-
}
|
|
195
|
+
});
|
|
260
196
|
process.stdout.write(lines.join("\n") + "\n");
|
|
261
197
|
}
|
|
262
|
-
async function
|
|
263
|
-
const
|
|
264
|
-
const { entry, raw } = await docs.resolvePage(normalized);
|
|
198
|
+
async function renderPage(docs, resolved, pagePath, plain) {
|
|
199
|
+
const { entry, raw } = resolved;
|
|
265
200
|
if (!raw) {
|
|
266
|
-
|
|
201
|
+
printNotFound(docs, pagePath);
|
|
267
202
|
process.exit(1);
|
|
268
203
|
}
|
|
204
|
+
const normalized = pagePath.startsWith("/") ? pagePath : "/" + pagePath;
|
|
269
205
|
const slug = (entry?.entry.path || normalized).split("/").pop() || "";
|
|
270
206
|
const navEntry = entry?.entry || {
|
|
271
207
|
slug,
|
|
@@ -273,13 +209,60 @@ async function pageMode(docs, pagePath, plain) {
|
|
|
273
209
|
title: parseMeta(raw).title || slug,
|
|
274
210
|
order: 0
|
|
275
211
|
};
|
|
276
|
-
if (plain)
|
|
277
|
-
|
|
278
|
-
|
|
212
|
+
if (plain) {
|
|
213
|
+
process.stdout.write(renderToText(raw) + "\n");
|
|
214
|
+
if (isAgent) process.stdout.write(agentTrailer(docs, normalized));
|
|
215
|
+
} else {
|
|
216
|
+
const lines = await renderContent(raw, navEntry);
|
|
279
217
|
process.stdout.write(lines.join("\n") + "\n");
|
|
280
218
|
}
|
|
281
219
|
}
|
|
282
|
-
async function
|
|
220
|
+
async function searchMode(docs, query, plain) {
|
|
221
|
+
let count = 0;
|
|
222
|
+
const matchedPaths = [];
|
|
223
|
+
const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
224
|
+
for await (const { flat: f, titleMatch, heading } of docs.search(query)) {
|
|
225
|
+
if (count === 0) process.stdout.write(bold(`Search results for "${query}":\n\n`));
|
|
226
|
+
count++;
|
|
227
|
+
matchedPaths.push(f.entry.path);
|
|
228
|
+
const raw = await docs.getContent(f);
|
|
229
|
+
const plainText = raw ? renderToText(raw) : "";
|
|
230
|
+
const snippets = plainText ? extractSnippets(plainText, terms) : [];
|
|
231
|
+
if (plain) {
|
|
232
|
+
const desc = f.entry.description ? ` — ${f.entry.description}` : "";
|
|
233
|
+
const badge = titleMatch && snippets.length > 0 ? " (title + content)" : titleMatch ? " (title)" : heading ? ` (heading: ${heading})` : ` (${snippets.length} match${snippets.length !== 1 ? "es" : ""})`;
|
|
234
|
+
process.stdout.write(`- **${f.entry.title}** \`${f.entry.path}\`${desc}${badge}\n`);
|
|
235
|
+
for (const s of snippets) process.stdout.write(` > ${s}\n`);
|
|
236
|
+
} else {
|
|
237
|
+
const subtitle = heading ? ` › ${cyan(heading)}` : "";
|
|
238
|
+
process.stdout.write(`${bold(cyan(f.entry.title))}${subtitle} ${dim(f.entry.path)}\n`);
|
|
239
|
+
for (const s of snippets) process.stdout.write(` ${highlight(s, query)}\n`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
if (count === 0) {
|
|
243
|
+
console.log(`No results for "${query}".`);
|
|
244
|
+
const suggestions = docs.suggest(query);
|
|
245
|
+
if (suggestions.length > 0) {
|
|
246
|
+
console.log("");
|
|
247
|
+
console.log(plain ? "Related pages:" : bold("Related pages:"));
|
|
248
|
+
for (const s of suggestions) if (plain) console.log(` - [${s.entry.title}](${s.entry.path})`);
|
|
249
|
+
else console.log(` ${bold(cyan(s.entry.title))} ${dim(s.entry.path)}`);
|
|
250
|
+
if (isAgent) {
|
|
251
|
+
console.log("");
|
|
252
|
+
console.log("To read a specific page, run this command again with the page path as second argument.");
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
} else if (isAgent) process.stdout.write([
|
|
256
|
+
"",
|
|
257
|
+
"---",
|
|
258
|
+
"",
|
|
259
|
+
`Found ${count} page${count > 1 ? "s" : ""} matching "${query}".`,
|
|
260
|
+
"To read a specific page, run this command again with the page path as second argument, for example:",
|
|
261
|
+
...[...new Set(matchedPaths)].slice(0, 3).map((p) => ` mdzilla <source> ${p}`),
|
|
262
|
+
""
|
|
263
|
+
].join("\n"));
|
|
264
|
+
}
|
|
265
|
+
async function tocMode(docs) {
|
|
283
266
|
const navigable = docs.pages;
|
|
284
267
|
if (navigable.length === 0) {
|
|
285
268
|
console.log("No pages found.");
|
|
@@ -291,451 +274,46 @@ async function plainMode(docs, pagePath) {
|
|
|
291
274
|
tocLines.push(`${indent}- [${f.entry.title}](${f.entry.path})`);
|
|
292
275
|
}
|
|
293
276
|
process.stdout.write(tocLines.join("\n") + "\n");
|
|
294
|
-
|
|
295
|
-
if (
|
|
296
|
-
|
|
297
|
-
if (resolved.raw) process.stdout.write(renderToText(resolved.raw) + "\n\n");
|
|
298
|
-
} else {
|
|
299
|
-
const raw = await docs.getContent(targetEntry);
|
|
300
|
-
if (raw) process.stdout.write(renderToText(raw) + "\n\n");
|
|
301
|
-
}
|
|
277
|
+
const raw = await docs.getContent(navigable[0]);
|
|
278
|
+
if (raw) process.stdout.write("\n" + renderToText(raw) + "\n");
|
|
279
|
+
if (isAgent && navigable.length > 1) process.stdout.write("\n---\n\nTo read a specific page, run this command again with the page path as second argument.\nTo search within pages, pass a search query as second argument.\n");
|
|
302
280
|
}
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
const
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
}
|
|
314
|
-
function renderNavPanel(flat, cursor, maxRows, width, search, searchMatches) {
|
|
315
|
-
const lines = [];
|
|
316
|
-
lines.push(search !== void 0 ? bold(" mdzilla") + " " + dim("/") + search + "▌" : bold(" mdzilla"));
|
|
317
|
-
lines.push("");
|
|
318
|
-
const listRows = maxRows - 2;
|
|
319
|
-
let start = 0;
|
|
320
|
-
if (flat.length > listRows) start = Math.max(0, Math.min(cursor - Math.floor(listRows / 2), flat.length - listRows));
|
|
321
|
-
const end = Math.min(start + listRows, flat.length);
|
|
322
|
-
const treePrefixes = computeTreePrefixes(flat, computeIsLastChild(flat));
|
|
323
|
-
for (let i = start; i < end; i++) {
|
|
324
|
-
const { entry, depth } = flat[i];
|
|
325
|
-
const isPage = entry.page !== false;
|
|
326
|
-
const active = i === cursor;
|
|
327
|
-
const maxTitle = width - (depth === 0 ? 3 : 2 + 2 * depth) - 1;
|
|
328
|
-
let displayTitle = entry.title;
|
|
329
|
-
if (displayTitle.length > maxTitle && maxTitle > 1) displayTitle = displayTitle.slice(0, maxTitle - 1) + "…";
|
|
330
|
-
const isMatch = searchMatches ? searchMatches.has(i) : false;
|
|
331
|
-
const title = search ? highlight(displayTitle, search) : displayTitle;
|
|
332
|
-
let label;
|
|
333
|
-
if (depth === 0) label = `${isPage ? dim("◆") : dim("◇")} ${search ? isMatch ? title : dim(displayTitle) : active || isPage ? title : dim(title)}`;
|
|
334
|
-
else label = ` ${dim(treePrefixes[i])}${search ? isMatch ? title : dim(displayTitle) : active || isPage ? title : dim(title)}`;
|
|
335
|
-
lines.push(active ? bgGray(padTo(label, width)) : label);
|
|
336
|
-
}
|
|
337
|
-
if (flat.length > listRows) lines.push(dim(` ${start > 0 ? "↑" : " "} ${end < flat.length ? "↓" : " "} ${cursor + 1}/${flat.length}`));
|
|
338
|
-
return lines;
|
|
339
|
-
}
|
|
340
|
-
function computeIsLastChild(flat) {
|
|
341
|
-
const result = Array.from({ length: flat.length });
|
|
342
|
-
for (let i = 0; i < flat.length; i++) {
|
|
343
|
-
const d = flat[i].depth;
|
|
344
|
-
let last = true;
|
|
345
|
-
for (let j = i + 1; j < flat.length; j++) {
|
|
346
|
-
if (flat[j].depth < d) break;
|
|
347
|
-
if (flat[j].depth === d) {
|
|
348
|
-
last = false;
|
|
349
|
-
break;
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
result[i] = last;
|
|
353
|
-
}
|
|
354
|
-
return result;
|
|
355
|
-
}
|
|
356
|
-
function computeTreePrefixes(flat, isLast) {
|
|
357
|
-
const prefixes = Array.from({ length: flat.length });
|
|
358
|
-
const continues = [];
|
|
359
|
-
for (let i = 0; i < flat.length; i++) {
|
|
360
|
-
const d = flat[i].depth;
|
|
361
|
-
if (d === 0) {
|
|
362
|
-
prefixes[i] = "";
|
|
363
|
-
continues.length = 1;
|
|
364
|
-
continues[0] = !isLast[i];
|
|
365
|
-
continue;
|
|
366
|
-
}
|
|
367
|
-
let prefix = "";
|
|
368
|
-
for (let level = 1; level < d; level++) prefix += continues[level] ? "│ " : " ";
|
|
369
|
-
prefix += isLast[i] ? "╰─" : "├─";
|
|
370
|
-
prefixes[i] = prefix;
|
|
371
|
-
continues[d] = !isLast[i];
|
|
372
|
-
continues.length = d + 1;
|
|
373
|
-
}
|
|
374
|
-
return prefixes;
|
|
281
|
+
async function serverMode(source, _docs) {
|
|
282
|
+
const { serve } = await import("srvx");
|
|
283
|
+
const { createDocsServer } = await import("../_chunks/server.mjs");
|
|
284
|
+
const server = serve({
|
|
285
|
+
fetch: (await createDocsServer({ source })).fetch,
|
|
286
|
+
gracefulShutdown: false
|
|
287
|
+
});
|
|
288
|
+
await server.ready();
|
|
289
|
+
await server.fetch(new Request(new URL("/api/meta", server.url)));
|
|
290
|
+
openInBrowser(server.url);
|
|
375
291
|
}
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
const
|
|
380
|
-
const
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
if (sidebarVisible) output.push(reset + padTo(navLines[i] || "", navWidth) + reset + (isFocusContent ? cyan("│") : dim("│")) + reset + " " + truncateTo(right, contentWidth) + reset);
|
|
393
|
-
else output.push(reset + " " + truncateTo(right, contentWidth) + reset);
|
|
394
|
-
}
|
|
395
|
-
output.push(search !== void 0 ? dim(" esc") + " cancel " + dim("enter") + " go" : focus === "content-search" ? dim(" /") + contentSearch + "▌ " + dim("esc") + " cancel " + dim("enter") + " confirm" : focus === "content" ? dim(" ↑↓") + " scroll " + dim("tab") + " links " + dim("⏎") + " open " + dim("/") + " search" + (contentSearch ? " " + dim("n") + "/" + dim("N") + " next/prev" : "") + " " + dim("t") + " sidebar " + dim("⌫") + " back " + dim("q") + " quit" : dim(" ↑↓") + " navigate " + dim("⏎") + " read " + dim("space") + " page ↓ " + dim("/") + " search " + dim("t") + " sidebar " + dim("q") + " quit");
|
|
396
|
-
const eol = "\x1B[K";
|
|
397
|
-
return output.map((l) => l + eol).join("\n");
|
|
292
|
+
function agentTrailer(docs, currentPath) {
|
|
293
|
+
const pages = docs.pages;
|
|
294
|
+
if (pages.length <= 1) return "";
|
|
295
|
+
const normalized = currentPath?.startsWith("/") ? currentPath : currentPath ? "/" + currentPath : void 0;
|
|
296
|
+
const otherPages = pages.filter((p) => p.entry.path !== normalized);
|
|
297
|
+
if (otherPages.length === 0) return "";
|
|
298
|
+
return "\n" + [
|
|
299
|
+
"---",
|
|
300
|
+
"",
|
|
301
|
+
"Other available pages:",
|
|
302
|
+
...otherPages.map((p) => ` - [${p.entry.title}](${p.entry.path})`),
|
|
303
|
+
"",
|
|
304
|
+
"To read a specific page, run this command again with the page path as second argument.",
|
|
305
|
+
"To search within pages, pass a search query as second argument.",
|
|
306
|
+
""
|
|
307
|
+
].join("\n");
|
|
398
308
|
}
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
process.exit(0);
|
|
309
|
+
function printNotFound(docs, pagePath) {
|
|
310
|
+
process.stderr.write(`Page not found: ${pagePath}\n`);
|
|
311
|
+
const suggestions = docs.suggest(pagePath);
|
|
312
|
+
if (suggestions.length > 0) {
|
|
313
|
+
process.stderr.write("\nDid you mean:\n");
|
|
314
|
+
for (const s of suggestions) process.stderr.write(` - ${s.entry.title} (${s.entry.path})\n`);
|
|
406
315
|
}
|
|
407
|
-
|
|
408
|
-
const nextNavigable = (list, from, dir) => {
|
|
409
|
-
let i = from + dir;
|
|
410
|
-
while (i >= 0 && i < list.length) {
|
|
411
|
-
if (isNavigable(list, i)) return i;
|
|
412
|
-
i += dir;
|
|
413
|
-
}
|
|
414
|
-
return from;
|
|
415
|
-
};
|
|
416
|
-
const firstNavigable = (list) => {
|
|
417
|
-
for (let i = 0; i < list.length; i++) if (isNavigable(list, i)) return i;
|
|
418
|
-
return 0;
|
|
419
|
-
};
|
|
420
|
-
let cursor = firstNavigable(flat);
|
|
421
|
-
let searching = false;
|
|
422
|
-
let searchQuery = "";
|
|
423
|
-
let searchMatches = [];
|
|
424
|
-
let contentScroll = 0;
|
|
425
|
-
let contentLines = [];
|
|
426
|
-
let loadedPath = "";
|
|
427
|
-
let focusContent = false;
|
|
428
|
-
let contentSearching = false;
|
|
429
|
-
let contentSearchQuery = "";
|
|
430
|
-
let contentMatches = [];
|
|
431
|
-
let contentMatchIdx = 0;
|
|
432
|
-
let sidebarVisible = true;
|
|
433
|
-
let contentLinks = [];
|
|
434
|
-
let linkIdx = -1;
|
|
435
|
-
const extractLinks = (lines) => {
|
|
436
|
-
const links = [];
|
|
437
|
-
const re = /\x1B\]8;;([^\x07\x1B]+?)(?:\x07|\x1B\\)/g;
|
|
438
|
-
for (let i = 0; i < lines.length; i++) {
|
|
439
|
-
re.lastIndex = 0;
|
|
440
|
-
let m;
|
|
441
|
-
let occ = 0;
|
|
442
|
-
while ((m = re.exec(lines[i])) !== null) links.push({
|
|
443
|
-
line: i,
|
|
444
|
-
url: m[1],
|
|
445
|
-
occurrence: occ++
|
|
446
|
-
});
|
|
447
|
-
}
|
|
448
|
-
return links;
|
|
449
|
-
};
|
|
450
|
-
const highlightLinkOnLine = (line, occurrence) => {
|
|
451
|
-
const oscRe = /\x1B\]8;;([^\x07\x1B]*?)(?:\x07|\x1B\\)/g;
|
|
452
|
-
let occ = 0;
|
|
453
|
-
let m;
|
|
454
|
-
while ((m = oscRe.exec(line)) !== null) {
|
|
455
|
-
if (!m[1]) continue;
|
|
456
|
-
if (occ === occurrence) {
|
|
457
|
-
const openerEnd = m.index + m[0].length;
|
|
458
|
-
const closer = oscRe.exec(line);
|
|
459
|
-
if (!closer) break;
|
|
460
|
-
return line.slice(0, openerEnd) + "\x1B[7m" + line.slice(openerEnd, closer.index) + "\x1B[27m" + line.slice(closer.index);
|
|
461
|
-
}
|
|
462
|
-
occ++;
|
|
463
|
-
}
|
|
464
|
-
return line;
|
|
465
|
-
};
|
|
466
|
-
const findContentMatches = (query) => {
|
|
467
|
-
if (!query) return [];
|
|
468
|
-
const lower = query.toLowerCase();
|
|
469
|
-
const matches = [];
|
|
470
|
-
for (let i = 0; i < contentLines.length; i++) if (stripAnsi(contentLines[i]).toLowerCase().includes(lower)) matches.push(i);
|
|
471
|
-
return matches;
|
|
472
|
-
};
|
|
473
|
-
const scrollToMatch = () => {
|
|
474
|
-
if (contentMatches.length === 0) return;
|
|
475
|
-
const rows = process.stdout.rows || 24;
|
|
476
|
-
const line = contentMatches[contentMatchIdx];
|
|
477
|
-
contentScroll = Math.max(0, Math.min(line - Math.floor(rows / 2), contentLines.length - rows + 2));
|
|
478
|
-
};
|
|
479
|
-
process.stdin.setRawMode(true);
|
|
480
|
-
process.stdin.resume();
|
|
481
|
-
enterAltScreen();
|
|
482
|
-
hideCursor();
|
|
483
|
-
const draw = () => {
|
|
484
|
-
let displayLines = contentLines;
|
|
485
|
-
if (linkIdx >= 0 && linkIdx < contentLinks.length) {
|
|
486
|
-
const link = contentLinks[linkIdx];
|
|
487
|
-
displayLines = [...contentLines];
|
|
488
|
-
displayLines[link.line] = highlightLinkOnLine(displayLines[link.line], link.occurrence);
|
|
489
|
-
}
|
|
490
|
-
const frame = renderSplit(flat, cursor, displayLines, contentScroll, searching ? searchQuery : void 0, contentSearching ? "content-search" : focusContent ? "content" : "nav", contentSearching ? contentSearchQuery : contentSearchQuery || void 0, searching ? new Set(searchMatches) : void 0, sidebarVisible);
|
|
491
|
-
process.stdout.write(`\x1B[H${frame}`);
|
|
492
|
-
};
|
|
493
|
-
const loadContent = (entry) => {
|
|
494
|
-
if (!entry?.filePath || entry.entry.page === false) {
|
|
495
|
-
if (loadedPath !== "") {
|
|
496
|
-
contentLines = [];
|
|
497
|
-
contentScroll = 0;
|
|
498
|
-
loadedPath = "";
|
|
499
|
-
draw();
|
|
500
|
-
}
|
|
501
|
-
return;
|
|
502
|
-
}
|
|
503
|
-
if (entry.filePath === loadedPath) return;
|
|
504
|
-
const targetPath = entry.filePath;
|
|
505
|
-
docs.getContent(entry).then(async (raw) => {
|
|
506
|
-
if (!raw || flat[cursor]?.filePath !== targetPath) return;
|
|
507
|
-
contentLines = await renderContent(raw, entry.entry, sidebarVisible ? calcNavWidth(flat) : 0);
|
|
508
|
-
contentScroll = 0;
|
|
509
|
-
contentSearchQuery = "";
|
|
510
|
-
contentMatches = [];
|
|
511
|
-
contentLinks = extractLinks(contentLines);
|
|
512
|
-
linkIdx = -1;
|
|
513
|
-
loadedPath = targetPath;
|
|
514
|
-
draw();
|
|
515
|
-
});
|
|
516
|
-
};
|
|
517
|
-
let cleaned = false;
|
|
518
|
-
const cleanup = (code = 0) => {
|
|
519
|
-
if (cleaned) return;
|
|
520
|
-
cleaned = true;
|
|
521
|
-
showCursor();
|
|
522
|
-
leaveAltScreen();
|
|
523
|
-
process.exit(code);
|
|
524
|
-
};
|
|
525
|
-
const reloadContent = () => {
|
|
526
|
-
const entry = flat[cursor];
|
|
527
|
-
if (!entry?.filePath || entry.entry.page === false) return;
|
|
528
|
-
docs.invalidate(entry.filePath);
|
|
529
|
-
loadedPath = "";
|
|
530
|
-
loadContent(entry);
|
|
531
|
-
};
|
|
532
|
-
process.on("SIGINT", () => cleanup());
|
|
533
|
-
process.on("SIGTERM", () => cleanup());
|
|
534
|
-
process.on("uncaughtException", (err) => {
|
|
535
|
-
if (cleaned) return;
|
|
536
|
-
cleaned = true;
|
|
537
|
-
showCursor();
|
|
538
|
-
leaveAltScreen();
|
|
539
|
-
console.error(err);
|
|
540
|
-
process.exit(1);
|
|
541
|
-
});
|
|
542
|
-
process.on("unhandledRejection", (err) => {
|
|
543
|
-
if (cleaned) return;
|
|
544
|
-
cleaned = true;
|
|
545
|
-
showCursor();
|
|
546
|
-
leaveAltScreen();
|
|
547
|
-
console.error(err);
|
|
548
|
-
process.exit(1);
|
|
549
|
-
});
|
|
550
|
-
process.stdout.on("resize", () => {
|
|
551
|
-
reloadContent();
|
|
552
|
-
draw();
|
|
553
|
-
});
|
|
554
|
-
clear();
|
|
555
|
-
draw();
|
|
556
|
-
loadContent(flat[cursor]);
|
|
557
|
-
process.stdin.on("data", (data) => {
|
|
558
|
-
const key = data.toString();
|
|
559
|
-
if (key === "") return cleanup();
|
|
560
|
-
if (searching) handleSearch(key);
|
|
561
|
-
else if (contentSearching) handleContentSearch(key);
|
|
562
|
-
else if (focusContent) handleContent(key);
|
|
563
|
-
else handleNav(key);
|
|
564
|
-
draw();
|
|
565
|
-
});
|
|
566
|
-
function handleNav(key) {
|
|
567
|
-
const rows = process.stdout.rows || 24;
|
|
568
|
-
const maxScroll = Math.max(0, contentLines.length - rows + 2);
|
|
569
|
-
if (key === "q") return cleanup();
|
|
570
|
-
if (key === "/") {
|
|
571
|
-
searching = true;
|
|
572
|
-
searchQuery = "";
|
|
573
|
-
searchMatches = [];
|
|
574
|
-
showCursor();
|
|
575
|
-
} else if (key === "\x1B[A" || key === "k") {
|
|
576
|
-
cursor = nextNavigable(flat, cursor, -1);
|
|
577
|
-
loadContent(flat[cursor]);
|
|
578
|
-
} else if (key === "\x1B[B" || key === "j") {
|
|
579
|
-
cursor = nextNavigable(flat, cursor, 1);
|
|
580
|
-
loadContent(flat[cursor]);
|
|
581
|
-
} else if (key === "\r" || key === "\n" || key === " " || key === "\x1B[C") {
|
|
582
|
-
if (contentLines.length > 0) focusContent = true;
|
|
583
|
-
} else if (key === " " || key === "\x1B[6~") contentScroll = Math.min(maxScroll, contentScroll + rows - 2);
|
|
584
|
-
else if (key === "b" || key === "\x1B[5~") contentScroll = Math.max(0, contentScroll - rows + 2);
|
|
585
|
-
else if (key === "g") {
|
|
586
|
-
cursor = firstNavigable(flat);
|
|
587
|
-
loadContent(flat[cursor]);
|
|
588
|
-
} else if (key === "G") {
|
|
589
|
-
for (let i = flat.length - 1; i >= 0; i--) if (isNavigable(flat, i)) {
|
|
590
|
-
cursor = i;
|
|
591
|
-
break;
|
|
592
|
-
}
|
|
593
|
-
loadContent(flat[cursor]);
|
|
594
|
-
} else if (key === "t") {
|
|
595
|
-
sidebarVisible = !sidebarVisible;
|
|
596
|
-
reloadContent();
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
const scrollToLink = () => {
|
|
600
|
-
if (linkIdx < 0 || linkIdx >= contentLinks.length) return;
|
|
601
|
-
const rows = process.stdout.rows || 24;
|
|
602
|
-
const line = contentLinks[linkIdx].line;
|
|
603
|
-
if (line < contentScroll || line >= contentScroll + rows - 2) contentScroll = Math.max(0, Math.min(line - Math.floor(rows / 3), contentLines.length - rows + 2));
|
|
604
|
-
};
|
|
605
|
-
const activateLink = (url) => {
|
|
606
|
-
if (url.startsWith("http://") || url.startsWith("https://")) try {
|
|
607
|
-
execSync(`open ${JSON.stringify(url)}`, { stdio: "ignore" });
|
|
608
|
-
} catch {}
|
|
609
|
-
else {
|
|
610
|
-
const target = url.replace(/^\.\//, "/").replace(/\/$/, "");
|
|
611
|
-
const idx = flat.indexOf(docs.findByPath(target));
|
|
612
|
-
if (idx >= 0) {
|
|
613
|
-
cursor = idx;
|
|
614
|
-
focusContent = false;
|
|
615
|
-
linkIdx = -1;
|
|
616
|
-
loadContent(flat[cursor]);
|
|
617
|
-
}
|
|
618
|
-
}
|
|
619
|
-
};
|
|
620
|
-
function handleContent(key) {
|
|
621
|
-
const rows = process.stdout.rows || 24;
|
|
622
|
-
const maxScroll = Math.max(0, contentLines.length - rows + 2);
|
|
623
|
-
if (key === "" || key === "\b" || key === "\x1B[D" || key === "\x1B") {
|
|
624
|
-
focusContent = false;
|
|
625
|
-
linkIdx = -1;
|
|
626
|
-
contentSearchQuery = "";
|
|
627
|
-
contentMatches = [];
|
|
628
|
-
} else if (key === " ") {
|
|
629
|
-
if (contentLinks.length > 0) {
|
|
630
|
-
linkIdx = linkIdx < contentLinks.length - 1 ? linkIdx + 1 : 0;
|
|
631
|
-
scrollToLink();
|
|
632
|
-
}
|
|
633
|
-
} else if (key === "\x1B[Z") {
|
|
634
|
-
if (contentLinks.length > 0) {
|
|
635
|
-
linkIdx = linkIdx > 0 ? linkIdx - 1 : contentLinks.length - 1;
|
|
636
|
-
scrollToLink();
|
|
637
|
-
}
|
|
638
|
-
} else if ((key === "\r" || key === "\n") && linkIdx >= 0 && linkIdx < contentLinks.length) activateLink(contentLinks[linkIdx].url);
|
|
639
|
-
else if (key === "\x1B[A" || key === "k") contentScroll = Math.max(0, contentScroll - 1);
|
|
640
|
-
else if (key === "\x1B[B" || key === "j") contentScroll = Math.min(maxScroll, contentScroll + 1);
|
|
641
|
-
else if (key === " " || key === "\x1B[6~") contentScroll = Math.min(maxScroll, contentScroll + rows - 2);
|
|
642
|
-
else if (key === "b" || key === "\x1B[5~") contentScroll = Math.max(0, contentScroll - rows + 2);
|
|
643
|
-
else if (key === "/") {
|
|
644
|
-
contentSearching = true;
|
|
645
|
-
contentSearchQuery = "";
|
|
646
|
-
contentMatches = [];
|
|
647
|
-
contentMatchIdx = 0;
|
|
648
|
-
showCursor();
|
|
649
|
-
} else if (key === "n" && contentMatches.length > 0) {
|
|
650
|
-
contentMatchIdx = (contentMatchIdx + 1) % contentMatches.length;
|
|
651
|
-
scrollToMatch();
|
|
652
|
-
} else if (key === "N" && contentMatches.length > 0) {
|
|
653
|
-
contentMatchIdx = (contentMatchIdx - 1 + contentMatches.length) % contentMatches.length;
|
|
654
|
-
scrollToMatch();
|
|
655
|
-
} else if (key === "g") contentScroll = 0;
|
|
656
|
-
else if (key === "G") contentScroll = maxScroll;
|
|
657
|
-
else if (key === "t") {
|
|
658
|
-
sidebarVisible = !sidebarVisible;
|
|
659
|
-
reloadContent();
|
|
660
|
-
} else if (key === "q") return cleanup();
|
|
661
|
-
}
|
|
662
|
-
function handleContentSearch(key) {
|
|
663
|
-
if (key === "\x1B") {
|
|
664
|
-
contentSearching = false;
|
|
665
|
-
contentSearchQuery = "";
|
|
666
|
-
contentMatches = [];
|
|
667
|
-
hideCursor();
|
|
668
|
-
} else if (key === "\r" || key === "\n") {
|
|
669
|
-
contentSearching = false;
|
|
670
|
-
hideCursor();
|
|
671
|
-
} else if (key === "" || key === "\b") {
|
|
672
|
-
contentSearchQuery = contentSearchQuery.slice(0, -1);
|
|
673
|
-
contentMatches = findContentMatches(contentSearchQuery);
|
|
674
|
-
contentMatchIdx = 0;
|
|
675
|
-
scrollToMatch();
|
|
676
|
-
} else if (key === "\x1B[A") {
|
|
677
|
-
if (contentMatches.length > 0) {
|
|
678
|
-
contentMatchIdx = (contentMatchIdx - 1 + contentMatches.length) % contentMatches.length;
|
|
679
|
-
scrollToMatch();
|
|
680
|
-
}
|
|
681
|
-
} else if (key === "\x1B[B") {
|
|
682
|
-
if (contentMatches.length > 0) {
|
|
683
|
-
contentMatchIdx = (contentMatchIdx + 1) % contentMatches.length;
|
|
684
|
-
scrollToMatch();
|
|
685
|
-
}
|
|
686
|
-
} else if (key.length === 1 && key >= " ") {
|
|
687
|
-
contentSearchQuery += key;
|
|
688
|
-
contentMatches = findContentMatches(contentSearchQuery);
|
|
689
|
-
contentMatchIdx = 0;
|
|
690
|
-
scrollToMatch();
|
|
691
|
-
}
|
|
692
|
-
}
|
|
693
|
-
function updateSearchMatches() {
|
|
694
|
-
searchMatches = docs.matchIndices(searchQuery);
|
|
695
|
-
if (searchMatches.length > 0) cursor = searchMatches[0];
|
|
696
|
-
else if (!searchQuery) cursor = firstNavigable(flat);
|
|
697
|
-
loadContent(flat[cursor]);
|
|
698
|
-
}
|
|
699
|
-
function nextSearchMatch(dir) {
|
|
700
|
-
if (searchMatches.length === 0) return;
|
|
701
|
-
const curIdx = searchMatches.indexOf(cursor);
|
|
702
|
-
if (curIdx < 0) cursor = searchMatches[0];
|
|
703
|
-
else {
|
|
704
|
-
const next = curIdx + dir;
|
|
705
|
-
cursor = searchMatches[(next + searchMatches.length) % searchMatches.length];
|
|
706
|
-
}
|
|
707
|
-
loadContent(flat[cursor]);
|
|
708
|
-
}
|
|
709
|
-
function handleSearch(key) {
|
|
710
|
-
if (key === "\x1B" || key === "\x1B[D") {
|
|
711
|
-
searching = false;
|
|
712
|
-
searchMatches = [];
|
|
713
|
-
cursor = firstNavigable(flat);
|
|
714
|
-
hideCursor();
|
|
715
|
-
loadContent(flat[cursor]);
|
|
716
|
-
} else if (key === "\r" || key === "\n") {
|
|
717
|
-
searching = false;
|
|
718
|
-
searchMatches = [];
|
|
719
|
-
hideCursor();
|
|
720
|
-
loadContent(flat[cursor]);
|
|
721
|
-
} else if (key === "" || key === "\b") {
|
|
722
|
-
searchQuery = searchQuery.slice(0, -1);
|
|
723
|
-
updateSearchMatches();
|
|
724
|
-
} else if (key === "\x1B[A") nextSearchMatch(-1);
|
|
725
|
-
else if (key === "\x1B[B") nextSearchMatch(1);
|
|
726
|
-
else if (key.length === 1 && key >= " ") {
|
|
727
|
-
searchQuery += key;
|
|
728
|
-
updateSearchMatches();
|
|
729
|
-
}
|
|
730
|
-
}
|
|
731
|
-
}
|
|
732
|
-
//#endregion
|
|
733
|
-
//#region src/cli/_utils.ts
|
|
734
|
-
function openInBrowser(url) {
|
|
735
|
-
const parsed = new URL(url);
|
|
736
|
-
if (parsed.hostname === "[::]" || parsed.hostname === "[::1]" || parsed.hostname === "127.0.0.1") parsed.hostname = "localhost";
|
|
737
|
-
url = parsed.href;
|
|
738
|
-
exec(process.platform === "win32" ? `start ${url}` : process.platform === "darwin" ? `open ${url}` : `xdg-open ${url}`, () => {});
|
|
316
|
+
process.stderr.write("\nTo search within pages, pass a search query as second argument.\n");
|
|
739
317
|
}
|
|
740
318
|
//#endregion
|
|
741
319
|
//#region src/cli/main.ts
|
|
@@ -746,62 +324,55 @@ async function main() {
|
|
|
746
324
|
});
|
|
747
325
|
const { values, positionals } = parseArgs({
|
|
748
326
|
args: process.argv.slice(2),
|
|
327
|
+
allowPositionals: true,
|
|
749
328
|
options: {
|
|
750
329
|
help: {
|
|
751
330
|
type: "boolean",
|
|
752
331
|
short: "h"
|
|
753
332
|
},
|
|
754
333
|
export: { type: "string" },
|
|
755
|
-
page: {
|
|
756
|
-
type: "string",
|
|
757
|
-
short: "p"
|
|
758
|
-
},
|
|
759
334
|
plain: {
|
|
760
335
|
type: "boolean",
|
|
761
336
|
default: isAgent || !process.stdout.isTTY
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
tui: { type: "boolean" }
|
|
765
|
-
},
|
|
766
|
-
allowPositionals: true
|
|
337
|
+
}
|
|
338
|
+
}
|
|
767
339
|
});
|
|
768
|
-
const
|
|
769
|
-
const
|
|
770
|
-
const plain = values.plain ||
|
|
771
|
-
if (values.help || !
|
|
772
|
-
const isURL =
|
|
773
|
-
if (
|
|
774
|
-
const source =
|
|
775
|
-
const docs = new
|
|
340
|
+
const input = positionals[0];
|
|
341
|
+
const query = positionals[1];
|
|
342
|
+
const plain = values.plain || input?.startsWith("npm:") || false;
|
|
343
|
+
if (values.help || !input) return printUsage(!!input);
|
|
344
|
+
const isURL = input.startsWith("http://") || input.startsWith("https://");
|
|
345
|
+
if (input.endsWith(".md")) return singleFileMode(input, plain, isURL);
|
|
346
|
+
const source = resolveSource(input);
|
|
347
|
+
const docs = new Collection(source);
|
|
776
348
|
await docs.load();
|
|
777
|
-
if (
|
|
778
|
-
await
|
|
779
|
-
console.log(`Exported ${docs.pages.length} pages to ${
|
|
349
|
+
if (values.export) {
|
|
350
|
+
await writeCollection(docs, values.export, { plainText: plain });
|
|
351
|
+
console.log(`Exported ${docs.pages.length} pages to ${values.export}`);
|
|
780
352
|
return;
|
|
781
353
|
}
|
|
782
|
-
let
|
|
783
|
-
if (!
|
|
784
|
-
const urlPath = new URL(
|
|
785
|
-
if (urlPath && urlPath !== "/")
|
|
354
|
+
let resolvedQuery = query;
|
|
355
|
+
if (!resolvedQuery && isURL) {
|
|
356
|
+
const urlPath = new URL(input).pathname.replace(/\/+$/, "");
|
|
357
|
+
if (urlPath && urlPath !== "/") resolvedQuery = urlPath;
|
|
786
358
|
}
|
|
787
|
-
if (
|
|
788
|
-
if (plain) return
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
359
|
+
if (resolvedQuery) return smartResolve(docs, resolvedQuery, plain);
|
|
360
|
+
if (plain) return tocMode(docs);
|
|
361
|
+
return serverMode(source, docs);
|
|
362
|
+
}
|
|
363
|
+
async function smartResolve(docs, query, plain) {
|
|
364
|
+
const normalized = query.startsWith("/") ? query : "/" + query;
|
|
365
|
+
const resolved = await docs.resolvePage(normalized);
|
|
366
|
+
if (resolved.raw) return renderPage(docs, resolved, normalized, plain);
|
|
367
|
+
const fuzzy = docs.filter(query);
|
|
368
|
+
if (fuzzy.length === 1 && fuzzy[0].entry.page !== false) {
|
|
369
|
+
const match = fuzzy[0];
|
|
370
|
+
const page = await docs.resolvePage(match.entry.path);
|
|
371
|
+
if (page.raw) return renderPage(docs, page, match.entry.path, plain);
|
|
800
372
|
}
|
|
373
|
+
return searchMode(docs, query, plain);
|
|
801
374
|
}
|
|
802
375
|
main().catch((err) => {
|
|
803
|
-
showCursor();
|
|
804
|
-
leaveAltScreen();
|
|
805
376
|
console.error(err);
|
|
806
377
|
process.exit(1);
|
|
807
378
|
});
|