@aloth/olcli 0.1.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/LICENSE +21 -0
- package/README.md +226 -0
- package/dist/cli.d.ts +9 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +873 -0
- package/dist/cli.js.map +1 -0
- package/dist/client.d.ts +163 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +572 -0
- package/dist/client.js.map +1 -0
- package/dist/config.d.ts +16 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +70 -0
- package/dist/config.js.map +1 -0
- package/package.json +59 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,873 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* olcli - Overleaf Command Line Interface
|
|
4
|
+
*
|
|
5
|
+
* Command-line access to Overleaf projects using session cookies
|
|
6
|
+
* for authentication. Download, upload, sync, and compile LaTeX projects.
|
|
7
|
+
*/
|
|
8
|
+
import { Command } from 'commander';
|
|
9
|
+
import chalk from 'chalk';
|
|
10
|
+
import ora from 'ora';
|
|
11
|
+
import { writeFileSync, readFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
12
|
+
import { join, dirname, basename } from 'node:path';
|
|
13
|
+
import { OverleafClient } from './client.js';
|
|
14
|
+
import { getSessionCookie, setSessionCookie, setLastProject, getConfigPath, saveOlAuth, clearConfig } from './config.js';
|
|
15
|
+
const program = new Command();
|
|
16
|
+
program
|
|
17
|
+
.name('olcli')
|
|
18
|
+
.description('Overleaf CLI - interact with Overleaf projects from the command line')
|
|
19
|
+
.version('0.1.0');
|
|
20
|
+
/**
|
|
21
|
+
* Helper to get authenticated client
|
|
22
|
+
*/
|
|
23
|
+
async function getClient(cookieOpt) {
|
|
24
|
+
const cookie = cookieOpt || getSessionCookie();
|
|
25
|
+
if (!cookie) {
|
|
26
|
+
console.error(chalk.red('No session cookie found.'));
|
|
27
|
+
console.error('Set one with: olcli auth --cookie <session_cookie>');
|
|
28
|
+
console.error('Or set OVERLEAF_SESSION environment variable');
|
|
29
|
+
console.error('Or create .olauth file in current directory');
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
return OverleafClient.fromSessionCookie(cookie);
|
|
33
|
+
}
|
|
34
|
+
async function resolveProject(client, projectArg, dir = '.') {
|
|
35
|
+
// If project argument provided, use it
|
|
36
|
+
if (projectArg) {
|
|
37
|
+
// If it looks like a valid MongoDB ObjectId (24 hex chars), trust it directly
|
|
38
|
+
if (/^[a-f0-9]{24}$/i.test(projectArg)) {
|
|
39
|
+
// Trust the ID, use a placeholder name (will be overwritten on next list)
|
|
40
|
+
return { id: projectArg, name: projectArg };
|
|
41
|
+
}
|
|
42
|
+
// Otherwise, look up by name
|
|
43
|
+
let proj = await client.getProject(projectArg);
|
|
44
|
+
if (!proj) {
|
|
45
|
+
throw new Error(`Project not found: ${projectArg}`);
|
|
46
|
+
}
|
|
47
|
+
return { id: proj.id, name: proj.name };
|
|
48
|
+
}
|
|
49
|
+
// Otherwise, check for .olcli.json
|
|
50
|
+
const metaPath = join(dir, '.olcli.json');
|
|
51
|
+
if (existsSync(metaPath)) {
|
|
52
|
+
const meta = JSON.parse(readFileSync(metaPath, 'utf-8'));
|
|
53
|
+
if (meta.projectId && meta.projectName) {
|
|
54
|
+
return { id: meta.projectId, name: meta.projectName };
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
// No project found
|
|
58
|
+
throw new Error('No project specified. Provide a project name/ID or run from a synced directory.');
|
|
59
|
+
}
|
|
60
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
61
|
+
// AUTH COMMANDS
|
|
62
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
63
|
+
program
|
|
64
|
+
.command('auth')
|
|
65
|
+
.description('Authenticate with Overleaf using session cookie')
|
|
66
|
+
.option('--cookie <session>', 'Session cookie (overleaf_session2 value)')
|
|
67
|
+
.option('--save-local', 'Save to .olauth in current directory')
|
|
68
|
+
.action(async (options) => {
|
|
69
|
+
if (!options.cookie) {
|
|
70
|
+
console.log(chalk.yellow('To authenticate, provide your session cookie:'));
|
|
71
|
+
console.log();
|
|
72
|
+
console.log('1. Log into overleaf.com in your browser');
|
|
73
|
+
console.log('2. Open Developer Tools (F12) → Application → Cookies');
|
|
74
|
+
console.log('3. Find the cookie named "overleaf_session2"');
|
|
75
|
+
console.log('4. Copy its value and run:');
|
|
76
|
+
console.log();
|
|
77
|
+
console.log(chalk.cyan(' olcli auth --cookie "your_session_cookie_value"'));
|
|
78
|
+
console.log();
|
|
79
|
+
console.log('Or set OVERLEAF_SESSION environment variable');
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const spinner = ora('Verifying session...').start();
|
|
83
|
+
try {
|
|
84
|
+
const client = await OverleafClient.fromSessionCookie(options.cookie);
|
|
85
|
+
const projects = await client.listProjects();
|
|
86
|
+
setSessionCookie(options.cookie);
|
|
87
|
+
if (options.saveLocal) {
|
|
88
|
+
saveOlAuth(options.cookie);
|
|
89
|
+
spinner.succeed(`Authenticated! Found ${projects.length} projects. Saved to .olauth`);
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
spinner.succeed(`Authenticated! Found ${projects.length} projects.`);
|
|
93
|
+
}
|
|
94
|
+
console.log(chalk.dim(`Config saved to: ${getConfigPath()}`));
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
spinner.fail(`Authentication failed: ${error.message}`);
|
|
98
|
+
process.exit(1);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
program
|
|
102
|
+
.command('whoami')
|
|
103
|
+
.description('Show current authentication status')
|
|
104
|
+
.action(async () => {
|
|
105
|
+
const cookie = getSessionCookie();
|
|
106
|
+
if (!cookie) {
|
|
107
|
+
console.log(chalk.yellow('Not authenticated'));
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const spinner = ora('Checking session...').start();
|
|
111
|
+
try {
|
|
112
|
+
const client = await OverleafClient.fromSessionCookie(cookie);
|
|
113
|
+
const projects = await client.listProjects();
|
|
114
|
+
spinner.succeed(`Authenticated with access to ${projects.length} projects`);
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
spinner.fail(`Session invalid: ${error.message}`);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
program
|
|
121
|
+
.command('logout')
|
|
122
|
+
.description('Clear stored credentials')
|
|
123
|
+
.action(() => {
|
|
124
|
+
clearConfig();
|
|
125
|
+
console.log(chalk.green('Credentials cleared'));
|
|
126
|
+
});
|
|
127
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
128
|
+
// PROJECT COMMANDS
|
|
129
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
130
|
+
program
|
|
131
|
+
.command('list')
|
|
132
|
+
.alias('ls')
|
|
133
|
+
.description('List all projects')
|
|
134
|
+
.option('--json', 'Output as JSON')
|
|
135
|
+
.option('-n, --limit <n>', 'Limit number of results', parseInt)
|
|
136
|
+
.option('--cookie <session>', 'Session cookie override')
|
|
137
|
+
.action(async (options) => {
|
|
138
|
+
const spinner = ora('Fetching projects...').start();
|
|
139
|
+
try {
|
|
140
|
+
const client = await getClient(options.cookie);
|
|
141
|
+
let projects = await client.listProjects();
|
|
142
|
+
if (options.limit) {
|
|
143
|
+
projects = projects.slice(0, options.limit);
|
|
144
|
+
}
|
|
145
|
+
spinner.stop();
|
|
146
|
+
if (options.json) {
|
|
147
|
+
console.log(JSON.stringify(projects, null, 2));
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (projects.length === 0) {
|
|
151
|
+
console.log(chalk.yellow('No projects found'));
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
console.log(chalk.bold(`Found ${projects.length} project(s):\n`));
|
|
155
|
+
for (const p of projects) {
|
|
156
|
+
const date = new Date(p.lastUpdated).toLocaleDateString();
|
|
157
|
+
console.log(` ${chalk.cyan(p.id)} - ${chalk.bold(p.name)}`);
|
|
158
|
+
console.log(` ${chalk.dim(`Last updated: ${date}`)}`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
spinner.fail(`Failed: ${error.message}`);
|
|
163
|
+
process.exit(1);
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
program
|
|
167
|
+
.command('info [project]')
|
|
168
|
+
.description('Show project details (by name or ID)')
|
|
169
|
+
.option('--json', 'Output as JSON')
|
|
170
|
+
.option('--cookie <session>', 'Session cookie override')
|
|
171
|
+
.action(async (project, options) => {
|
|
172
|
+
const spinner = ora('Fetching project info...').start();
|
|
173
|
+
try {
|
|
174
|
+
const client = await getClient(options.cookie);
|
|
175
|
+
const proj = await resolveProject(client, project);
|
|
176
|
+
// Get entities (works without parsing HTML)
|
|
177
|
+
const entities = await client.getEntities(proj.id);
|
|
178
|
+
spinner.stop();
|
|
179
|
+
if (options.json) {
|
|
180
|
+
console.log(JSON.stringify({ project: proj, entities }, null, 2));
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
console.log(chalk.bold(`Project: ${proj.name}`));
|
|
184
|
+
console.log(` ID: ${chalk.cyan(proj.id)}`);
|
|
185
|
+
console.log();
|
|
186
|
+
// Print file list grouped by folder
|
|
187
|
+
console.log(chalk.bold('Files:'));
|
|
188
|
+
// Sort entities by path for nice display
|
|
189
|
+
const sorted = entities.sort((a, b) => a.path.localeCompare(b.path));
|
|
190
|
+
for (const entity of sorted) {
|
|
191
|
+
const icon = entity.type === 'doc' ? '📄' : '📎';
|
|
192
|
+
console.log(` ${icon} ${entity.path}`);
|
|
193
|
+
}
|
|
194
|
+
setLastProject(proj.id);
|
|
195
|
+
}
|
|
196
|
+
catch (error) {
|
|
197
|
+
spinner.fail(`Failed: ${error.message}`);
|
|
198
|
+
process.exit(1);
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
function printFolder(folder, indent) {
|
|
202
|
+
// Print subfolders
|
|
203
|
+
for (const f of folder.folders || []) {
|
|
204
|
+
console.log(`${indent}📁 ${chalk.blue(f.name)}/`);
|
|
205
|
+
printFolder(f, indent + ' ');
|
|
206
|
+
}
|
|
207
|
+
// Print docs
|
|
208
|
+
for (const d of folder.docs || []) {
|
|
209
|
+
console.log(`${indent}📄 ${d.name}`);
|
|
210
|
+
}
|
|
211
|
+
// Print files
|
|
212
|
+
for (const f of folder.fileRefs || []) {
|
|
213
|
+
console.log(`${indent}📎 ${f.name}`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
217
|
+
// DOWNLOAD COMMANDS
|
|
218
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
219
|
+
program
|
|
220
|
+
.command('download <file> [project]')
|
|
221
|
+
.description('Download a single file from project')
|
|
222
|
+
.option('-o, --output <path>', 'Output path (default: same as file name)')
|
|
223
|
+
.option('--cookie <session>', 'Session cookie override')
|
|
224
|
+
.action(async (file, project, options) => {
|
|
225
|
+
const spinner = ora('Downloading file...').start();
|
|
226
|
+
try {
|
|
227
|
+
const client = await getClient(options.cookie);
|
|
228
|
+
const proj = await resolveProject(client, project);
|
|
229
|
+
const content = await client.downloadByPath(proj.id, file);
|
|
230
|
+
const outputPath = options.output || basename(file);
|
|
231
|
+
writeFileSync(outputPath, content);
|
|
232
|
+
spinner.succeed(`Downloaded: ${outputPath} (${(content.length / 1024).toFixed(1)} KB)`);
|
|
233
|
+
setLastProject(proj.id);
|
|
234
|
+
}
|
|
235
|
+
catch (error) {
|
|
236
|
+
spinner.fail(`Failed: ${error.message}`);
|
|
237
|
+
process.exit(1);
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
program
|
|
241
|
+
.command('zip [project]')
|
|
242
|
+
.description('Download project as zip archive')
|
|
243
|
+
.option('-o, --output <path>', 'Output path (default: <project-name>.zip)')
|
|
244
|
+
.option('--cookie <session>', 'Session cookie override')
|
|
245
|
+
.action(async (project, options) => {
|
|
246
|
+
const spinner = ora('Downloading project...').start();
|
|
247
|
+
try {
|
|
248
|
+
const client = await getClient(options.cookie);
|
|
249
|
+
const proj = await resolveProject(client, project);
|
|
250
|
+
const zip = await client.downloadProject(proj.id);
|
|
251
|
+
const outputPath = options.output || `${proj.name.replace(/[^a-zA-Z0-9-_]/g, '_')}.zip`;
|
|
252
|
+
writeFileSync(outputPath, zip);
|
|
253
|
+
spinner.succeed(`Downloaded: ${outputPath} (${(zip.length / 1024).toFixed(1)} KB)`);
|
|
254
|
+
setLastProject(proj.id);
|
|
255
|
+
}
|
|
256
|
+
catch (error) {
|
|
257
|
+
spinner.fail(`Failed: ${error.message}`);
|
|
258
|
+
process.exit(1);
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
program
|
|
262
|
+
.command('pdf [project]')
|
|
263
|
+
.description('Compile and download PDF')
|
|
264
|
+
.option('-o, --output <path>', 'Output path (default: <project-name>.pdf)')
|
|
265
|
+
.option('--cookie <session>', 'Session cookie override')
|
|
266
|
+
.action(async (project, options) => {
|
|
267
|
+
const spinner = ora('Compiling project...').start();
|
|
268
|
+
try {
|
|
269
|
+
const client = await getClient(options.cookie);
|
|
270
|
+
const proj = await resolveProject(client, project);
|
|
271
|
+
spinner.text = 'Compiling...';
|
|
272
|
+
const pdf = await client.downloadPdf(proj.id);
|
|
273
|
+
const outputPath = options.output || `${proj.name.replace(/[^a-zA-Z0-9-_]/g, '_')}.pdf`;
|
|
274
|
+
writeFileSync(outputPath, pdf);
|
|
275
|
+
spinner.succeed(`Downloaded PDF: ${outputPath} (${(pdf.length / 1024).toFixed(1)} KB)`);
|
|
276
|
+
setLastProject(proj.id);
|
|
277
|
+
}
|
|
278
|
+
catch (error) {
|
|
279
|
+
spinner.fail(`Failed: ${error.message}`);
|
|
280
|
+
process.exit(1);
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
program
|
|
284
|
+
.command('output [type]')
|
|
285
|
+
.description('Download compile output files (bbl, log, aux, etc.)')
|
|
286
|
+
.option('-o, --output <path>', 'Output path')
|
|
287
|
+
.option('--list', 'List available output files')
|
|
288
|
+
.option('--project <name>', 'Project name or ID')
|
|
289
|
+
.option('--cookie <session>', 'Session cookie override')
|
|
290
|
+
.action(async (type, options) => {
|
|
291
|
+
const spinner = ora('Compiling project...').start();
|
|
292
|
+
try {
|
|
293
|
+
const client = await getClient(options.cookie);
|
|
294
|
+
// If type looks like a project name (contains spaces or is in project list), treat it as project
|
|
295
|
+
let actualType = type;
|
|
296
|
+
let projectArg = options.project;
|
|
297
|
+
if (type && !projectArg && !['bbl', 'log', 'aux', 'blg', 'pdf', 'out', 'fls', 'fdb_latexmk', 'stderr', 'pdfxref', 'chktex'].includes(type)) {
|
|
298
|
+
// Type might actually be a project name
|
|
299
|
+
const projects = await client.listProjects();
|
|
300
|
+
const matchedProject = projects.find(p => p.name === type || p.id === type);
|
|
301
|
+
if (matchedProject) {
|
|
302
|
+
projectArg = type;
|
|
303
|
+
actualType = undefined;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
const proj = await resolveProject(client, projectArg);
|
|
307
|
+
const result = await client.compileWithOutputs(proj.id);
|
|
308
|
+
if (result.status !== 'success') {
|
|
309
|
+
spinner.warn(`Compilation ${result.status}, but output files may still be available`);
|
|
310
|
+
}
|
|
311
|
+
if (options.list || !actualType) {
|
|
312
|
+
spinner.stop();
|
|
313
|
+
console.log(chalk.bold('Available output files:'));
|
|
314
|
+
for (const file of result.outputFiles) {
|
|
315
|
+
console.log(` ${chalk.cyan(file.type.padEnd(12))} ${file.path}`);
|
|
316
|
+
}
|
|
317
|
+
console.log();
|
|
318
|
+
console.log(chalk.dim('Usage: olcli output <type>'));
|
|
319
|
+
console.log(chalk.dim('Example: olcli output bbl'));
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
// Find matching output file
|
|
323
|
+
const outputFile = result.outputFiles.find(f => f.type === actualType || f.path.endsWith(`.${actualType}`));
|
|
324
|
+
if (!outputFile) {
|
|
325
|
+
spinner.fail(`Output file not found: ${actualType}`);
|
|
326
|
+
console.log(chalk.dim('Use --list to see available files'));
|
|
327
|
+
process.exit(1);
|
|
328
|
+
}
|
|
329
|
+
spinner.text = `Downloading ${outputFile.path}...`;
|
|
330
|
+
const content = await client.downloadOutputFile(outputFile.url);
|
|
331
|
+
const outputPath = options.output || outputFile.path.replace('output.', '');
|
|
332
|
+
writeFileSync(outputPath, content);
|
|
333
|
+
spinner.succeed(`Downloaded: ${outputPath} (${(content.length / 1024).toFixed(1)} KB)`);
|
|
334
|
+
setLastProject(proj.id);
|
|
335
|
+
}
|
|
336
|
+
catch (error) {
|
|
337
|
+
spinner.fail(`Failed: ${error.message}`);
|
|
338
|
+
process.exit(1);
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
342
|
+
// UPLOAD COMMANDS
|
|
343
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
344
|
+
program
|
|
345
|
+
.command('upload <file> [project]')
|
|
346
|
+
.description('Upload a file to a project')
|
|
347
|
+
.option('--folder <id>', 'Target folder ID (default: root)')
|
|
348
|
+
.option('--cookie <session>', 'Session cookie override')
|
|
349
|
+
.action(async (file, project, options) => {
|
|
350
|
+
const spinner = ora('Uploading file...').start();
|
|
351
|
+
try {
|
|
352
|
+
const client = await getClient(options.cookie);
|
|
353
|
+
const proj = await resolveProject(client, project);
|
|
354
|
+
if (!existsSync(file)) {
|
|
355
|
+
spinner.fail(`File not found: ${file}`);
|
|
356
|
+
process.exit(1);
|
|
357
|
+
}
|
|
358
|
+
const content = readFileSync(file);
|
|
359
|
+
const fileName = basename(file);
|
|
360
|
+
// Pass folder ID or null for root folder (client will compute it)
|
|
361
|
+
const folderId = options.folder || null;
|
|
362
|
+
const result = await client.uploadFile(proj.id, folderId, fileName, content);
|
|
363
|
+
if (result.success) {
|
|
364
|
+
spinner.succeed(`Uploaded: ${fileName} → "${proj.name}"`);
|
|
365
|
+
}
|
|
366
|
+
else {
|
|
367
|
+
spinner.fail(`Upload failed for: ${fileName}`);
|
|
368
|
+
process.exit(1);
|
|
369
|
+
}
|
|
370
|
+
setLastProject(proj.id);
|
|
371
|
+
}
|
|
372
|
+
catch (error) {
|
|
373
|
+
spinner.fail(`Failed: ${error.message}`);
|
|
374
|
+
process.exit(1);
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
// NOTE: delete and rename commands are disabled - they require entity IDs
|
|
378
|
+
// which are not exposed via the current Overleaf API without Socket.IO.
|
|
379
|
+
// Use the Overleaf web UI for these operations.
|
|
380
|
+
//
|
|
381
|
+
// program
|
|
382
|
+
// .command('delete <file> [project]')
|
|
383
|
+
// .alias('rm')
|
|
384
|
+
// .description('Delete a file from a project')
|
|
385
|
+
// .option('--cookie <session>', 'Session cookie override')
|
|
386
|
+
// .action(async (file, project, options) => {
|
|
387
|
+
// const spinner = ora('Deleting file...').start();
|
|
388
|
+
// try {
|
|
389
|
+
// const client = await getClient(options.cookie);
|
|
390
|
+
// const proj = await resolveProject(client, project);
|
|
391
|
+
// await client.deleteByPath(proj.id, file);
|
|
392
|
+
// spinner.succeed(`Deleted: ${file}`);
|
|
393
|
+
// setLastProject(proj.id);
|
|
394
|
+
// } catch (error: any) {
|
|
395
|
+
// spinner.fail(`Failed: ${error.message}`);
|
|
396
|
+
// process.exit(1);
|
|
397
|
+
// }
|
|
398
|
+
// });
|
|
399
|
+
//
|
|
400
|
+
// program
|
|
401
|
+
// .command('rename <oldname> <newname> [project]')
|
|
402
|
+
// .alias('mv')
|
|
403
|
+
// .description('Rename a file in a project')
|
|
404
|
+
// .option('--cookie <session>', 'Session cookie override')
|
|
405
|
+
// .action(async (oldname, newname, project, options) => {
|
|
406
|
+
// const spinner = ora('Renaming file...').start();
|
|
407
|
+
// try {
|
|
408
|
+
// const client = await getClient(options.cookie);
|
|
409
|
+
// const proj = await resolveProject(client, project);
|
|
410
|
+
// await client.renameByPath(proj.id, oldname, newname);
|
|
411
|
+
// spinner.succeed(`Renamed: ${oldname} → ${newname}`);
|
|
412
|
+
// setLastProject(proj.id);
|
|
413
|
+
// } catch (error: any) {
|
|
414
|
+
// spinner.fail(`Failed: ${error.message}`);
|
|
415
|
+
// process.exit(1);
|
|
416
|
+
// }
|
|
417
|
+
// });
|
|
418
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
419
|
+
// COMPILE COMMAND
|
|
420
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
421
|
+
program
|
|
422
|
+
.command('compile [project]')
|
|
423
|
+
.description('Compile a project (trigger PDF generation)')
|
|
424
|
+
.option('--cookie <session>', 'Session cookie override')
|
|
425
|
+
.action(async (project, options) => {
|
|
426
|
+
const spinner = ora('Compiling...').start();
|
|
427
|
+
try {
|
|
428
|
+
const client = await getClient(options.cookie);
|
|
429
|
+
const proj = await resolveProject(client, project);
|
|
430
|
+
const result = await client.compileProject(proj.id);
|
|
431
|
+
spinner.succeed(`Compiled "${proj.name}"`);
|
|
432
|
+
console.log(chalk.dim(`PDF URL: ${result.pdfUrl}`));
|
|
433
|
+
setLastProject(proj.id);
|
|
434
|
+
}
|
|
435
|
+
catch (error) {
|
|
436
|
+
spinner.fail(`Compilation failed: ${error.message}`);
|
|
437
|
+
process.exit(1);
|
|
438
|
+
}
|
|
439
|
+
});
|
|
440
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
441
|
+
// SYNC COMMANDS
|
|
442
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
443
|
+
program
|
|
444
|
+
.command('pull [project] [dir]')
|
|
445
|
+
.description('Download project files to local directory')
|
|
446
|
+
.option('--force', 'Overwrite local files even if newer')
|
|
447
|
+
.option('--cookie <session>', 'Session cookie override')
|
|
448
|
+
.action(async (project, dir, options) => {
|
|
449
|
+
let targetDir = dir || '.';
|
|
450
|
+
let projectId;
|
|
451
|
+
let projectName;
|
|
452
|
+
// Check for existing .olcli.json if no project specified
|
|
453
|
+
const metaPath = join(targetDir, '.olcli.json');
|
|
454
|
+
if (!project && existsSync(metaPath)) {
|
|
455
|
+
const meta = JSON.parse(readFileSync(metaPath, 'utf-8'));
|
|
456
|
+
projectId = meta.projectId;
|
|
457
|
+
projectName = meta.projectName;
|
|
458
|
+
}
|
|
459
|
+
else if (!project) {
|
|
460
|
+
console.error(chalk.red('No project specified.'));
|
|
461
|
+
console.error('Usage: olcli pull <project> [dir]');
|
|
462
|
+
console.error('Or run from a directory with .olcli.json');
|
|
463
|
+
process.exit(1);
|
|
464
|
+
}
|
|
465
|
+
const spinner = ora('Fetching project...').start();
|
|
466
|
+
try {
|
|
467
|
+
const client = await getClient(options.cookie);
|
|
468
|
+
// Resolve project if needed
|
|
469
|
+
if (!projectId) {
|
|
470
|
+
let proj = await client.getProjectById(project);
|
|
471
|
+
if (!proj) {
|
|
472
|
+
proj = await client.getProject(project);
|
|
473
|
+
}
|
|
474
|
+
if (!proj) {
|
|
475
|
+
spinner.fail(`Project not found: ${project}`);
|
|
476
|
+
process.exit(1);
|
|
477
|
+
}
|
|
478
|
+
projectId = proj.id;
|
|
479
|
+
projectName = proj.name;
|
|
480
|
+
// Default directory is project name (sanitized) if not specified
|
|
481
|
+
if (!dir) {
|
|
482
|
+
targetDir = proj.name.replace(/[^a-zA-Z0-9-_]/g, '_');
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
spinner.text = 'Downloading project...';
|
|
486
|
+
const zipBuffer = await client.downloadProject(projectId);
|
|
487
|
+
// Extract zip
|
|
488
|
+
spinner.text = 'Extracting files...';
|
|
489
|
+
const AdmZip = (await import('adm-zip')).default;
|
|
490
|
+
const zip = new AdmZip(zipBuffer);
|
|
491
|
+
// Create target directory
|
|
492
|
+
if (!existsSync(targetDir)) {
|
|
493
|
+
mkdirSync(targetDir, { recursive: true });
|
|
494
|
+
}
|
|
495
|
+
// Get local file modification times for safety check
|
|
496
|
+
const { statSync } = await import('node:fs');
|
|
497
|
+
const localMetaPath = join(targetDir, '.olcli.json');
|
|
498
|
+
let lastPull;
|
|
499
|
+
if (existsSync(localMetaPath)) {
|
|
500
|
+
const meta = JSON.parse(readFileSync(localMetaPath, 'utf-8'));
|
|
501
|
+
lastPull = meta.lastPull ? new Date(meta.lastPull) : undefined;
|
|
502
|
+
}
|
|
503
|
+
// Extract files with safety check
|
|
504
|
+
const entries = zip.getEntries();
|
|
505
|
+
let fileCount = 0;
|
|
506
|
+
let skippedCount = 0;
|
|
507
|
+
const skippedFiles = [];
|
|
508
|
+
for (const entry of entries) {
|
|
509
|
+
if (!entry.isDirectory) {
|
|
510
|
+
const filePath = join(targetDir, entry.entryName);
|
|
511
|
+
const fileDir = dirname(filePath);
|
|
512
|
+
// Check if local file exists and is newer than last pull
|
|
513
|
+
if (!options.force && existsSync(filePath) && lastPull) {
|
|
514
|
+
try {
|
|
515
|
+
const stats = statSync(filePath);
|
|
516
|
+
if (stats.mtime > lastPull) {
|
|
517
|
+
// Local file is newer - skip unless --force
|
|
518
|
+
skippedCount++;
|
|
519
|
+
skippedFiles.push(entry.entryName);
|
|
520
|
+
continue;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
catch (e) {
|
|
524
|
+
// File doesn't exist or can't stat, proceed with download
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
if (!existsSync(fileDir)) {
|
|
528
|
+
mkdirSync(fileDir, { recursive: true });
|
|
529
|
+
}
|
|
530
|
+
writeFileSync(filePath, entry.getData());
|
|
531
|
+
fileCount++;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
// Save project metadata
|
|
535
|
+
writeFileSync(join(targetDir, '.olcli.json'), JSON.stringify({
|
|
536
|
+
projectId,
|
|
537
|
+
projectName,
|
|
538
|
+
lastPull: new Date().toISOString()
|
|
539
|
+
}, null, 2));
|
|
540
|
+
if (skippedCount > 0) {
|
|
541
|
+
spinner.warn(`Downloaded ${fileCount} files, skipped ${skippedCount} locally modified files`);
|
|
542
|
+
console.log(chalk.yellow(' Skipped (local is newer):'));
|
|
543
|
+
for (const f of skippedFiles.slice(0, 5)) {
|
|
544
|
+
console.log(chalk.dim(` ${f}`));
|
|
545
|
+
}
|
|
546
|
+
if (skippedFiles.length > 5) {
|
|
547
|
+
console.log(chalk.dim(` ... and ${skippedFiles.length - 5} more`));
|
|
548
|
+
}
|
|
549
|
+
console.log(chalk.dim(' Use --force to overwrite'));
|
|
550
|
+
}
|
|
551
|
+
else {
|
|
552
|
+
spinner.succeed(`Downloaded ${fileCount} files to ${targetDir}/`);
|
|
553
|
+
}
|
|
554
|
+
setLastProject(projectId);
|
|
555
|
+
}
|
|
556
|
+
catch (error) {
|
|
557
|
+
spinner.fail(`Failed: ${error.message}`);
|
|
558
|
+
process.exit(1);
|
|
559
|
+
}
|
|
560
|
+
});
|
|
561
|
+
program
|
|
562
|
+
.command('push [dir]')
|
|
563
|
+
.description('Upload local changes to Overleaf project')
|
|
564
|
+
.option('--project <name>', 'Project name or ID (overrides .olcli.json)')
|
|
565
|
+
.option('--all', 'Upload all files (not just changed)')
|
|
566
|
+
.option('--dry-run', 'Show what would be uploaded without uploading')
|
|
567
|
+
.option('--cookie <session>', 'Session cookie override')
|
|
568
|
+
.action(async (dir, options) => {
|
|
569
|
+
const targetDir = dir || '.';
|
|
570
|
+
const metaPath = join(targetDir, '.olcli.json');
|
|
571
|
+
// Check for project metadata
|
|
572
|
+
let projectId;
|
|
573
|
+
let projectName;
|
|
574
|
+
let lastPull;
|
|
575
|
+
if (existsSync(metaPath)) {
|
|
576
|
+
const meta = JSON.parse(readFileSync(metaPath, 'utf-8'));
|
|
577
|
+
projectId = meta.projectId;
|
|
578
|
+
projectName = meta.projectName;
|
|
579
|
+
lastPull = meta.lastPull ? new Date(meta.lastPull) : undefined;
|
|
580
|
+
}
|
|
581
|
+
if (options.project) {
|
|
582
|
+
// Override with command line option
|
|
583
|
+
projectId = undefined;
|
|
584
|
+
projectName = options.project;
|
|
585
|
+
}
|
|
586
|
+
if (!projectId && !projectName) {
|
|
587
|
+
console.error(chalk.red('No project specified.'));
|
|
588
|
+
console.error('Either run from a directory with .olcli.json or use --project');
|
|
589
|
+
process.exit(1);
|
|
590
|
+
}
|
|
591
|
+
const spinner = ora('Connecting...').start();
|
|
592
|
+
try {
|
|
593
|
+
const client = await getClient(options.cookie);
|
|
594
|
+
// Resolve project if needed
|
|
595
|
+
if (!projectId) {
|
|
596
|
+
let proj = await client.getProjectById(projectName);
|
|
597
|
+
if (!proj) {
|
|
598
|
+
proj = await client.getProject(projectName);
|
|
599
|
+
}
|
|
600
|
+
if (!proj) {
|
|
601
|
+
spinner.fail(`Project not found: ${projectName}`);
|
|
602
|
+
process.exit(1);
|
|
603
|
+
}
|
|
604
|
+
projectId = proj.id;
|
|
605
|
+
projectName = proj.name;
|
|
606
|
+
}
|
|
607
|
+
spinner.text = 'Scanning files...';
|
|
608
|
+
// Get list of files to upload
|
|
609
|
+
const { readdirSync, statSync } = await import('node:fs');
|
|
610
|
+
const filesToUpload = [];
|
|
611
|
+
function scanDir(currentDir, relativeBase = '') {
|
|
612
|
+
const entries = readdirSync(currentDir, { withFileTypes: true });
|
|
613
|
+
for (const entry of entries) {
|
|
614
|
+
// Skip hidden files and .olcli.json
|
|
615
|
+
if (entry.name.startsWith('.'))
|
|
616
|
+
continue;
|
|
617
|
+
const fullPath = join(currentDir, entry.name);
|
|
618
|
+
const relativePath = relativeBase ? `${relativeBase}/${entry.name}` : entry.name;
|
|
619
|
+
if (entry.isDirectory()) {
|
|
620
|
+
scanDir(fullPath, relativePath);
|
|
621
|
+
}
|
|
622
|
+
else {
|
|
623
|
+
// Check if file is newer than last pull (unless --all)
|
|
624
|
+
if (options.all || !lastPull) {
|
|
625
|
+
filesToUpload.push({ path: fullPath, relativePath });
|
|
626
|
+
}
|
|
627
|
+
else {
|
|
628
|
+
const stats = statSync(fullPath);
|
|
629
|
+
if (stats.mtime > lastPull) {
|
|
630
|
+
filesToUpload.push({ path: fullPath, relativePath });
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
scanDir(targetDir);
|
|
637
|
+
if (filesToUpload.length === 0) {
|
|
638
|
+
spinner.info('No files to upload');
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
if (options.dryRun) {
|
|
642
|
+
spinner.stop();
|
|
643
|
+
console.log(chalk.bold(`Would upload ${filesToUpload.length} file(s) to "${projectName}":`));
|
|
644
|
+
for (const f of filesToUpload) {
|
|
645
|
+
console.log(` ${chalk.cyan(f.relativePath)}`);
|
|
646
|
+
}
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
spinner.text = `Uploading ${filesToUpload.length} file(s)...`;
|
|
650
|
+
let uploaded = 0;
|
|
651
|
+
let failed = 0;
|
|
652
|
+
for (const file of filesToUpload) {
|
|
653
|
+
try {
|
|
654
|
+
const content = readFileSync(file.path);
|
|
655
|
+
await client.uploadFile(projectId, null, file.relativePath, content);
|
|
656
|
+
uploaded++;
|
|
657
|
+
spinner.text = `Uploading... (${uploaded}/${filesToUpload.length})`;
|
|
658
|
+
}
|
|
659
|
+
catch (error) {
|
|
660
|
+
console.error(chalk.yellow(`\n Warning: Failed to upload ${file.relativePath}: ${error.message}`));
|
|
661
|
+
failed++;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
// Update last pull time
|
|
665
|
+
if (existsSync(metaPath)) {
|
|
666
|
+
const meta = JSON.parse(readFileSync(metaPath, 'utf-8'));
|
|
667
|
+
meta.lastPush = new Date().toISOString();
|
|
668
|
+
writeFileSync(metaPath, JSON.stringify(meta, null, 2));
|
|
669
|
+
}
|
|
670
|
+
if (failed > 0) {
|
|
671
|
+
spinner.warn(`Uploaded ${uploaded} files, ${failed} failed`);
|
|
672
|
+
}
|
|
673
|
+
else {
|
|
674
|
+
spinner.succeed(`Uploaded ${uploaded} file(s) to "${projectName}"`);
|
|
675
|
+
}
|
|
676
|
+
setLastProject(projectId);
|
|
677
|
+
}
|
|
678
|
+
catch (error) {
|
|
679
|
+
spinner.fail(`Failed: ${error.message}`);
|
|
680
|
+
process.exit(1);
|
|
681
|
+
}
|
|
682
|
+
});
|
|
683
|
+
program
|
|
684
|
+
.command('sync [dir]')
|
|
685
|
+
.description('Pull then push (bidirectional sync)')
|
|
686
|
+
.option('--project <name>', 'Project name or ID')
|
|
687
|
+
.option('--verbose', 'Show detailed file operations')
|
|
688
|
+
.option('--cookie <session>', 'Session cookie override')
|
|
689
|
+
.action(async (dir, options) => {
|
|
690
|
+
const targetDir = dir || '.';
|
|
691
|
+
// Check if this is an existing project directory
|
|
692
|
+
const metaPath = join(targetDir, '.olcli.json');
|
|
693
|
+
let projectId;
|
|
694
|
+
let projectName;
|
|
695
|
+
if (existsSync(metaPath)) {
|
|
696
|
+
const meta = JSON.parse(readFileSync(metaPath, 'utf-8'));
|
|
697
|
+
projectId = meta.projectId;
|
|
698
|
+
projectName = meta.projectName;
|
|
699
|
+
}
|
|
700
|
+
if (options.project) {
|
|
701
|
+
projectName = options.project;
|
|
702
|
+
projectId = undefined;
|
|
703
|
+
}
|
|
704
|
+
if (!projectId && !projectName) {
|
|
705
|
+
console.error(chalk.red('No project specified.'));
|
|
706
|
+
console.error('Either run from a directory with .olcli.json or use --project');
|
|
707
|
+
process.exit(1);
|
|
708
|
+
}
|
|
709
|
+
const spinner = ora('Connecting...').start();
|
|
710
|
+
try {
|
|
711
|
+
const client = await getClient(options.cookie);
|
|
712
|
+
// Resolve project
|
|
713
|
+
if (!projectId) {
|
|
714
|
+
let proj = await client.getProjectById(projectName);
|
|
715
|
+
if (!proj) {
|
|
716
|
+
proj = await client.getProject(projectName);
|
|
717
|
+
}
|
|
718
|
+
if (!proj) {
|
|
719
|
+
spinner.fail(`Project not found: ${projectName}`);
|
|
720
|
+
process.exit(1);
|
|
721
|
+
}
|
|
722
|
+
projectId = proj.id;
|
|
723
|
+
projectName = proj.name;
|
|
724
|
+
}
|
|
725
|
+
// Step 1: Download current state
|
|
726
|
+
spinner.text = 'Downloading project...';
|
|
727
|
+
const zipBuffer = await client.downloadProject(projectId);
|
|
728
|
+
const AdmZip = (await import('adm-zip')).default;
|
|
729
|
+
const zip = new AdmZip(zipBuffer);
|
|
730
|
+
// Create target directory
|
|
731
|
+
if (!existsSync(targetDir)) {
|
|
732
|
+
mkdirSync(targetDir, { recursive: true });
|
|
733
|
+
}
|
|
734
|
+
// Track local modifications
|
|
735
|
+
const localFiles = new Map();
|
|
736
|
+
const { readdirSync, statSync } = await import('node:fs');
|
|
737
|
+
function scanLocalFiles(currentDir, relativeBase = '') {
|
|
738
|
+
if (!existsSync(currentDir))
|
|
739
|
+
return;
|
|
740
|
+
const entries = readdirSync(currentDir, { withFileTypes: true });
|
|
741
|
+
for (const entry of entries) {
|
|
742
|
+
if (entry.name.startsWith('.'))
|
|
743
|
+
continue;
|
|
744
|
+
const fullPath = join(currentDir, entry.name);
|
|
745
|
+
const relativePath = relativeBase ? `${relativeBase}/${entry.name}` : entry.name;
|
|
746
|
+
if (entry.isDirectory()) {
|
|
747
|
+
scanLocalFiles(fullPath, relativePath);
|
|
748
|
+
}
|
|
749
|
+
else {
|
|
750
|
+
const stats = statSync(fullPath);
|
|
751
|
+
localFiles.set(relativePath, {
|
|
752
|
+
mtime: stats.mtime,
|
|
753
|
+
content: readFileSync(fullPath)
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
// Read local files before overwriting
|
|
759
|
+
if (existsSync(metaPath)) {
|
|
760
|
+
scanLocalFiles(targetDir);
|
|
761
|
+
}
|
|
762
|
+
// Extract remote files
|
|
763
|
+
const remoteFiles = new Map();
|
|
764
|
+
for (const entry of zip.getEntries()) {
|
|
765
|
+
if (!entry.isDirectory) {
|
|
766
|
+
remoteFiles.set(entry.entryName, entry.getData());
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
// Merge: local changes take precedence for files modified after last pull
|
|
770
|
+
let lastPull;
|
|
771
|
+
if (existsSync(metaPath)) {
|
|
772
|
+
const meta = JSON.parse(readFileSync(metaPath, 'utf-8'));
|
|
773
|
+
lastPull = meta.lastPull ? new Date(meta.lastPull) : undefined;
|
|
774
|
+
}
|
|
775
|
+
const filesToUpload = [];
|
|
776
|
+
const filesUpdatedLocally = [];
|
|
777
|
+
const filesKeptLocal = [];
|
|
778
|
+
const filesNewLocal = [];
|
|
779
|
+
spinner.text = 'Comparing files...';
|
|
780
|
+
// Write remote files, but preserve local modifications
|
|
781
|
+
for (const [path, remoteContent] of remoteFiles) {
|
|
782
|
+
const filePath = join(targetDir, path);
|
|
783
|
+
const fileDir = dirname(filePath);
|
|
784
|
+
if (!existsSync(fileDir)) {
|
|
785
|
+
mkdirSync(fileDir, { recursive: true });
|
|
786
|
+
}
|
|
787
|
+
const localFile = localFiles.get(path);
|
|
788
|
+
if (localFile && lastPull && localFile.mtime > lastPull) {
|
|
789
|
+
// Local file was modified after last pull - keep local, queue for upload if different
|
|
790
|
+
if (!localFile.content.equals(remoteContent)) {
|
|
791
|
+
filesToUpload.push({ path, content: localFile.content });
|
|
792
|
+
filesKeptLocal.push(path);
|
|
793
|
+
}
|
|
794
|
+
// Don't overwrite local file
|
|
795
|
+
}
|
|
796
|
+
else {
|
|
797
|
+
// Write remote version
|
|
798
|
+
writeFileSync(filePath, remoteContent);
|
|
799
|
+
filesUpdatedLocally.push(path);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
// Check for new local files (not in remote)
|
|
803
|
+
for (const [path, localFile] of localFiles) {
|
|
804
|
+
if (!remoteFiles.has(path)) {
|
|
805
|
+
filesToUpload.push({ path, content: localFile.content });
|
|
806
|
+
filesNewLocal.push(path);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
// Upload local changes
|
|
810
|
+
if (filesToUpload.length > 0) {
|
|
811
|
+
spinner.text = `Uploading ${filesToUpload.length} local change(s)...`;
|
|
812
|
+
for (const file of filesToUpload) {
|
|
813
|
+
await client.uploadFile(projectId, null, file.path, file.content);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
// Update metadata
|
|
817
|
+
writeFileSync(metaPath, JSON.stringify({
|
|
818
|
+
projectId,
|
|
819
|
+
projectName,
|
|
820
|
+
lastPull: new Date().toISOString(),
|
|
821
|
+
lastSync: new Date().toISOString()
|
|
822
|
+
}, null, 2));
|
|
823
|
+
spinner.succeed(`Synced "${projectName}"`);
|
|
824
|
+
// Summary
|
|
825
|
+
console.log(chalk.dim(` ↓ ${filesUpdatedLocally.length} pulled from remote`));
|
|
826
|
+
console.log(chalk.dim(` ↑ ${filesToUpload.length} pushed to remote`));
|
|
827
|
+
if (options.verbose) {
|
|
828
|
+
if (filesKeptLocal.length > 0) {
|
|
829
|
+
console.log(chalk.yellow('\n Local changes pushed (local was newer):'));
|
|
830
|
+
for (const f of filesKeptLocal) {
|
|
831
|
+
console.log(chalk.dim(` ${f}`));
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
if (filesNewLocal.length > 0) {
|
|
835
|
+
console.log(chalk.green('\n New local files pushed:'));
|
|
836
|
+
for (const f of filesNewLocal) {
|
|
837
|
+
console.log(chalk.dim(` ${f}`));
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
setLastProject(projectId);
|
|
842
|
+
}
|
|
843
|
+
catch (error) {
|
|
844
|
+
spinner.fail(`Failed: ${error.message}`);
|
|
845
|
+
process.exit(1);
|
|
846
|
+
}
|
|
847
|
+
});
|
|
848
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
849
|
+
// HELP
|
|
850
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
851
|
+
program
|
|
852
|
+
.command('check')
|
|
853
|
+
.description('Show credential sources and config path')
|
|
854
|
+
.action(() => {
|
|
855
|
+
console.log(chalk.bold('Configuration:'));
|
|
856
|
+
console.log(` Config file: ${getConfigPath()}`);
|
|
857
|
+
console.log();
|
|
858
|
+
console.log(chalk.bold('Credential sources (in order):'));
|
|
859
|
+
console.log(' 1. OVERLEAF_SESSION environment variable');
|
|
860
|
+
console.log(' 2. .olauth file in current directory');
|
|
861
|
+
console.log(' 3. Global config file');
|
|
862
|
+
console.log();
|
|
863
|
+
const cookie = getSessionCookie();
|
|
864
|
+
if (cookie) {
|
|
865
|
+
console.log(chalk.green('✓ Session cookie found'));
|
|
866
|
+
console.log(chalk.dim(` Value: ${cookie.substring(0, 20)}...`));
|
|
867
|
+
}
|
|
868
|
+
else {
|
|
869
|
+
console.log(chalk.yellow('✗ No session cookie found'));
|
|
870
|
+
}
|
|
871
|
+
});
|
|
872
|
+
program.parse();
|
|
873
|
+
//# sourceMappingURL=cli.js.map
|