@aloth/olcli 0.12.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 +76 -0
- package/dist/cli.js +280 -6
- package/dist/cli.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/package.json +1 -1
package/README.md
CHANGED
|
@@ -23,6 +23,7 @@ Work with Overleaf projects directly from your command line. Edit locally with y
|
|
|
23
23
|
- ⬆️ **Push** local changes back to Overleaf
|
|
24
24
|
- 🔄 **Sync** bidirectionally with smart conflict detection
|
|
25
25
|
- 🔍 **Diff** local files against the live remote before pushing
|
|
26
|
+
- 📝 **Marked-up revisions** — `diff --latexdiff` produces the struck-through/underlined PDF advisors and journals ask for
|
|
26
27
|
- 🔀 **Git remote** — use Overleaf as a native git remote ([docs](docs/GIT-REMOTE.md))
|
|
27
28
|
- ✌️ **Two-way deletions** — files removed locally are deleted on Overleaf on next sync
|
|
28
29
|
- 🗑️ **Delete** and ✏️ **rename** remote files by path
|
|
@@ -217,6 +218,7 @@ olcli diff # every changed file, as patches
|
|
|
217
218
|
olcli diff --name-only # just the changed paths
|
|
218
219
|
olcli diff --file main.tex # one file
|
|
219
220
|
olcli diff -U 8 # wider context
|
|
221
|
+
olcli diff --exit-code # exit 1 if anything differs, for CI
|
|
220
222
|
```
|
|
221
223
|
|
|
222
224
|
**The remote side is fetched fresh on every run.** The diff describes the
|
|
@@ -240,6 +242,80 @@ than the last pull, because that is what `push` uploads; `diff` lists files
|
|
|
240
242
|
whose **contents** actually differ. A file you touched without editing appears
|
|
241
243
|
in the first and not the second.
|
|
242
244
|
|
|
245
|
+
#### Marked-up revisions with latexdiff
|
|
246
|
+
|
|
247
|
+
A unified diff is the right artifact for a developer and the wrong one for a
|
|
248
|
+
thesis advisor. `--latexdiff` marks the same revision up inside the document
|
|
249
|
+
instead — deletions struck through, additions underlined — which is what
|
|
250
|
+
advisors and journals ask for.
|
|
251
|
+
|
|
252
|
+
```bash
|
|
253
|
+
olcli diff --latexdiff # write .olcli-diff/main-diff.tex
|
|
254
|
+
olcli diff --latexdiff --pdf # ...and compile it, download .olcli-diff/main-diff.pdf
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
Requires `latexdiff` on your PATH. It ships with TeX Live and MacTeX; nothing
|
|
258
|
+
else in `olcli diff` needs an external tool.
|
|
259
|
+
|
|
260
|
+
**`--pdf` compiles on Overleaf, so you do not need a local TeX installation.**
|
|
261
|
+
The compile endpoint can only build a file that is in the project, so the
|
|
262
|
+
marked-up document is uploaded as `olcli-latexdiff.tex` next to your root
|
|
263
|
+
document, compiled, downloaded, and then removed. The command says so before it
|
|
264
|
+
does it, refuses to overwrite a file of that name if one already exists, and
|
|
265
|
+
prints the exact `olcli rm` command if the cleanup itself fails.
|
|
266
|
+
|
|
267
|
+
`\input` and `\include` are inlined before comparing (`--no-flatten` to opt
|
|
268
|
+
out). Without that, a remote compile would resolve those against the files
|
|
269
|
+
sitting in the project — the old content — and quietly produce a PDF showing
|
|
270
|
+
every input file as unchanged.
|
|
271
|
+
|
|
272
|
+
Output goes to `.olcli-diff/`, which is dotted so that `push` and `sync` never
|
|
273
|
+
pick it up; `-o <path>` puts it elsewhere. The root document is the `.tex` file
|
|
274
|
+
declaring `\documentclass`; if several do, `--main <path>` picks one rather
|
|
275
|
+
than the command guessing. `--latexdiff-opt` passes anything else straight
|
|
276
|
+
through, e.g. `--latexdiff-opt --math-markup=0`.
|
|
277
|
+
|
|
278
|
+
One limit worth knowing: `--pdf` compiles against the project, not against your
|
|
279
|
+
working directory, so anything the markup needs must already be on Overleaf. A
|
|
280
|
+
`.sty` or `.cls` you added locally fails the compile — the compiler log is
|
|
281
|
+
written next to the marked-up source when that happens. A *figure* you added
|
|
282
|
+
locally does not fail it; Overleaf draws a placeholder box naming the missing
|
|
283
|
+
file and the rest of the PDF is fine.
|
|
284
|
+
|
|
285
|
+
#### Using `diff` as a CI gate
|
|
286
|
+
|
|
287
|
+
`--exit-code` turns the command into a check, with the statuses `diff(1)` uses:
|
|
288
|
+
|
|
289
|
+
| Status | Meaning |
|
|
290
|
+
|--------|---------|
|
|
291
|
+
| `0` | Nothing differs |
|
|
292
|
+
| `1` | Something differs |
|
|
293
|
+
| `2` | The run failed — bad flags, no session, project unreachable |
|
|
294
|
+
|
|
295
|
+
```yaml
|
|
296
|
+
- name: Fail if the paper on Overleaf has drifted from the repo
|
|
297
|
+
run: olcli diff --exit-code --name-only
|
|
298
|
+
env:
|
|
299
|
+
OVERLEAF_SESSION: ${{ secrets.OVERLEAF_SESSION }}
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
**The `1` versus `2` split is the point of the flag.** Without it a pipeline
|
|
303
|
+
cannot tell a changed file from an expired session cookie, and a job that goes
|
|
304
|
+
red for the second reason sends whoever reads the log hunting for a diff that
|
|
305
|
+
was never computed. Every other olcli command reports failure as `1`, and
|
|
306
|
+
`olcli diff` still does when `--exit-code` is absent, so adding the flag does
|
|
307
|
+
not change what existing scripts see.
|
|
308
|
+
|
|
309
|
+
The gate covers whatever was compared: `--file main.tex` narrows it to one
|
|
310
|
+
file, the way a `git diff --exit-code` pathspec does, and a `--file` that
|
|
311
|
+
matches nothing is `0` rather than an error. Under `--latexdiff` it still
|
|
312
|
+
reports on the project as a whole — a changed figure is a real difference even
|
|
313
|
+
though a marked-up root document cannot show it.
|
|
314
|
+
|
|
315
|
+
Note that this compares against Overleaf **now**, not against your last pull,
|
|
316
|
+
so the check is "has anyone drifted from what is committed here", which is what
|
|
317
|
+
makes it worth running on a schedule as well as on a push.
|
|
318
|
+
|
|
243
319
|
#### How deletion propagation works
|
|
244
320
|
|
|
245
321
|
`olcli` records a manifest of remote files in `.olcli.json`. On next sync:
|
package/dist/cli.js
CHANGED
|
@@ -8,14 +8,16 @@
|
|
|
8
8
|
import { Command } from 'commander';
|
|
9
9
|
import chalk from 'chalk';
|
|
10
10
|
import ora from 'ora';
|
|
11
|
-
import { writeFileSync, readFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
11
|
+
import { writeFileSync, readFileSync, existsSync, mkdirSync, mkdtempSync, rmSync } from 'node:fs';
|
|
12
12
|
import { join, dirname, basename } from 'node:path';
|
|
13
|
+
import { tmpdir } from 'node:os';
|
|
13
14
|
import { fileURLToPath } from 'node:url';
|
|
14
15
|
import { OverleafClient } from './client.js';
|
|
15
16
|
import { resolveRemotePath, resolveWithin, normalizeRemotePath } from './paths.js';
|
|
16
17
|
import { planProjectRenames } from './rename-plan.js';
|
|
17
18
|
import { scanLocalFiles } from './scan.js';
|
|
18
|
-
import { compareTrees, filterRemoteTree, renderFileDiff, statusLetter } from './diff.js';
|
|
19
|
+
import { compareTrees, DIFF_EXIT_FAILURE, differencesExitCode, filterRemoteTree, renderFileDiff, statusLetter, } from './diff.js';
|
|
20
|
+
import { DIFF_OUTPUT_DIR, LatexdiffError, RootDocumentError, buildLatexdiffArgs, diffOutputPath, isTexPath, latexdiffInstallHint, materializeTree, remoteScratchPath, resolveRootDocument, runLatexdiff, } from './latexdiff.js';
|
|
19
21
|
import { loadIgnore } from './ignore.js';
|
|
20
22
|
// Read version from package.json
|
|
21
23
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -1620,6 +1622,13 @@ program
|
|
|
1620
1622
|
.option('--name-only', 'List changed paths instead of printing patches')
|
|
1621
1623
|
.option('--file <path>', 'Diff a single file')
|
|
1622
1624
|
.option('-U, --unified <n>', 'Lines of context around each hunk (default: 3)', parseInt)
|
|
1625
|
+
.option('--exit-code', 'Exit 1 if anything differs, 0 if nothing does, 2 on failure (for CI)')
|
|
1626
|
+
.option('--latexdiff', 'Mark the revision up inside the document with latexdiff (requires latexdiff on PATH)')
|
|
1627
|
+
.option('--pdf', 'Compile the marked-up document on Overleaf and download the PDF (implies --latexdiff)')
|
|
1628
|
+
.option('--main <path>', 'Root .tex document to mark up (default: the only file declaring \\documentclass)')
|
|
1629
|
+
.option('-o, --output <path>', `Where to write the marked-up .tex (default: ${DIFF_OUTPUT_DIR}/<root>-diff.tex)`)
|
|
1630
|
+
.option('--no-flatten', 'Leave \\input/\\include in place instead of inlining them')
|
|
1631
|
+
.option('--latexdiff-opt <opt>', 'Pass an option straight through to latexdiff (repeatable)', (value, previous = []) => [...previous, value])
|
|
1623
1632
|
.option('--no-default-ignore', 'Disable built-in LaTeX artifact ignore list (only .olignore applies)')
|
|
1624
1633
|
.option('--no-ignore', 'Disable all ignore filtering')
|
|
1625
1634
|
.option('--cookie <session>', 'Session cookie override')
|
|
@@ -1628,12 +1637,56 @@ The remote side is fetched fresh on every run, so the diff describes the
|
|
|
1628
1637
|
project as it is right now - which is what a subsequent push would overwrite.
|
|
1629
1638
|
It is not a comparison against the last pull. A collaborator editing between
|
|
1630
1639
|
diff and push can still change the outcome; the fetch time is printed for that
|
|
1631
|
-
reason
|
|
1640
|
+
reason.
|
|
1641
|
+
|
|
1642
|
+
--latexdiff hands those same two sides to latexdiff and marks the revision up
|
|
1643
|
+
inside the document instead: struck through is what a push would overwrite,
|
|
1644
|
+
underlined is what it would upload. --pdf additionally uploads the marked-up
|
|
1645
|
+
document to the project for one compile, downloads the PDF, and removes it
|
|
1646
|
+
again - so a reviewable PDF needs no local TeX installation.
|
|
1647
|
+
|
|
1648
|
+
--exit-code makes the command a CI gate, with diff(1)'s statuses: 0 when
|
|
1649
|
+
nothing differs, 1 when something does, and 2 when the run itself failed. The
|
|
1650
|
+
last one matters - without it a pipeline cannot tell a changed file from an
|
|
1651
|
+
expired session cookie. It applies to whatever was compared, so --file narrows
|
|
1652
|
+
the gate to one file, and it reports on the project even under --latexdiff,
|
|
1653
|
+
where the markup only covers the root document.`)
|
|
1632
1654
|
.action(async (project, dir, options) => {
|
|
1633
1655
|
const targetDir = dir || '.';
|
|
1656
|
+
// Every way this command can fail, as opposed to finding differences.
|
|
1657
|
+
// Under --exit-code that has to be distinguishable from status 1, or a
|
|
1658
|
+
// pipeline reads "could not reach Overleaf" as "the paper changed"; with
|
|
1659
|
+
// the flag absent it stays 1, which is what every other command exits.
|
|
1660
|
+
const failureCode = options.exitCode ? DIFF_EXIT_FAILURE : 1;
|
|
1634
1661
|
if (!existsSync(targetDir)) {
|
|
1635
1662
|
console.error(chalk.red(`Directory not found: ${targetDir}`));
|
|
1636
|
-
process.exit(
|
|
1663
|
+
process.exit(failureCode);
|
|
1664
|
+
}
|
|
1665
|
+
// Checked before connecting: an unusable combination of flags should not
|
|
1666
|
+
// cost a login and a full project download first.
|
|
1667
|
+
const latexdiffMode = Boolean(options.latexdiff || options.pdf);
|
|
1668
|
+
const conflicting = [
|
|
1669
|
+
options.nameOnly ? '--name-only' : null,
|
|
1670
|
+
options.file ? '--file' : null,
|
|
1671
|
+
options.unified !== undefined ? '-U/--unified' : null,
|
|
1672
|
+
].filter(Boolean);
|
|
1673
|
+
if (latexdiffMode && conflicting.length > 0) {
|
|
1674
|
+
console.error(chalk.red(`--latexdiff cannot be combined with ${conflicting.join(', ')}`));
|
|
1675
|
+
console.error('It marks up one root document; those options select and shape unified patch output.');
|
|
1676
|
+
process.exit(failureCode);
|
|
1677
|
+
}
|
|
1678
|
+
if (!latexdiffMode) {
|
|
1679
|
+
// Accepting these silently would produce a normal patch and no markup,
|
|
1680
|
+
// with nothing in the output saying the flag was dropped.
|
|
1681
|
+
const latexdiffOnly = [
|
|
1682
|
+
options.main ? '--main' : null,
|
|
1683
|
+
options.output ? '--output' : null,
|
|
1684
|
+
options.latexdiffOpt?.length ? '--latexdiff-opt' : null,
|
|
1685
|
+
].filter(Boolean);
|
|
1686
|
+
if (latexdiffOnly.length > 0) {
|
|
1687
|
+
console.error(chalk.red(`${latexdiffOnly.join(', ')} only applies with --latexdiff`));
|
|
1688
|
+
process.exit(failureCode);
|
|
1689
|
+
}
|
|
1637
1690
|
}
|
|
1638
1691
|
const spinner = ora('Connecting...').start();
|
|
1639
1692
|
try {
|
|
@@ -1645,7 +1698,7 @@ reason.`)
|
|
|
1645
1698
|
catch (error) {
|
|
1646
1699
|
spinner.fail(error.message);
|
|
1647
1700
|
console.error('Either run from a directory with .olcli.json or pass a project name/ID');
|
|
1648
|
-
process.exit(
|
|
1701
|
+
process.exit(failureCode);
|
|
1649
1702
|
}
|
|
1650
1703
|
const { id: projectId, name: projectName } = resolved;
|
|
1651
1704
|
// The whole project arrives as one zip in a single request - the same
|
|
@@ -1671,6 +1724,28 @@ reason.`)
|
|
|
1671
1724
|
localFiles.set(file.relativePath, readFileSync(file.path));
|
|
1672
1725
|
}
|
|
1673
1726
|
let entries = compareTrees(localFiles, remoteFiles).filter((e) => e.status !== 'unchanged');
|
|
1727
|
+
if (latexdiffMode) {
|
|
1728
|
+
await runLatexdiffMode({
|
|
1729
|
+
client,
|
|
1730
|
+
projectId,
|
|
1731
|
+
projectName,
|
|
1732
|
+
targetDir,
|
|
1733
|
+
localFiles,
|
|
1734
|
+
remoteFiles,
|
|
1735
|
+
entries,
|
|
1736
|
+
fetchedAt,
|
|
1737
|
+
failureCode,
|
|
1738
|
+
options,
|
|
1739
|
+
spinner,
|
|
1740
|
+
});
|
|
1741
|
+
setLastProject(projectId);
|
|
1742
|
+
// Reports on the comparison, not on the markup: a changed figure is a
|
|
1743
|
+
// difference in the project even though a marked-up root document
|
|
1744
|
+
// cannot show it. The `No .tex file differs` line above says as much.
|
|
1745
|
+
if (options.exitCode)
|
|
1746
|
+
process.exitCode = differencesExitCode(entries);
|
|
1747
|
+
return;
|
|
1748
|
+
}
|
|
1674
1749
|
if (options.file) {
|
|
1675
1750
|
const wanted = normalizeRemotePath(options.file);
|
|
1676
1751
|
entries = entries.filter((e) => e.path === wanted);
|
|
@@ -1721,12 +1796,211 @@ reason.`)
|
|
|
1721
1796
|
}
|
|
1722
1797
|
console.log(chalk.dim(` a/ = remote as of ${fetchedAt.toISOString()}, b/ = local`));
|
|
1723
1798
|
setLastProject(projectId);
|
|
1799
|
+
// Set rather than exited: `process.exit` drops whatever is still
|
|
1800
|
+
// buffered on a non-TTY stdout, and `olcli diff --exit-code > patch.txt`
|
|
1801
|
+
// is precisely a large patch going into a pipe. Returning lets node
|
|
1802
|
+
// flush and then exit with this status on its own.
|
|
1803
|
+
if (options.exitCode)
|
|
1804
|
+
process.exitCode = differencesExitCode(entries);
|
|
1724
1805
|
}
|
|
1725
1806
|
catch (error) {
|
|
1726
1807
|
spinner.fail(`Failed: ${error.message}`);
|
|
1727
|
-
process.exit(
|
|
1808
|
+
process.exit(failureCode);
|
|
1728
1809
|
}
|
|
1729
1810
|
});
|
|
1811
|
+
/**
|
|
1812
|
+
* `--latexdiff` / `--pdf`: mark the revision up inside the document.
|
|
1813
|
+
*
|
|
1814
|
+
* Runs on the two sides `diff` has already fetched, so the semantics are the
|
|
1815
|
+
* ones documented for the command - old is the remote as of this run, new is
|
|
1816
|
+
* the working directory - and no extra request is made to produce the markup.
|
|
1817
|
+
*
|
|
1818
|
+
* The remote side has to reach the filesystem before `latexdiff` can read it,
|
|
1819
|
+
* and the whole tree is written rather than the root document alone, because
|
|
1820
|
+
* `--flatten` resolves each `\input` relative to its own side.
|
|
1821
|
+
*/
|
|
1822
|
+
async function runLatexdiffMode(params) {
|
|
1823
|
+
const { client, projectId, projectName, targetDir, localFiles, remoteFiles, entries, fetchedAt, failureCode, options, spinner, } = params;
|
|
1824
|
+
let root = '';
|
|
1825
|
+
try {
|
|
1826
|
+
root = resolveRootDocument(localFiles, options.main ? normalizeRemotePath(options.main) : undefined);
|
|
1827
|
+
}
|
|
1828
|
+
catch (error) {
|
|
1829
|
+
if (!(error instanceof RootDocumentError))
|
|
1830
|
+
throw error;
|
|
1831
|
+
spinner.stop();
|
|
1832
|
+
console.error(chalk.red(error.message));
|
|
1833
|
+
for (const candidate of error.candidates) {
|
|
1834
|
+
console.error(chalk.dim(` ${candidate}`));
|
|
1835
|
+
}
|
|
1836
|
+
process.exit(failureCode);
|
|
1837
|
+
}
|
|
1838
|
+
// latexdiff needs two versions of the same document. A root document that
|
|
1839
|
+
// only exists locally has one, and diffing it against an empty file would
|
|
1840
|
+
// mark up the entire paper as an addition.
|
|
1841
|
+
if (!remoteFiles.has(root)) {
|
|
1842
|
+
spinner.stop();
|
|
1843
|
+
console.error(chalk.red(`${root} is not in "${projectName}" yet, so there is no earlier version to mark up.`));
|
|
1844
|
+
console.error(chalk.dim(' Push it first, or use --main to name a document that exists on both sides.'));
|
|
1845
|
+
process.exit(failureCode);
|
|
1846
|
+
}
|
|
1847
|
+
if (!entries.some((e) => isTexPath(e.path))) {
|
|
1848
|
+
spinner.info(`No .tex file differs from "${projectName}" - the markup will show no changes`);
|
|
1849
|
+
}
|
|
1850
|
+
const tmpRoot = mkdtempSync(join(tmpdir(), 'olcli-latexdiff-'));
|
|
1851
|
+
const cleanupTmp = () => rmSync(tmpRoot, { recursive: true, force: true });
|
|
1852
|
+
try {
|
|
1853
|
+
spinner.start('Writing the remote side to a temporary directory...');
|
|
1854
|
+
materializeTree(tmpRoot, remoteFiles);
|
|
1855
|
+
spinner.text = `Running latexdiff on ${root}...`;
|
|
1856
|
+
let markup = '';
|
|
1857
|
+
let warnings = '';
|
|
1858
|
+
try {
|
|
1859
|
+
const result = await runLatexdiff(buildLatexdiffArgs(join(tmpRoot, root), join(targetDir, root), { flatten: options.flatten !== false, extra: options.latexdiffOpt }));
|
|
1860
|
+
markup = result.markup;
|
|
1861
|
+
warnings = result.stderr.trim();
|
|
1862
|
+
}
|
|
1863
|
+
catch (error) {
|
|
1864
|
+
if (!(error instanceof LatexdiffError))
|
|
1865
|
+
throw error;
|
|
1866
|
+
spinner.fail(error.message);
|
|
1867
|
+
if (error.missing) {
|
|
1868
|
+
console.error(chalk.dim(` ${latexdiffInstallHint()}`));
|
|
1869
|
+
console.error(chalk.dim(' Everything else in olcli diff needs no external tools.'));
|
|
1870
|
+
}
|
|
1871
|
+
else if (error.stderr) {
|
|
1872
|
+
console.error(error.stderr);
|
|
1873
|
+
}
|
|
1874
|
+
cleanupTmp();
|
|
1875
|
+
process.exit(failureCode);
|
|
1876
|
+
}
|
|
1877
|
+
// An explicit --output is a path the user chose, so it is taken relative to
|
|
1878
|
+
// the current directory; the default belongs to the project directory being
|
|
1879
|
+
// diffed. Either way the PDF sits next to the source it was built from.
|
|
1880
|
+
const texPath = options.output || join(targetDir, diffOutputPath(root, 'tex'));
|
|
1881
|
+
const pdfPath = texPath.replace(/\.(tex|ltx)$/i, '') + '.pdf';
|
|
1882
|
+
mkdirSync(dirname(texPath), { recursive: true });
|
|
1883
|
+
writeFileSync(texPath, markup, 'utf-8');
|
|
1884
|
+
spinner.succeed(`Marked up ${root} (${(Buffer.byteLength(markup) / 1024).toFixed(1)} KB)`);
|
|
1885
|
+
if (warnings) {
|
|
1886
|
+
for (const line of warnings.split('\n')) {
|
|
1887
|
+
console.log(chalk.yellow(` latexdiff: ${line}`));
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1890
|
+
if (options.pdf) {
|
|
1891
|
+
await compileMarkupOnOverleaf({ client, projectId, projectName, root, markup, texPath, pdfPath, failureCode, spinner });
|
|
1892
|
+
}
|
|
1893
|
+
console.log();
|
|
1894
|
+
console.log(chalk.bold(`Revision of ${root} against "${projectName}"`));
|
|
1895
|
+
console.log(` ${texPath}`);
|
|
1896
|
+
if (options.pdf)
|
|
1897
|
+
console.log(` ${pdfPath}`);
|
|
1898
|
+
console.log(chalk.dim(` struck through = remote as of ${fetchedAt.toISOString()}, underlined = local`));
|
|
1899
|
+
if (options.flatten !== false) {
|
|
1900
|
+
console.log(chalk.dim(' \\input/\\include were inlined; pass --no-flatten to keep them'));
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
finally {
|
|
1904
|
+
cleanupTmp();
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
/**
|
|
1908
|
+
* Compile a marked-up document with Overleaf's compiler and download the PDF.
|
|
1909
|
+
*
|
|
1910
|
+
* The compile endpoint takes a `rootResourcePath` that has to already exist in
|
|
1911
|
+
* the project - there is no way to compile a document that is not in it. So
|
|
1912
|
+
* the markup is uploaded, compiled, and removed again, which is a real
|
|
1913
|
+
* mutation of someone's project for the duration of one compile and is
|
|
1914
|
+
* announced before it happens.
|
|
1915
|
+
*
|
|
1916
|
+
* Three things follow from that and are not incidental:
|
|
1917
|
+
*
|
|
1918
|
+
* - The upload refuses to overwrite. If the scratch path is taken, that file
|
|
1919
|
+
* belongs to the user, and clobbering it to produce a diff would be a worse
|
|
1920
|
+
* outcome than not producing one.
|
|
1921
|
+
* - The delete runs from a `finally`, and nothing between the upload and it
|
|
1922
|
+
* calls `process.exit` - that would terminate before the cleanup and leave
|
|
1923
|
+
* the file on the project. Failures are collected and reported afterwards.
|
|
1924
|
+
* - An interrupt cannot be cleaned up after reliably, so it prints the exact
|
|
1925
|
+
* command that removes the file rather than leaving it to be discovered.
|
|
1926
|
+
*/
|
|
1927
|
+
async function compileMarkupOnOverleaf(params) {
|
|
1928
|
+
const { client, projectId, projectName, root, markup, texPath, pdfPath, failureCode, spinner } = params;
|
|
1929
|
+
const scratch = remoteScratchPath(root);
|
|
1930
|
+
spinner.start(`Checking ${scratch} is free...`);
|
|
1931
|
+
if (await client.findEntityByPath(projectId, scratch)) {
|
|
1932
|
+
spinner.fail(`"${projectName}" already has a file named ${scratch}`);
|
|
1933
|
+
console.error(chalk.dim(' --pdf uploads the marked-up document under that name for one compile and'));
|
|
1934
|
+
console.error(chalk.dim(' removes it again; it will not overwrite a file that is already there.'));
|
|
1935
|
+
console.error(chalk.dim(` The marked-up source was still written: ${texPath}`));
|
|
1936
|
+
process.exit(failureCode);
|
|
1937
|
+
}
|
|
1938
|
+
spinner.stop();
|
|
1939
|
+
console.log(chalk.dim(` uploading ${scratch} to "${projectName}" for one compile, then removing it`));
|
|
1940
|
+
const onInterrupt = () => {
|
|
1941
|
+
console.error(chalk.yellow(`\nInterrupted with ${scratch} still in "${projectName}".`));
|
|
1942
|
+
console.error(chalk.yellow(`Remove it with: olcli rm ${scratch}`));
|
|
1943
|
+
process.exit(130);
|
|
1944
|
+
};
|
|
1945
|
+
spinner.start('Uploading the marked-up document...');
|
|
1946
|
+
await client.uploadFile(projectId, null, scratch, Buffer.from(markup, 'utf-8'));
|
|
1947
|
+
process.once('SIGINT', onInterrupt);
|
|
1948
|
+
let failure = null;
|
|
1949
|
+
try {
|
|
1950
|
+
spinner.text = 'Compiling on Overleaf...';
|
|
1951
|
+
const compile = await client.compileWithOutputs(projectId, scratch);
|
|
1952
|
+
if (compile.status !== 'success' || !compile.pdfUrl) {
|
|
1953
|
+
failure = [`Overleaf reported "${compile.status}" compiling ${scratch}`];
|
|
1954
|
+
// The log is the only thing that explains a LaTeX failure, and it stops
|
|
1955
|
+
// being reachable as soon as the scratch file is removed below.
|
|
1956
|
+
const logFile = compile.outputFiles.find((f) => f.path === 'output.log');
|
|
1957
|
+
if (logFile) {
|
|
1958
|
+
const logPath = pdfPath.replace(/\.pdf$/, '.log');
|
|
1959
|
+
writeFileSync(logPath, await client.downloadOutputFile(logFile.url));
|
|
1960
|
+
failure.push(` compiler log: ${logPath}`);
|
|
1961
|
+
}
|
|
1962
|
+
failure.push(` marked-up source: ${texPath}`);
|
|
1963
|
+
// A .sty or .cls that only exists locally is the common one: Overleaf
|
|
1964
|
+
// compiles against the project, so anything the markup needs has to be
|
|
1965
|
+
// in the project. A missing *figure* does not fail - Overleaf draws a
|
|
1966
|
+
// placeholder box naming the file and reports success.
|
|
1967
|
+
failure.push(' A class, style or input file that exists only locally is the usual cause;');
|
|
1968
|
+
failure.push(' Overleaf compiles against the project, not against your working directory.');
|
|
1969
|
+
// A PDF from an earlier run would otherwise sit next to the log, dated
|
|
1970
|
+
// now by the directory listing and describing a different revision.
|
|
1971
|
+
rmSync(pdfPath, { force: true });
|
|
1972
|
+
}
|
|
1973
|
+
else {
|
|
1974
|
+
spinner.text = 'Downloading the PDF...';
|
|
1975
|
+
const pdf = await client.downloadOutputFile(compile.pdfUrl);
|
|
1976
|
+
writeFileSync(pdfPath, pdf);
|
|
1977
|
+
spinner.succeed(`Compiled ${basename(pdfPath)} (${(pdf.length / 1024).toFixed(1)} KB)`);
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
catch (error) {
|
|
1981
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1982
|
+
failure = [`Compiling ${scratch} failed: ${message}`, ` marked-up source: ${texPath}`];
|
|
1983
|
+
}
|
|
1984
|
+
finally {
|
|
1985
|
+
process.off('SIGINT', onInterrupt);
|
|
1986
|
+
spinner.start(`Removing ${scratch} from "${projectName}"...`);
|
|
1987
|
+
try {
|
|
1988
|
+
await client.deleteByPath(projectId, scratch);
|
|
1989
|
+
spinner.stop();
|
|
1990
|
+
}
|
|
1991
|
+
catch (error) {
|
|
1992
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1993
|
+
spinner.warn(`${scratch} is still in "${projectName}": ${message}`);
|
|
1994
|
+
console.error(chalk.yellow(` remove it with: olcli rm ${scratch}`));
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
if (failure) {
|
|
1998
|
+
spinner.fail(failure[0]);
|
|
1999
|
+
for (const line of failure.slice(1))
|
|
2000
|
+
console.error(chalk.dim(line));
|
|
2001
|
+
process.exit(failureCode);
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
1730
2004
|
/**
|
|
1731
2005
|
* Colourize one line of a rendered patch. chalk already no-ops when stdout is
|
|
1732
2006
|
* not a TTY, so this needs no flag of its own.
|