@aloth/olcli 0.3.0 → 0.4.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 +40 -3
- package/dist/cli.js +194 -3
- package/dist/cli.js.map +1 -1
- package/dist/client.d.ts +88 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +524 -7
- package/dist/client.js.map +1 -1
- package/package.json +1 -1
package/dist/client.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAgBH,MAAM,WAAW,OAAO;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,KAAK,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACjE,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,WAAW,EAAE,CAAC;CAC3B;AAED,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,WAAW,EAAE,CAAC;IACvB,IAAI,EAAE,QAAQ,EAAE,CAAC;IACjB,QAAQ,EAAE,SAAS,EAAE,CAAC;CACvB;AAED,MAAM,WAAW,QAAQ;IACvB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,SAAS;IACxB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
|
package/dist/client.js
CHANGED
|
@@ -19,11 +19,40 @@ export class OverleafClient {
|
|
|
19
19
|
cookies;
|
|
20
20
|
csrf;
|
|
21
21
|
baseUrl;
|
|
22
|
+
verbose = false;
|
|
23
|
+
// Cache per-project folder trees so repeated uploads in sync/upload calls
|
|
24
|
+
// don't re-fetch the tree via Socket.IO on every file.
|
|
25
|
+
folderTreeCache = new Map();
|
|
22
26
|
constructor(credentials) {
|
|
23
27
|
this.cookies = credentials.cookies;
|
|
24
28
|
this.csrf = credentials.csrf;
|
|
25
29
|
this.baseUrl = credentials.baseUrl || DEFAULT_BASE_URL;
|
|
26
30
|
}
|
|
31
|
+
/** Enable or disable verbose request/response logging to stderr. */
|
|
32
|
+
setVerbose(v) {
|
|
33
|
+
this.verbose = v;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Resolve (and cache) the folder tree for a project. Falls back to a
|
|
37
|
+
* minimal tree containing only the root folder when the Socket.IO probe
|
|
38
|
+
* fails (e.g. self-hosted Overleaf without that endpoint).
|
|
39
|
+
*/
|
|
40
|
+
async getOrLoadFolderTree(projectId) {
|
|
41
|
+
const cached = this.folderTreeCache.get(projectId);
|
|
42
|
+
if (cached)
|
|
43
|
+
return cached;
|
|
44
|
+
let tree = await this.getFolderTreeFromSocket(projectId);
|
|
45
|
+
if (!tree) {
|
|
46
|
+
const rootId = await this.getRootFolderId(projectId);
|
|
47
|
+
tree = { '': rootId };
|
|
48
|
+
}
|
|
49
|
+
this.folderTreeCache.set(projectId, tree);
|
|
50
|
+
return tree;
|
|
51
|
+
}
|
|
52
|
+
/** Drop the cached folder tree for a project (e.g. after rename/delete). */
|
|
53
|
+
invalidateFolderTree(projectId) {
|
|
54
|
+
this.folderTreeCache.delete(projectId);
|
|
55
|
+
}
|
|
27
56
|
projectUrl() {
|
|
28
57
|
return `${this.baseUrl}/project`;
|
|
29
58
|
}
|
|
@@ -127,6 +156,12 @@ export class OverleafClient {
|
|
|
127
156
|
}
|
|
128
157
|
}
|
|
129
158
|
}
|
|
159
|
+
logVerbose(...args) {
|
|
160
|
+
if (this.verbose) {
|
|
161
|
+
// eslint-disable-next-line no-console
|
|
162
|
+
console.error('[olcli]', ...args);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
130
165
|
async httpRequest(url, options = {}) {
|
|
131
166
|
const method = options.method || 'GET';
|
|
132
167
|
const timeoutMs = options.timeoutMs ?? 10000;
|
|
@@ -158,6 +193,7 @@ export class OverleafClient {
|
|
|
158
193
|
const status = res.statusCode || 0;
|
|
159
194
|
const resHeaders = res.headers;
|
|
160
195
|
if (status >= 300 && status < 400 && res.headers.location && redirectsLeft > 0) {
|
|
196
|
+
this.logVerbose(`${method} ${reqUrl} -> ${status} redirect -> ${res.headers.location}`);
|
|
161
197
|
const redirectUrl = new URL(res.headers.location, reqUrl).toString();
|
|
162
198
|
res.resume();
|
|
163
199
|
doRequest(redirectUrl, redirectsLeft - 1).then(resolve, reject);
|
|
@@ -176,10 +212,21 @@ export class OverleafClient {
|
|
|
176
212
|
body = JSON.parse(buffer.toString('utf-8'));
|
|
177
213
|
}
|
|
178
214
|
catch (e) {
|
|
215
|
+
this.logVerbose(`${method} ${reqUrl} -> ${status} (invalid JSON, ${buffer.length} bytes)`);
|
|
179
216
|
return reject(new Error(`Failed to parse JSON response from ${reqUrl}`));
|
|
180
217
|
}
|
|
181
218
|
}
|
|
182
|
-
|
|
219
|
+
const ok = status >= 200 && status < 300;
|
|
220
|
+
if (this.verbose) {
|
|
221
|
+
const ct = (resHeaders['content-type'] || '');
|
|
222
|
+
let snippet = '';
|
|
223
|
+
if (!ok) {
|
|
224
|
+
const text = expect === 'buffer' ? '' : (typeof body === 'string' ? body : JSON.stringify(body));
|
|
225
|
+
snippet = text ? ` body=${text.slice(0, 200).replace(/\s+/g, ' ')}` : '';
|
|
226
|
+
}
|
|
227
|
+
this.logVerbose(`${method} ${reqUrl} -> ${status} (${buffer.length}B ${ct})${snippet}`);
|
|
228
|
+
}
|
|
229
|
+
resolve({ status, ok, headers: resHeaders, body });
|
|
183
230
|
});
|
|
184
231
|
res.on('error', reject);
|
|
185
232
|
});
|
|
@@ -476,8 +523,11 @@ export class OverleafClient {
|
|
|
476
523
|
if (!pdfFile) {
|
|
477
524
|
throw new Error('No PDF output found');
|
|
478
525
|
}
|
|
526
|
+
// Overleaf's CDN requires ?clsiserverid=<id> for build-output downloads.
|
|
527
|
+
// Without it the build URL 404s. See: https://github.com/aloth/olcli/issues/22
|
|
528
|
+
const qs = data.clsiServerId ? `?clsiserverid=${encodeURIComponent(data.clsiServerId)}` : '';
|
|
479
529
|
return {
|
|
480
|
-
pdfUrl: `${this.baseUrl}${pdfFile.url}`,
|
|
530
|
+
pdfUrl: `${this.baseUrl}${pdfFile.url}${qs}`,
|
|
481
531
|
logs: data.compileGroup ? [`Compile group: ${data.compileGroup}`] : []
|
|
482
532
|
};
|
|
483
533
|
}
|
|
@@ -554,6 +604,225 @@ export class OverleafClient {
|
|
|
554
604
|
}
|
|
555
605
|
return packets;
|
|
556
606
|
}
|
|
607
|
+
encodeSocketIoEvent(id, name, args) {
|
|
608
|
+
return `5:${id}+::${JSON.stringify({ name, args })}`;
|
|
609
|
+
}
|
|
610
|
+
parseSocketIoAck(packet, id) {
|
|
611
|
+
const match = packet.match(/^6:::(\d+)(.*)$/);
|
|
612
|
+
if (!match || Number.parseInt(match[1], 10) !== id) {
|
|
613
|
+
return null;
|
|
614
|
+
}
|
|
615
|
+
let payload = match[2] || '';
|
|
616
|
+
if (payload.startsWith('+')) {
|
|
617
|
+
payload = payload.slice(1);
|
|
618
|
+
}
|
|
619
|
+
if (!payload)
|
|
620
|
+
return [];
|
|
621
|
+
const args = JSON.parse(payload);
|
|
622
|
+
return Array.isArray(args) ? args : [args];
|
|
623
|
+
}
|
|
624
|
+
decodeOverleafUtf8(text) {
|
|
625
|
+
return Buffer.from(text, 'binary').toString('utf-8');
|
|
626
|
+
}
|
|
627
|
+
generateCommentThreadId() {
|
|
628
|
+
const timestamp = Math.floor(Date.now() / 1000).toString(16).padStart(8, '0');
|
|
629
|
+
const machine = Math.floor(Math.random() * 0xffffff).toString(16).padStart(6, '0');
|
|
630
|
+
const pid = Math.floor(Math.random() * 0x7fff).toString(16).padStart(4, '0');
|
|
631
|
+
return `${timestamp}${machine}${pid}000001`;
|
|
632
|
+
}
|
|
633
|
+
positionToLineColumn(content, position) {
|
|
634
|
+
const prefix = content.slice(0, position);
|
|
635
|
+
const lines = prefix.split('\n');
|
|
636
|
+
return {
|
|
637
|
+
line: lines.length,
|
|
638
|
+
column: lines[lines.length - 1].length + 1
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
buildCommentContext(content, line, contextLines = 0) {
|
|
642
|
+
if (contextLines <= 0)
|
|
643
|
+
return undefined;
|
|
644
|
+
const lines = content.split('\n');
|
|
645
|
+
const lineIndex = line - 1;
|
|
646
|
+
const beforeStart = Math.max(0, lineIndex - contextLines);
|
|
647
|
+
const afterEnd = Math.min(lines.length, lineIndex + contextLines + 1);
|
|
648
|
+
return {
|
|
649
|
+
startLine: beforeStart + 1,
|
|
650
|
+
endLine: afterEnd,
|
|
651
|
+
before: lines.slice(beforeStart, lineIndex),
|
|
652
|
+
line: lines[lineIndex] || '',
|
|
653
|
+
after: lines.slice(lineIndex + 1, afterEnd)
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
collectProjectDocs(projectInfo) {
|
|
657
|
+
const docs = [];
|
|
658
|
+
function walk(folder, folderPath) {
|
|
659
|
+
for (const doc of folder.docs || []) {
|
|
660
|
+
docs.push({
|
|
661
|
+
id: doc._id,
|
|
662
|
+
path: folderPath ? `${folderPath}/${doc.name}` : doc.name
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
for (const child of folder.folders || []) {
|
|
666
|
+
const childPath = folderPath ? `${folderPath}/${child.name}` : child.name;
|
|
667
|
+
walk(child, childPath);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
for (const folder of projectInfo.rootFolder || []) {
|
|
671
|
+
walk(folder, '');
|
|
672
|
+
}
|
|
673
|
+
return docs;
|
|
674
|
+
}
|
|
675
|
+
async openProjectSocket(projectId) {
|
|
676
|
+
const handshakeUrl = `${this.baseUrl}/socket.io/1/?projectId=${encodeURIComponent(projectId)}&t=${Date.now()}`;
|
|
677
|
+
const handshakeResponse = await this.httpRequest(handshakeUrl, {
|
|
678
|
+
headers: { 'Cookie': this.getCookieHeader(), 'User-Agent': USER_AGENT },
|
|
679
|
+
expect: 'text',
|
|
680
|
+
timeoutMs: 5000
|
|
681
|
+
});
|
|
682
|
+
if (!handshakeResponse.ok) {
|
|
683
|
+
throw new Error(`Failed to open project socket: ${handshakeResponse.status}`);
|
|
684
|
+
}
|
|
685
|
+
this.applySetCookieHeaders(handshakeResponse.headers['set-cookie']);
|
|
686
|
+
const sid = handshakeResponse.body.trim().split(':')[0];
|
|
687
|
+
if (!sid) {
|
|
688
|
+
throw new Error('Failed to open project socket: missing session id');
|
|
689
|
+
}
|
|
690
|
+
const session = {
|
|
691
|
+
sid,
|
|
692
|
+
projectId,
|
|
693
|
+
pollUrl: () => `${this.baseUrl}/socket.io/1/xhr-polling/${sid}?projectId=${encodeURIComponent(projectId)}&t=${Date.now()}`
|
|
694
|
+
};
|
|
695
|
+
for (let attempt = 0; attempt < 8; attempt++) {
|
|
696
|
+
const packets = await this.pollProjectSocket(session);
|
|
697
|
+
if (packets.some(packet => {
|
|
698
|
+
if (!packet.startsWith('5:::'))
|
|
699
|
+
return false;
|
|
700
|
+
try {
|
|
701
|
+
return JSON.parse(packet.slice(4))?.name === 'joinProjectResponse';
|
|
702
|
+
}
|
|
703
|
+
catch {
|
|
704
|
+
return false;
|
|
705
|
+
}
|
|
706
|
+
})) {
|
|
707
|
+
return session;
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
throw new Error('Project socket did not return joinProjectResponse');
|
|
711
|
+
}
|
|
712
|
+
async pollProjectSocket(session) {
|
|
713
|
+
const response = await this.httpRequest(session.pollUrl(), {
|
|
714
|
+
headers: { 'Cookie': this.getCookieHeader(), 'User-Agent': USER_AGENT },
|
|
715
|
+
expect: 'text',
|
|
716
|
+
timeoutMs: 7000
|
|
717
|
+
});
|
|
718
|
+
if (!response.ok) {
|
|
719
|
+
throw new Error(`Socket poll failed: ${response.status}`);
|
|
720
|
+
}
|
|
721
|
+
this.applySetCookieHeaders(response.headers['set-cookie']);
|
|
722
|
+
const packets = this.decodeSocketIoPayload(response.body);
|
|
723
|
+
for (const packet of packets) {
|
|
724
|
+
if (packet.startsWith('2::')) {
|
|
725
|
+
const heartbeatResponse = await this.httpRequest(session.pollUrl(), {
|
|
726
|
+
method: 'POST',
|
|
727
|
+
headers: {
|
|
728
|
+
'Cookie': this.getCookieHeader(),
|
|
729
|
+
'User-Agent': USER_AGENT,
|
|
730
|
+
'Content-Type': 'text/plain;charset=UTF-8'
|
|
731
|
+
},
|
|
732
|
+
body: '2::',
|
|
733
|
+
expect: 'text',
|
|
734
|
+
timeoutMs: 5000
|
|
735
|
+
});
|
|
736
|
+
this.applySetCookieHeaders(heartbeatResponse.headers['set-cookie']);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
return packets;
|
|
740
|
+
}
|
|
741
|
+
async postProjectSocketPacket(session, packet) {
|
|
742
|
+
const response = await this.httpRequest(session.pollUrl(), {
|
|
743
|
+
method: 'POST',
|
|
744
|
+
headers: {
|
|
745
|
+
'Cookie': this.getCookieHeader(),
|
|
746
|
+
'User-Agent': USER_AGENT,
|
|
747
|
+
'Content-Type': 'text/plain;charset=UTF-8'
|
|
748
|
+
},
|
|
749
|
+
body: packet,
|
|
750
|
+
expect: 'text',
|
|
751
|
+
timeoutMs: 5000
|
|
752
|
+
});
|
|
753
|
+
if (!response.ok) {
|
|
754
|
+
throw new Error(`Socket post failed: ${response.status}`);
|
|
755
|
+
}
|
|
756
|
+
this.applySetCookieHeaders(response.headers['set-cookie']);
|
|
757
|
+
}
|
|
758
|
+
async socketRpc(session, name, args) {
|
|
759
|
+
const id = Math.floor(Math.random() * 0x7fffffff);
|
|
760
|
+
await this.postProjectSocketPacket(session, this.encodeSocketIoEvent(id, name, args));
|
|
761
|
+
for (let attempt = 0; attempt < 10; attempt++) {
|
|
762
|
+
const packets = await this.pollProjectSocket(session);
|
|
763
|
+
for (const packet of packets) {
|
|
764
|
+
const ackArgs = this.parseSocketIoAck(packet, id);
|
|
765
|
+
if (ackArgs) {
|
|
766
|
+
const [error, ...result] = ackArgs;
|
|
767
|
+
if (error) {
|
|
768
|
+
const message = typeof error === 'string' ? error : error.message || JSON.stringify(error);
|
|
769
|
+
throw new Error(`${name} failed: ${message}`);
|
|
770
|
+
}
|
|
771
|
+
return result;
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
throw new Error(`${name} did not return an acknowledgement`);
|
|
776
|
+
}
|
|
777
|
+
async closeProjectSocket(session) {
|
|
778
|
+
try {
|
|
779
|
+
await this.postProjectSocketPacket(session, '0::');
|
|
780
|
+
}
|
|
781
|
+
catch {
|
|
782
|
+
// Best-effort socket cleanup only.
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
normalizeJoinedDocument(docId, args) {
|
|
786
|
+
const [lines, version, _updates, ranges, type = 'sharejs-text-ot'] = args;
|
|
787
|
+
if (type === 'history-ot') {
|
|
788
|
+
const content = typeof lines?.content === 'string' ? lines.content : '';
|
|
789
|
+
return {
|
|
790
|
+
docId,
|
|
791
|
+
lines: content.split('\n'),
|
|
792
|
+
content,
|
|
793
|
+
version,
|
|
794
|
+
ranges: lines,
|
|
795
|
+
type
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
const decodedLines = Array.isArray(lines)
|
|
799
|
+
? lines.map((line) => this.decodeOverleafUtf8(line))
|
|
800
|
+
: [];
|
|
801
|
+
const decodedRanges = ranges || {};
|
|
802
|
+
for (const comment of decodedRanges.comments || []) {
|
|
803
|
+
if (comment?.op?.c) {
|
|
804
|
+
comment.op.c = this.decodeOverleafUtf8(comment.op.c);
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
return {
|
|
808
|
+
docId,
|
|
809
|
+
lines: decodedLines,
|
|
810
|
+
content: decodedLines.join('\n'),
|
|
811
|
+
version,
|
|
812
|
+
ranges: decodedRanges,
|
|
813
|
+
type
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
async joinDocument(session, docId) {
|
|
817
|
+
const args = await this.socketRpc(session, 'joinDoc', [
|
|
818
|
+
docId,
|
|
819
|
+
{
|
|
820
|
+
encodeRanges: true,
|
|
821
|
+
supportsHistoryOT: true
|
|
822
|
+
}
|
|
823
|
+
]);
|
|
824
|
+
return this.normalizeJoinedDocument(docId, args);
|
|
825
|
+
}
|
|
557
826
|
/**
|
|
558
827
|
* Extract root folder ID from a Socket.IO event packet (joinProjectResponse).
|
|
559
828
|
*/
|
|
@@ -914,11 +1183,15 @@ export class OverleafClient {
|
|
|
914
1183
|
async uploadFile(projectId, folderId, fileName, content, folderTree) {
|
|
915
1184
|
// Extract just the filename without path
|
|
916
1185
|
const baseName = fileName.split('/').pop() || fileName;
|
|
917
|
-
// Resolve target folder: if fileName has a directory part
|
|
1186
|
+
// Resolve target folder: if fileName has a directory part, place the file there.
|
|
1187
|
+
// Lazy-load + cache the folder tree when caller didn't supply one, so external
|
|
1188
|
+
// callers (and our own `upload`/`sync` paths) don't silently dump files into root.
|
|
1189
|
+
// See: https://github.com/aloth/olcli/issues/22 follow-up + 0.3.1 upload-fix.
|
|
918
1190
|
const dirPart = fileName.includes('/') ? fileName.split('/').slice(0, -1).join('/') : '';
|
|
919
1191
|
let targetFolderId;
|
|
920
|
-
if (dirPart
|
|
921
|
-
|
|
1192
|
+
if (dirPart) {
|
|
1193
|
+
const tree = folderTree || await this.getOrLoadFolderTree(projectId);
|
|
1194
|
+
targetFolderId = await this.resolveFolderId(projectId, tree, dirPart);
|
|
922
1195
|
}
|
|
923
1196
|
else {
|
|
924
1197
|
targetFolderId = folderId || await this.getRootFolderId(projectId);
|
|
@@ -1168,6 +1441,247 @@ export class OverleafClient {
|
|
|
1168
1441
|
}
|
|
1169
1442
|
throw new Error(`File not found in archive: ${path}`);
|
|
1170
1443
|
}
|
|
1444
|
+
async getCommentThreads(projectId) {
|
|
1445
|
+
const response = await this.httpRequest(`${this.baseUrl}/project/${projectId}/threads`, {
|
|
1446
|
+
headers: this.getHeaders(),
|
|
1447
|
+
expect: 'json'
|
|
1448
|
+
});
|
|
1449
|
+
if (!response.ok) {
|
|
1450
|
+
throw new Error(`Failed to fetch comment threads: ${response.status}`);
|
|
1451
|
+
}
|
|
1452
|
+
this.applySetCookieHeaders(response.headers['set-cookie']);
|
|
1453
|
+
return response.body;
|
|
1454
|
+
}
|
|
1455
|
+
async listComments(projectId, options = {}) {
|
|
1456
|
+
const status = options.status || 'all';
|
|
1457
|
+
const contextLines = options.contextLines || 0;
|
|
1458
|
+
const projectInfo = await this.getProjectInfo(projectId);
|
|
1459
|
+
const docs = this.collectProjectDocs(projectInfo);
|
|
1460
|
+
const threads = await this.getCommentThreads(projectId);
|
|
1461
|
+
const comments = [];
|
|
1462
|
+
const session = await this.openProjectSocket(projectId);
|
|
1463
|
+
try {
|
|
1464
|
+
for (const doc of docs) {
|
|
1465
|
+
const joinedDoc = await this.joinDocument(session, doc.id);
|
|
1466
|
+
if (joinedDoc.type === 'history-ot') {
|
|
1467
|
+
for (const comment of joinedDoc.ranges.comments || []) {
|
|
1468
|
+
const ranges = comment.ranges || [];
|
|
1469
|
+
const firstRange = ranges[0];
|
|
1470
|
+
if (!firstRange)
|
|
1471
|
+
continue;
|
|
1472
|
+
const selectedText = ranges
|
|
1473
|
+
.map((range) => joinedDoc.content.slice(range.pos, range.pos + range.length))
|
|
1474
|
+
.join('');
|
|
1475
|
+
const location = this.positionToLineColumn(joinedDoc.content, firstRange.pos);
|
|
1476
|
+
const thread = threads[comment.id] || { messages: [] };
|
|
1477
|
+
const resolved = Boolean(comment.resolved || thread.resolved);
|
|
1478
|
+
comments.push({
|
|
1479
|
+
threadId: comment.id,
|
|
1480
|
+
docId: doc.id,
|
|
1481
|
+
path: doc.path,
|
|
1482
|
+
position: firstRange.pos,
|
|
1483
|
+
line: location.line,
|
|
1484
|
+
column: location.column,
|
|
1485
|
+
selectedText,
|
|
1486
|
+
resolved,
|
|
1487
|
+
messages: thread.messages || [],
|
|
1488
|
+
context: this.buildCommentContext(joinedDoc.content, location.line, contextLines)
|
|
1489
|
+
});
|
|
1490
|
+
}
|
|
1491
|
+
continue;
|
|
1492
|
+
}
|
|
1493
|
+
for (const comment of joinedDoc.ranges.comments || []) {
|
|
1494
|
+
const op = comment.op || {};
|
|
1495
|
+
const threadId = op.t || comment.id;
|
|
1496
|
+
if (!threadId || typeof op.p !== 'number')
|
|
1497
|
+
continue;
|
|
1498
|
+
const selectedText = typeof op.c === 'string'
|
|
1499
|
+
? op.c
|
|
1500
|
+
: joinedDoc.content.slice(op.p, op.p + (op.c?.length || 0));
|
|
1501
|
+
const location = this.positionToLineColumn(joinedDoc.content, op.p);
|
|
1502
|
+
const thread = threads[threadId] || { messages: [] };
|
|
1503
|
+
const resolved = Boolean(comment.resolved || op.resolved || thread.resolved);
|
|
1504
|
+
comments.push({
|
|
1505
|
+
threadId,
|
|
1506
|
+
docId: doc.id,
|
|
1507
|
+
path: doc.path,
|
|
1508
|
+
position: op.p,
|
|
1509
|
+
line: location.line,
|
|
1510
|
+
column: location.column,
|
|
1511
|
+
selectedText,
|
|
1512
|
+
resolved,
|
|
1513
|
+
messages: thread.messages || [],
|
|
1514
|
+
context: this.buildCommentContext(joinedDoc.content, location.line, contextLines)
|
|
1515
|
+
});
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
finally {
|
|
1520
|
+
await this.closeProjectSocket(session);
|
|
1521
|
+
}
|
|
1522
|
+
return comments
|
|
1523
|
+
.filter(comment => {
|
|
1524
|
+
if (status === 'all')
|
|
1525
|
+
return true;
|
|
1526
|
+
return status === 'resolved' ? comment.resolved : !comment.resolved;
|
|
1527
|
+
})
|
|
1528
|
+
.sort((a, b) => a.path.localeCompare(b.path) || a.position - b.position);
|
|
1529
|
+
}
|
|
1530
|
+
async resolveComment(projectId, threadId) {
|
|
1531
|
+
const comment = await this.findComment(projectId, threadId);
|
|
1532
|
+
const response = await this.httpRequest(`${this.baseUrl}/project/${projectId}/doc/${comment.docId}/thread/${threadId}/resolve`, {
|
|
1533
|
+
method: 'POST',
|
|
1534
|
+
headers: this.getHeaders(true),
|
|
1535
|
+
body: '',
|
|
1536
|
+
expect: 'text'
|
|
1537
|
+
});
|
|
1538
|
+
if (!response.ok) {
|
|
1539
|
+
throw new Error(`Failed to resolve comment: ${response.status}`);
|
|
1540
|
+
}
|
|
1541
|
+
this.applySetCookieHeaders(response.headers['set-cookie']);
|
|
1542
|
+
return comment;
|
|
1543
|
+
}
|
|
1544
|
+
async reopenComment(projectId, threadId) {
|
|
1545
|
+
const comment = await this.findComment(projectId, threadId);
|
|
1546
|
+
const response = await this.httpRequest(`${this.baseUrl}/project/${projectId}/doc/${comment.docId}/thread/${threadId}/reopen`, {
|
|
1547
|
+
method: 'POST',
|
|
1548
|
+
headers: this.getHeaders(true),
|
|
1549
|
+
body: '',
|
|
1550
|
+
expect: 'text'
|
|
1551
|
+
});
|
|
1552
|
+
if (!response.ok) {
|
|
1553
|
+
throw new Error(`Failed to reopen comment: ${response.status}`);
|
|
1554
|
+
}
|
|
1555
|
+
this.applySetCookieHeaders(response.headers['set-cookie']);
|
|
1556
|
+
return comment;
|
|
1557
|
+
}
|
|
1558
|
+
async deleteComment(projectId, threadId) {
|
|
1559
|
+
const comment = await this.findComment(projectId, threadId);
|
|
1560
|
+
const response = await this.httpRequest(`${this.baseUrl}/project/${projectId}/doc/${comment.docId}/thread/${threadId}`, {
|
|
1561
|
+
method: 'DELETE',
|
|
1562
|
+
headers: this.getHeaders(true),
|
|
1563
|
+
body: '',
|
|
1564
|
+
expect: 'text'
|
|
1565
|
+
});
|
|
1566
|
+
if (!response.ok) {
|
|
1567
|
+
throw new Error(`Failed to delete comment: ${response.status}`);
|
|
1568
|
+
}
|
|
1569
|
+
this.applySetCookieHeaders(response.headers['set-cookie']);
|
|
1570
|
+
return comment;
|
|
1571
|
+
}
|
|
1572
|
+
async findComment(projectId, threadId) {
|
|
1573
|
+
const comments = await this.listComments(projectId);
|
|
1574
|
+
const comment = comments.find(item => item.threadId === threadId);
|
|
1575
|
+
if (!comment) {
|
|
1576
|
+
throw new Error(`Comment thread not found: ${threadId}`);
|
|
1577
|
+
}
|
|
1578
|
+
return comment;
|
|
1579
|
+
}
|
|
1580
|
+
resolveCommentSelection(doc, options) {
|
|
1581
|
+
if (options.selectedText) {
|
|
1582
|
+
const occurrence = options.occurrence || 1;
|
|
1583
|
+
let fromIndex = 0;
|
|
1584
|
+
let position = -1;
|
|
1585
|
+
for (let index = 0; index < occurrence; index++) {
|
|
1586
|
+
position = doc.content.indexOf(options.selectedText, fromIndex);
|
|
1587
|
+
if (position === -1)
|
|
1588
|
+
break;
|
|
1589
|
+
fromIndex = position + options.selectedText.length;
|
|
1590
|
+
}
|
|
1591
|
+
if (position === -1) {
|
|
1592
|
+
throw new Error(`Selected text not found in ${options.filePath}`);
|
|
1593
|
+
}
|
|
1594
|
+
return { position, selectedText: options.selectedText };
|
|
1595
|
+
}
|
|
1596
|
+
let position = options.position;
|
|
1597
|
+
if (position == null) {
|
|
1598
|
+
if (options.line == null || options.column == null) {
|
|
1599
|
+
throw new Error('Add comment requires either --text, --position, or both --line and --column');
|
|
1600
|
+
}
|
|
1601
|
+
const lines = doc.content.split('\n');
|
|
1602
|
+
if (options.line < 1 || options.line > lines.length) {
|
|
1603
|
+
throw new Error(`Line out of range: ${options.line}`);
|
|
1604
|
+
}
|
|
1605
|
+
if (options.column < 1 || options.column > lines[options.line - 1].length + 1) {
|
|
1606
|
+
throw new Error(`Column out of range: ${options.column}`);
|
|
1607
|
+
}
|
|
1608
|
+
position = lines.slice(0, options.line - 1).reduce((sum, line) => sum + line.length + 1, 0) + options.column - 1;
|
|
1609
|
+
}
|
|
1610
|
+
const length = options.length || 1;
|
|
1611
|
+
if (position < 0 || position + length > doc.content.length) {
|
|
1612
|
+
throw new Error('Comment selection is outside the document');
|
|
1613
|
+
}
|
|
1614
|
+
return {
|
|
1615
|
+
position,
|
|
1616
|
+
selectedText: doc.content.slice(position, position + length)
|
|
1617
|
+
};
|
|
1618
|
+
}
|
|
1619
|
+
async addComment(projectId, options) {
|
|
1620
|
+
const projectInfo = await this.getProjectInfo(projectId);
|
|
1621
|
+
const docs = this.collectProjectDocs(projectInfo);
|
|
1622
|
+
const normalizedPath = options.filePath.replace(/^\//, '');
|
|
1623
|
+
const doc = docs.find(item => item.path === normalizedPath || item.path.replace(/^\//, '') === normalizedPath);
|
|
1624
|
+
if (!doc) {
|
|
1625
|
+
throw new Error(`Doc not found: ${options.filePath}`);
|
|
1626
|
+
}
|
|
1627
|
+
const session = await this.openProjectSocket(projectId);
|
|
1628
|
+
try {
|
|
1629
|
+
const joinedDoc = await this.joinDocument(session, doc.id);
|
|
1630
|
+
const selection = this.resolveCommentSelection(joinedDoc, options);
|
|
1631
|
+
const threadId = this.generateCommentThreadId();
|
|
1632
|
+
await this.postCommentMessage(projectId, threadId, options.content);
|
|
1633
|
+
const op = joinedDoc.type === 'history-ot'
|
|
1634
|
+
? {
|
|
1635
|
+
commentId: threadId,
|
|
1636
|
+
ranges: [{ pos: selection.position, length: selection.selectedText.length }]
|
|
1637
|
+
}
|
|
1638
|
+
: {
|
|
1639
|
+
c: selection.selectedText,
|
|
1640
|
+
p: selection.position,
|
|
1641
|
+
t: threadId
|
|
1642
|
+
};
|
|
1643
|
+
await this.socketRpc(session, 'applyOtUpdate', [doc.id, {
|
|
1644
|
+
doc: doc.id,
|
|
1645
|
+
op: [op],
|
|
1646
|
+
v: joinedDoc.version
|
|
1647
|
+
}]);
|
|
1648
|
+
const location = this.positionToLineColumn(joinedDoc.content, selection.position);
|
|
1649
|
+
return {
|
|
1650
|
+
threadId,
|
|
1651
|
+
docId: doc.id,
|
|
1652
|
+
path: doc.path,
|
|
1653
|
+
position: selection.position,
|
|
1654
|
+
line: location.line,
|
|
1655
|
+
column: location.column,
|
|
1656
|
+
selectedText: selection.selectedText,
|
|
1657
|
+
resolved: false,
|
|
1658
|
+
messages: []
|
|
1659
|
+
};
|
|
1660
|
+
}
|
|
1661
|
+
finally {
|
|
1662
|
+
await this.closeProjectSocket(session);
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
async postCommentMessage(projectId, threadId, content) {
|
|
1666
|
+
const response = await this.httpRequest(`${this.baseUrl}/project/${projectId}/thread/${threadId}/messages`, {
|
|
1667
|
+
method: 'POST',
|
|
1668
|
+
headers: this.getHeaders(true),
|
|
1669
|
+
body: JSON.stringify({ content }),
|
|
1670
|
+
expect: 'text'
|
|
1671
|
+
});
|
|
1672
|
+
if (!response.ok) {
|
|
1673
|
+
throw new Error(`Failed to post comment message: ${response.status}`);
|
|
1674
|
+
}
|
|
1675
|
+
this.applySetCookieHeaders(response.headers['set-cookie']);
|
|
1676
|
+
if (!response.body)
|
|
1677
|
+
return null;
|
|
1678
|
+
try {
|
|
1679
|
+
return JSON.parse(response.body);
|
|
1680
|
+
}
|
|
1681
|
+
catch {
|
|
1682
|
+
return null;
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1171
1685
|
/**
|
|
1172
1686
|
* Compile project and get all output files
|
|
1173
1687
|
*/
|
|
@@ -1189,13 +1703,16 @@ export class OverleafClient {
|
|
|
1189
1703
|
this.applySetCookieHeaders(response.headers['set-cookie']);
|
|
1190
1704
|
const data = response.body;
|
|
1191
1705
|
const pdfFile = data.outputFiles?.find((f) => f.type === 'pdf');
|
|
1706
|
+
// Overleaf's CDN requires ?clsiserverid=<id> for build-output downloads.
|
|
1707
|
+
// Without it every output (pdf/log/bbl/...) 404s. See issue #22.
|
|
1708
|
+
const qs = data.clsiServerId ? `?clsiserverid=${encodeURIComponent(data.clsiServerId)}` : '';
|
|
1192
1709
|
return {
|
|
1193
1710
|
status: data.status,
|
|
1194
|
-
pdfUrl: pdfFile ? `${this.baseUrl}${pdfFile.url}` : undefined,
|
|
1711
|
+
pdfUrl: pdfFile ? `${this.baseUrl}${pdfFile.url}${qs}` : undefined,
|
|
1195
1712
|
outputFiles: (data.outputFiles || []).map((f) => ({
|
|
1196
1713
|
path: f.path,
|
|
1197
1714
|
type: f.type,
|
|
1198
|
-
url: `${this.baseUrl}${f.url}`
|
|
1715
|
+
url: `${this.baseUrl}${f.url}${qs}`
|
|
1199
1716
|
}))
|
|
1200
1717
|
};
|
|
1201
1718
|
}
|