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
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,29 @@

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

## [Unreleased]

### Added
- **`olcli diff [project] [dir]`** ([#45](https://github.com/aloth/olcli/issues/45)) - content-level preview of what a push would change
- `push --dry-run` answers *which files*; there was no way to see *what changed inside them* short of pulling into a scratch directory and running `diff(1)` by hand
- Unified diff to stdout, colourized when stdout is a TTY. `--name-only` for paths only, `--file <path>` for a single file, `-U <n>` for context width
- **The remote side is fetched fresh on every run**, and the command says so in `--help` and in its output footer. `.olcli.json` records remote *paths*, never remote *contents*, so there is no stored snapshot to compare against - "diff against the last pull" would have meant inventing a content cache, not reusing one. Fetching fresh is also what makes the diff describe what a subsequent `push` will overwrite, which is the question the command exists to answer
- Cost of fetching fresh is one request: `downloadProject` returns the whole project as a single archive, the same call `pull` and `sync` already make. Per-file fetching would have been one request per file and still could not have identified which files differ without downloading them
- `a/` is the remote and `b/` is local, so a `+` line is content `push` would upload and a `-` line is content it would overwrite
- Binary files (PDFs, images) are reported as `Binary files ... differ`, detected by a NUL byte in the first 8000 bytes. No attempt is made to be cleverer
- Both sides pass through the same ignore layers and the same dotfile rule. Filtering only the local side would have listed `output.pdf` and every stray `.aux` on Overleaf as a local deletion on every run
- Remote-only files are reported but flagged as untouched by a plain `push`, since only `push --delete` removes them
- Archive entries whose names escape the target directory are dropped, consistent with what `pull` refuses to extract

### Changed
- Local file scanning extracted into `src/scan.ts`. `push` and `sync` each carried their own copy of the same walk-and-filter loop and the two had already drifted (`sync` guarded against a missing directory, `push` did not); `diff` would have made a third. Same reasoning as `src/rename-plan.ts` in 0.9.0
- `push --dry-run` now notes that its list is selected by modification time and points at `olcli diff` for content changes. The two commands answer different questions and will disagree - a file touched but not edited appears in `push --dry-run` and not in `diff` - so the overlap is resolved by making each one say what it measures rather than by merging them

### Notes
- New runtime dependency: [`diff`](https://www.npmjs.com/package/diff) `^9.0.0`, which has no dependencies of its own
- Comparison, rendering and remote-tree filtering live in `src/diff.ts` as pure functions, so they are unit-tested without an Overleaf account (`npm test`). Like `rename-plan.ts`, they are not re-exported from the package root
- `latexdiff` integration (`--latexdiff`, `--pdf`) is deliberately left out of this change and will follow separately

## [0.9.2] - 2026-09-03

### Fixed
Expand Down
35 changes: 35 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Work with Overleaf projects directly from your command line. Edit locally with y
- ⬇️ **Pull** project files to local directory for offline editing
- ⬆️ **Push** local changes back to Overleaf
- 🔄 **Sync** bidirectionally with smart conflict detection
- 🔍 **Diff** local files against the live remote before pushing
- 🔀 **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 @@ -132,6 +133,7 @@ All commands auto-detect the project when run from a synced directory (contains
| `olcli pull [project] [dir]` | Download project files to local directory |
| `olcli push [dir]` | Upload local changes to Overleaf (`--delete` also removes files deleted locally) |
| `olcli sync [dir]` | Bidirectional sync (pull + push) |
| `olcli diff [project] [dir]` | Show content-level changes between local files and the remote |
| `olcli upload <file> [project]` | Upload a single file (`--to <path>` sets the remote destination) |
| `olcli download <file> [project]` | Download a single file |
| `olcli delete <file> [project]` | Delete a remote file or folder (alias: `rm`) |
Expand Down Expand Up @@ -192,6 +194,39 @@ Useful in multi-doc projects: each `-r` run compiles the file as if it were the
- **Propagates local deletions** — use `--no-delete` to opt out
- Use `--dry-run` to preview without applying

### Diff

`olcli diff` compares the bytes of your local files against the project's
current contents and prints a unified diff.

```bash
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
```

**The remote side is fetched fresh on every run.** The diff describes the
project as it is at that moment — which is what a subsequent `push` would
overwrite — not a comparison against your last `pull`. `.olcli.json` records
remote *paths*, never remote *contents*, so there is no stored snapshot to
compare against; and the whole project arrives in a single request, the same
one `pull` makes, so fetching fresh costs one round trip rather than one per
file. A collaborator editing between `diff` and `push` can still change the
outcome, which is why the fetch time is printed.

In the output, `a/` is the remote and `b/` is local: a `+` line is content
`push` would upload, a `-` line is content it would overwrite. Files that
differ only in bytes that are not text (PDFs, images) are reported as
`Binary files ... differ`. Both sides pass through the same ignore layers, so
build artifacts sitting on Overleaf are not reported as locally deleted.

`diff --name-only` and `push --dry-run` answer different questions and will
disagree. `push --dry-run` lists files whose **modification time** is newer
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.

#### How deletion propagation works

`olcli` records a manifest of remote files in `.olcli.json`. On next sync:
Expand Down
14 changes: 14 additions & 0 deletions SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,18 @@ olcli sync # Bidirectional sync (pull + push, propagates local dele
olcli sync --no-delete # Sync without propagating local deletions to remote
```

### Review changes before pushing

```bash
olcli diff # unified diff of every changed file
olcli diff --name-only # changed paths only
olcli diff --file main.tex # a single file
```

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.

### Delete or rename remote files

```bash
Expand Down Expand Up @@ -243,6 +255,7 @@ zip arxiv.zip *.tex main.bbl figures/*.pdf
| `olcli pull [project] [dir]` | Download project files |
| `olcli push [dir]` | Upload local changes |
| `olcli sync [dir]` | Bidirectional sync |
| `olcli diff [project] [dir]` | Content-level diff of local files vs. the live remote |
| `olcli upload <file> [project]` | Upload a single file (`--to <path>` sets the remote destination) |
| `olcli download <file> [project]` | Download a single file |
| `olcli delete <file> [project]` | Delete a remote file or folder (alias: `rm`) |
Expand All @@ -266,6 +279,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
- **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
18 changes: 18 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,13 +63,15 @@
"cheerio": "^1.0.0",
"commander": "^12.1.0",
"conf": "^13.0.0",
"diff": "^9.0.0",
"ignore": "^7.0.5",
"ora": "^8.0.1",
"tough-cookie": "^4.1.4",
"zod": "^4.4.3"
},
"devDependencies": {
"@types/adm-zip": "^0.5.7",
"@types/diff": "^7.0.2",
"@types/node": "^22.0.0",
"@types/tough-cookie": "^4.0.5",
"eslint": "^9.39.5",
Expand Down
Loading
Loading