@nbtca/prompt 1.5.0 → 1.5.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/dist/app/app.js +142 -13
- package/dist/app/chrome.js +13 -2
- package/dist/app/frame.js +14 -2
- package/dist/app/keys.js +109 -1
- package/dist/app/views/docs-render.js +3 -2
- package/dist/app/views/docs.js +90 -25
- package/dist/app/views/events-render.js +2 -1
- package/dist/app/views/events.js +13 -2
- package/dist/app/views/home.js +46 -24
- package/dist/app/views/schedule-render.js +4 -3
- package/dist/app/views/schedule.js +101 -33
- package/dist/cli.js +570 -0
- package/dist/config/preferences.js +7 -0
- package/dist/core/canvas.js +1 -0
- package/dist/core/components/menu.js +23 -22
- package/dist/core/components/spinner.js +17 -1
- package/dist/core/text.js +7 -3
- package/dist/core/vim-keys.js +149 -6
- package/dist/features/calendar-store.js +27 -0
- package/dist/features/calendar.js +55 -6
- package/dist/features/docs-client.js +225 -0
- package/dist/features/docs.js +224 -68
- package/dist/features/links.js +51 -0
- package/dist/features/status.js +83 -14
- package/dist/features/student-timetable.js +1 -2
- package/dist/features/theme.js +3 -3
- package/dist/features/update.js +3 -2
- package/dist/i18n/locales/en.json +7 -2
- package/dist/i18n/locales/zh.json +7 -2
- package/dist/index.js +5 -498
- package/package.json +2 -1
package/dist/features/docs.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { marked } from 'marked';
|
|
2
2
|
import { markedTerminal } from 'marked-terminal';
|
|
3
3
|
import chalk from 'chalk';
|
|
4
|
-
import open from 'open';
|
|
5
4
|
import { createHash } from 'node:crypto';
|
|
6
5
|
import { runMenu, menuFooter } from '../core/components/menu.js';
|
|
7
6
|
import { runTextInput } from '../core/components/text-input.js';
|
|
@@ -12,8 +11,9 @@ import { spawn, execFileSync } from 'child_process';
|
|
|
12
11
|
import { URLS } from '../config/data.js';
|
|
13
12
|
import { t, fmt, getCurrentLanguage } from '../i18n/index.js';
|
|
14
13
|
import { enterScreen, breadcrumb } from '../core/transitions.js';
|
|
15
|
-
import { sanitizeTerminalLine, sanitizeTerminalText, truncate } from '../core/text.js';
|
|
16
|
-
import {
|
|
14
|
+
import { sanitizeTerminalLine, sanitizeTerminalText, stripAnsi, truncate } from '../core/text.js';
|
|
15
|
+
import { clearDocsClients, runDocsClientOperation } from './docs-client.js';
|
|
16
|
+
import { launchBrowserUrl } from './links.js';
|
|
17
17
|
function detectTerminalType() {
|
|
18
18
|
const term = (process.env['TERM'] ?? '').toLowerCase();
|
|
19
19
|
const termProgram = (process.env['TERM_PROGRAM'] ?? '').toLowerCase();
|
|
@@ -62,7 +62,8 @@ export function ensureMarkedConfigured() {
|
|
|
62
62
|
if (_markedConfigured)
|
|
63
63
|
return;
|
|
64
64
|
_markedConfigured = true;
|
|
65
|
-
const
|
|
65
|
+
const terminalType = getTerminalType();
|
|
66
|
+
const extension = markedTerminal(getRendererOptions(terminalType));
|
|
66
67
|
const renderer = extension.renderer ?? (extension.renderer = {});
|
|
67
68
|
const renderExternalLink = renderer.link;
|
|
68
69
|
if (renderExternalLink) {
|
|
@@ -147,9 +148,7 @@ const METADATA_CACHE_TTL_MS = 10 * 60 * 1000;
|
|
|
147
148
|
const METADATA_CACHE_MAX = 200;
|
|
148
149
|
const METADATA_CONCURRENCY = 4;
|
|
149
150
|
const metadataCache = new Map();
|
|
150
|
-
const metadataRequests = new Map();
|
|
151
151
|
let cacheGeneration = 0;
|
|
152
|
-
const docsClient = createDocsClient();
|
|
153
152
|
function getFreshRender(key) {
|
|
154
153
|
const entry = renderCache.get(key);
|
|
155
154
|
return entry && entry.expiresAt > Date.now() ? entry.value : null;
|
|
@@ -176,14 +175,13 @@ function renderCacheKey(filePath) {
|
|
|
176
175
|
}
|
|
177
176
|
export function clearDocsCache() {
|
|
178
177
|
cacheGeneration += 1;
|
|
179
|
-
|
|
178
|
+
clearDocsClients();
|
|
180
179
|
renderCache.clear();
|
|
181
180
|
metadataCache.clear();
|
|
182
|
-
metadataRequests.clear();
|
|
183
181
|
}
|
|
184
|
-
async function fetchDocument(path) {
|
|
182
|
+
async function fetchDocument(path, signal) {
|
|
185
183
|
try {
|
|
186
|
-
return await
|
|
184
|
+
return await runDocsClientOperation(signal, (client) => client.getDocument(path));
|
|
187
185
|
}
|
|
188
186
|
catch (err) {
|
|
189
187
|
const trans = t();
|
|
@@ -218,31 +216,31 @@ function setMetadata(path, value) {
|
|
|
218
216
|
metadataCache.delete(oldest);
|
|
219
217
|
}
|
|
220
218
|
}
|
|
221
|
-
function loadDocMetadata(path) {
|
|
219
|
+
function loadDocMetadata(path, signal) {
|
|
222
220
|
const cached = getFreshMetadata(path);
|
|
223
221
|
if (cached)
|
|
224
222
|
return Promise.resolve(cached);
|
|
225
|
-
const pending = metadataRequests.get(path);
|
|
226
|
-
if (pending)
|
|
227
|
-
return pending;
|
|
228
223
|
const generation = cacheGeneration;
|
|
229
|
-
|
|
224
|
+
return fetchDocument(path, signal).then((page) => {
|
|
230
225
|
const metadata = metadataFromPage(page);
|
|
231
226
|
if (generation === cacheGeneration)
|
|
232
227
|
setMetadata(path, metadata);
|
|
233
228
|
return metadata;
|
|
234
229
|
});
|
|
235
|
-
metadataRequests.set(path, request);
|
|
236
|
-
const release = () => {
|
|
237
|
-
if (metadataRequests.get(path) === request)
|
|
238
|
-
metadataRequests.delete(path);
|
|
239
|
-
};
|
|
240
|
-
void request.then(release, release);
|
|
241
|
-
return request;
|
|
242
230
|
}
|
|
243
|
-
|
|
231
|
+
function normalizeRenderedTasks(content, type) {
|
|
232
|
+
if (type !== 'basic')
|
|
233
|
+
return content;
|
|
234
|
+
return content
|
|
235
|
+
.split('\n')
|
|
236
|
+
.map((line) => /^\s*(?:[-*+]|\d+[.)])\s+\[X\](?:\s|$)/.test(stripAnsi(line))
|
|
237
|
+
? line.replace('[X]', '[x]')
|
|
238
|
+
: line)
|
|
239
|
+
.join('\n');
|
|
240
|
+
}
|
|
241
|
+
async function loadRenderedDoc(filePath, signal) {
|
|
244
242
|
const generation = cacheGeneration;
|
|
245
|
-
const page = await fetchDocument(filePath);
|
|
243
|
+
const page = await fetchDocument(filePath, signal);
|
|
246
244
|
if (generation === cacheGeneration)
|
|
247
245
|
setMetadata(filePath, metadataFromPage(page));
|
|
248
246
|
const rawContent = page.content;
|
|
@@ -251,12 +249,14 @@ async function loadRenderedDoc(filePath) {
|
|
|
251
249
|
const cached = getFreshRender(cacheKey);
|
|
252
250
|
if (cached?.fingerprint === fingerprint)
|
|
253
251
|
return { rawContent, renderedDoc: cached };
|
|
254
|
-
const
|
|
252
|
+
const terminalType = getTerminalType();
|
|
253
|
+
const cleaned = cleanMarkdownContent(rawContent, terminalType);
|
|
255
254
|
const title = sanitizeTerminalLine(page.title) || cleanFileName(filePath.split('/').pop() ?? filePath);
|
|
255
|
+
const markedOutput = normalizeRenderedTasks(await marked(cleaned), terminalType);
|
|
256
256
|
const renderedDoc = {
|
|
257
257
|
fingerprint,
|
|
258
258
|
cleaned,
|
|
259
|
-
rendered:
|
|
259
|
+
rendered: chalk.level === 0 ? sanitizeTerminalText(markedOutput) : markedOutput,
|
|
260
260
|
title,
|
|
261
261
|
readTime: estimateReadTime(cleaned),
|
|
262
262
|
};
|
|
@@ -392,10 +392,101 @@ function replaceDocumentComponents(content) {
|
|
|
392
392
|
});
|
|
393
393
|
return result;
|
|
394
394
|
}
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
395
|
+
function replaceHtmlCheckboxes(content, type) {
|
|
396
|
+
return content.replace(/<input\b([^>]*)\/?\s*>/gi, (tag, attributes) => {
|
|
397
|
+
const typeMatch = /(?:^|\s)type\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>/]+))/i.exec(attributes);
|
|
398
|
+
const inputType = (typeMatch?.[1] ?? typeMatch?.[2] ?? typeMatch?.[3] ?? '')
|
|
399
|
+
.trim()
|
|
400
|
+
.toLowerCase();
|
|
401
|
+
if (inputType !== 'checkbox')
|
|
402
|
+
return tag;
|
|
403
|
+
const checked = /(?:^|\s)checked(?=\s|=|\/|$)/i.test(attributes);
|
|
404
|
+
if (type === 'basic')
|
|
405
|
+
return checked ? '[x]' : '[ ]';
|
|
406
|
+
return checked ? '☑' : '☐';
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
function expandHtmlDetails(content) {
|
|
410
|
+
return content
|
|
411
|
+
.replace(/<summary\b[^>]*>([\s\S]*?)<\/summary\s*>/gi, (_match, label) => {
|
|
412
|
+
const summary = sanitizeTerminalLine(label);
|
|
413
|
+
return summary ? `\n\n### ${summary}\n\n` : '\n';
|
|
414
|
+
})
|
|
415
|
+
.replace(/<\/?(?:details|summary)\b[^>]*>/gi, '');
|
|
416
|
+
}
|
|
417
|
+
function transformOutsideFencedCodeBlocks(content, transform) {
|
|
418
|
+
const output = [];
|
|
419
|
+
let plain = [];
|
|
420
|
+
let fenceCharacter = '';
|
|
421
|
+
let fenceWidth = 0;
|
|
422
|
+
let listIndent = 0;
|
|
423
|
+
let listQuoteDepth = 0;
|
|
424
|
+
const flushPlain = () => {
|
|
425
|
+
if (plain.length === 0)
|
|
426
|
+
return;
|
|
427
|
+
output.push(transform(plain.join('\n')));
|
|
428
|
+
plain = [];
|
|
429
|
+
};
|
|
430
|
+
const lineContext = (line) => {
|
|
431
|
+
let rest = line;
|
|
432
|
+
let quoteDepth = 0;
|
|
433
|
+
for (;;) {
|
|
434
|
+
const quote = /^[ \t]{0,3}>[ \t]?/.exec(rest)?.[0];
|
|
435
|
+
if (!quote)
|
|
436
|
+
break;
|
|
437
|
+
quoteDepth += 1;
|
|
438
|
+
rest = rest.slice(quote.length);
|
|
439
|
+
}
|
|
440
|
+
return { quoteDepth, rest };
|
|
441
|
+
};
|
|
442
|
+
const fenceMarker = (line) => {
|
|
443
|
+
let { rest } = lineContext(line);
|
|
444
|
+
const list = /^[ \t]*(?:[-+*]|\d+[.)])[ \t]+/.exec(rest)?.[0];
|
|
445
|
+
if (list)
|
|
446
|
+
rest = rest.slice(list.length);
|
|
447
|
+
else if (listIndent > 0 && rest.startsWith(' '.repeat(listIndent))) {
|
|
448
|
+
rest = rest.slice(listIndent);
|
|
449
|
+
}
|
|
450
|
+
return /^ {0,3}(`{3,}|~{3,})/.exec(rest)?.[1];
|
|
451
|
+
};
|
|
452
|
+
for (const line of content.split('\n')) {
|
|
453
|
+
if (fenceWidth === 0) {
|
|
454
|
+
const { quoteDepth, rest } = lineContext(line);
|
|
455
|
+
const list = /^([ ]*)(?:[-+*]|\d+[.)])([ \t]+)/.exec(rest);
|
|
456
|
+
if (list) {
|
|
457
|
+
listIndent = list[0].length;
|
|
458
|
+
listQuoteDepth = quoteDepth;
|
|
459
|
+
}
|
|
460
|
+
else if (rest.trim() && (quoteDepth !== listQuoteDepth || rest.search(/\S/) < listIndent)) {
|
|
461
|
+
listIndent = 0;
|
|
462
|
+
listQuoteDepth = quoteDepth;
|
|
463
|
+
}
|
|
464
|
+
const opening = fenceMarker(line);
|
|
465
|
+
if (!opening) {
|
|
466
|
+
plain.push(line);
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
469
|
+
flushPlain();
|
|
470
|
+
fenceCharacter = opening[0] ?? '';
|
|
471
|
+
fenceWidth = opening.length;
|
|
472
|
+
output.push(line);
|
|
473
|
+
continue;
|
|
474
|
+
}
|
|
475
|
+
output.push(line);
|
|
476
|
+
const closing = fenceMarker(line);
|
|
477
|
+
const closingSuffix = closing ? line.slice(line.lastIndexOf(closing) + closing.length) : line;
|
|
478
|
+
if (closing?.[0] === fenceCharacter &&
|
|
479
|
+
closing.length >= fenceWidth &&
|
|
480
|
+
closingSuffix.trim() === '') {
|
|
481
|
+
fenceCharacter = '';
|
|
482
|
+
fenceWidth = 0;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
flushPlain();
|
|
486
|
+
return output.join('\n');
|
|
487
|
+
}
|
|
488
|
+
function cleanMarkdownOutsideFences(content, terminalType) {
|
|
489
|
+
let c = content;
|
|
399
490
|
c = c.replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, '');
|
|
400
491
|
c = c.replace(/<style\b[^>]*>[\s\S]*?<\/style>/gi, '');
|
|
401
492
|
c = replaceDocumentComponents(c);
|
|
@@ -414,7 +505,7 @@ export function cleanMarkdownContent(content, type = getTerminalType()) {
|
|
|
414
505
|
c = c.replace(/\[\[toc\]\]/gi, '');
|
|
415
506
|
c = c.replace(/^(#{1,6}\s+[^\n]*?)\s*\{#[^}]+\}\s*$/gm, '$1');
|
|
416
507
|
c = c.replace(/==([^=\n]+)==/g, '**$1**');
|
|
417
|
-
if (
|
|
508
|
+
if (terminalType === 'basic') {
|
|
418
509
|
c = c.replace(/!\[([^\]]*)\]\([^)]+\)/g, (_match, alt) => `${pickIcon('📎', '[image]')} ${alt.length > 0 ? alt : 'image'}`);
|
|
419
510
|
}
|
|
420
511
|
else {
|
|
@@ -425,14 +516,27 @@ export function cleanMarkdownContent(content, type = getTerminalType()) {
|
|
|
425
516
|
});
|
|
426
517
|
}
|
|
427
518
|
c = c.replace(/<!--[\s\S]*?-->/g, '');
|
|
428
|
-
c = c
|
|
429
|
-
c = c
|
|
519
|
+
c = replaceHtmlCheckboxes(c, terminalType);
|
|
520
|
+
c = expandHtmlDetails(c);
|
|
521
|
+
c = c.replace(/<br\s*\/?>/gi, '\n');
|
|
522
|
+
c = c.replace(/<(?:hr|input|link|meta)\b[^>]*\/?>/gi, '');
|
|
430
523
|
c = c.replace(/<([a-z][a-z0-9]*)\b[^>]*>([\s\S]*?)<\/\1>/gi, '$2');
|
|
431
524
|
c = c.replace(/<[a-z][a-z0-9]*\b[^>]*\/>/gi, '');
|
|
432
525
|
c = c.replace(/<\/(?:Split|TimelineEntry)>/gi, '');
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
526
|
+
if (terminalType === 'basic') {
|
|
527
|
+
c = c.replace(/^(\s*(?:[-*+]|\d+[.)]) )\[x\]/gim, '$1[x]');
|
|
528
|
+
}
|
|
529
|
+
else {
|
|
530
|
+
c = c.replace(/^(\s*(?:[-*+]|\d+[.)]) )\[x\] /gim, '$1☑ ');
|
|
531
|
+
c = c.replace(/^(\s*(?:[-*+]|\d+[.)]) )\[ \] /gm, '$1☐ ');
|
|
532
|
+
}
|
|
533
|
+
return c.replace(/\n{3,}/g, '\n\n');
|
|
534
|
+
}
|
|
535
|
+
export function cleanMarkdownContent(content, type = getTerminalType()) {
|
|
536
|
+
let c = sanitizeTerminalText(content);
|
|
537
|
+
c = c.replace(/^---\n[\s\S]*?\n---(?:\n|$)/, '');
|
|
538
|
+
c = processFencedCodeBlocks(c);
|
|
539
|
+
c = transformOutsideFencedCodeBlocks(c, (value) => cleanMarkdownOutsideFences(value, type));
|
|
436
540
|
return c.trim();
|
|
437
541
|
}
|
|
438
542
|
function estimateReadTime(text) {
|
|
@@ -494,9 +598,9 @@ export function resolveInternalHref(href, fromPath) {
|
|
|
494
598
|
target += '.md';
|
|
495
599
|
return target;
|
|
496
600
|
}
|
|
497
|
-
export async function loadDocForReader(filePath) {
|
|
601
|
+
export async function loadDocForReader(filePath, signal) {
|
|
498
602
|
ensureMarkedConfigured();
|
|
499
|
-
const { renderedDoc } = await loadRenderedDoc(filePath);
|
|
603
|
+
const { renderedDoc } = await loadRenderedDoc(filePath, signal);
|
|
500
604
|
const seen = new Set();
|
|
501
605
|
const links = [];
|
|
502
606
|
for (const raw of extractInternalLinks(renderedDoc.cleaned)) {
|
|
@@ -544,7 +648,7 @@ function listedDoc(item, metadata) {
|
|
|
544
648
|
summary: sanitizeTerminalLine(metadata?.summary ?? ''),
|
|
545
649
|
};
|
|
546
650
|
}
|
|
547
|
-
export async function fetchDocMetadata(items) {
|
|
651
|
+
export async function fetchDocMetadata(items, signal) {
|
|
548
652
|
const results = items.map((item) => listedDoc(item));
|
|
549
653
|
let nextIndex = 0;
|
|
550
654
|
async function worker() {
|
|
@@ -556,10 +660,17 @@ export async function fetchDocMetadata(items) {
|
|
|
556
660
|
const item = items[index];
|
|
557
661
|
if (!item)
|
|
558
662
|
continue;
|
|
663
|
+
if (signal?.aborted) {
|
|
664
|
+
throw signal.reason instanceof Error
|
|
665
|
+
? signal.reason
|
|
666
|
+
: new DOMException('Aborted', 'AbortError');
|
|
667
|
+
}
|
|
559
668
|
try {
|
|
560
|
-
results[index] = listedDoc(item, await loadDocMetadata(item.path));
|
|
669
|
+
results[index] = listedDoc(item, await loadDocMetadata(item.path, signal));
|
|
561
670
|
}
|
|
562
|
-
catch {
|
|
671
|
+
catch (error) {
|
|
672
|
+
if (signal?.aborted)
|
|
673
|
+
throw error;
|
|
563
674
|
results[index] = listedDoc(item);
|
|
564
675
|
}
|
|
565
676
|
}
|
|
@@ -568,8 +679,8 @@ export async function fetchDocMetadata(items) {
|
|
|
568
679
|
await Promise.all(Array.from({ length: workerCount }, worker));
|
|
569
680
|
return results;
|
|
570
681
|
}
|
|
571
|
-
export async function fetchSectionMetadata(section) {
|
|
572
|
-
const files = await fetchDocMetadata(section.files);
|
|
682
|
+
export async function fetchSectionMetadata(section, signal) {
|
|
683
|
+
const files = await fetchDocMetadata(section.files, signal);
|
|
573
684
|
return { ...section, count: files.length, files };
|
|
574
685
|
}
|
|
575
686
|
function searchDoc(result) {
|
|
@@ -585,8 +696,8 @@ function searchDoc(result) {
|
|
|
585
696
|
section: result.section,
|
|
586
697
|
};
|
|
587
698
|
}
|
|
588
|
-
export async function searchDocuments(query) {
|
|
589
|
-
return (await
|
|
699
|
+
export async function searchDocuments(query, signal) {
|
|
700
|
+
return (await runDocsClientOperation(signal, (client) => client.search(query, { limit: 20 }))).map(searchDoc);
|
|
590
701
|
}
|
|
591
702
|
export function buildSections(all) {
|
|
592
703
|
const groups = new Map();
|
|
@@ -627,11 +738,11 @@ export function getArchivedGroups(files) {
|
|
|
627
738
|
}
|
|
628
739
|
return groups;
|
|
629
740
|
}
|
|
630
|
-
export async function fetchAllDocs() {
|
|
631
|
-
return
|
|
741
|
+
export async function fetchAllDocs(signal) {
|
|
742
|
+
return runDocsClientOperation(signal, (client) => client.listAll());
|
|
632
743
|
}
|
|
633
|
-
export async function fetchSections() {
|
|
634
|
-
return buildSections(await fetchAllDocs());
|
|
744
|
+
export async function fetchSections(signal) {
|
|
745
|
+
return buildSections(await fetchAllDocs(signal));
|
|
635
746
|
}
|
|
636
747
|
async function loadSections() {
|
|
637
748
|
const trans = t();
|
|
@@ -646,9 +757,19 @@ async function loadSections() {
|
|
|
646
757
|
return null;
|
|
647
758
|
}
|
|
648
759
|
}
|
|
649
|
-
function pipeToPager(command, args, content) {
|
|
760
|
+
function pipeToPager(command, args, content, plain = false) {
|
|
650
761
|
return new Promise((resolve) => {
|
|
651
|
-
const
|
|
762
|
+
const env = { ...process.env };
|
|
763
|
+
if (plain) {
|
|
764
|
+
delete env['FORCE_COLOR'];
|
|
765
|
+
env['NO_COLOR'] = '1';
|
|
766
|
+
env['CLICOLOR'] = '0';
|
|
767
|
+
env['CLICOLOR_FORCE'] = '0';
|
|
768
|
+
}
|
|
769
|
+
const child = spawn(command, args, {
|
|
770
|
+
stdio: ['pipe', 'inherit', 'inherit'],
|
|
771
|
+
...(plain ? { env } : {}),
|
|
772
|
+
});
|
|
652
773
|
let settled = false;
|
|
653
774
|
const finish = (started) => {
|
|
654
775
|
if (settled)
|
|
@@ -673,7 +794,9 @@ function pipeToPager(command, args, content) {
|
|
|
673
794
|
}
|
|
674
795
|
});
|
|
675
796
|
}
|
|
676
|
-
async function displayWithGlow(cleanedMarkdown) {
|
|
797
|
+
export async function displayWithGlow(cleanedMarkdown) {
|
|
798
|
+
if (chalk.level === 0)
|
|
799
|
+
return false;
|
|
677
800
|
const cols = String(Math.min(process.stdout.columns || 80, 80));
|
|
678
801
|
return pipeToPager('glow', ['--pager', '--width', cols, '-'], cleanedMarkdown);
|
|
679
802
|
}
|
|
@@ -699,16 +822,21 @@ async function displayWithLess(rendered, title, filePath, readTime, toc) {
|
|
|
699
822
|
'',
|
|
700
823
|
].join('\n');
|
|
701
824
|
const footer = ['', rule, chalk.dim(` ${trans.docs.endOfDocument}`), ''].join('\n');
|
|
702
|
-
const
|
|
825
|
+
const plain = chalk.level === 0;
|
|
826
|
+
const fullContent = plain
|
|
827
|
+
? sanitizeTerminalText(header + rendered + footer)
|
|
828
|
+
: header + rendered + footer;
|
|
703
829
|
const pagerSetting = (process.env['PAGER'] ?? 'less').trim();
|
|
704
830
|
const [pagerCommand = 'less', ...pagerArgs] = pagerSetting.split(/\s+/).filter(Boolean);
|
|
705
831
|
const isLess = /(?:^|[\\/])less(?:\.exe)?$/i.test(pagerCommand);
|
|
706
|
-
const args = isLess
|
|
832
|
+
const args = isLess
|
|
833
|
+
? [...pagerArgs, ...(plain ? [] : ['-R']), '-F', '-X', '-i', '-j4']
|
|
834
|
+
: pagerArgs;
|
|
707
835
|
if (!commandExists(pagerCommand)) {
|
|
708
836
|
console.log(fullContent);
|
|
709
837
|
return;
|
|
710
838
|
}
|
|
711
|
-
if (!(await pipeToPager(pagerCommand, args, fullContent)))
|
|
839
|
+
if (!(await pipeToPager(pagerCommand, args, fullContent, plain)))
|
|
712
840
|
console.log(fullContent);
|
|
713
841
|
}
|
|
714
842
|
async function showDocSection(section) {
|
|
@@ -798,7 +926,7 @@ async function viewMarkdownFile(filePath) {
|
|
|
798
926
|
const { rawContent, renderedDoc } = await loadRenderedDoc(filePath);
|
|
799
927
|
s.stop(`${chalk.bold(renderedDoc.title)} ${chalk.dim(renderedDoc.readTime)}`);
|
|
800
928
|
const toc = extractTOC(renderedDoc.cleaned);
|
|
801
|
-
if (hasGlow()) {
|
|
929
|
+
if (chalk.level > 0 && hasGlow()) {
|
|
802
930
|
if (!(await displayWithGlow(renderedDoc.cleaned))) {
|
|
803
931
|
await displayWithLess(renderedDoc.rendered, renderedDoc.title, filePath, renderedDoc.readTime, toc);
|
|
804
932
|
}
|
|
@@ -833,32 +961,60 @@ async function viewMarkdownFile(filePath) {
|
|
|
833
961
|
}
|
|
834
962
|
}
|
|
835
963
|
}
|
|
836
|
-
export async function openDocsInBrowser(path) {
|
|
964
|
+
export async function openDocsInBrowser(path, signal) {
|
|
837
965
|
const trans = t();
|
|
838
966
|
const s = createSpinner(trans.docs.opening);
|
|
967
|
+
let url = docsUrlFromPath(path);
|
|
839
968
|
try {
|
|
840
|
-
let route = path ? docsRouteFromPath(path) : '';
|
|
841
969
|
if (path) {
|
|
842
970
|
try {
|
|
843
|
-
|
|
971
|
+
url = docsUrlFromRoute((await loadDocMetadata(path, signal)).route);
|
|
844
972
|
}
|
|
845
973
|
catch {
|
|
846
|
-
|
|
974
|
+
if (signal?.aborted) {
|
|
975
|
+
s.stop();
|
|
976
|
+
return false;
|
|
977
|
+
}
|
|
978
|
+
url = docsUrlFromPath(path);
|
|
847
979
|
}
|
|
848
980
|
}
|
|
849
|
-
|
|
850
|
-
.
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
981
|
+
if (signal?.aborted) {
|
|
982
|
+
s.stop();
|
|
983
|
+
return false;
|
|
984
|
+
}
|
|
985
|
+
if (!(await launchBrowserUrl(url)))
|
|
986
|
+
throw new Error('Browser launcher failed');
|
|
987
|
+
if (signal?.aborted) {
|
|
988
|
+
s.stop();
|
|
989
|
+
return false;
|
|
990
|
+
}
|
|
855
991
|
s.stop(trans.docs.browserOpened);
|
|
992
|
+
console.log();
|
|
993
|
+
return true;
|
|
856
994
|
}
|
|
857
995
|
catch {
|
|
996
|
+
if (signal?.aborted) {
|
|
997
|
+
s.stop();
|
|
998
|
+
return false;
|
|
999
|
+
}
|
|
858
1000
|
s.error(trans.docs.browserError);
|
|
859
|
-
console.log(chalk.gray(` ${
|
|
1001
|
+
console.log(chalk.gray(` ${fmt(t().links.openManually, { url })}`));
|
|
1002
|
+
console.log();
|
|
1003
|
+
return false;
|
|
860
1004
|
}
|
|
861
|
-
|
|
1005
|
+
}
|
|
1006
|
+
function docsUrlFromRoute(route) {
|
|
1007
|
+
const safeRoute = sanitizeTerminalLine(route);
|
|
1008
|
+
if (!safeRoute)
|
|
1009
|
+
return URLS.docs;
|
|
1010
|
+
const encodedRoute = safeRoute
|
|
1011
|
+
.split('/')
|
|
1012
|
+
.map((segment) => encodeURIComponent(segment))
|
|
1013
|
+
.join('/');
|
|
1014
|
+
return `${URLS.docs}${encodedRoute}`;
|
|
1015
|
+
}
|
|
1016
|
+
export function docsUrlFromPath(path) {
|
|
1017
|
+
return path ? docsUrlFromRoute(docsRouteFromPath(sanitizeTerminalLine(path))) : URLS.docs;
|
|
862
1018
|
}
|
|
863
1019
|
export function docsRouteFromPath(path) {
|
|
864
1020
|
const withoutExtension = path.replace(/\.md$/i, '');
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import open from 'open';
|
|
3
|
+
import { sanitizeTerminalLine } from '../core/text.js';
|
|
4
|
+
import { fmt, t } from '../i18n/index.js';
|
|
5
|
+
const BROWSER_LAUNCH_SETTLE_MS = 1000;
|
|
6
|
+
function settleBrowserLauncher(child) {
|
|
7
|
+
return new Promise((resolve) => {
|
|
8
|
+
let settled = false;
|
|
9
|
+
const timer = setTimeout(() => {
|
|
10
|
+
finish(true);
|
|
11
|
+
}, BROWSER_LAUNCH_SETTLE_MS);
|
|
12
|
+
function finish(success) {
|
|
13
|
+
if (settled)
|
|
14
|
+
return;
|
|
15
|
+
settled = true;
|
|
16
|
+
child.off('close', onClose);
|
|
17
|
+
child.off('error', onError);
|
|
18
|
+
clearTimeout(timer);
|
|
19
|
+
resolve(success);
|
|
20
|
+
}
|
|
21
|
+
function onClose(code, signal) {
|
|
22
|
+
finish(code === 0 && signal === null);
|
|
23
|
+
}
|
|
24
|
+
function onError() {
|
|
25
|
+
finish(false);
|
|
26
|
+
}
|
|
27
|
+
child.once('close', onClose);
|
|
28
|
+
child.once('error', onError);
|
|
29
|
+
if (child.exitCode !== null || child.signalCode !== null) {
|
|
30
|
+
finish(child.exitCode === 0 && child.signalCode === null);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
export async function launchBrowserUrl(url) {
|
|
36
|
+
try {
|
|
37
|
+
return await settleBrowserLauncher(await open(url));
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export async function openUrlInBrowser(url) {
|
|
44
|
+
const safeUrl = sanitizeTerminalLine(url);
|
|
45
|
+
if (await launchBrowserUrl(safeUrl))
|
|
46
|
+
return true;
|
|
47
|
+
const trans = t().links;
|
|
48
|
+
console.error(chalk.red(trans.error));
|
|
49
|
+
console.error(chalk.dim(fmt(trans.openManually, { url: safeUrl })));
|
|
50
|
+
return false;
|
|
51
|
+
}
|
package/dist/features/status.js
CHANGED
|
@@ -4,6 +4,66 @@ import { pickIcon } from '../core/icons.js';
|
|
|
4
4
|
import { padEndV, sanitizeTerminalLine, visualWidth } from '../core/text.js';
|
|
5
5
|
import { c } from '../core/theme.js';
|
|
6
6
|
import { t } from '../i18n/index.js';
|
|
7
|
+
function abortError(signal) {
|
|
8
|
+
return signal.reason instanceof Error
|
|
9
|
+
? signal.reason
|
|
10
|
+
: new DOMException('The status check was aborted.', 'AbortError');
|
|
11
|
+
}
|
|
12
|
+
function cancelResponseBody(response) {
|
|
13
|
+
try {
|
|
14
|
+
const body = response?.body;
|
|
15
|
+
if (!body)
|
|
16
|
+
return;
|
|
17
|
+
void body.cancel().catch(() => {
|
|
18
|
+
// Cancellation is best-effort and must not delay status results.
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
// A custom transport may expose an inaccessible, locked, or closed body.
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function raceWithSignal(operation, signal, onLateValue) {
|
|
26
|
+
return new Promise((resolve, reject) => {
|
|
27
|
+
let settled = false;
|
|
28
|
+
const cleanup = () => {
|
|
29
|
+
signal.removeEventListener('abort', onAbort);
|
|
30
|
+
};
|
|
31
|
+
const onAbort = () => {
|
|
32
|
+
if (settled)
|
|
33
|
+
return;
|
|
34
|
+
settled = true;
|
|
35
|
+
cleanup();
|
|
36
|
+
reject(abortError(signal));
|
|
37
|
+
};
|
|
38
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
39
|
+
void operation
|
|
40
|
+
.then((value) => {
|
|
41
|
+
if (settled) {
|
|
42
|
+
try {
|
|
43
|
+
onLateValue?.(value);
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
// Late cleanup is best-effort.
|
|
47
|
+
}
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
settled = true;
|
|
51
|
+
cleanup();
|
|
52
|
+
resolve(value);
|
|
53
|
+
}, (error) => {
|
|
54
|
+
if (settled)
|
|
55
|
+
return;
|
|
56
|
+
settled = true;
|
|
57
|
+
cleanup();
|
|
58
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
59
|
+
})
|
|
60
|
+
.catch(() => {
|
|
61
|
+
// Both operation outcomes are handled above. This only guards cleanup code.
|
|
62
|
+
});
|
|
63
|
+
if (signal.aborted)
|
|
64
|
+
onAbort();
|
|
65
|
+
});
|
|
66
|
+
}
|
|
7
67
|
function getServiceTargets() {
|
|
8
68
|
const trans = t();
|
|
9
69
|
return [
|
|
@@ -17,25 +77,34 @@ function getServiceTargets() {
|
|
|
17
77
|
{ name: trans.status.serviceMirror, url: URLS.mirror, group: 'intranet', intranet: true },
|
|
18
78
|
];
|
|
19
79
|
}
|
|
20
|
-
async function checkService(name, url, timeoutMs) {
|
|
80
|
+
async function checkService(name, url, timeoutMs, signal) {
|
|
21
81
|
const start = Date.now();
|
|
22
82
|
const controller = new AbortController();
|
|
83
|
+
const onAbort = () => {
|
|
84
|
+
controller.abort(signal?.reason);
|
|
85
|
+
};
|
|
86
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
87
|
+
if (signal?.aborted)
|
|
88
|
+
onAbort();
|
|
23
89
|
const timeout = setTimeout(() => {
|
|
24
90
|
controller.abort();
|
|
25
91
|
}, timeoutMs);
|
|
26
|
-
timeout.unref();
|
|
27
92
|
let response;
|
|
28
93
|
try {
|
|
29
|
-
response = await fetch(url, {
|
|
94
|
+
response = await raceWithSignal(fetch(url, {
|
|
30
95
|
signal: controller.signal,
|
|
31
96
|
redirect: 'follow',
|
|
32
97
|
headers: { 'User-Agent': `NBTCA-CLI/${APP_INFO.version}` },
|
|
33
|
-
});
|
|
98
|
+
}), controller.signal, cancelResponseBody);
|
|
99
|
+
if (signal?.aborted)
|
|
100
|
+
throw abortError(signal);
|
|
34
101
|
const latencyMs = Date.now() - start;
|
|
35
102
|
const ok = response.status >= 200 && response.status < 400;
|
|
36
103
|
return { name, url, ok, statusCode: response.status, latencyMs };
|
|
37
104
|
}
|
|
38
105
|
catch (err) {
|
|
106
|
+
if (signal?.aborted)
|
|
107
|
+
throw abortError(signal);
|
|
39
108
|
const latencyMs = Date.now() - start;
|
|
40
109
|
const error = sanitizeTerminalLine(err instanceof Error
|
|
41
110
|
? err.name === 'AbortError'
|
|
@@ -46,21 +115,19 @@ async function checkService(name, url, timeoutMs) {
|
|
|
46
115
|
}
|
|
47
116
|
finally {
|
|
48
117
|
clearTimeout(timeout);
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
52
|
-
catch {
|
|
53
|
-
// The response may already be closed by the runtime.
|
|
54
|
-
}
|
|
118
|
+
signal?.removeEventListener('abort', onAbort);
|
|
119
|
+
cancelResponseBody(response);
|
|
55
120
|
}
|
|
56
121
|
}
|
|
57
|
-
async function checkServiceWithRetry(target, timeoutMs, retries) {
|
|
58
|
-
let lastResult = await checkService(target.name, target.url, timeoutMs);
|
|
122
|
+
async function checkServiceWithRetry(target, timeoutMs, retries, signal) {
|
|
123
|
+
let lastResult = await checkService(target.name, target.url, timeoutMs, signal);
|
|
59
124
|
if (!lastResult.ok) {
|
|
60
125
|
for (let attempt = 0; attempt < retries; attempt++) {
|
|
61
126
|
if (!lastResult.error && !(lastResult.statusCode != null && lastResult.statusCode >= 500))
|
|
62
127
|
break;
|
|
63
|
-
|
|
128
|
+
if (signal?.aborted)
|
|
129
|
+
throw abortError(signal);
|
|
130
|
+
lastResult = await checkService(target.name, target.url, timeoutMs, signal);
|
|
64
131
|
if (lastResult.ok)
|
|
65
132
|
break;
|
|
66
133
|
}
|
|
@@ -74,7 +141,9 @@ async function checkServiceWithRetry(target, timeoutMs, retries) {
|
|
|
74
141
|
export async function checkServices(options = {}) {
|
|
75
142
|
const timeoutMs = options.timeoutMs ?? 6000;
|
|
76
143
|
const retries = options.retries ?? 1;
|
|
77
|
-
|
|
144
|
+
if (options.signal?.aborted)
|
|
145
|
+
throw abortError(options.signal);
|
|
146
|
+
return Promise.all(getServiceTargets().map((target) => checkServiceWithRetry(target, timeoutMs, retries, options.signal)));
|
|
78
147
|
}
|
|
79
148
|
export function serializeServiceStatus(items) {
|
|
80
149
|
return items.map((item) => ({
|