Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,28 @@

All notable changes to this project will be documented in this file.

## [Unreleased]

### Added
- **`olcli diff --latexdiff` marks the revision up inside the document** ([#55](https://github.com/aloth/olcli/issues/55)) - the follow-up left open when the core `diff` command shipped in 0.10.0. A unified diff is the right artifact for a developer and the wrong one for a thesis advisor, who expects deletions struck through and additions underlined. It runs on the two sides `diff` has already fetched, so the markup describes exactly what the patch output does - struck through is what a push would overwrite, underlined is what it would upload - and costs no extra request
- Requires `latexdiff` on PATH, which ships with TeX Live and MacTeX. A missing binary is reported as a setup problem with the install command for the platform, not as a failure of `diff`; nothing else in the command needs an external tool
- `\input` and `\include` are inlined before comparing, with `--no-flatten` to opt out. Without it a remote compile resolves those against the files in the project - the *old* content - and produces a PDF marking up the root document while showing every input file as unchanged. Wrong in a way that is very hard to notice
- Output goes to `.olcli-diff/`, dotted because `scanLocalFiles` skips dotted entries before any ignore rule is consulted. A plain `main-diff.tex` next to the document would be uploaded to Overleaf by the next `push`
- The root document is the `.tex` file declaring `\documentclass`. Several candidates are reported and listed rather than resolved by preferring `main.tex`: marking up the wrong document produces a plausible PDF describing the wrong revision
- `--latexdiff-opt` passes options straight through (`--latexdiff-opt --math-markup=0`), since the remote tree is on disk only for the duration of the run and cannot be handed to `latexdiff` by hand afterwards
- **`--pdf` compiles the marked-up document on Overleaf**, so a reviewable PDF needs no local TeX installation - the reason the flag was proposed in [#45](https://github.com/aloth/olcli/issues/45)
- Overleaf's compile endpoint takes a path that must already be in the project; there is no way to compile a document that is not. So the markup is uploaded as `olcli-latexdiff.tex` next to the root document, compiled, downloaded, and removed. That is a real mutation of the project for the duration of one compile, and the command says so before it does it
- It refuses rather than overwrites if a file of that name already exists, deletes from a `finally` with nothing in between able to terminate the process first, and prints the exact `olcli rm` command if the delete itself fails or the run is interrupted
- Placed next to the root document rather than at the project root, so relative `\includegraphics` and `\bibliography` paths resolve exactly as they do for the document it was built from
- A compile failure writes the CLSI log next to the marked-up source instead of reporting only a status, and deletes a PDF left by an earlier run rather than leaving one that describes a different revision. The compile runs against the project, so a `.sty` or `.cls` that exists only locally is the usual cause and the message says so. A missing *figure* is not: Overleaf draws a placeholder box naming the file and still reports success
- **`src/latexdiff.ts`** - root document detection, argument construction, output naming and failure interpretation are functions over data, unit-tested with no Overleaf account and no `latexdiff` binary. 21 tests, in the suite CI already runs
- **`olcli diff --exit-code` makes the command a CI gate** ([#48](https://github.com/aloth/olcli/pull/48)) - the second follow-up left open when the core `diff` command shipped in 0.10.0. It reports `diff(1)`'s statuses: `0` when nothing differs, `1` when something does, `2` when the run itself failed
- The `1` vs `2` split is the whole feature. A pipeline that cannot separate "the project differs" from "the run failed" reads an expired session cookie as a content change, and a job that goes red for the wrong reason sends whoever reads the log looking for a diff that was never computed. All ten failure paths in the command move to `2` together - bad flag combinations, a missing directory, an unresolvable project, a `latexdiff` that is not installed, a failed remote compile - so no failure can be mistaken for a difference
- Failures stay `1` when the flag is absent, which is what every other command exits, so scripts that check `olcli diff` for success are unaffected
- The status is *set* rather than exited on. `process.exit` discards whatever is still buffered on a non-TTY stdout, and `olcli diff --exit-code > patch.txt` is exactly a large patch going into a pipe: measured here, exiting outright truncated a 1.5 MB patch to the 128 KB pipe buffer and lost 91% of it mid-hunk. Returning lets node flush first
- The gate covers whatever was compared, so `--file` narrows it to one file the way a `git diff --exit-code` pathspec does, and a `--file` matching nothing is `0` rather than an error. Under `--latexdiff` it reports on the project rather than on the markup - a changed figure is a real difference even though a marked-up root document cannot show one
- Exit statuses and the "unchanged files are not differences" rule are pinned by unit tests in `test/diff.test.ts`; `test/e2e.sh` gains eight cases covering the clean tree, a difference, `--file` narrowing, the redirected-output case and both failure statuses

## [0.12.0] - 2026-09-06

### Added
Expand Down
76 changes: 76 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ Work with Overleaf projects directly from your command line. Edit locally with y
- ⬆️ **Push** local changes back to Overleaf
- 🔄 **Sync** bidirectionally with smart conflict detection
- 🔍 **Diff** local files against the live remote before pushing
- 📝 **Marked-up revisions** — `diff --latexdiff` produces the struck-through/underlined PDF advisors and journals ask for
- 🔀 **Git remote** — use Overleaf as a native git remote ([docs](docs/GIT-REMOTE.md))
- ✌️ **Two-way deletions** — files removed locally are deleted on Overleaf on next sync
- 🗑️ **Delete** and ✏️ **rename** remote files by path
Expand Down Expand Up @@ -217,6 +218,7 @@ olcli diff # every changed file, as patches
olcli diff --name-only # just the changed paths
olcli diff --file main.tex # one file
olcli diff -U 8 # wider context
olcli diff --exit-code # exit 1 if anything differs, for CI
```

**The remote side is fetched fresh on every run.** The diff describes the
Expand All @@ -240,6 +242,80 @@ than the last pull, because that is what `push` uploads; `diff` lists files
whose **contents** actually differ. A file you touched without editing appears
in the first and not the second.

#### Marked-up revisions with latexdiff

A unified diff is the right artifact for a developer and the wrong one for a
thesis advisor. `--latexdiff` marks the same revision up inside the document
instead — deletions struck through, additions underlined — which is what
advisors and journals ask for.

```bash
olcli diff --latexdiff # write .olcli-diff/main-diff.tex
olcli diff --latexdiff --pdf # ...and compile it, download .olcli-diff/main-diff.pdf
```

Requires `latexdiff` on your PATH. It ships with TeX Live and MacTeX; nothing
else in `olcli diff` needs an external tool.

**`--pdf` compiles on Overleaf, so you do not need a local TeX installation.**
The compile endpoint can only build a file that is in the project, so the
marked-up document is uploaded as `olcli-latexdiff.tex` next to your root
document, compiled, downloaded, and then removed. The command says so before it
does it, refuses to overwrite a file of that name if one already exists, and
prints the exact `olcli rm` command if the cleanup itself fails.

`\input` and `\include` are inlined before comparing (`--no-flatten` to opt
out). Without that, a remote compile would resolve those against the files
sitting in the project — the old content — and quietly produce a PDF showing
every input file as unchanged.

Output goes to `.olcli-diff/`, which is dotted so that `push` and `sync` never
pick it up; `-o <path>` puts it elsewhere. The root document is the `.tex` file
declaring `\documentclass`; if several do, `--main <path>` picks one rather
than the command guessing. `--latexdiff-opt` passes anything else straight
through, e.g. `--latexdiff-opt --math-markup=0`.

One limit worth knowing: `--pdf` compiles against the project, not against your
working directory, so anything the markup needs must already be on Overleaf. A
`.sty` or `.cls` you added locally fails the compile — the compiler log is
written next to the marked-up source when that happens. A *figure* you added
locally does not fail it; Overleaf draws a placeholder box naming the missing
file and the rest of the PDF is fine.

#### Using `diff` as a CI gate

`--exit-code` turns the command into a check, with the statuses `diff(1)` uses:

| Status | Meaning |
|--------|---------|
| `0` | Nothing differs |
| `1` | Something differs |
| `2` | The run failed — bad flags, no session, project unreachable |

```yaml
- name: Fail if the paper on Overleaf has drifted from the repo
run: olcli diff --exit-code --name-only
env:
OVERLEAF_SESSION: ${{ secrets.OVERLEAF_SESSION }}
```

**The `1` versus `2` split is the point of the flag.** Without it a pipeline
cannot tell a changed file from an expired session cookie, and a job that goes
red for the second reason sends whoever reads the log hunting for a diff that
was never computed. Every other olcli command reports failure as `1`, and
`olcli diff` still does when `--exit-code` is absent, so adding the flag does
not change what existing scripts see.

The gate covers whatever was compared: `--file main.tex` narrows it to one
file, the way a `git diff --exit-code` pathspec does, and a `--file` that
matches nothing is `0` rather than an error. Under `--latexdiff` it still
reports on the project as a whole — a changed figure is a real difference even
though a marked-up root document cannot show it.

Note that this compares against Overleaf **now**, not against your last pull,
so the check is "has anyone drifted from what is committed here", which is what
makes it worth running on a schedule as well as on a push.

#### How deletion propagation works

`olcli` records a manifest of remote files in `.olcli.json`. On next sync:
Expand Down
8 changes: 8 additions & 0 deletions SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,12 +157,19 @@ olcli sync --no-delete # Sync without propagating local deletions to remote
olcli diff # unified diff of every changed file
olcli diff --name-only # changed paths only
olcli diff --file main.tex # a single file
olcli diff --exit-code # CI gate: 0 same, 1 differs, 2 failed
```

The remote side is fetched fresh each run, so this shows what a subsequent
`push` would overwrite — not a comparison against the last `pull`. `a/` is the
remote, `b/` is local. Binary files are reported as differing without a patch.

`--exit-code` uses `diff(1)`'s statuses so the command can gate a pipeline:
`0` nothing differs, `1` something does, `2` the run itself failed. The last
one matters — without it a job cannot tell a changed file from an expired
session. Failures stay `1` when the flag is absent, so existing scripts are
unaffected.

### Delete or rename remote files

```bash
Expand Down Expand Up @@ -304,6 +311,7 @@ zip arxiv.zip *.tex main.bbl figures/*.pdf
- **Auto-detect project**: Run commands from a synced directory (contains `.olcli.json`) to skip the project argument
- **Dry run**: Use `olcli push --dry-run` or `olcli sync --dry-run` to preview before applying
- **Preview content**: `push --dry-run` lists files by modification time; `olcli diff` compares actual contents, so the two lists can differ
- **CI gate**: `olcli diff --exit-code` exits 1 when anything differs and 2 when the run failed, so a pipeline can distinguish drift from breakage
- **Force overwrite**: Use `olcli pull --force` to overwrite local changes
- **Two-way deletes**: `olcli sync` propagates *local* deletions to the remote; use `--no-delete` to opt out per run
- **Build artifacts**: `.aux`, `.bbl`, `.log`, `.synctex.gz` etc. are filtered by default. Add custom patterns to a `.olignore` file (gitignore-style)
Expand Down
9 changes: 8 additions & 1 deletion docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,11 +115,13 @@ Which files need an Overleaf account to exercise, and which do not. This is the
main thing to know before adding a feature, because it decides where the logic
should go.

**Pure — data in, data out. No network, no filesystem, unit-tested:**
**Needs no Overleaf account, so unit-tested directly. Data in, data out;
`scan.ts` and `latexdiff.ts` also touch the local filesystem:**

| Module | Responsibility |
|---|---|
| `diff.ts` | Compare two file trees; render unified diffs |
| `latexdiff.ts` | Root document detection; build and run the `latexdiff` command |
| `ignore.ts` | The three ignore layers and the `.pdf`-next-to-`.tex` rule |
| `paths.ts` | Remote path normalization; zip-slip containment |
| `rename-plan.ts` | Plan bulk project renames before applying any |
Expand Down Expand Up @@ -147,6 +149,11 @@ exists at all: `push` and `sync` each carried their own copy of the same walk
loop and had already drifted apart, and `diff` would have made a third. The
same reasoning produced `rename-plan.ts` and `diff.ts`.

`latexdiff.ts` is the one module that shells out to something olcli does not
ship. That is confined to a single `execFile` call with an argv array — never a
shell — and a missing binary is reported as a setup problem with a fix rather
than as a failure of the command.

`client.ts` request *construction* can also be tested without an account, by
pointing the client at a local HTTP server that captures the outgoing request —
see `test/client.test.ts`.
Expand Down
Loading
Loading