@aloth/olcli 0.11.0 → 0.13.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 +109 -2
- package/dist/cli.js +398 -17
- package/dist/cli.js.map +1 -1
- package/dist/client.d.ts +56 -3
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +61 -4
- package/dist/client.js.map +1 -1
- package/dist/config.d.ts +37 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +34 -2
- package/dist/config.js.map +1 -1
- package/dist/diff.d.ts +30 -0
- package/dist/diff.d.ts.map +1 -1
- package/dist/diff.js +34 -0
- package/dist/diff.js.map +1 -1
- package/dist/latexdiff.d.ts +165 -0
- package/dist/latexdiff.d.ts.map +1 -0
- package/dist/latexdiff.js +269 -0
- package/dist/latexdiff.js.map +1 -0
- package/dist/prompt.d.ts +63 -0
- package/dist/prompt.d.ts.map +1 -0
- package/dist/prompt.js +146 -0
- package/dist/prompt.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `latexdiff` integration for `olcli diff`.
|
|
3
|
+
*
|
|
4
|
+
* `diff` already computes both sides of the comparison - the project as it is
|
|
5
|
+
* on Overleaf right now, and the working directory - and prints them as
|
|
6
|
+
* unified patches. That is the right artifact for a developer and the wrong
|
|
7
|
+
* one for a thesis advisor, who expects the revision marked up in the document
|
|
8
|
+
* itself. `latexdiff` produces that, and the only parts a user cannot do by
|
|
9
|
+
* hand are getting the remote side onto disk and compiling the result without
|
|
10
|
+
* a local TeX installation.
|
|
11
|
+
*
|
|
12
|
+
* Everything decidable from data is decided here as a pure function so it can
|
|
13
|
+
* be unit-tested with no Overleaf account and no `latexdiff` binary; the spawn
|
|
14
|
+
* and the upload/compile/delete sequence stay in `cli.ts` with the rest of the
|
|
15
|
+
* IO. Same split as `diff.ts`.
|
|
16
|
+
*
|
|
17
|
+
* Orientation matches `diff.ts` and is not negotiable: **old is the remote,
|
|
18
|
+
* new is local**. Struck-through text in the markup is content a subsequent
|
|
19
|
+
* push would overwrite, underlined text is content it would upload.
|
|
20
|
+
*/
|
|
21
|
+
/** Binary looked up on PATH. Not configurable; `latexdiff` is its only name. */
|
|
22
|
+
export declare const LATEXDIFF_BIN = "latexdiff";
|
|
23
|
+
/**
|
|
24
|
+
* Directory the marked-up files are written to, relative to the target
|
|
25
|
+
* directory.
|
|
26
|
+
*
|
|
27
|
+
* Dotted on purpose. `scanLocalFiles` skips dotted entries unconditionally,
|
|
28
|
+
* before any ignore rule is consulted, so this is the one location whose
|
|
29
|
+
* contents cannot leak into a later `push`. A plain `main-diff.tex` next to
|
|
30
|
+
* the document would be uploaded to Overleaf on the next sync.
|
|
31
|
+
*/
|
|
32
|
+
export declare const DIFF_OUTPUT_DIR = ".olcli-diff";
|
|
33
|
+
/**
|
|
34
|
+
* Name of the file `--pdf` uploads to the project for the duration of one
|
|
35
|
+
* compile.
|
|
36
|
+
*
|
|
37
|
+
* Fixed rather than randomized: a name a user can recognize and delete by hand
|
|
38
|
+
* is worth more than one that never collides, and a collision is refused
|
|
39
|
+
* rather than resolved anyway.
|
|
40
|
+
*/
|
|
41
|
+
export declare const REMOTE_SCRATCH_NAME = "olcli-latexdiff.tex";
|
|
42
|
+
/**
|
|
43
|
+
* True when the file declares a document class outside of a comment.
|
|
44
|
+
*
|
|
45
|
+
* This is what separates a root document from the chapters it inputs. A
|
|
46
|
+
* commented-out `\documentclass` is common in subfiles - people leave the
|
|
47
|
+
* standalone preamble behind when they split a document up - and counting it
|
|
48
|
+
* would report every chapter as a candidate root.
|
|
49
|
+
*/
|
|
50
|
+
export declare function declaresDocumentClass(content: Buffer): boolean;
|
|
51
|
+
export declare function isTexPath(path: string): boolean;
|
|
52
|
+
/**
|
|
53
|
+
* Every `.tex` file in the tree that declares a document class, sorted by path
|
|
54
|
+
* so the list a user is shown is stable across runs.
|
|
55
|
+
*/
|
|
56
|
+
export declare function findRootCandidates(files: Map<string, Buffer>): string[];
|
|
57
|
+
/** Raised when the root document cannot be determined; carries the advice. */
|
|
58
|
+
export declare class RootDocumentError extends Error {
|
|
59
|
+
readonly candidates: string[];
|
|
60
|
+
constructor(message: string, candidates?: string[]);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Decide which document to mark up.
|
|
64
|
+
*
|
|
65
|
+
* An explicit `--main` always wins, including for layouts this heuristic
|
|
66
|
+
* cannot read. Otherwise the document class is the signal, and ambiguity is
|
|
67
|
+
* reported rather than guessed at: a project holding both `paper.tex` and
|
|
68
|
+
* `poster.tex` has no correct default, and marking up the wrong one produces a
|
|
69
|
+
* plausible PDF describing the wrong revision - the failure that is hardest to
|
|
70
|
+
* notice.
|
|
71
|
+
*
|
|
72
|
+
* @param files The local tree, already filtered the same way `diff` filters it.
|
|
73
|
+
*/
|
|
74
|
+
export declare function resolveRootDocument(files: Map<string, Buffer>, explicit?: string): string;
|
|
75
|
+
export interface LatexdiffArgsOptions {
|
|
76
|
+
/**
|
|
77
|
+
* Inline `\input` and `\include` before comparing. On by default; see
|
|
78
|
+
* `buildLatexdiffArgs`.
|
|
79
|
+
*/
|
|
80
|
+
flatten?: boolean;
|
|
81
|
+
/** Verbatim `latexdiff` options from `--latexdiff-opt`, passed through. */
|
|
82
|
+
extra?: string[];
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Build the `latexdiff` argument list.
|
|
86
|
+
*
|
|
87
|
+
* `--flatten` is the default, and the reason is the remote compile rather than
|
|
88
|
+
* taste. Without it the marked-up file still contains `\input{sections/intro}`,
|
|
89
|
+
* and compiling it on Overleaf resolves those against the files sitting in the
|
|
90
|
+
* project - the old content - so a multi-file thesis would produce a PDF that
|
|
91
|
+
* marks up the root document and silently shows every input file as unchanged.
|
|
92
|
+
* `--no-flatten` is there for anyone who wants the raw single-file markup.
|
|
93
|
+
*
|
|
94
|
+
* Extra options come before the file arguments because `latexdiff` reads the
|
|
95
|
+
* last two positional arguments as old and new.
|
|
96
|
+
*/
|
|
97
|
+
export declare function buildLatexdiffArgs(oldPath: string, newPath: string, options?: LatexdiffArgsOptions): string[];
|
|
98
|
+
/**
|
|
99
|
+
* Where a marked-up artifact is written, relative to the target directory.
|
|
100
|
+
*
|
|
101
|
+
* Flat rather than mirroring the root document's folder: the output of a
|
|
102
|
+
* flattened run is a single self-contained file, and burying it under
|
|
103
|
+
* `.olcli-diff/chapters/` would only make it harder to find.
|
|
104
|
+
*/
|
|
105
|
+
export declare function diffOutputPath(rootPath: string, extension: string): string;
|
|
106
|
+
/**
|
|
107
|
+
* Project path `--pdf` uploads the marked-up file to.
|
|
108
|
+
*
|
|
109
|
+
* Placed next to the root document rather than at the project root, so that
|
|
110
|
+
* relative `\includegraphics` and `\bibliography` paths in the flattened file
|
|
111
|
+
* resolve exactly as they do for the document it was built from.
|
|
112
|
+
*/
|
|
113
|
+
export declare function remoteScratchPath(rootPath: string): string;
|
|
114
|
+
/**
|
|
115
|
+
* Write a file tree to a directory, for use as `latexdiff`'s old side.
|
|
116
|
+
*
|
|
117
|
+
* `latexdiff --flatten` resolves `\input` relative to each side's own file, so
|
|
118
|
+
* the remote tree has to exist on disk with its folder structure intact -
|
|
119
|
+
* handing it just the root document would break every input in a multi-file
|
|
120
|
+
* project.
|
|
121
|
+
*
|
|
122
|
+
* Entry names are re-checked against the destination even though `diff` has
|
|
123
|
+
* already filtered them against the target directory: this writes files, and a
|
|
124
|
+
* path that was safe relative to one base is not automatically safe relative
|
|
125
|
+
* to another. See #44.
|
|
126
|
+
*/
|
|
127
|
+
export declare function materializeTree(destination: string, files: Map<string, Buffer>): number;
|
|
128
|
+
export interface LatexdiffResult {
|
|
129
|
+
/** The marked-up document, as printed to stdout. */
|
|
130
|
+
markup: string;
|
|
131
|
+
/** Warnings `latexdiff` printed while succeeding. */
|
|
132
|
+
stderr: string;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Raised when `latexdiff` could not be run or exited non-zero. `missing`
|
|
136
|
+
* separates "not installed" - a setup problem with a known fix - from a real
|
|
137
|
+
* failure on the documents, which needs the tool's own message.
|
|
138
|
+
*/
|
|
139
|
+
export declare class LatexdiffError extends Error {
|
|
140
|
+
readonly missing: boolean;
|
|
141
|
+
readonly stderr: string;
|
|
142
|
+
constructor(message: string, missing: boolean, stderr?: string);
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Run `latexdiff`, returning its stdout.
|
|
146
|
+
*
|
|
147
|
+
* `execFile`, never a shell: the arguments include user-supplied paths and
|
|
148
|
+
* pass-through options, and an argv array cannot be talked into running
|
|
149
|
+
* something else.
|
|
150
|
+
*
|
|
151
|
+
* The default `maxBuffer` is 1 MB, which a flattened thesis passes without
|
|
152
|
+
* trying. Exceeding it truncates stdout and reports it as an error, which
|
|
153
|
+
* would read as "latexdiff failed" on exactly the documents this feature
|
|
154
|
+
* exists for.
|
|
155
|
+
*/
|
|
156
|
+
export declare function runLatexdiff(args: string[]): Promise<LatexdiffResult>;
|
|
157
|
+
/**
|
|
158
|
+
* Install advice for a missing binary, by platform.
|
|
159
|
+
*
|
|
160
|
+
* `latexdiff` ships with TeX Live and MacTeX, so on most machines the answer
|
|
161
|
+
* is that the whole distribution is missing rather than this one tool - which
|
|
162
|
+
* is also the case where `--pdf` is most useful, since it needs no local TeX.
|
|
163
|
+
*/
|
|
164
|
+
export declare function latexdiffInstallHint(platform?: string): string;
|
|
165
|
+
//# sourceMappingURL=latexdiff.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"latexdiff.d.ts","sourceRoot":"","sources":["../src/latexdiff.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAUH,gFAAgF;AAChF,eAAO,MAAM,aAAa,cAAc,CAAC;AAEzC;;;;;;;;GAQG;AACH,eAAO,MAAM,eAAe,gBAAgB,CAAC;AAE7C;;;;;;;GAOG;AACH,eAAO,MAAM,mBAAmB,wBAAwB,CAAC;AAsBzD;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAK9D;AAED,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAG/C;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,EAAE,CAKvE;AAED,8EAA8E;AAC9E,qBAAa,iBAAkB,SAAQ,KAAK;IACb,QAAQ,CAAC,UAAU,EAAE,MAAM,EAAE;gBAA9C,OAAO,EAAE,MAAM,EAAW,UAAU,GAAE,MAAM,EAAO;CAIhE;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CA4BzF;AAED,MAAM,WAAW,oBAAoB;IACnC;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,2EAA2E;IAC3E,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;CAClB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,oBAAyB,GACjC,MAAM,EAAE,CAMV;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAG1E;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAG1D;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,eAAe,CAAC,WAAW,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,CAUvF;AAED,MAAM,WAAW,eAAe;IAC9B,oDAAoD;IACpD,MAAM,EAAE,MAAM,CAAC;IACf,qDAAqD;IACrD,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;;GAIG;AACH,qBAAa,cAAe,SAAQ,KAAK;IACV,QAAQ,CAAC,OAAO,EAAE,OAAO;IAAE,QAAQ,CAAC,MAAM;gBAA3D,OAAO,EAAE,MAAM,EAAW,OAAO,EAAE,OAAO,EAAW,MAAM,SAAK;CAI7E;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,eAAe,CAAC,CAsB3E;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,GAAE,MAAyB,GAAG,MAAM,CAShF"}
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `latexdiff` integration for `olcli diff`.
|
|
3
|
+
*
|
|
4
|
+
* `diff` already computes both sides of the comparison - the project as it is
|
|
5
|
+
* on Overleaf right now, and the working directory - and prints them as
|
|
6
|
+
* unified patches. That is the right artifact for a developer and the wrong
|
|
7
|
+
* one for a thesis advisor, who expects the revision marked up in the document
|
|
8
|
+
* itself. `latexdiff` produces that, and the only parts a user cannot do by
|
|
9
|
+
* hand are getting the remote side onto disk and compiling the result without
|
|
10
|
+
* a local TeX installation.
|
|
11
|
+
*
|
|
12
|
+
* Everything decidable from data is decided here as a pure function so it can
|
|
13
|
+
* be unit-tested with no Overleaf account and no `latexdiff` binary; the spawn
|
|
14
|
+
* and the upload/compile/delete sequence stay in `cli.ts` with the rest of the
|
|
15
|
+
* IO. Same split as `diff.ts`.
|
|
16
|
+
*
|
|
17
|
+
* Orientation matches `diff.ts` and is not negotiable: **old is the remote,
|
|
18
|
+
* new is local**. Struck-through text in the markup is content a subsequent
|
|
19
|
+
* push would overwrite, underlined text is content it would upload.
|
|
20
|
+
*/
|
|
21
|
+
import { execFile } from 'node:child_process';
|
|
22
|
+
import { promisify } from 'node:util';
|
|
23
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
24
|
+
import { dirname } from 'node:path';
|
|
25
|
+
import { resolveWithin } from './paths.js';
|
|
26
|
+
const execFileAsync = promisify(execFile);
|
|
27
|
+
/** Binary looked up on PATH. Not configurable; `latexdiff` is its only name. */
|
|
28
|
+
export const LATEXDIFF_BIN = 'latexdiff';
|
|
29
|
+
/**
|
|
30
|
+
* Directory the marked-up files are written to, relative to the target
|
|
31
|
+
* directory.
|
|
32
|
+
*
|
|
33
|
+
* Dotted on purpose. `scanLocalFiles` skips dotted entries unconditionally,
|
|
34
|
+
* before any ignore rule is consulted, so this is the one location whose
|
|
35
|
+
* contents cannot leak into a later `push`. A plain `main-diff.tex` next to
|
|
36
|
+
* the document would be uploaded to Overleaf on the next sync.
|
|
37
|
+
*/
|
|
38
|
+
export const DIFF_OUTPUT_DIR = '.olcli-diff';
|
|
39
|
+
/**
|
|
40
|
+
* Name of the file `--pdf` uploads to the project for the duration of one
|
|
41
|
+
* compile.
|
|
42
|
+
*
|
|
43
|
+
* Fixed rather than randomized: a name a user can recognize and delete by hand
|
|
44
|
+
* is worth more than one that never collides, and a collision is refused
|
|
45
|
+
* rather than resolved anyway.
|
|
46
|
+
*/
|
|
47
|
+
export const REMOTE_SCRATCH_NAME = 'olcli-latexdiff.tex';
|
|
48
|
+
/** Extensions treated as LaTeX source when looking for the root document. */
|
|
49
|
+
const TEX_EXTENSIONS = ['.tex', '.ltx'];
|
|
50
|
+
/**
|
|
51
|
+
* Drop the comment part of a single line of TeX.
|
|
52
|
+
*
|
|
53
|
+
* A `%` starts a comment unless it is escaped, and `\\%` is an escaped
|
|
54
|
+
* backslash followed by a comment, so the run of backslashes before the `%`
|
|
55
|
+
* has to be counted rather than just the character in front of it.
|
|
56
|
+
*/
|
|
57
|
+
function stripTexComment(line) {
|
|
58
|
+
for (let i = 0; i < line.length; i++) {
|
|
59
|
+
if (line[i] !== '%')
|
|
60
|
+
continue;
|
|
61
|
+
let backslashes = 0;
|
|
62
|
+
for (let j = i - 1; j >= 0 && line[j] === '\\'; j--)
|
|
63
|
+
backslashes++;
|
|
64
|
+
if (backslashes % 2 === 0)
|
|
65
|
+
return line.slice(0, i);
|
|
66
|
+
}
|
|
67
|
+
return line;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* True when the file declares a document class outside of a comment.
|
|
71
|
+
*
|
|
72
|
+
* This is what separates a root document from the chapters it inputs. A
|
|
73
|
+
* commented-out `\documentclass` is common in subfiles - people leave the
|
|
74
|
+
* standalone preamble behind when they split a document up - and counting it
|
|
75
|
+
* would report every chapter as a candidate root.
|
|
76
|
+
*/
|
|
77
|
+
export function declaresDocumentClass(content) {
|
|
78
|
+
const text = content.toString('utf-8');
|
|
79
|
+
return text
|
|
80
|
+
.split('\n')
|
|
81
|
+
.some((line) => /\\documentclass\s*[[{]/.test(stripTexComment(line)));
|
|
82
|
+
}
|
|
83
|
+
export function isTexPath(path) {
|
|
84
|
+
const lower = path.toLowerCase();
|
|
85
|
+
return TEX_EXTENSIONS.some((ext) => lower.endsWith(ext));
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Every `.tex` file in the tree that declares a document class, sorted by path
|
|
89
|
+
* so the list a user is shown is stable across runs.
|
|
90
|
+
*/
|
|
91
|
+
export function findRootCandidates(files) {
|
|
92
|
+
return [...files.keys()]
|
|
93
|
+
.filter((path) => isTexPath(path))
|
|
94
|
+
.filter((path) => declaresDocumentClass(files.get(path)))
|
|
95
|
+
.sort();
|
|
96
|
+
}
|
|
97
|
+
/** Raised when the root document cannot be determined; carries the advice. */
|
|
98
|
+
export class RootDocumentError extends Error {
|
|
99
|
+
candidates;
|
|
100
|
+
constructor(message, candidates = []) {
|
|
101
|
+
super(message);
|
|
102
|
+
this.candidates = candidates;
|
|
103
|
+
this.name = 'RootDocumentError';
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Decide which document to mark up.
|
|
108
|
+
*
|
|
109
|
+
* An explicit `--main` always wins, including for layouts this heuristic
|
|
110
|
+
* cannot read. Otherwise the document class is the signal, and ambiguity is
|
|
111
|
+
* reported rather than guessed at: a project holding both `paper.tex` and
|
|
112
|
+
* `poster.tex` has no correct default, and marking up the wrong one produces a
|
|
113
|
+
* plausible PDF describing the wrong revision - the failure that is hardest to
|
|
114
|
+
* notice.
|
|
115
|
+
*
|
|
116
|
+
* @param files The local tree, already filtered the same way `diff` filters it.
|
|
117
|
+
*/
|
|
118
|
+
export function resolveRootDocument(files, explicit) {
|
|
119
|
+
if (explicit) {
|
|
120
|
+
if (!files.has(explicit)) {
|
|
121
|
+
throw new RootDocumentError(`Root document not found locally: ${explicit}\n` +
|
|
122
|
+
' It must be a file in the target directory that diff also reports ' +
|
|
123
|
+
'(an ignore rule can hide it).');
|
|
124
|
+
}
|
|
125
|
+
return explicit;
|
|
126
|
+
}
|
|
127
|
+
const candidates = findRootCandidates(files);
|
|
128
|
+
if (candidates.length === 1)
|
|
129
|
+
return candidates[0];
|
|
130
|
+
if (candidates.length === 0) {
|
|
131
|
+
throw new RootDocumentError('No .tex file declaring \\documentclass was found, so there is no root document to mark up.\n' +
|
|
132
|
+
' Pass --main <path> to name one.');
|
|
133
|
+
}
|
|
134
|
+
throw new RootDocumentError(`${candidates.length} files declare \\documentclass, so the root document is ambiguous.\n` +
|
|
135
|
+
' Pass --main <path> to choose one.', candidates);
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Build the `latexdiff` argument list.
|
|
139
|
+
*
|
|
140
|
+
* `--flatten` is the default, and the reason is the remote compile rather than
|
|
141
|
+
* taste. Without it the marked-up file still contains `\input{sections/intro}`,
|
|
142
|
+
* and compiling it on Overleaf resolves those against the files sitting in the
|
|
143
|
+
* project - the old content - so a multi-file thesis would produce a PDF that
|
|
144
|
+
* marks up the root document and silently shows every input file as unchanged.
|
|
145
|
+
* `--no-flatten` is there for anyone who wants the raw single-file markup.
|
|
146
|
+
*
|
|
147
|
+
* Extra options come before the file arguments because `latexdiff` reads the
|
|
148
|
+
* last two positional arguments as old and new.
|
|
149
|
+
*/
|
|
150
|
+
export function buildLatexdiffArgs(oldPath, newPath, options = {}) {
|
|
151
|
+
const args = [];
|
|
152
|
+
if (options.flatten !== false)
|
|
153
|
+
args.push('--flatten');
|
|
154
|
+
args.push(...(options.extra ?? []));
|
|
155
|
+
args.push(oldPath, newPath);
|
|
156
|
+
return args;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Where a marked-up artifact is written, relative to the target directory.
|
|
160
|
+
*
|
|
161
|
+
* Flat rather than mirroring the root document's folder: the output of a
|
|
162
|
+
* flattened run is a single self-contained file, and burying it under
|
|
163
|
+
* `.olcli-diff/chapters/` would only make it harder to find.
|
|
164
|
+
*/
|
|
165
|
+
export function diffOutputPath(rootPath, extension) {
|
|
166
|
+
const base = rootPath.split('/').pop().replace(/\.(tex|ltx)$/i, '');
|
|
167
|
+
return `${DIFF_OUTPUT_DIR}/${base}-diff.${extension}`;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Project path `--pdf` uploads the marked-up file to.
|
|
171
|
+
*
|
|
172
|
+
* Placed next to the root document rather than at the project root, so that
|
|
173
|
+
* relative `\includegraphics` and `\bibliography` paths in the flattened file
|
|
174
|
+
* resolve exactly as they do for the document it was built from.
|
|
175
|
+
*/
|
|
176
|
+
export function remoteScratchPath(rootPath) {
|
|
177
|
+
const folder = rootPath.includes('/') ? rootPath.slice(0, rootPath.lastIndexOf('/')) : '';
|
|
178
|
+
return folder ? `${folder}/${REMOTE_SCRATCH_NAME}` : REMOTE_SCRATCH_NAME;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Write a file tree to a directory, for use as `latexdiff`'s old side.
|
|
182
|
+
*
|
|
183
|
+
* `latexdiff --flatten` resolves `\input` relative to each side's own file, so
|
|
184
|
+
* the remote tree has to exist on disk with its folder structure intact -
|
|
185
|
+
* handing it just the root document would break every input in a multi-file
|
|
186
|
+
* project.
|
|
187
|
+
*
|
|
188
|
+
* Entry names are re-checked against the destination even though `diff` has
|
|
189
|
+
* already filtered them against the target directory: this writes files, and a
|
|
190
|
+
* path that was safe relative to one base is not automatically safe relative
|
|
191
|
+
* to another. See #44.
|
|
192
|
+
*/
|
|
193
|
+
export function materializeTree(destination, files) {
|
|
194
|
+
let written = 0;
|
|
195
|
+
for (const [path, data] of files) {
|
|
196
|
+
const target = resolveWithin(destination, path);
|
|
197
|
+
if (!target)
|
|
198
|
+
continue;
|
|
199
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
200
|
+
writeFileSync(target, data);
|
|
201
|
+
written++;
|
|
202
|
+
}
|
|
203
|
+
return written;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Raised when `latexdiff` could not be run or exited non-zero. `missing`
|
|
207
|
+
* separates "not installed" - a setup problem with a known fix - from a real
|
|
208
|
+
* failure on the documents, which needs the tool's own message.
|
|
209
|
+
*/
|
|
210
|
+
export class LatexdiffError extends Error {
|
|
211
|
+
missing;
|
|
212
|
+
stderr;
|
|
213
|
+
constructor(message, missing, stderr = '') {
|
|
214
|
+
super(message);
|
|
215
|
+
this.missing = missing;
|
|
216
|
+
this.stderr = stderr;
|
|
217
|
+
this.name = 'LatexdiffError';
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Run `latexdiff`, returning its stdout.
|
|
222
|
+
*
|
|
223
|
+
* `execFile`, never a shell: the arguments include user-supplied paths and
|
|
224
|
+
* pass-through options, and an argv array cannot be talked into running
|
|
225
|
+
* something else.
|
|
226
|
+
*
|
|
227
|
+
* The default `maxBuffer` is 1 MB, which a flattened thesis passes without
|
|
228
|
+
* trying. Exceeding it truncates stdout and reports it as an error, which
|
|
229
|
+
* would read as "latexdiff failed" on exactly the documents this feature
|
|
230
|
+
* exists for.
|
|
231
|
+
*/
|
|
232
|
+
export async function runLatexdiff(args) {
|
|
233
|
+
try {
|
|
234
|
+
const { stdout, stderr } = await execFileAsync(LATEXDIFF_BIN, args, {
|
|
235
|
+
encoding: 'utf-8',
|
|
236
|
+
maxBuffer: 256 * 1024 * 1024,
|
|
237
|
+
});
|
|
238
|
+
return { markup: stdout, stderr };
|
|
239
|
+
}
|
|
240
|
+
catch (error) {
|
|
241
|
+
// execFile rejects with the spawn error for a missing binary and with the
|
|
242
|
+
// captured streams for a non-zero exit; `code` carries the errno in the
|
|
243
|
+
// first case and the exit status in the second.
|
|
244
|
+
const failure = error;
|
|
245
|
+
if (failure.code === 'ENOENT') {
|
|
246
|
+
throw new LatexdiffError(`${LATEXDIFF_BIN} was not found on PATH`, true);
|
|
247
|
+
}
|
|
248
|
+
const stderr = typeof failure.stderr === 'string' ? failure.stderr.trim() : '';
|
|
249
|
+
throw new LatexdiffError(`${LATEXDIFF_BIN} exited with status ${failure.code ?? 'unknown'}`, false, stderr);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Install advice for a missing binary, by platform.
|
|
254
|
+
*
|
|
255
|
+
* `latexdiff` ships with TeX Live and MacTeX, so on most machines the answer
|
|
256
|
+
* is that the whole distribution is missing rather than this one tool - which
|
|
257
|
+
* is also the case where `--pdf` is most useful, since it needs no local TeX.
|
|
258
|
+
*/
|
|
259
|
+
export function latexdiffInstallHint(platform = process.platform) {
|
|
260
|
+
switch (platform) {
|
|
261
|
+
case 'darwin':
|
|
262
|
+
return 'Install MacTeX (brew install --cask mactex-no-gui) or brew install latexdiff';
|
|
263
|
+
case 'win32':
|
|
264
|
+
return 'Install MiKTeX or TeX Live; both ship latexdiff';
|
|
265
|
+
default:
|
|
266
|
+
return 'Install it from your TeX distribution (apt install latexdiff, or TeX Live)';
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
//# sourceMappingURL=latexdiff.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"latexdiff.js","sourceRoot":"","sources":["../src/latexdiff.ts"],"names":[],"mappings":"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"}
|
package/dist/prompt.d.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interactive terminal prompts.
|
|
3
|
+
*
|
|
4
|
+
* Exists for one reason: reading a password without it appearing on the
|
|
5
|
+
* command line, and therefore in shell history. See issue #50.
|
|
6
|
+
*
|
|
7
|
+
* No dependency for this. A masked read is a raw-mode loop over stdin, and
|
|
8
|
+
* pulling in a prompt library to avoid writing it would cost more than it
|
|
9
|
+
* saves - the bar in this repo is low dependency count, not zero.
|
|
10
|
+
*
|
|
11
|
+
* The keystroke handling is a pure reducer (`applyChunk`) with the terminal
|
|
12
|
+
* wiring wrapped around it, for the same reason `diff.ts` is pure: a raw-mode
|
|
13
|
+
* loop cannot be exercised without a pty, and the part that can silently
|
|
14
|
+
* corrupt a password is the character handling, not the plumbing.
|
|
15
|
+
*/
|
|
16
|
+
/** Raised when the user aborts the prompt (Ctrl+C / Ctrl+D). */
|
|
17
|
+
export declare class PromptCancelled extends Error {
|
|
18
|
+
constructor();
|
|
19
|
+
}
|
|
20
|
+
/** Raised when there is no terminal to prompt on. */
|
|
21
|
+
export declare class NotATerminal extends Error {
|
|
22
|
+
constructor(message: string);
|
|
23
|
+
}
|
|
24
|
+
export interface KeyState {
|
|
25
|
+
/** What the user has typed so far. */
|
|
26
|
+
value: string;
|
|
27
|
+
/**
|
|
28
|
+
* Where we are in an ANSI escape sequence.
|
|
29
|
+
*
|
|
30
|
+
* 'none' - ordinary input
|
|
31
|
+
* 'esc' - saw ESC, waiting to see whether a sequence follows
|
|
32
|
+
* 'csi' - inside `ESC [` or `ESC O`, swallowing until the final byte
|
|
33
|
+
*/
|
|
34
|
+
escape: 'none' | 'esc' | 'csi';
|
|
35
|
+
outcome: 'pending' | 'submit' | 'cancel';
|
|
36
|
+
}
|
|
37
|
+
export declare function initialKeyState(): KeyState;
|
|
38
|
+
/**
|
|
39
|
+
* Fold one chunk of terminal input into the state.
|
|
40
|
+
*
|
|
41
|
+
* A chunk is not one keystroke: a paste arrives whole, and a single arrow key
|
|
42
|
+
* arrives as the three characters `ESC [ A`. Both have to be handled here.
|
|
43
|
+
*
|
|
44
|
+
* Escape sequences are swallowed rather than filtered character by character.
|
|
45
|
+
* Dropping only the ESC leaves `[` and `A` behind, and both are printable - so
|
|
46
|
+
* an arrow key pressed mid-entry would append "[A" to the password and the
|
|
47
|
+
* user would never see it. That is the failure this state machine exists to
|
|
48
|
+
* prevent; it was found by driving the prompt over a pty.
|
|
49
|
+
*/
|
|
50
|
+
export declare function applyChunk(state: KeyState, chunk: string): KeyState;
|
|
51
|
+
/**
|
|
52
|
+
* Read a line from stdin without echoing it.
|
|
53
|
+
*
|
|
54
|
+
* Nothing is written back as the user types - not even asterisks. `sudo`
|
|
55
|
+
* behaves the same way, and echoing one character per keystroke leaks the
|
|
56
|
+
* length of the secret to anyone looking at the screen.
|
|
57
|
+
*
|
|
58
|
+
* Throws `NotATerminal` when stdin is piped or redirected. A masked read is
|
|
59
|
+
* impossible there, and silently falling back to an unmasked one would defeat
|
|
60
|
+
* the point; the caller is expected to name the scripted alternative instead.
|
|
61
|
+
*/
|
|
62
|
+
export declare function promptHidden(label: string): Promise<string>;
|
|
63
|
+
//# sourceMappingURL=prompt.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prompt.d.ts","sourceRoot":"","sources":["../src/prompt.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AASH,gEAAgE;AAChE,qBAAa,eAAgB,SAAQ,KAAK;;CAKzC;AAED,qDAAqD;AACrD,qBAAa,YAAa,SAAQ,KAAK;gBACzB,OAAO,EAAE,MAAM;CAI5B;AAED,MAAM,WAAW,QAAQ;IACvB,sCAAsC;IACtC,KAAK,EAAE,MAAM,CAAC;IACd;;;;;;OAMG;IACH,MAAM,EAAE,MAAM,GAAG,KAAK,GAAG,KAAK,CAAC;IAC/B,OAAO,EAAE,SAAS,GAAG,QAAQ,GAAG,QAAQ,CAAC;CAC1C;AAED,wBAAgB,eAAe,IAAI,QAAQ,CAE1C;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,GAAG,QAAQ,CA4CnE;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CA4C3D"}
|
package/dist/prompt.js
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interactive terminal prompts.
|
|
3
|
+
*
|
|
4
|
+
* Exists for one reason: reading a password without it appearing on the
|
|
5
|
+
* command line, and therefore in shell history. See issue #50.
|
|
6
|
+
*
|
|
7
|
+
* No dependency for this. A masked read is a raw-mode loop over stdin, and
|
|
8
|
+
* pulling in a prompt library to avoid writing it would cost more than it
|
|
9
|
+
* saves - the bar in this repo is low dependency count, not zero.
|
|
10
|
+
*
|
|
11
|
+
* The keystroke handling is a pure reducer (`applyChunk`) with the terminal
|
|
12
|
+
* wiring wrapped around it, for the same reason `diff.ts` is pure: a raw-mode
|
|
13
|
+
* loop cannot be exercised without a pty, and the part that can silently
|
|
14
|
+
* corrupt a password is the character handling, not the plumbing.
|
|
15
|
+
*/
|
|
16
|
+
import { stdin, stdout } from 'node:process';
|
|
17
|
+
const CTRL_C = '\u0003';
|
|
18
|
+
const CTRL_D = '\u0004';
|
|
19
|
+
const DEL = '\u007f';
|
|
20
|
+
const ESC = '\u001b';
|
|
21
|
+
/** Raised when the user aborts the prompt (Ctrl+C / Ctrl+D). */
|
|
22
|
+
export class PromptCancelled extends Error {
|
|
23
|
+
constructor() {
|
|
24
|
+
super('Cancelled');
|
|
25
|
+
this.name = 'PromptCancelled';
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/** Raised when there is no terminal to prompt on. */
|
|
29
|
+
export class NotATerminal extends Error {
|
|
30
|
+
constructor(message) {
|
|
31
|
+
super(message);
|
|
32
|
+
this.name = 'NotATerminal';
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export function initialKeyState() {
|
|
36
|
+
return { value: '', escape: 'none', outcome: 'pending' };
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Fold one chunk of terminal input into the state.
|
|
40
|
+
*
|
|
41
|
+
* A chunk is not one keystroke: a paste arrives whole, and a single arrow key
|
|
42
|
+
* arrives as the three characters `ESC [ A`. Both have to be handled here.
|
|
43
|
+
*
|
|
44
|
+
* Escape sequences are swallowed rather than filtered character by character.
|
|
45
|
+
* Dropping only the ESC leaves `[` and `A` behind, and both are printable - so
|
|
46
|
+
* an arrow key pressed mid-entry would append "[A" to the password and the
|
|
47
|
+
* user would never see it. That is the failure this state machine exists to
|
|
48
|
+
* prevent; it was found by driving the prompt over a pty.
|
|
49
|
+
*/
|
|
50
|
+
export function applyChunk(state, chunk) {
|
|
51
|
+
let { value, escape, outcome } = state;
|
|
52
|
+
for (const ch of chunk) {
|
|
53
|
+
if (outcome !== 'pending')
|
|
54
|
+
break;
|
|
55
|
+
if (escape === 'csi') {
|
|
56
|
+
// Parameter and intermediate bytes continue the sequence; a final byte
|
|
57
|
+
// in @..~ ends it. `ESC [ 3 ~` (Delete) needs the parameter handling.
|
|
58
|
+
if (ch >= '@' && ch <= '~')
|
|
59
|
+
escape = 'none';
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (escape === 'esc') {
|
|
63
|
+
// `ESC [` is CSI, `ESC O` is the alternate cursor mode some terminals
|
|
64
|
+
// use for arrows. Anything else was a lone ESC plus a real character.
|
|
65
|
+
escape = ch === '[' || ch === 'O' ? 'csi' : 'none';
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
switch (ch) {
|
|
69
|
+
case '\r':
|
|
70
|
+
case '\n':
|
|
71
|
+
outcome = 'submit';
|
|
72
|
+
break;
|
|
73
|
+
case CTRL_C:
|
|
74
|
+
case CTRL_D:
|
|
75
|
+
outcome = 'cancel';
|
|
76
|
+
break;
|
|
77
|
+
case DEL:
|
|
78
|
+
case '\b':
|
|
79
|
+
value = value.slice(0, -1);
|
|
80
|
+
break;
|
|
81
|
+
case ESC:
|
|
82
|
+
escape = 'esc';
|
|
83
|
+
break;
|
|
84
|
+
default:
|
|
85
|
+
// Remaining control characters are not password material. Appending
|
|
86
|
+
// them would corrupt the value invisibly.
|
|
87
|
+
if (ch >= ' ')
|
|
88
|
+
value += ch;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return { value, escape, outcome };
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Read a line from stdin without echoing it.
|
|
95
|
+
*
|
|
96
|
+
* Nothing is written back as the user types - not even asterisks. `sudo`
|
|
97
|
+
* behaves the same way, and echoing one character per keystroke leaks the
|
|
98
|
+
* length of the secret to anyone looking at the screen.
|
|
99
|
+
*
|
|
100
|
+
* Throws `NotATerminal` when stdin is piped or redirected. A masked read is
|
|
101
|
+
* impossible there, and silently falling back to an unmasked one would defeat
|
|
102
|
+
* the point; the caller is expected to name the scripted alternative instead.
|
|
103
|
+
*/
|
|
104
|
+
export function promptHidden(label) {
|
|
105
|
+
return new Promise((resolve, reject) => {
|
|
106
|
+
if (!stdin.isTTY) {
|
|
107
|
+
reject(new NotATerminal('stdin is not a terminal, so a password cannot be prompted for'));
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
// Restore whatever mode the caller was in rather than assuming cooked:
|
|
111
|
+
// olcli may be driven from a wrapper that set raw mode itself.
|
|
112
|
+
const wasRaw = stdin.isRaw;
|
|
113
|
+
let state = initialKeyState();
|
|
114
|
+
let settled = false;
|
|
115
|
+
const cleanup = () => {
|
|
116
|
+
if (settled)
|
|
117
|
+
return;
|
|
118
|
+
settled = true;
|
|
119
|
+
stdin.removeListener('data', onData);
|
|
120
|
+
stdin.setRawMode(wasRaw);
|
|
121
|
+
stdin.pause();
|
|
122
|
+
stdout.write('\n');
|
|
123
|
+
};
|
|
124
|
+
const onData = (chunk) => {
|
|
125
|
+
state = applyChunk(state, chunk);
|
|
126
|
+
if (state.outcome === 'submit') {
|
|
127
|
+
cleanup();
|
|
128
|
+
resolve(state.value);
|
|
129
|
+
}
|
|
130
|
+
else if (state.outcome === 'cancel') {
|
|
131
|
+
cleanup();
|
|
132
|
+
reject(new PromptCancelled());
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
// Raw mode goes on before the label is written. The terminal's line
|
|
136
|
+
// discipline echoes whatever arrives while ECHO is still set, so a caller
|
|
137
|
+
// that answers the instant the prompt appears - anything automated - would
|
|
138
|
+
// otherwise have its first keystrokes echoed to the screen.
|
|
139
|
+
stdin.setRawMode(true);
|
|
140
|
+
stdin.resume();
|
|
141
|
+
stdin.setEncoding('utf8');
|
|
142
|
+
stdin.on('data', onData);
|
|
143
|
+
stdout.write(label);
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
//# sourceMappingURL=prompt.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prompt.js","sourceRoot":"","sources":["../src/prompt.ts"],"names":[],"mappings":"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"}
|