@aloth/olcli 0.15.2 → 0.16.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 +140 -437
- package/dist/browser-auth.d.ts +144 -0
- package/dist/browser-auth.d.ts.map +1 -0
- package/dist/browser-auth.js +442 -0
- package/dist/browser-auth.js.map +1 -0
- package/dist/cli.js +455 -177
- package/dist/cli.js.map +1 -1
- package/dist/client.d.ts +52 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +199 -42
- package/dist/client.js.map +1 -1
- package/dist/config.d.ts +25 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +40 -7
- package/dist/config.js.map +1 -1
- package/dist/errors.d.ts +50 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +115 -0
- package/dist/errors.js.map +1 -0
- package/dist/mcp-server.d.ts.map +1 -1
- package/dist/mcp-server.js +27 -19
- package/dist/mcp-server.js.map +1 -1
- package/dist/mcp.js +1 -1
- package/dist/mcp.js.map +1 -1
- package/dist/move-plan.d.ts +75 -0
- package/dist/move-plan.d.ts.map +1 -0
- package/dist/move-plan.js +168 -0
- package/dist/move-plan.js.map +1 -0
- package/dist/paths.d.ts +18 -0
- package/dist/paths.d.ts.map +1 -1
- package/dist/paths.js +45 -0
- package/dist/paths.js.map +1 -1
- package/dist/project-match.d.ts +37 -0
- package/dist/project-match.d.ts.map +1 -0
- package/dist/project-match.js +97 -0
- package/dist/project-match.js.map +1 -0
- package/dist/proxy.d.ts +52 -0
- package/dist/proxy.d.ts.map +1 -0
- package/dist/proxy.js +189 -0
- package/dist/proxy.js.map +1 -0
- package/dist/remote-helper.js +3 -2
- package/dist/remote-helper.js.map +1 -1
- package/dist/term.d.ts +25 -0
- package/dist/term.d.ts.map +1 -0
- package/dist/term.js +42 -0
- package/dist/term.js.map +1 -0
- package/package.json +9 -4
package/dist/client.js
CHANGED
|
@@ -18,15 +18,28 @@ import * as cheerio from 'cheerio';
|
|
|
18
18
|
import { readFileSync } from 'node:fs';
|
|
19
19
|
import { dirname, join } from 'node:path';
|
|
20
20
|
import { fileURLToPath } from 'node:url';
|
|
21
|
-
import
|
|
22
|
-
import * as http from 'node:http';
|
|
21
|
+
import { routeRequest } from './proxy.js';
|
|
23
22
|
import { RejectConflictError, buildUserDirectory, planRejection, renderRejectionDiff, describeAuthor, parseHistoryOtChanges, parseShareJsChanges } from './tracked-changes.js';
|
|
23
|
+
import { planEntityMove } from './move-plan.js';
|
|
24
|
+
import { matchProject } from './project-match.js';
|
|
25
|
+
import { HttpStatusError, SessionExpiredError, httpError, isAuthStatus } from './errors.js';
|
|
24
26
|
// Read version from package.json
|
|
25
27
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
26
28
|
const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf-8'));
|
|
27
29
|
const USER_AGENT = `olcli/${pkg.version}`;
|
|
28
30
|
const DEFAULT_BASE_URL = 'https://www.overleaf.com';
|
|
29
31
|
const RESOURCE_PATH_FAILURE_HINT = ' (Perhaps the resource file you specified does not exist in the project?)';
|
|
32
|
+
/**
|
|
33
|
+
* True when a redirect from `fromUrl` points at Overleaf's login page. A
|
|
34
|
+
* request to /login itself is the password flow and is never treated as one.
|
|
35
|
+
*/
|
|
36
|
+
export function isLoginRedirect(fromUrl, location) {
|
|
37
|
+
const from = new URL(fromUrl);
|
|
38
|
+
if (from.pathname === '/login')
|
|
39
|
+
return false;
|
|
40
|
+
const target = new URL(location, fromUrl);
|
|
41
|
+
return target.pathname === '/login' || target.pathname.endsWith('/login');
|
|
42
|
+
}
|
|
30
43
|
export const DEFAULT_COMMENT_CONCURRENCY = 4;
|
|
31
44
|
export const MAX_COMMENT_CONCURRENCY = 8;
|
|
32
45
|
/**
|
|
@@ -50,6 +63,8 @@ export class OverleafClient {
|
|
|
50
63
|
csrf;
|
|
51
64
|
baseUrl;
|
|
52
65
|
verbose = false;
|
|
66
|
+
/** Email of the signed-in account, when the bootstrap page named it. */
|
|
67
|
+
accountEmail;
|
|
53
68
|
timeoutMs = 10000;
|
|
54
69
|
// Cache per-project folder trees so repeated uploads in sync/upload calls
|
|
55
70
|
// don't re-fetch the tree via Socket.IO on every file.
|
|
@@ -162,21 +177,23 @@ export class OverleafClient {
|
|
|
162
177
|
expect: 'text'
|
|
163
178
|
});
|
|
164
179
|
if (!response.ok) {
|
|
165
|
-
throw
|
|
180
|
+
throw httpError('Failed to fetch projects page', response.status);
|
|
166
181
|
}
|
|
167
182
|
bootstrapClient.applySetCookieHeaders(response.headers['set-cookie']);
|
|
168
183
|
const html = response.body;
|
|
169
184
|
const $ = cheerio.load(html);
|
|
170
185
|
if (OverleafClient.isLoginPage($)) {
|
|
171
|
-
throw new
|
|
186
|
+
throw new SessionExpiredError('Overleaf answered with its login page');
|
|
172
187
|
}
|
|
173
188
|
const csrf = OverleafClient.extractCsrfToken($);
|
|
174
189
|
if (!csrf) {
|
|
175
|
-
throw new
|
|
190
|
+
throw new SessionExpiredError('No CSRF token on the project page');
|
|
176
191
|
}
|
|
177
192
|
// Update cookies if the bootstrap request added anything
|
|
178
193
|
const updatedCookies = bootstrapClient.cookies;
|
|
179
|
-
|
|
194
|
+
const client = new OverleafClient({ cookies: updatedCookies, csrf, baseUrl });
|
|
195
|
+
client.accountEmail = OverleafClient.extractAccountEmail($);
|
|
196
|
+
return client;
|
|
180
197
|
}
|
|
181
198
|
/**
|
|
182
199
|
* Create client by submitting Overleaf's email/password login form.
|
|
@@ -188,7 +205,7 @@ export class OverleafClient {
|
|
|
188
205
|
expect: 'text'
|
|
189
206
|
});
|
|
190
207
|
if (!loginPage.ok) {
|
|
191
|
-
throw new
|
|
208
|
+
throw new HttpStatusError('Failed to fetch login page', loginPage.status);
|
|
192
209
|
}
|
|
193
210
|
bootstrapClient.applySetCookieHeaders(loginPage.headers['set-cookie']);
|
|
194
211
|
const loginHtml = loginPage.body;
|
|
@@ -222,7 +239,7 @@ export class OverleafClient {
|
|
|
222
239
|
if (loginFailure) {
|
|
223
240
|
throw new Error('Password login failed. Email or password may be incorrect.');
|
|
224
241
|
}
|
|
225
|
-
throw new
|
|
242
|
+
throw new HttpStatusError('Password login failed', loginResponse.status);
|
|
226
243
|
}
|
|
227
244
|
const projectUrl = new URL(loginResponse.headers.location || '/project', baseUrl).toString();
|
|
228
245
|
const projectPage = await bootstrapClient.httpRequest(projectUrl, {
|
|
@@ -233,7 +250,7 @@ export class OverleafClient {
|
|
|
233
250
|
expect: 'text'
|
|
234
251
|
});
|
|
235
252
|
if (!projectPage.ok) {
|
|
236
|
-
throw
|
|
253
|
+
throw httpError('Failed to fetch projects page after login', projectPage.status);
|
|
237
254
|
}
|
|
238
255
|
bootstrapClient.applySetCookieHeaders(projectPage.headers['set-cookie']);
|
|
239
256
|
const projectHtml = projectPage.body;
|
|
@@ -245,7 +262,9 @@ export class OverleafClient {
|
|
|
245
262
|
if (!projectCsrf) {
|
|
246
263
|
throw new Error('Could not find CSRF token after password login.');
|
|
247
264
|
}
|
|
248
|
-
|
|
265
|
+
const client = new OverleafClient({ cookies: bootstrapClient.cookies, csrf: projectCsrf, baseUrl });
|
|
266
|
+
client.accountEmail = OverleafClient.extractAccountEmail($project) ?? email;
|
|
267
|
+
return client;
|
|
249
268
|
}
|
|
250
269
|
/**
|
|
251
270
|
* Pull the CSRF token out of a rendered Overleaf page.
|
|
@@ -281,6 +300,28 @@ export class OverleafClient {
|
|
|
281
300
|
}
|
|
282
301
|
return csrf;
|
|
283
302
|
}
|
|
303
|
+
/**
|
|
304
|
+
* The signed-in account's email from a rendered Overleaf page, when the
|
|
305
|
+
* page carries one. `ol-usersEmail` is on the project dashboard; `ol-user`
|
|
306
|
+
* (JSON) is on the editor and on older releases. Display only.
|
|
307
|
+
*/
|
|
308
|
+
static extractAccountEmail($) {
|
|
309
|
+
const direct = $('meta[name="ol-usersEmail"]').attr('content');
|
|
310
|
+
if (direct)
|
|
311
|
+
return direct;
|
|
312
|
+
const user = $('meta[name="ol-user"]').attr('content');
|
|
313
|
+
if (user) {
|
|
314
|
+
try {
|
|
315
|
+
const parsed = JSON.parse(user);
|
|
316
|
+
if (typeof parsed?.email === 'string' && parsed.email)
|
|
317
|
+
return parsed.email;
|
|
318
|
+
}
|
|
319
|
+
catch {
|
|
320
|
+
// Not JSON on this release; no email to show.
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
return undefined;
|
|
324
|
+
}
|
|
284
325
|
static isLoginPage($) {
|
|
285
326
|
return $('form[name="loginForm"]').length > 0 || $('input[name="password"]').length > 0;
|
|
286
327
|
}
|
|
@@ -348,12 +389,25 @@ export class OverleafClient {
|
|
|
348
389
|
}
|
|
349
390
|
const doRequest = (reqUrl, redirectsLeft) => {
|
|
350
391
|
return new Promise((resolve, reject) => {
|
|
351
|
-
const parsedUrl = new URL(reqUrl);
|
|
352
|
-
const transport = parsedUrl.protocol === 'https:' ? https : http;
|
|
353
392
|
const headers = this.normalizeHeaders({ ...extraHeaders, ...options.headers });
|
|
354
|
-
|
|
393
|
+
// HTTPS_PROXY / HTTP_PROXY / NO_PROXY: every request, the socket
|
|
394
|
+
// polling included, goes through here. See src/proxy.ts.
|
|
395
|
+
const route = routeRequest(reqUrl, headers);
|
|
396
|
+
if (route.proxy)
|
|
397
|
+
this.logVerbose(`${method} ${reqUrl} via proxy ${route.proxy.host}`);
|
|
398
|
+
const req = route.transport.request(route.url, { ...route.options, method }, (res) => {
|
|
355
399
|
const status = res.statusCode || 0;
|
|
356
400
|
const resHeaders = res.headers;
|
|
401
|
+
// Overleaf answers an expired session on any page with a redirect to
|
|
402
|
+
// /login. Following it would hand an HTML login form to a caller
|
|
403
|
+
// that expected JSON or a zip, and the user would see a parse error.
|
|
404
|
+
if (status >= 300 && status < 400 && res.headers.location
|
|
405
|
+
&& isLoginRedirect(reqUrl, res.headers.location)) {
|
|
406
|
+
this.logVerbose(`${method} ${reqUrl} -> ${status} redirect to login page`);
|
|
407
|
+
res.resume();
|
|
408
|
+
reject(new SessionExpiredError(`HTTP ${status} redirect to the login page from ${new URL(reqUrl).pathname}`, status));
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
357
411
|
if (status >= 300 && status < 400 && res.headers.location && redirectsLeft > 0) {
|
|
358
412
|
this.logVerbose(`${method} ${reqUrl} -> ${status} redirect -> ${res.headers.location}`);
|
|
359
413
|
const redirectUrl = new URL(res.headers.location, reqUrl).toString();
|
|
@@ -415,7 +469,7 @@ export class OverleafClient {
|
|
|
415
469
|
expect: 'text'
|
|
416
470
|
});
|
|
417
471
|
if (!response.ok) {
|
|
418
|
-
throw
|
|
472
|
+
throw httpError('Failed to fetch projects', response.status);
|
|
419
473
|
}
|
|
420
474
|
this.applySetCookieHeaders(response.headers['set-cookie']);
|
|
421
475
|
const html = response.body;
|
|
@@ -488,6 +542,19 @@ export class OverleafClient {
|
|
|
488
542
|
const projects = await this.listProjects();
|
|
489
543
|
return projects.find(p => p.name === name);
|
|
490
544
|
}
|
|
545
|
+
/**
|
|
546
|
+
* Find a project by ID or by what a user typed for its name: exact name,
|
|
547
|
+
* then case-insensitive, then a unique prefix. Throws ProjectNotFoundError
|
|
548
|
+
* (with "Did you mean" suggestions) or AmbiguousProjectError.
|
|
549
|
+
* `getProject` keeps its exact-match contract for library callers.
|
|
550
|
+
*/
|
|
551
|
+
async findProject(query) {
|
|
552
|
+
const projects = await this.listProjects();
|
|
553
|
+
const byId = projects.find(p => p.id === query);
|
|
554
|
+
if (byId)
|
|
555
|
+
return byId;
|
|
556
|
+
return matchProject(projects, query);
|
|
557
|
+
}
|
|
491
558
|
/**
|
|
492
559
|
* Get project by ID
|
|
493
560
|
*/
|
|
@@ -517,7 +584,7 @@ export class OverleafClient {
|
|
|
517
584
|
expect: 'json'
|
|
518
585
|
});
|
|
519
586
|
if (!response.ok) {
|
|
520
|
-
throw
|
|
587
|
+
throw httpError('Failed to create project', response.status);
|
|
521
588
|
}
|
|
522
589
|
this.applySetCookieHeaders(response.headers['set-cookie']);
|
|
523
590
|
const projectId = response.body?.project_id;
|
|
@@ -540,7 +607,7 @@ export class OverleafClient {
|
|
|
540
607
|
expect: 'text'
|
|
541
608
|
});
|
|
542
609
|
if (!response.ok) {
|
|
543
|
-
throw
|
|
610
|
+
throw httpError('Failed to fetch project info', response.status);
|
|
544
611
|
}
|
|
545
612
|
this.applySetCookieHeaders(response.headers['set-cookie']);
|
|
546
613
|
const html = response.body;
|
|
@@ -696,7 +763,7 @@ export class OverleafClient {
|
|
|
696
763
|
timeoutMs
|
|
697
764
|
});
|
|
698
765
|
if (!response.ok) {
|
|
699
|
-
throw
|
|
766
|
+
throw httpError('Download failed', response.status);
|
|
700
767
|
}
|
|
701
768
|
this.applySetCookieHeaders(response.headers['set-cookie']);
|
|
702
769
|
return response.body;
|
|
@@ -721,7 +788,7 @@ export class OverleafClient {
|
|
|
721
788
|
expect: 'json'
|
|
722
789
|
});
|
|
723
790
|
if (!response.ok) {
|
|
724
|
-
throw
|
|
791
|
+
throw httpError('Failed to compile project', response.status);
|
|
725
792
|
}
|
|
726
793
|
this.applySetCookieHeaders(response.headers['set-cookie']);
|
|
727
794
|
const data = response.body;
|
|
@@ -769,7 +836,7 @@ export class OverleafClient {
|
|
|
769
836
|
throw new Error('Folder already exists');
|
|
770
837
|
}
|
|
771
838
|
if (!response.ok) {
|
|
772
|
-
throw
|
|
839
|
+
throw httpError('Failed to create folder', response.status);
|
|
773
840
|
}
|
|
774
841
|
this.applySetCookieHeaders(response.headers['set-cookie']);
|
|
775
842
|
const data = response.body;
|
|
@@ -893,9 +960,7 @@ export class OverleafClient {
|
|
|
893
960
|
timeoutMs: 5000
|
|
894
961
|
});
|
|
895
962
|
if (!handshakeResponse.ok) {
|
|
896
|
-
|
|
897
|
-
error.status = handshakeResponse.status;
|
|
898
|
-
throw error;
|
|
963
|
+
throw httpError('Failed to open project socket', handshakeResponse.status);
|
|
899
964
|
}
|
|
900
965
|
this.applySetCookieHeaders(handshakeResponse.headers['set-cookie']);
|
|
901
966
|
const sid = handshakeResponse.body.trim().split(':')[0];
|
|
@@ -931,7 +996,7 @@ export class OverleafClient {
|
|
|
931
996
|
timeoutMs: 7000
|
|
932
997
|
});
|
|
933
998
|
if (!response.ok) {
|
|
934
|
-
throw
|
|
999
|
+
throw httpError('Socket poll failed', response.status);
|
|
935
1000
|
}
|
|
936
1001
|
this.applySetCookieHeaders(response.headers['set-cookie']);
|
|
937
1002
|
const packets = this.decodeSocketIoPayload(response.body);
|
|
@@ -966,7 +1031,7 @@ export class OverleafClient {
|
|
|
966
1031
|
timeoutMs: 5000
|
|
967
1032
|
});
|
|
968
1033
|
if (!response.ok) {
|
|
969
|
-
throw
|
|
1034
|
+
throw httpError('Socket post failed', response.status);
|
|
970
1035
|
}
|
|
971
1036
|
this.applySetCookieHeaders(response.headers['set-cookie']);
|
|
972
1037
|
}
|
|
@@ -1467,6 +1532,8 @@ export class OverleafClient {
|
|
|
1467
1532
|
expect: 'text'
|
|
1468
1533
|
});
|
|
1469
1534
|
if (!response.ok) {
|
|
1535
|
+
if (isAuthStatus(response.status))
|
|
1536
|
+
throw httpError('Failed to upload file', response.status);
|
|
1470
1537
|
const text = response.body;
|
|
1471
1538
|
// Overleaf returns folder_not_found as HTTP 422 JSON.
|
|
1472
1539
|
// Parse the body first so caller can trigger folder probing fallback.
|
|
@@ -1530,7 +1597,7 @@ export class OverleafClient {
|
|
|
1530
1597
|
expect: 'text'
|
|
1531
1598
|
});
|
|
1532
1599
|
if (!response.ok) {
|
|
1533
|
-
throw
|
|
1600
|
+
throw httpError('Failed to delete entity', response.status);
|
|
1534
1601
|
}
|
|
1535
1602
|
this.applySetCookieHeaders(response.headers['set-cookie']);
|
|
1536
1603
|
}
|
|
@@ -1543,7 +1610,7 @@ export class OverleafClient {
|
|
|
1543
1610
|
expect: 'json'
|
|
1544
1611
|
});
|
|
1545
1612
|
if (!response.ok) {
|
|
1546
|
-
throw
|
|
1613
|
+
throw httpError('Failed to get entities', response.status);
|
|
1547
1614
|
}
|
|
1548
1615
|
this.applySetCookieHeaders(response.headers['set-cookie']);
|
|
1549
1616
|
const data = response.body;
|
|
@@ -1597,7 +1664,7 @@ export class OverleafClient {
|
|
|
1597
1664
|
expect: fileType === 'doc' ? 'json' : 'buffer'
|
|
1598
1665
|
});
|
|
1599
1666
|
if (!response.ok) {
|
|
1600
|
-
throw
|
|
1667
|
+
throw httpError('Failed to download file', response.status);
|
|
1601
1668
|
}
|
|
1602
1669
|
this.applySetCookieHeaders(response.headers['set-cookie']);
|
|
1603
1670
|
if (fileType === 'doc') {
|
|
@@ -1621,7 +1688,7 @@ export class OverleafClient {
|
|
|
1621
1688
|
expect: 'text'
|
|
1622
1689
|
});
|
|
1623
1690
|
if (!response.ok) {
|
|
1624
|
-
throw
|
|
1691
|
+
throw httpError('Failed to rename entity', response.status);
|
|
1625
1692
|
}
|
|
1626
1693
|
this.applySetCookieHeaders(response.headers['set-cookie']);
|
|
1627
1694
|
}
|
|
@@ -1644,7 +1711,7 @@ export class OverleafClient {
|
|
|
1644
1711
|
expect: 'text'
|
|
1645
1712
|
});
|
|
1646
1713
|
if (!response.ok) {
|
|
1647
|
-
throw
|
|
1714
|
+
throw httpError('Failed to rename project', response.status);
|
|
1648
1715
|
}
|
|
1649
1716
|
this.applySetCookieHeaders(response.headers['set-cookie']);
|
|
1650
1717
|
}
|
|
@@ -1659,14 +1726,104 @@ export class OverleafClient {
|
|
|
1659
1726
|
await this.deleteEntity(projectId, entity.id, entity.type);
|
|
1660
1727
|
}
|
|
1661
1728
|
/**
|
|
1662
|
-
*
|
|
1729
|
+
* Move a file, doc, or folder into another folder, keeping its name.
|
|
1730
|
+
* Overleaf answers 400 when the name is already taken in the target folder.
|
|
1663
1731
|
*/
|
|
1664
|
-
async
|
|
1665
|
-
const
|
|
1666
|
-
|
|
1667
|
-
|
|
1732
|
+
async moveEntity(projectId, entityId, entityType, folderId) {
|
|
1733
|
+
const response = await this.httpRequest(`${this.baseUrl}/project/${projectId}/${entityType}/${entityId}/move`, {
|
|
1734
|
+
method: 'POST',
|
|
1735
|
+
headers: this.getHeaders(true),
|
|
1736
|
+
body: JSON.stringify({ folder_id: folderId }),
|
|
1737
|
+
expect: 'text'
|
|
1738
|
+
});
|
|
1739
|
+
if (!response.ok) {
|
|
1740
|
+
const detail = typeof response.body === 'string' && response.body.trim() ? ` (${response.body.trim().slice(0, 200)})` : '';
|
|
1741
|
+
throw httpError('Failed to move entity', response.status, detail.trim());
|
|
1742
|
+
}
|
|
1743
|
+
this.applySetCookieHeaders(response.headers['set-cookie']);
|
|
1744
|
+
}
|
|
1745
|
+
/**
|
|
1746
|
+
* Work out what `rename|mv <source> <destination>` does, without writing.
|
|
1747
|
+
* Throws MoveRefusedError when the request is refused. See move-plan.ts.
|
|
1748
|
+
*/
|
|
1749
|
+
async planMove(projectId, source, destination) {
|
|
1750
|
+
const projectInfo = await this.getProjectInfo(projectId);
|
|
1751
|
+
const root = projectInfo.rootFolder?.[0];
|
|
1752
|
+
if (!root?._id)
|
|
1753
|
+
throw new Error('Could not read the project file tree');
|
|
1754
|
+
return planEntityMove(root, source, destination);
|
|
1755
|
+
}
|
|
1756
|
+
/**
|
|
1757
|
+
* Carry out a plan from planMove. Missing folders are created right before
|
|
1758
|
+
* the move. If the second of two writes fails, the first one is undone so
|
|
1759
|
+
* the entity keeps its old path; folders created on the way stay.
|
|
1760
|
+
*/
|
|
1761
|
+
async applyMovePlan(projectId, plan) {
|
|
1762
|
+
const { entity } = plan;
|
|
1763
|
+
const createdFolders = [];
|
|
1764
|
+
if (plan.noop)
|
|
1765
|
+
return { from: plan.from, to: plan.to, createdFolders };
|
|
1766
|
+
const ensureTargetFolder = async () => {
|
|
1767
|
+
if (plan.targetFolderId)
|
|
1768
|
+
return plan.targetFolderId;
|
|
1769
|
+
const known = { ...plan.knownFolders };
|
|
1770
|
+
for (const path of plan.createFolders) {
|
|
1771
|
+
const parentPath = path.includes('/') ? path.slice(0, path.lastIndexOf('/')) : '';
|
|
1772
|
+
const name = path.slice(path.lastIndexOf('/') + 1);
|
|
1773
|
+
known[path] = await this.createFolder(projectId, known[parentPath], name);
|
|
1774
|
+
createdFolders.push(path);
|
|
1775
|
+
}
|
|
1776
|
+
return known[plan.targetFolderPath];
|
|
1777
|
+
};
|
|
1778
|
+
const doRename = () => this.renameEntity(projectId, entity.id, entity.type, plan.newName);
|
|
1779
|
+
const doMove = async () => this.moveEntity(projectId, entity.id, entity.type, await ensureTargetFolder());
|
|
1780
|
+
try {
|
|
1781
|
+
if (!plan.move) {
|
|
1782
|
+
await doRename();
|
|
1783
|
+
}
|
|
1784
|
+
else if (!plan.rename) {
|
|
1785
|
+
await doMove();
|
|
1786
|
+
}
|
|
1787
|
+
else if (plan.order === 'rename-then-move') {
|
|
1788
|
+
await doRename();
|
|
1789
|
+
try {
|
|
1790
|
+
await doMove();
|
|
1791
|
+
}
|
|
1792
|
+
catch (error) {
|
|
1793
|
+
await this.renameEntity(projectId, entity.id, entity.type, entity.name).catch(() => undefined);
|
|
1794
|
+
throw error;
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
else {
|
|
1798
|
+
await doMove();
|
|
1799
|
+
try {
|
|
1800
|
+
await doRename();
|
|
1801
|
+
}
|
|
1802
|
+
catch (error) {
|
|
1803
|
+
await this.moveEntity(projectId, entity.id, entity.type, plan.sourceFolderId).catch(() => undefined);
|
|
1804
|
+
throw error;
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
finally {
|
|
1809
|
+
this.invalidateFolderTree(projectId);
|
|
1668
1810
|
}
|
|
1669
|
-
|
|
1811
|
+
return { from: plan.from, to: plan.to, createdFolders };
|
|
1812
|
+
}
|
|
1813
|
+
/**
|
|
1814
|
+
* Rename and/or move an entity by path. A destination without `/` renames
|
|
1815
|
+
* in place; with a folder part it moves (creating missing folders); ending
|
|
1816
|
+
* in `/` it moves and keeps the name; `/` alone is the project root.
|
|
1817
|
+
*/
|
|
1818
|
+
async moveByPath(projectId, source, destination) {
|
|
1819
|
+
const plan = await this.planMove(projectId, source, destination);
|
|
1820
|
+
return this.applyMovePlan(projectId, plan);
|
|
1821
|
+
}
|
|
1822
|
+
/**
|
|
1823
|
+
* Rename a file by path. `newName` may be a path, see moveByPath.
|
|
1824
|
+
*/
|
|
1825
|
+
async renameByPath(projectId, oldPath, newName) {
|
|
1826
|
+
await this.moveByPath(projectId, oldPath, newName);
|
|
1670
1827
|
}
|
|
1671
1828
|
/**
|
|
1672
1829
|
* Download a file by path (uses zip as fallback if ID not available)
|
|
@@ -1707,7 +1864,7 @@ export class OverleafClient {
|
|
|
1707
1864
|
expect: 'json'
|
|
1708
1865
|
});
|
|
1709
1866
|
if (!response.ok) {
|
|
1710
|
-
throw
|
|
1867
|
+
throw httpError('Failed to fetch comment threads', response.status);
|
|
1711
1868
|
}
|
|
1712
1869
|
this.applySetCookieHeaders(response.headers['set-cookie']);
|
|
1713
1870
|
return response.body;
|
|
@@ -1941,7 +2098,7 @@ export class OverleafClient {
|
|
|
1941
2098
|
expect: 'text'
|
|
1942
2099
|
});
|
|
1943
2100
|
if (!response.ok) {
|
|
1944
|
-
throw
|
|
2101
|
+
throw httpError(`Failed to accept tracked changes in ${changes[0].path}`, response.status);
|
|
1945
2102
|
}
|
|
1946
2103
|
this.applySetCookieHeaders(response.headers['set-cookie']);
|
|
1947
2104
|
}
|
|
@@ -2088,7 +2245,7 @@ export class OverleafClient {
|
|
|
2088
2245
|
expect: 'text'
|
|
2089
2246
|
});
|
|
2090
2247
|
if (!response.ok) {
|
|
2091
|
-
throw
|
|
2248
|
+
throw httpError('Failed to resolve comment', response.status);
|
|
2092
2249
|
}
|
|
2093
2250
|
this.applySetCookieHeaders(response.headers['set-cookie']);
|
|
2094
2251
|
return comment;
|
|
@@ -2102,7 +2259,7 @@ export class OverleafClient {
|
|
|
2102
2259
|
expect: 'text'
|
|
2103
2260
|
});
|
|
2104
2261
|
if (!response.ok) {
|
|
2105
|
-
throw
|
|
2262
|
+
throw httpError('Failed to reopen comment', response.status);
|
|
2106
2263
|
}
|
|
2107
2264
|
this.applySetCookieHeaders(response.headers['set-cookie']);
|
|
2108
2265
|
return comment;
|
|
@@ -2116,7 +2273,7 @@ export class OverleafClient {
|
|
|
2116
2273
|
expect: 'text'
|
|
2117
2274
|
});
|
|
2118
2275
|
if (!response.ok) {
|
|
2119
|
-
throw
|
|
2276
|
+
throw httpError('Failed to delete comment', response.status);
|
|
2120
2277
|
}
|
|
2121
2278
|
this.applySetCookieHeaders(response.headers['set-cookie']);
|
|
2122
2279
|
return comment;
|
|
@@ -2222,7 +2379,7 @@ export class OverleafClient {
|
|
|
2222
2379
|
expect: 'text'
|
|
2223
2380
|
});
|
|
2224
2381
|
if (!response.ok) {
|
|
2225
|
-
throw
|
|
2382
|
+
throw httpError('Failed to post comment message', response.status);
|
|
2226
2383
|
}
|
|
2227
2384
|
this.applySetCookieHeaders(response.headers['set-cookie']);
|
|
2228
2385
|
if (!response.body)
|
|
@@ -2245,7 +2402,7 @@ export class OverleafClient {
|
|
|
2245
2402
|
expect: 'json'
|
|
2246
2403
|
});
|
|
2247
2404
|
if (!response.ok) {
|
|
2248
|
-
throw
|
|
2405
|
+
throw httpError('Failed to compile project', response.status);
|
|
2249
2406
|
}
|
|
2250
2407
|
this.applySetCookieHeaders(response.headers['set-cookie']);
|
|
2251
2408
|
const data = response.body;
|