pi-quiver 5.2.4 → 5.3.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/CHANGELOG.md +24 -0
- package/README.md +36 -10
- package/dist/bin/pi-quiver.js +702 -259
- package/dist/lib/unpdf-worker.js +58 -0
- package/extensions/doc_to_md.ts +52 -62
- package/lib/doc-to-md-bundle.ts +139 -0
- package/lib/doc-to-md-core.ts +261 -301
- package/lib/doc-to-md-handle.ts +108 -0
- package/lib/doc-to-md-options.ts +186 -0
- package/lib/unpdf-worker.ts +54 -0
- package/package.json +6 -4
- package/scripts/doc_to_md.py +369 -0
- package/scripts/pdf_to_md.py +0 -30
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// lib/unpdf-worker.ts
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { getDocumentProxy } from "unpdf";
|
|
4
|
+
var SEP = (n) => `--- end of page.page_number=${n} ---`;
|
|
5
|
+
async function readStdin() {
|
|
6
|
+
const chunks = [];
|
|
7
|
+
for await (const c of process.stdin) chunks.push(c);
|
|
8
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
9
|
+
}
|
|
10
|
+
async function main() {
|
|
11
|
+
const mode = process.argv[2];
|
|
12
|
+
if (mode !== "info" && mode !== "pdf-text") {
|
|
13
|
+
process.stderr.write("usage: unpdf-worker <info|pdf-text>\n");
|
|
14
|
+
return 1;
|
|
15
|
+
}
|
|
16
|
+
const o = JSON.parse(await readStdin() || "{}");
|
|
17
|
+
try {
|
|
18
|
+
const pdf = await getDocumentProxy(new Uint8Array(readFileSync(o.path)));
|
|
19
|
+
const pageCount = pdf.numPages;
|
|
20
|
+
if (mode === "info") {
|
|
21
|
+
const meta = (await pdf.getMetadata().catch(() => null))?.info;
|
|
22
|
+
const metadata = {};
|
|
23
|
+
for (const k of ["Title", "Author", "Subject", "Creator", "Producer"]) if (typeof meta?.[k] === "string" && meta[k]) metadata[k.toLowerCase()] = meta[k];
|
|
24
|
+
process.stdout.write(JSON.stringify({ pageCount, metadata, toc: [] }));
|
|
25
|
+
return 0;
|
|
26
|
+
}
|
|
27
|
+
const pages = o.pages ?? Array.from({ length: pageCount }, (_, i) => i + 1);
|
|
28
|
+
const bad = pages.filter((p) => p < 1 || p > pageCount);
|
|
29
|
+
if (bad.length) {
|
|
30
|
+
process.stdout.write(JSON.stringify({ error: `pages out of range: ${bad.join(", ")} (document has ${pageCount} pages)`, pageCount }));
|
|
31
|
+
return 3;
|
|
32
|
+
}
|
|
33
|
+
const out = [], emptyPages = [], failedPages = [];
|
|
34
|
+
for (const n of pages) {
|
|
35
|
+
let text = "";
|
|
36
|
+
try {
|
|
37
|
+
const page = await pdf.getPage(n);
|
|
38
|
+
const content = await page.getTextContent();
|
|
39
|
+
text = content.items.map((it) => "str" in it ? it.str + (it.hasEOL ? "\n" : " ") : "").join("").trim();
|
|
40
|
+
} catch (e) {
|
|
41
|
+
failedPages.push({ page: n, error: String(e).slice(0, 300) });
|
|
42
|
+
}
|
|
43
|
+
if (!text) emptyPages.push(n);
|
|
44
|
+
out.push(text, SEP(n));
|
|
45
|
+
}
|
|
46
|
+
if (failedPages.length === pages.length) throw new Error(`every selected page failed: ${failedPages[0].error}`);
|
|
47
|
+
process.stdout.write(JSON.stringify({ markdown: `${out.join("\n\n")}
|
|
48
|
+
`, pages, pageCount, emptyPages, failedPages, notes: ["No images: unpdf backend"] }));
|
|
49
|
+
return 0;
|
|
50
|
+
} catch (e) {
|
|
51
|
+
process.stderr.write(`${e instanceof Error ? e.stack ?? e.message : String(e)}
|
|
52
|
+
`);
|
|
53
|
+
return 1;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
main().then((code) => {
|
|
57
|
+
process.exitCode = code;
|
|
58
|
+
});
|
package/extensions/doc_to_md.ts
CHANGED
|
@@ -1,85 +1,75 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* doc_to_md Extension
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* to Markdown. Primary engine: pymupdf4llm via ephemeral `uv run --with`
|
|
6
|
-
* (warm-once per process, no repo venv). Fallback: unpdf pure-JS text
|
|
7
|
-
* extraction (degraded, explicitly marked). DOCX/PPTX convert to PDF first
|
|
8
|
-
* via headless soffice, then feed the PDF pipeline. Output over 32 KB or
|
|
9
|
-
* 1000 lines is spilled to a temp .md file with a 60-line preview; smaller
|
|
10
|
-
* content is returned inline.
|
|
2
|
+
* doc_to_md Extension - converts a local PDF/DOCX/PPTX/XLSX/XLS to a Markdown bundle on disk
|
|
3
|
+
* (<stem>.md + images/) and returns a bounded handle; `info: true` inspects without converting.
|
|
4
|
+
* Schema and settings shape derive from the core's option descriptors (single source of truth).
|
|
11
5
|
*/
|
|
12
|
-
|
|
13
6
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
14
|
-
import {
|
|
7
|
+
import { keyHint } from "@earendil-works/pi-coding-agent";
|
|
15
8
|
import { Text } from "@earendil-works/pi-tui";
|
|
16
|
-
import { Type } from "@sinclair/typebox";
|
|
17
|
-
import {
|
|
9
|
+
import { Type, type TObject, type TSchema } from "@sinclair/typebox";
|
|
10
|
+
import { resolveConfig } from "../lib/extension-config.ts";
|
|
11
|
+
import {
|
|
12
|
+
DOC_TO_MD_OPTIONS, type DocToMdDetails, type OptionDescriptor, type PerCallInput, type Tunables,
|
|
13
|
+
coerceDocToMdSettings, convertDocument, inspectDocument, resolveOptions,
|
|
14
|
+
} from "../lib/doc-to-md-core.ts";
|
|
15
|
+
import { resolve } from "node:path";
|
|
16
|
+
|
|
17
|
+
function schemaFor(d: OptionDescriptor): TSchema {
|
|
18
|
+
const desc = { description: d.help };
|
|
19
|
+
switch (d.type) {
|
|
20
|
+
case "bool": return Type.Boolean(desc);
|
|
21
|
+
case "int": return Type.Integer({ ...desc, minimum: 1 });
|
|
22
|
+
case "enum": return Type.Union(d.enumValues!.map((v) => Type.Literal(v)), desc);
|
|
23
|
+
default: return Type.String(desc);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function buildSchema(): TObject {
|
|
28
|
+
const props: Record<string, TSchema> = {};
|
|
29
|
+
for (const d of DOC_TO_MD_OPTIONS) props[d.key] = d.key === "path" ? schemaFor(d) : Type.Optional(schemaFor(d));
|
|
30
|
+
return Type.Object(props);
|
|
31
|
+
}
|
|
18
32
|
|
|
19
33
|
export default function docToMdExtension(pi: ExtensionAPI) {
|
|
20
34
|
pi.registerTool({
|
|
21
35
|
name: "doc_to_md",
|
|
22
|
-
label: "Convert doc to Markdown",
|
|
36
|
+
label: "Convert doc to Markdown bundle",
|
|
23
37
|
description:
|
|
24
|
-
"Convert a local PDF/DOCX/PPTX
|
|
25
|
-
promptSnippet: "Convert a local PDF/DOCX/PPTX to Markdown",
|
|
26
|
-
parameters:
|
|
27
|
-
path: Type.String({ description: "Local path to a .pdf, .docx, or .pptx file" }),
|
|
28
|
-
}),
|
|
38
|
+
"Convert a local PDF/DOCX/PPTX/XLSX/XLS to a Markdown bundle on disk and return a handle (Saved-To, Images-Dir, Page-Count, Outline, diagnostics) - the Markdown itself is never inlined; read the Saved-To file (offset/limit) for content. `info: true` returns page count, metadata and TOC (or the sheet inventory) without converting - use it to pick `pages`. `pages` selects inclusive 1-based pages (PDF/DOCX/PPTX); every page ends with `--- end of page.page_number=N ---`. Images are always extracted into images/ with relative links. Primary engine pymupdf4llm, fallback PyMuPDF text (degraded, marked), pure-JS unpdf only when no Python backend exists. Excel yields per-worksheet matrices with formulas, cached values, merged/hidden disclosure. DOCX/PPTX need LibreOffice (soffice). Use `outputDir` for a durable bundle; without it the bundle lands in a per-call temp dir. Input must be a local file path (use fetch first for URLs).",
|
|
39
|
+
promptSnippet: "Convert a local PDF/DOCX/PPTX/XLSX to a Markdown bundle (handle returned; read Saved-To)",
|
|
40
|
+
parameters: buildSchema(),
|
|
29
41
|
|
|
30
|
-
async execute(_toolCallId, params, signal) {
|
|
31
|
-
const
|
|
32
|
-
const
|
|
33
|
-
|
|
42
|
+
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
43
|
+
const settings = resolveConfig<Partial<Tunables>>(ctx.cwd, "docToMd", {}, (raw) => coerceDocToMdSettings(raw, (m) => console.warn(m)), (m) => console.warn(m));
|
|
44
|
+
const p = params as unknown as PerCallInput;
|
|
45
|
+
const perCall: PerCallInput = { ...p, path: resolve(ctx.cwd, p.path), ...(p.outputDir ? { outputDir: resolve(ctx.cwd, p.outputDir) } : {}) };
|
|
46
|
+
const o = resolveOptions(perCall, settings, process.env);
|
|
47
|
+
const r = o.info ? await inspectDocument(o, signal) : await convertDocument(o, signal);
|
|
48
|
+
return { content: [{ type: "text" as const, text: r.output }], details: r.details };
|
|
34
49
|
},
|
|
35
50
|
|
|
36
|
-
renderCall(args, theme
|
|
37
|
-
|
|
38
|
-
text
|
|
51
|
+
renderCall(args, theme) {
|
|
52
|
+
const p = args as unknown as PerCallInput;
|
|
53
|
+
let text = theme.fg("toolTitle", theme.bold(p.info ? "doc_to_md --info " : "doc_to_md "));
|
|
54
|
+
text += theme.fg("accent", p.path ?? "");
|
|
55
|
+
if (p.pages) text += theme.fg("dim", ` pages ${p.pages}`);
|
|
39
56
|
return new Text(text, 0, 0);
|
|
40
57
|
},
|
|
41
58
|
|
|
42
59
|
renderResult(result, { expanded, isPartial }, theme, context) {
|
|
43
|
-
if (isPartial)
|
|
44
|
-
return new Text(theme.fg("warning", "Converting..."), 0, 0);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const details = result.details as DocToMdDetails | undefined;
|
|
60
|
+
if (isPartial) return new Text(theme.fg("warning", "Converting..."), 0, 0);
|
|
48
61
|
const content = result.content[0];
|
|
49
62
|
const fullText = content?.type === "text" ? content.text : "";
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
const firstLine = fullText.split("\n")[0] || "doc_to_md failed";
|
|
53
|
-
return new Text(theme.fg("error", firstLine), 0, 0);
|
|
54
|
-
}
|
|
55
|
-
|
|
63
|
+
if (context.isError) return new Text(theme.fg("error", fullText.split("\n")[0] || "doc_to_md failed"), 0, 0);
|
|
64
|
+
const d = result.details as Partial<DocToMdDetails> | undefined;
|
|
56
65
|
const sep = theme.fg("dim", " · ");
|
|
57
|
-
const parts
|
|
58
|
-
parts.push(theme.fg("
|
|
59
|
-
parts.push(
|
|
60
|
-
|
|
61
|
-
? theme.fg("warning", "unpdf (degraded)")
|
|
62
|
-
: theme.fg("muted", "pymupdf4llm"),
|
|
63
|
-
);
|
|
64
|
-
parts.push(theme.fg("dim", formatSize(details?.bytes ?? 0)));
|
|
65
|
-
if (details?.spilled) parts.push(theme.fg("warning", "→ file"));
|
|
66
|
-
|
|
66
|
+
const parts = [theme.fg("muted", d?.type ?? "?")];
|
|
67
|
+
if (d?.engine) parts.push(d.degraded ? theme.fg("warning", `${d.engine} (degraded)`) : theme.fg("muted", d.engine));
|
|
68
|
+
if (d?.pages !== undefined) parts.push(theme.fg("dim", d.pages ? `${d.pages.length} pages` : "all pages"));
|
|
69
|
+
if (d?.imageCount !== undefined) parts.push(theme.fg("dim", `${d.imageCount} images`));
|
|
67
70
|
let text = parts.join(sep);
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
const lineCount = details?.lines ?? (fullText ? fullText.split("\n").length : 0);
|
|
71
|
-
if (lineCount > 0) {
|
|
72
|
-
text += sep + theme.fg("dim", `${lineCount} lines`);
|
|
73
|
-
}
|
|
74
|
-
text += " " + theme.fg("dim", `(${keyHint("app.tools.expand", "to expand")})`);
|
|
75
|
-
return new Text(text, 0, 0);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
if (fullText) {
|
|
79
|
-
for (const line of fullText.split("\n")) {
|
|
80
|
-
text += `\n${theme.fg("toolOutput", line)}`;
|
|
81
|
-
}
|
|
82
|
-
}
|
|
71
|
+
if (!expanded) return new Text(`${text} ${theme.fg("dim", `(${keyHint("app.tools.expand", "to expand")})`)}`, 0, 0);
|
|
72
|
+
for (const line of fullText.split("\n")) text += `\n${theme.fg("toolOutput", line)}`;
|
|
83
73
|
return new Text(text, 0, 0);
|
|
84
74
|
},
|
|
85
75
|
});
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bundle protocol: a call owns `<stem>` for its whole duration via `<stem>.md.lock`;
|
|
3
|
+
* children stage images under `images/.stage-<lockId>/p<N>/` and mark pages `.done`;
|
|
4
|
+
* Node publishes done pages to `images/<stem>-p<N>-<n>.<ext>`, records every file it
|
|
5
|
+
* wrote in a manifest, and commits `<stem>.md` atomically (tmp + rename).
|
|
6
|
+
*/
|
|
7
|
+
import fs, { closeSync, existsSync, mkdirSync, openSync, readdirSync, readFileSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { tmpdir } from "node:os";
|
|
9
|
+
import { extname, join, resolve } from "node:path";
|
|
10
|
+
import { randomBytes } from "node:crypto";
|
|
11
|
+
|
|
12
|
+
export interface Bundle {
|
|
13
|
+
root: string; stem: string; mdPath: string; lockPath: string; imagesDir: string; stagingDir: string; lockId: string;
|
|
14
|
+
manifest: Set<string>;
|
|
15
|
+
sourceMap: Map<string, string>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function ownedPattern(stem: string): RegExp {
|
|
19
|
+
const esc = stem.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
20
|
+
return new RegExp(`^${esc}-(p|s)\\d+-\\d+\\.[a-z0-9]+$`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const IMG_LINK_RE = /!\[[^\]]*\]\(\s*(?:<([^>]*)>|([^)]*?))\s*\)|<img\b[^>]*\bsrc\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>"']+))/gi;
|
|
24
|
+
|
|
25
|
+
function imageTarget(m: RegExpMatchArray): string {
|
|
26
|
+
const target = (m[1] ?? m[2] ?? m[3] ?? m[4] ?? m[5] ?? "").trim();
|
|
27
|
+
const destination = target.replace(/^<([\s\S]*)>$/, "$1").trim();
|
|
28
|
+
return m[2] === undefined ? destination : destination.replace(/\s+(?:"[^"]*"|'[^']*'|\([^)]*\))$/, "");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function linkedFiles(md: string): string[] {
|
|
32
|
+
const out: string[] = [];
|
|
33
|
+
for (const m of md.matchAll(IMG_LINK_RE)) {
|
|
34
|
+
const target = imageTarget(m);
|
|
35
|
+
if (!target.startsWith("images/")) continue;
|
|
36
|
+
const file = target.slice("images/".length);
|
|
37
|
+
if (file && file !== "." && file !== ".." && /^[^/\\]+$/.test(file)) out.push(file);
|
|
38
|
+
}
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function openBundle(root: string, stem: string, overwrite: boolean): Bundle {
|
|
43
|
+
mkdirSync(root, { recursive: true });
|
|
44
|
+
const mdPath = join(root, `${stem}.md`);
|
|
45
|
+
const lockPath = `${mdPath}.lock`;
|
|
46
|
+
let fd: number;
|
|
47
|
+
try { fd = openSync(lockPath, "wx"); }
|
|
48
|
+
catch (e) {
|
|
49
|
+
if ((e as NodeJS.ErrnoException).code === "EEXIST") throw new Error(`Another conversion owns ${mdPath} (lock: ${lockPath}); if no conversion is running, delete the lock`);
|
|
50
|
+
throw new Error(`Output directory not writable: ${root} (${(e as Error).message})`);
|
|
51
|
+
}
|
|
52
|
+
closeSync(fd);
|
|
53
|
+
const imagesDir = join(root, "images");
|
|
54
|
+
try {
|
|
55
|
+
if (existsSync(mdPath)) {
|
|
56
|
+
if (!overwrite) throw new Error(`Output exists: ${mdPath} (pass overwrite)`);
|
|
57
|
+
const owned = ownedPattern(stem);
|
|
58
|
+
const linked = new Set(linkedFiles(readFileSync(mdPath, "utf8")));
|
|
59
|
+
unlinkSync(mdPath);
|
|
60
|
+
if (existsSync(imagesDir)) for (const f of readdirSync(imagesDir)) if (owned.test(f) || linked.has(f)) rmSync(join(imagesDir, f), { force: true });
|
|
61
|
+
}
|
|
62
|
+
const lockId = randomBytes(6).toString("hex");
|
|
63
|
+
const stagingDir = join(imagesDir, `.stage-${lockId}`);
|
|
64
|
+
mkdirSync(stagingDir, { recursive: true });
|
|
65
|
+
return { root, stem, mdPath, lockPath, imagesDir, stagingDir, lockId, manifest: new Set(), sourceMap: new Map() };
|
|
66
|
+
} catch (e) { rmSync(lockPath, { force: true }); throw e; }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Publish every `p<N>/` staging dir carrying `.done`; discard partial ones. Returns page -> published filenames. */
|
|
70
|
+
export function publishStaged(b: Bundle): Map<number, string[]> {
|
|
71
|
+
const out = new Map<number, string[]>();
|
|
72
|
+
if (!existsSync(b.stagingDir)) return out;
|
|
73
|
+
for (const dir of readdirSync(b.stagingDir).sort()) {
|
|
74
|
+
const m = dir.match(/^p(\d+)$/);
|
|
75
|
+
if (!m) continue;
|
|
76
|
+
const pageDir = join(b.stagingDir, dir);
|
|
77
|
+
if (!existsSync(join(pageDir, ".done"))) { rmSync(pageDir, { recursive: true, force: true }); continue; }
|
|
78
|
+
const page = Number(m[1]);
|
|
79
|
+
const files = readdirSync(pageDir).filter((f) => f !== ".done" && statSync(join(pageDir, f)).isFile()).sort();
|
|
80
|
+
const names: string[] = [];
|
|
81
|
+
files.forEach((f, i) => {
|
|
82
|
+
const name = `${b.stem}-p${page}-${i + 1}${extname(f).toLowerCase()}`;
|
|
83
|
+
renameSync(join(pageDir, f), join(b.imagesDir, name));
|
|
84
|
+
b.manifest.add(name);
|
|
85
|
+
b.sourceMap.set(`${dir}/${f}`, `images/${name}`);
|
|
86
|
+
names.push(name);
|
|
87
|
+
});
|
|
88
|
+
rmSync(pageDir, { recursive: true, force: true });
|
|
89
|
+
out.set(page, names);
|
|
90
|
+
}
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Excel: files staged flat in stagingDir as `s<idx>-<n>.<ext>` -> `images/<stem>-s<idx>-<n>.<ext>`. */
|
|
95
|
+
export function publishSheetImages(b: Bundle): void {
|
|
96
|
+
for (const f of readdirSync(b.stagingDir).sort()) {
|
|
97
|
+
if (!/^s\d+-\d+\.[a-z0-9]+$/i.test(f)) continue;
|
|
98
|
+
const name = `${b.stem}-${f.toLowerCase()}`;
|
|
99
|
+
renameSync(join(b.stagingDir, f), join(b.imagesDir, name));
|
|
100
|
+
b.manifest.add(name);
|
|
101
|
+
b.sourceMap.set(f, `images/${name}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function rewriteImageLinks(md: string, sourceMap: Map<string, string>): string {
|
|
106
|
+
return md.replace(IMG_LINK_RE, (whole, mdAngle, mdPlain, htmlDouble, htmlSingle, htmlUnquoted) => {
|
|
107
|
+
const target = imageTarget([whole, mdAngle, mdPlain, htmlDouble, htmlSingle, htmlUnquoted] as unknown as RegExpMatchArray);
|
|
108
|
+
const dest = sourceMap.get(target) ?? sourceMap.get(target.replace(/^\.\//, ""));
|
|
109
|
+
return dest ? whole.replace(mdAngle === undefined ? target : `<${mdAngle}>`, dest) : whole;
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function validateImageLinks(md: string, manifest: Set<string>): void {
|
|
114
|
+
for (const m of md.matchAll(IMG_LINK_RE)) {
|
|
115
|
+
const target = imageTarget(m);
|
|
116
|
+
if (!target.startsWith("images/") || !manifest.has(target.slice("images/".length))) throw new Error(`unexpected image reference in output: ${target}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function commitBundle(b: Bundle, markdown: string): void {
|
|
121
|
+
const tmp = `${b.mdPath}.tmp`;
|
|
122
|
+
writeFileSync(tmp, markdown, "utf8");
|
|
123
|
+
renameSync(tmp, b.mdPath);
|
|
124
|
+
try { fs.rmSync(b.stagingDir, { recursive: true, force: true }); } catch { /* Markdown is published; cleanup is best-effort. */ }
|
|
125
|
+
try { fs.rmSync(b.lockPath, { force: true }); } catch { /* Markdown is published; cleanup is best-effort. */ }
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function abortBundle(b: Bundle): void {
|
|
129
|
+
for (const f of b.manifest) rmSync(join(b.imagesDir, f), { force: true });
|
|
130
|
+
rmSync(`${b.mdPath}.tmp`, { force: true });
|
|
131
|
+
rmSync(b.stagingDir, { recursive: true, force: true });
|
|
132
|
+
rmSync(b.lockPath, { force: true });
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function tempBundleRoot(): string {
|
|
136
|
+
const dir = join(tmpdir(), `pi-quiver-doc-to-md-${randomBytes(4).toString("hex")}`);
|
|
137
|
+
mkdirSync(dir, { recursive: true });
|
|
138
|
+
return resolve(dir);
|
|
139
|
+
}
|