Skip to content

Commit 95007ca

Browse files
committed
v0.3.1: fix clsiserverid 404 (#22), subfolder uploads, add --verbose (#21)
- fix(client): append ?clsiserverid=<id> to all build-output URLs so olcli pdf / olcli output <type> work again. Overleaf's CDN now requires the hint or returns 404. Closes #22. - fix(upload): preserve the relative path (figures/fig01.png) instead of basename-stripping it; lazy-load and cache the folder tree inside OverleafClient.uploadFile so files land in the correct subfolder. - fix(sync): subfolder files in the sync upload pass now also land in the right folder via the same self-healing path in uploadFile. - feat(cli): global --verbose flag prints every HTTP request, status, and (on errors) a response-body snippet to stderr. Closes #21. - docs(README): new Global options section; cross-link from Sync and Self-hosted Overleaf sections. - chore: bump to 0.3.1, update CHANGELOG.
1 parent e0c039d commit 95007ca

5 files changed

Lines changed: 113 additions & 14 deletions

File tree

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,19 @@
22

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

5+
## [0.3.1] - 2026-05-18
6+
7+
### Fixed
8+
- **`olcli pdf` / `olcli output <type>` returned `Download failed: 404`** (#22) — Overleaf's CDN now requires `?clsiserverid=<id>` on every build-output download. We were stripping that hint. The compile response's `clsiServerId` field is now appended to all output URLs (`pdf`, `bbl`, `log`, `aux`, …).
9+
- **`olcli upload figures/fig01.png` placed the file in project root** instead of inside `figures/`. The CLI previously called `basename()` on the path and never resolved the folder tree. Now the relative path is preserved and the folder tree is loaded (and cached) on demand. Subfolders are auto-created if missing.
10+
- **`olcli sync` upload pass** also dropped subfolder information when pushing local-newer files. Same fix as above (`uploadFile` now self-resolves the folder tree when the caller doesn't pass one).
11+
12+
### Added
13+
- **Global `--verbose` flag** (#21) — prints every HTTP request, status, content-type, and (on non-2xx) a 200-char snippet of the response body to stderr. Use as `olcli --verbose pdf` or `olcli pdf --verbose` for troubleshooting failed compiles, uploads, or auth.
14+
15+
### Internal
16+
- New `OverleafClient.getOrLoadFolderTree(projectId)` / `invalidateFolderTree(projectId)` helpers with per-project caching, so callers no longer have to plumb a `folderTree` argument through every code path.
17+
518
## [0.3.0] - 2026-04-27
619

720
### ⚠ Behavior change

README.md

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,25 @@ All commands auto-detect the project when run from a synced directory (contains
178178
| `olcli config set-cookie-name <name>` | Set the session cookie name |
179179
| `olcli check` | Show config paths and credential sources |
180180

181+
### Global options
182+
183+
These flags work with **every** command and may be placed before or after the command name:
184+
185+
| Flag | Description |
186+
|------|-------------|
187+
| `--verbose` | Print every HTTP request, status, content-type, and (on errors) a response-body snippet to stderr. Useful for debugging failed compiles, 404s on `pdf`/`output`, auth issues, or unexpected upload behavior. |
188+
| `--base-url <url>` | Override the Overleaf instance base URL (also `OVERLEAF_BASE_URL` env var or `olcli config set-url`). |
189+
| `--cookie-name <name>` | Override the session cookie name (default `overleaf_session2`; older instances use `overleaf.sid`). |
190+
191+
Examples:
192+
193+
```bash
194+
olcli --verbose pdf # see every request the compile makes
195+
olcli pdf --verbose # same thing, flag after command
196+
olcli --verbose sync # debug a sync that's misbehaving
197+
olcli --verbose upload figures/a.png # confirm the file is placed in figures/
198+
```
199+
181200
## Use Cases
182201

183202
### Local Editing with Overleaf Compilation
@@ -266,7 +285,7 @@ olcli output --list
266285
- Pushes local changes to remote
267286
- **Propagates local deletions to the remote** — if you delete a file locally, it's deleted on Overleaf on the next sync. Use `--no-delete` to opt out.
268287
- Filters out LaTeX build artifacts and OS noise
269-
- Use `--verbose` to see detailed file operations
288+
- Use `--verbose` to see detailed file operations (see [Global options](#global-options))
270289
- Use `--dry-run` to preview without applying
271290

272291
#### How deletion propagation works
@@ -337,14 +356,14 @@ s%3AyourSessionCookieValue...
337356

338357
### Self-hosted Overleaf / ShareLaTeX
339358

340-
You can point `olcli` at a self-hosted instance and override the session cookie name.
359+
You can point `olcli` at a self-hosted instance and override the session cookie name. Both flags are documented under [Global options](#global-options) and can be combined with any command.
341360

342361
```bash
343362
olcli --base-url https://latex.example.org list
344363
olcli --base-url https://latex.example.org --cookie-name overleaf.sid whoami
345364
```
346365

347-
Persist these settings in `olcli` config:
366+
Persist these settings in `olcli` config so you don't have to repeat them:
348367

349368
```bash
350369
olcli config set-url https://latex.example.org

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@aloth/olcli",
3-
"version": "0.3.0",
3+
"version": "0.3.1",
44
"description": "Command-line interface for Overleaf — Sync, manage, and compile LaTeX projects from your terminal",
55
"type": "module",
66
"bin": {

src/cli.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@ program
4646
.description('Overleaf CLI - interact with Overleaf projects from the command line')
4747
.version(VERSION)
4848
.option('--base-url <url>', 'Overleaf instance base URL (overrides OVERLEAF_BASE_URL and config)')
49-
.option('--cookie-name <name>', 'Session cookie name (default: overleaf_session2, use overleaf.sid for older instances)');
49+
.option('--cookie-name <name>', 'Session cookie name (default: overleaf_session2, use overleaf.sid for older instances)')
50+
.option('--verbose', 'Print every HTTP request, status, and error response body to stderr');
5051

5152
/**
5253
* Helper to get authenticated client
@@ -62,7 +63,9 @@ async function getClient(cookieOpt?: string, baseUrlOpt?: string): Promise<Overl
6263
}
6364
const baseUrl = baseUrlOpt || (program.opts().baseUrl as string | undefined) || getBaseUrl();
6465
const cookieName = (program.opts().cookieName as string | undefined) || getSessionCookieName();
65-
return OverleafClient.fromSessionCookie(cookie, baseUrl, cookieName);
66+
const client = await OverleafClient.fromSessionCookie(cookie, baseUrl, cookieName);
67+
if (program.opts().verbose) client.setVerbose(true);
68+
return client;
6669
}
6770

6871
/**
@@ -454,7 +457,11 @@ program
454457
}
455458

456459
const content = readFileSync(file);
457-
const fileName = basename(file);
460+
// Preserve the relative path (e.g. 'figures/fig01.png') so the file lands
461+
// in the correct subfolder, not in project root. uploadFile() will
462+
// lazy-resolve the folder tree when no folderId/tree is supplied.
463+
// Normalize: strip leading './' and any leading slashes.
464+
const fileName = file.replace(/^(\.\/)+/, '').replace(/^\/+/, '');
458465

459466
// Pass folder ID or null for root folder (client will compute it)
460467
const folderId = options.folder || null;

src/client.ts

Lines changed: 67 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -64,13 +64,44 @@ export class OverleafClient {
6464
private cookies: Record<string, string>;
6565
private csrf: string;
6666
private baseUrl: string;
67+
private verbose: boolean = false;
68+
// Cache per-project folder trees so repeated uploads in sync/upload calls
69+
// don't re-fetch the tree via Socket.IO on every file.
70+
private folderTreeCache: Map<string, Record<string, string>> = new Map();
6771

6872
constructor(credentials: Credentials) {
6973
this.cookies = credentials.cookies;
7074
this.csrf = credentials.csrf;
7175
this.baseUrl = credentials.baseUrl || DEFAULT_BASE_URL;
7276
}
7377

78+
/** Enable or disable verbose request/response logging to stderr. */
79+
setVerbose(v: boolean): void {
80+
this.verbose = v;
81+
}
82+
83+
/**
84+
* Resolve (and cache) the folder tree for a project. Falls back to a
85+
* minimal tree containing only the root folder when the Socket.IO probe
86+
* fails (e.g. self-hosted Overleaf without that endpoint).
87+
*/
88+
async getOrLoadFolderTree(projectId: string): Promise<Record<string, string>> {
89+
const cached = this.folderTreeCache.get(projectId);
90+
if (cached) return cached;
91+
let tree = await this.getFolderTreeFromSocket(projectId);
92+
if (!tree) {
93+
const rootId = await this.getRootFolderId(projectId);
94+
tree = { '': rootId };
95+
}
96+
this.folderTreeCache.set(projectId, tree);
97+
return tree;
98+
}
99+
100+
/** Drop the cached folder tree for a project (e.g. after rename/delete). */
101+
invalidateFolderTree(projectId: string): void {
102+
this.folderTreeCache.delete(projectId);
103+
}
104+
74105
private projectUrl(): string {
75106
return `${this.baseUrl}/project`;
76107
}
@@ -197,6 +228,13 @@ export class OverleafClient {
197228
}
198229
}
199230

231+
private logVerbose(...args: any[]): void {
232+
if (this.verbose) {
233+
// eslint-disable-next-line no-console
234+
console.error('[olcli]', ...args);
235+
}
236+
}
237+
200238
private async httpRequest(url: string, options: {
201239
method?: string;
202240
headers?: Record<string, string>;
@@ -237,6 +275,7 @@ export class OverleafClient {
237275
const resHeaders = res.headers as Record<string, string | string[]>;
238276

239277
if (status >= 300 && status < 400 && res.headers.location && redirectsLeft > 0) {
278+
this.logVerbose(`${method} ${reqUrl} -> ${status} redirect -> ${res.headers.location}`);
240279
const redirectUrl = new URL(res.headers.location, reqUrl).toString();
241280
res.resume();
242281
doRequest(redirectUrl, redirectsLeft - 1).then(resolve, reject);
@@ -254,10 +293,21 @@ export class OverleafClient {
254293
try {
255294
body = JSON.parse(buffer.toString('utf-8'));
256295
} catch (e) {
296+
this.logVerbose(`${method} ${reqUrl} -> ${status} (invalid JSON, ${buffer.length} bytes)`);
257297
return reject(new Error(`Failed to parse JSON response from ${reqUrl}`));
258298
}
259299
}
260-
resolve({ status, ok: status >= 200 && status < 300, headers: resHeaders, body });
300+
const ok = status >= 200 && status < 300;
301+
if (this.verbose) {
302+
const ct = (resHeaders['content-type'] || '') as string;
303+
let snippet = '';
304+
if (!ok) {
305+
const text = expect === 'buffer' ? '' : (typeof body === 'string' ? body : JSON.stringify(body));
306+
snippet = text ? ` body=${text.slice(0, 200).replace(/\s+/g, ' ')}` : '';
307+
}
308+
this.logVerbose(`${method} ${reqUrl} -> ${status} (${buffer.length}B ${ct})${snippet}`);
309+
}
310+
resolve({ status, ok, headers: resHeaders, body });
261311
});
262312
res.on('error', reject);
263313
});
@@ -584,8 +634,11 @@ export class OverleafClient {
584634
throw new Error('No PDF output found');
585635
}
586636

637+
// Overleaf's CDN requires ?clsiserverid=<id> for build-output downloads.
638+
// Without it the build URL 404s. See: https://github.com/aloth/olcli/issues/22
639+
const qs = data.clsiServerId ? `?clsiserverid=${encodeURIComponent(data.clsiServerId)}` : '';
587640
return {
588-
pdfUrl: `${this.baseUrl}${pdfFile.url}`,
641+
pdfUrl: `${this.baseUrl}${pdfFile.url}${qs}`,
589642
logs: data.compileGroup ? [`Compile group: ${data.compileGroup}`] : []
590643
};
591644
}
@@ -1071,11 +1124,15 @@ export class OverleafClient {
10711124
// Extract just the filename without path
10721125
const baseName = fileName.split('/').pop() || fileName;
10731126

1074-
// Resolve target folder: if fileName has a directory part and we have a folderTree, use it
1127+
// Resolve target folder: if fileName has a directory part, place the file there.
1128+
// Lazy-load + cache the folder tree when caller didn't supply one, so external
1129+
// callers (and our own `upload`/`sync` paths) don't silently dump files into root.
1130+
// See: https://github.com/aloth/olcli/issues/22 follow-up + 0.3.1 upload-fix.
10751131
const dirPart = fileName.includes('/') ? fileName.split('/').slice(0, -1).join('/') : '';
10761132
let targetFolderId: string;
1077-
if (dirPart && folderTree) {
1078-
targetFolderId = await this.resolveFolderId(projectId, folderTree, dirPart);
1133+
if (dirPart) {
1134+
const tree = folderTree || await this.getOrLoadFolderTree(projectId);
1135+
targetFolderId = await this.resolveFolderId(projectId, tree, dirPart);
10791136
} else {
10801137
targetFolderId = folderId || await this.getRootFolderId(projectId);
10811138
}
@@ -1406,13 +1463,16 @@ export class OverleafClient {
14061463
const data = response.body as any;
14071464
const pdfFile = data.outputFiles?.find((f: any) => f.type === 'pdf');
14081465

1466+
// Overleaf's CDN requires ?clsiserverid=<id> for build-output downloads.
1467+
// Without it every output (pdf/log/bbl/...) 404s. See issue #22.
1468+
const qs = data.clsiServerId ? `?clsiserverid=${encodeURIComponent(data.clsiServerId)}` : '';
14091469
return {
14101470
status: data.status,
1411-
pdfUrl: pdfFile ? `${this.baseUrl}${pdfFile.url}` : undefined,
1471+
pdfUrl: pdfFile ? `${this.baseUrl}${pdfFile.url}${qs}` : undefined,
14121472
outputFiles: (data.outputFiles || []).map((f: any) => ({
14131473
path: f.path,
14141474
type: f.type,
1415-
url: `${this.baseUrl}${f.url}`
1475+
url: `${this.baseUrl}${f.url}${qs}`
14161476
}))
14171477
};
14181478
}

0 commit comments

Comments
 (0)