@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/cli.js
CHANGED
|
@@ -7,14 +7,15 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { Command } from 'commander';
|
|
9
9
|
import chalk from 'chalk';
|
|
10
|
-
import ora from '
|
|
10
|
+
import { colorDisabled, createSpinner as ora, icon } from './term.js';
|
|
11
|
+
import { formatError } from './errors.js';
|
|
11
12
|
import { writeFileSync, readFileSync, existsSync, mkdirSync, mkdtempSync, rmSync } from 'node:fs';
|
|
12
13
|
import { join, dirname, basename } from 'node:path';
|
|
13
14
|
import { tmpdir } from 'node:os';
|
|
14
15
|
import { fileURLToPath } from 'node:url';
|
|
15
16
|
import { OverleafClient, resolveCommentConcurrency } from './client.js';
|
|
16
17
|
import { formatAuthor } from './tracked-changes.js';
|
|
17
|
-
import { resolveRemotePath, resolveWithin, normalizeRemotePath } from './paths.js';
|
|
18
|
+
import { resolveRemotePath, resolveWithin, normalizeRemotePath, safeLocalName, legacyLocalName } from './paths.js';
|
|
18
19
|
import { planProjectRenames } from './rename-plan.js';
|
|
19
20
|
import { scanLocalFiles } from './scan.js';
|
|
20
21
|
import { compareTrees, DIFF_EXIT_FAILURE, differencesExitCode, filterRemoteTree, renderFileDiff, statusLetter, } from './diff.js';
|
|
@@ -24,17 +25,92 @@ import { loadIgnore } from './ignore.js';
|
|
|
24
25
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
25
26
|
const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf-8'));
|
|
26
27
|
const VERSION = pkg.version;
|
|
27
|
-
import { getSessionCookie, setSessionCookie, setLastProject, getConfigPath, saveOlAuth, clearConfig, getBaseUrl, setBaseUrl, getSessionCookieName, setSessionCookieName, getTimeout, setTimeout, getPasswordCredentials, setPasswordCredentials, clearOlAuth, inspectStoredCredentials } from './config.js';
|
|
28
|
+
import { getSessionCookie, setSessionCookie, setLastProject, getConfigPath, saveOlAuth, clearConfig, getBaseUrl, setBaseUrl, getSessionCookieName, setSessionCookieName, getTimeout, setTimeout, getPasswordCredentials, setPasswordCredentials, clearOlAuth, inspectStoredCredentials, parseCookieInput, resolveSessionCookie, maskSecret } from './config.js';
|
|
28
29
|
import { promptHidden, PromptCancelled, NotATerminal } from './prompt.js';
|
|
30
|
+
import { BrowserSignInAborted, browserSignIn, chromiumLauncher, findBrowser, requireWebSocket } from './browser-auth.js';
|
|
31
|
+
// NO_COLOR (no-color.org) and --no-color. chalk handles --no-color and a
|
|
32
|
+
// non-TTY stdout by itself, but not NO_COLOR; ora and log-symbols share this
|
|
33
|
+
// chalk instance, so one assignment covers them too.
|
|
34
|
+
if (colorDisabled())
|
|
35
|
+
chalk.level = 0;
|
|
29
36
|
const program = new Command();
|
|
30
37
|
program
|
|
31
38
|
.name('olcli')
|
|
32
39
|
.description('Overleaf CLI - interact with Overleaf projects from the command line')
|
|
33
40
|
.version(VERSION)
|
|
34
|
-
.option('--base-url <url>', 'Overleaf instance
|
|
35
|
-
.option('--cookie-name <name>', 'Session cookie name (default: overleaf_session2
|
|
41
|
+
.option('--base-url <url>', 'Overleaf instance URL (overrides OVERLEAF_BASE_URL and config)')
|
|
42
|
+
.option('--cookie-name <name>', 'Session cookie name (default: overleaf_session2; or overleaf.sid)')
|
|
36
43
|
.option('--timeout <ms>', 'HTTP request timeout in milliseconds', parseInt)
|
|
37
|
-
.option('--verbose', 'Print every HTTP request, status
|
|
44
|
+
.option('--verbose', 'Print every HTTP request, status and error body to stderr')
|
|
45
|
+
.option('--no-color', 'Turn off color (same as setting NO_COLOR)')
|
|
46
|
+
.addHelpText('after', `
|
|
47
|
+
Environment:
|
|
48
|
+
OVERLEAF_SESSION session cookie (instead of olcli auth)
|
|
49
|
+
OVERLEAF_EMAIL, OVERLEAF_PASSWORD
|
|
50
|
+
password login when no session cookie works
|
|
51
|
+
OVERLEAF_BASE_URL instance URL, like --base-url
|
|
52
|
+
OVERLEAF_COOKIE_NAME session cookie name, like --cookie-name
|
|
53
|
+
OVERLEAF_TIMEOUT HTTP timeout in milliseconds, like --timeout
|
|
54
|
+
OLCLI_CONCURRENCY documents fetched in parallel by comments list
|
|
55
|
+
HTTPS_PROXY proxy for https:// instances (CONNECT tunnel)
|
|
56
|
+
HTTP_PROXY proxy for http:// instances
|
|
57
|
+
NO_PROXY comma-separated hosts that bypass the proxy
|
|
58
|
+
(localhost and 127.0.0.1 are always direct)
|
|
59
|
+
NO_COLOR turn off color`);
|
|
60
|
+
/**
|
|
61
|
+
* The one way a command reports a failure: the cause (and the command that
|
|
62
|
+
* fixes it) on the spinner line, details such as the HTTP status on stderr
|
|
63
|
+
* after it. Exits with `code`, 1 unless the command reserves 1 for something
|
|
64
|
+
* else (`diff --exit-code`).
|
|
65
|
+
*/
|
|
66
|
+
function failWith(spinner, error, code = 1) {
|
|
67
|
+
const verbose = Boolean(program.opts().verbose);
|
|
68
|
+
const [first, ...rest] = formatError(error, { verbose });
|
|
69
|
+
spinner.fail(first);
|
|
70
|
+
for (const line of rest)
|
|
71
|
+
console.error(chalk.dim(` ${line}`));
|
|
72
|
+
process.exit(code);
|
|
73
|
+
}
|
|
74
|
+
/** Everything on stdin, as text. Used by `auth --cookie -`. */
|
|
75
|
+
async function readStdin() {
|
|
76
|
+
const chunks = [];
|
|
77
|
+
for await (const chunk of process.stdin)
|
|
78
|
+
chunks.push(Buffer.from(chunk));
|
|
79
|
+
return Buffer.concat(chunks).toString('utf-8');
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* What to run after a successful `auth`. Names a real project when there is
|
|
83
|
+
* one whose name can be quoted safely, the placeholder otherwise.
|
|
84
|
+
*/
|
|
85
|
+
function nextStepCommands(projects) {
|
|
86
|
+
const first = projects.find((p) => p.name && !/["$`\\]/.test(p.name));
|
|
87
|
+
return ['olcli list', `olcli pull "${first ? first.name : '<name>'}"`];
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Where `pull <project>` puts a project when no directory is given.
|
|
91
|
+
*
|
|
92
|
+
* Normally `safeLocalName(name)`. Up to 0.15 every non-ASCII letter became
|
|
93
|
+
* "_", so a directory pulled back then has a different name; when that one
|
|
94
|
+
* exists and its .olcli.json points at the same project, and the new name is
|
|
95
|
+
* not taken yet, it is reused rather than downloading a second copy.
|
|
96
|
+
*/
|
|
97
|
+
function defaultProjectDir(projectId, name, cwd = '.') {
|
|
98
|
+
const current = safeLocalName(name);
|
|
99
|
+
const legacy = legacyLocalName(name);
|
|
100
|
+
if (legacy === current || existsSync(join(cwd, current)))
|
|
101
|
+
return current;
|
|
102
|
+
const legacyMeta = join(cwd, legacy, '.olcli.json');
|
|
103
|
+
if (existsSync(legacyMeta)) {
|
|
104
|
+
try {
|
|
105
|
+
if (JSON.parse(readFileSync(legacyMeta, 'utf-8')).projectId === projectId)
|
|
106
|
+
return legacy;
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
// Unreadable metadata: not provably ours, use the new name.
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return current;
|
|
113
|
+
}
|
|
38
114
|
/**
|
|
39
115
|
* Helper to get authenticated client
|
|
40
116
|
*
|
|
@@ -65,10 +141,8 @@ async function getClient(cookieOpt, baseUrlOpt, failureCode = 1) {
|
|
|
65
141
|
return loginWithSavedPassword(passwordCredentials, baseUrl, cookieName);
|
|
66
142
|
}
|
|
67
143
|
console.error(chalk.red('No session cookie or password credentials found.'));
|
|
68
|
-
console.error('
|
|
69
|
-
console.error('Or
|
|
70
|
-
console.error('Or set OVERLEAF_SESSION environment variable');
|
|
71
|
-
console.error('Or create .olauth file in current directory');
|
|
144
|
+
console.error('Sign in with: olcli auth --cookie (prompts for the cookie; see olcli auth --help)');
|
|
145
|
+
console.error('Or set the OVERLEAF_SESSION environment variable.');
|
|
72
146
|
process.exit(failureCode);
|
|
73
147
|
}
|
|
74
148
|
async function loginWithSavedPassword(credentials, baseUrl, cookieName) {
|
|
@@ -97,10 +171,7 @@ async function resolveProject(client, projectArg, dir = '.') {
|
|
|
97
171
|
return { id: projectArg, name: projectArg };
|
|
98
172
|
}
|
|
99
173
|
// Otherwise, look up by name
|
|
100
|
-
const proj = await client.
|
|
101
|
-
if (!proj) {
|
|
102
|
-
throw new Error(`Project not found: ${projectArg}`);
|
|
103
|
-
}
|
|
174
|
+
const proj = await client.findProject(projectArg);
|
|
104
175
|
return { id: proj.id, name: proj.name };
|
|
105
176
|
}
|
|
106
177
|
// Otherwise, check for .olcli.json
|
|
@@ -117,16 +188,33 @@ async function resolveProject(client, projectArg, dir = '.') {
|
|
|
117
188
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
118
189
|
// AUTH COMMANDS
|
|
119
190
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
120
|
-
program
|
|
191
|
+
const authCmd = program
|
|
121
192
|
.command('auth')
|
|
122
193
|
.description('Authenticate with Overleaf using a session cookie or email/password')
|
|
123
|
-
.option('--cookie
|
|
194
|
+
.option('--cookie [session]', 'Session cookie; omit the value to be prompted, "-" reads stdin')
|
|
124
195
|
.option('--email <email>', 'Account email for password login')
|
|
125
196
|
.option('--password <password>', 'Account password (omit to be prompted; see warning below)')
|
|
126
197
|
.option('--save-password', 'Persist the password in the config file, in plaintext')
|
|
127
198
|
.option('--no-save-password', 'Do not persist the password (the default; kept for existing scripts)')
|
|
128
199
|
.option('--save-local', 'Save to .olauth in current directory')
|
|
200
|
+
.option('--browser', 'Sign in in a browser window; only the session cookie is kept')
|
|
129
201
|
.addHelpText('after', `
|
|
202
|
+
Easiest: olcli auth --browser opens the login page of the instance in Chrome,
|
|
203
|
+
Edge, Chromium or Brave, with a new temporary profile (not your own), and
|
|
204
|
+
waits up to 5 minutes until you have signed in. Works with SSO, Google, ORCID
|
|
205
|
+
and two-factor logins. olcli keeps only the session cookie; it never sees your
|
|
206
|
+
password. The browser and the profile are removed afterwards. Set
|
|
207
|
+
OLCLI_BROWSER to the browser (an .app bundle or the executable) when it is
|
|
208
|
+
not found. Needs Node 22 or newer.
|
|
209
|
+
|
|
210
|
+
Get your session cookie: sign in to Overleaf in a browser, open the developer
|
|
211
|
+
tools (F12), then Application (Chrome, Edge) or Storage (Firefox), Cookies,
|
|
212
|
+
and copy the value of overleaf_session2 (overleaf.sid on older self-hosted
|
|
213
|
+
instances). Then either:
|
|
214
|
+
olcli auth --cookie prompts, input stays hidden
|
|
215
|
+
pbpaste | olcli auth --cookie - reads the cookie from stdin
|
|
216
|
+
A value given on the command line works too, but lands in your shell history.
|
|
217
|
+
|
|
130
218
|
The password is not stored unless you ask for it with --save-password. A
|
|
131
219
|
session cookie is stored either way, and that is what later commands use; the
|
|
132
220
|
password only buys an automatic re-login once the cookie expires.
|
|
@@ -135,15 +223,28 @@ Passing --password puts the password in your shell history. Omit it to be
|
|
|
135
223
|
prompted instead, or set OVERLEAF_EMAIL and OVERLEAF_PASSWORD, which every
|
|
136
224
|
command reads without needing 'auth' at all.`)
|
|
137
225
|
.action(async (options) => {
|
|
226
|
+
if (options.browser) {
|
|
227
|
+
if (options.cookie || options.email || options.password) {
|
|
228
|
+
console.error(chalk.red('Use either --browser or --cookie or --email/--password, not several.'));
|
|
229
|
+
process.exit(1);
|
|
230
|
+
}
|
|
231
|
+
await authWithBrowser(options);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
138
234
|
if (!options.cookie && !options.email && !options.password) {
|
|
139
|
-
console.log(chalk.yellow('To authenticate,
|
|
235
|
+
console.log(chalk.yellow('To authenticate, sign in in a browser window:'));
|
|
140
236
|
console.log();
|
|
141
|
-
console.log('
|
|
142
|
-
console.log('2. Open Developer Tools (F12) → Application → Cookies');
|
|
143
|
-
console.log('3. Find the cookie named "overleaf_session2"');
|
|
144
|
-
console.log('4. Copy its value and run:');
|
|
237
|
+
console.log(chalk.cyan(' olcli auth --browser'));
|
|
145
238
|
console.log();
|
|
146
|
-
console.log(chalk.
|
|
239
|
+
console.log(chalk.yellow('Or provide a session cookie:'));
|
|
240
|
+
console.log();
|
|
241
|
+
console.log('1. Sign in to Overleaf in your browser');
|
|
242
|
+
console.log('2. Open the developer tools (F12), then Application (Chrome, Edge) or Storage (Firefox), then Cookies');
|
|
243
|
+
console.log('3. Copy the value of the cookie named "overleaf_session2"');
|
|
244
|
+
console.log('4. Run this and paste it when asked (input stays hidden):');
|
|
245
|
+
console.log();
|
|
246
|
+
console.log(chalk.cyan(' olcli auth --cookie'));
|
|
247
|
+
console.log(chalk.dim(' (or pipe it: pbpaste | olcli auth --cookie -)'));
|
|
147
248
|
console.log();
|
|
148
249
|
console.log('Or log in with email/password:');
|
|
149
250
|
console.log(chalk.cyan(' olcli auth --email "you@example.com"'));
|
|
@@ -160,12 +261,50 @@ command reads without needing 'auth' at all.`)
|
|
|
160
261
|
console.error(chalk.red('--email is required for password login.'));
|
|
161
262
|
process.exit(1);
|
|
162
263
|
}
|
|
264
|
+
// The cookie is a secret, so it does not have to travel through argv:
|
|
265
|
+
// no value prompts with hidden input, "-" reads stdin. Resolved before
|
|
266
|
+
// the spinner for the same reason as the password below.
|
|
267
|
+
const promptCookieName = program.opts().cookieName || getSessionCookieName();
|
|
268
|
+
let cookie;
|
|
269
|
+
if (options.cookie === true) {
|
|
270
|
+
try {
|
|
271
|
+
cookie = parseCookieInput(await promptHidden(`Paste the ${promptCookieName} cookie (input hidden): `), promptCookieName);
|
|
272
|
+
}
|
|
273
|
+
catch (error) {
|
|
274
|
+
if (error instanceof NotATerminal) {
|
|
275
|
+
console.error(chalk.red('No terminal available to prompt for the cookie.'));
|
|
276
|
+
console.error('Pipe it instead: olcli auth --cookie - < cookie.txt');
|
|
277
|
+
console.error('Or set OVERLEAF_SESSION, which every command reads.');
|
|
278
|
+
process.exit(1);
|
|
279
|
+
}
|
|
280
|
+
if (error instanceof PromptCancelled) {
|
|
281
|
+
console.error(chalk.red('Cancelled.'));
|
|
282
|
+
process.exit(1);
|
|
283
|
+
}
|
|
284
|
+
throw error;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
else if (options.cookie === '-') {
|
|
288
|
+
if (process.stdin.isTTY) {
|
|
289
|
+
console.error(chalk.dim('Reading the cookie from stdin; paste it, then press Ctrl+D.'));
|
|
290
|
+
}
|
|
291
|
+
cookie = parseCookieInput(await readStdin(), promptCookieName);
|
|
292
|
+
}
|
|
293
|
+
else if (typeof options.cookie === 'string') {
|
|
294
|
+
cookie = parseCookieInput(options.cookie, promptCookieName);
|
|
295
|
+
console.error(chalk.yellow('Warning: the cookie is now in your shell history.'));
|
|
296
|
+
console.error(chalk.dim(' Next time run olcli auth --cookie to be prompted, or pipe it to olcli auth --cookie -'));
|
|
297
|
+
}
|
|
298
|
+
if (options.cookie && !cookie) {
|
|
299
|
+
console.error(chalk.red('The session cookie must not be empty.'));
|
|
300
|
+
process.exit(1);
|
|
301
|
+
}
|
|
163
302
|
// Resolve the password before the spinner starts: a prompt and a spinner
|
|
164
303
|
// both own the terminal, and ora would redraw over the prompt line.
|
|
165
304
|
let password = options.password;
|
|
166
305
|
if (!options.cookie) {
|
|
167
306
|
if (password) {
|
|
168
|
-
console.log(chalk.yellow('⚠ --password is now in your shell history.'));
|
|
307
|
+
console.log(chalk.yellow(icon('⚠ ', '') + '--password is now in your shell history.'));
|
|
169
308
|
console.log(chalk.dim(' Omit it to be prompted, or set OVERLEAF_EMAIL/OVERLEAF_PASSWORD.'));
|
|
170
309
|
}
|
|
171
310
|
else {
|
|
@@ -195,12 +334,13 @@ command reads without needing 'auth' at all.`)
|
|
|
195
334
|
try {
|
|
196
335
|
const baseUrl = program.opts().baseUrl || getBaseUrl();
|
|
197
336
|
const cookieName = program.opts().cookieName || getSessionCookieName();
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
const
|
|
201
|
-
|
|
337
|
+
let projects;
|
|
338
|
+
if (cookie) {
|
|
339
|
+
const client = await OverleafClient.fromSessionCookie(cookie, baseUrl, cookieName);
|
|
340
|
+
projects = await client.listProjects();
|
|
341
|
+
setSessionCookie(cookie);
|
|
202
342
|
if (options.saveLocal) {
|
|
203
|
-
saveOlAuth(
|
|
343
|
+
saveOlAuth(cookie);
|
|
204
344
|
spinner.succeed(`Authenticated! Found ${projects.length} projects. Saved to .olauth`);
|
|
205
345
|
}
|
|
206
346
|
else {
|
|
@@ -210,7 +350,7 @@ command reads without needing 'auth' at all.`)
|
|
|
210
350
|
else {
|
|
211
351
|
spinner.text = 'Logging in with email/password...';
|
|
212
352
|
const client = await OverleafClient.fromPasswordLogin(options.email, password, baseUrl);
|
|
213
|
-
|
|
353
|
+
projects = await client.listProjects();
|
|
214
354
|
persistClientSession(client, cookieName);
|
|
215
355
|
setBaseUrl(baseUrl);
|
|
216
356
|
// Opt-in, not opt-out. The session cookie persisted just above is what
|
|
@@ -233,38 +373,201 @@ command reads without needing 'auth' at all.`)
|
|
|
233
373
|
console.log(chalk.dim('olcli auth when the session expires, or use --save-password.'));
|
|
234
374
|
}
|
|
235
375
|
}
|
|
236
|
-
|
|
376
|
+
printNextSteps(projects);
|
|
237
377
|
}
|
|
238
378
|
catch (error) {
|
|
239
|
-
spinner
|
|
240
|
-
process.exit(1);
|
|
379
|
+
failWith(spinner, error);
|
|
241
380
|
}
|
|
242
381
|
});
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
.
|
|
246
|
-
.
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
382
|
+
/** Print the same lines as every successful `auth`. */
|
|
383
|
+
function printNextSteps(projects) {
|
|
384
|
+
console.log(chalk.dim(`Config saved to: ${getConfigPath()}`));
|
|
385
|
+
console.log();
|
|
386
|
+
console.log('Next steps:');
|
|
387
|
+
for (const line of nextStepCommands(projects))
|
|
388
|
+
console.log(` ${chalk.cyan(line)}`);
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* `auth --browser`. The cookie found in the browser is validated with the
|
|
392
|
+
* same client call as `auth --cookie` (the project list) before it is
|
|
393
|
+
* stored, and only its name and value are stored. Ctrl-C and SIGTERM abort
|
|
394
|
+
* the wait; browserSignIn closes the browser and deletes the profile.
|
|
395
|
+
*/
|
|
396
|
+
async function authWithBrowser(options) {
|
|
397
|
+
const baseUrl = program.opts().baseUrl || getBaseUrl();
|
|
398
|
+
const cookieName = program.opts().cookieName || getSessionCookieName();
|
|
399
|
+
const timeout = program.opts().timeout || getTimeout();
|
|
400
|
+
let browser;
|
|
401
|
+
try {
|
|
402
|
+
requireWebSocket();
|
|
403
|
+
browser = findBrowser();
|
|
404
|
+
}
|
|
405
|
+
catch (error) {
|
|
406
|
+
console.error(chalk.red(error.message));
|
|
407
|
+
process.exit(1);
|
|
254
408
|
}
|
|
255
|
-
const
|
|
409
|
+
const abort = new AbortController();
|
|
410
|
+
const onSignal = () => abort.abort();
|
|
411
|
+
process.on('SIGINT', onSignal);
|
|
412
|
+
process.on('SIGTERM', onSignal);
|
|
413
|
+
console.error(`Opening ${browser.name} at ${baseUrl}/login with a temporary profile.`);
|
|
414
|
+
console.error(chalk.dim('Sign in there. olcli waits up to 5 minutes; Ctrl-C cancels.'));
|
|
415
|
+
const spinner = ora('Waiting for the sign-in...').start();
|
|
416
|
+
let projects = [];
|
|
256
417
|
try {
|
|
257
|
-
const
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
418
|
+
const session = await browserSignIn({
|
|
419
|
+
baseUrl,
|
|
420
|
+
cookieName,
|
|
421
|
+
startBrowser: chromiumLauncher(browser.path),
|
|
422
|
+
signal: abort.signal,
|
|
423
|
+
validate: async (candidate) => {
|
|
424
|
+
const client = await OverleafClient.fromSessionCookie(candidate.value, baseUrl, candidate.name);
|
|
425
|
+
client.setGlobalTimeout(timeout);
|
|
426
|
+
projects = await client.listProjects();
|
|
427
|
+
return true;
|
|
428
|
+
},
|
|
429
|
+
});
|
|
430
|
+
if (session.name !== cookieName)
|
|
431
|
+
setSessionCookieName(session.name);
|
|
432
|
+
setSessionCookie(session.value);
|
|
433
|
+
if (options.saveLocal) {
|
|
434
|
+
saveOlAuth(session.value);
|
|
435
|
+
spinner.succeed(`Authenticated! Found ${projects.length} projects. Saved to .olauth`);
|
|
436
|
+
}
|
|
437
|
+
else {
|
|
438
|
+
spinner.succeed(`Authenticated! Found ${projects.length} projects.`);
|
|
439
|
+
}
|
|
440
|
+
printNextSteps(projects);
|
|
262
441
|
}
|
|
263
442
|
catch (error) {
|
|
264
|
-
|
|
443
|
+
if (error instanceof BrowserSignInAborted) {
|
|
444
|
+
spinner.fail('Cancelled. The browser was closed and its temporary profile deleted.');
|
|
445
|
+
process.exit(130);
|
|
446
|
+
}
|
|
447
|
+
failWith(spinner, error);
|
|
448
|
+
}
|
|
449
|
+
finally {
|
|
450
|
+
process.off('SIGINT', onSignal);
|
|
451
|
+
process.off('SIGTERM', onSignal);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* `auth status`: one answer to "am I set up". Replaces `whoami` (is the
|
|
456
|
+
* session valid) and `check` (where do credentials come from), which both
|
|
457
|
+
* stay as hidden aliases.
|
|
458
|
+
*
|
|
459
|
+
* Exit status 1 when no credential is found or the session is rejected, so
|
|
460
|
+
* `olcli auth status || olcli auth` works. exitCode rather than exit(): the
|
|
461
|
+
* report still has to reach a pipe.
|
|
462
|
+
*/
|
|
463
|
+
async function authStatus(options) {
|
|
464
|
+
const baseUrl = program.opts().baseUrl || getBaseUrl();
|
|
465
|
+
const cookieName = program.opts().cookieName || getSessionCookieName();
|
|
466
|
+
const resolved = resolveSessionCookie();
|
|
467
|
+
const stored = inspectStoredCredentials();
|
|
468
|
+
const password = resolved ? undefined : getPasswordCredentials();
|
|
469
|
+
const sourceLabel = !resolved
|
|
470
|
+
? (password ? (stored.envPassword ? 'OVERLEAF_EMAIL/OVERLEAF_PASSWORD' : 'saved password (config file)') : 'none')
|
|
471
|
+
: resolved.source === 'env' ? 'OVERLEAF_SESSION environment variable'
|
|
472
|
+
: resolved.source === 'olauth' ? `.olauth (${resolved.path})`
|
|
473
|
+
: `config file (${resolved.path})`;
|
|
474
|
+
const report = {
|
|
475
|
+
instance: baseUrl,
|
|
476
|
+
account: null,
|
|
477
|
+
credentialSource: sourceLabel,
|
|
478
|
+
cookieName,
|
|
479
|
+
cookie: resolved ? maskSecret(resolved.value) : null,
|
|
480
|
+
configFile: getConfigPath(),
|
|
481
|
+
passwordStored: stored.password,
|
|
482
|
+
signedIn: false,
|
|
483
|
+
};
|
|
484
|
+
const print = () => {
|
|
485
|
+
if (options.json) {
|
|
486
|
+
console.log(JSON.stringify(report, null, 2));
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
const row = (label, value) => console.log(` ${label.padEnd(12)} ${value}`);
|
|
490
|
+
row('Instance', report.instance);
|
|
491
|
+
row('Account', report.account ?? chalk.dim(report.signedIn ? 'unknown' : '-'));
|
|
492
|
+
row('Credential', report.credentialSource);
|
|
493
|
+
row('Cookie', report.cookie ? `${report.cookieName} = ${report.cookie}` : chalk.dim('-'));
|
|
494
|
+
row('Config file', report.configFile);
|
|
495
|
+
if (report.passwordStored) {
|
|
496
|
+
row('Password', chalk.yellow('stored in plaintext in the config file (olcli logout removes it)'));
|
|
497
|
+
}
|
|
498
|
+
if (report.signedIn) {
|
|
499
|
+
row('Session', chalk.green(`valid, ${report.projects} project(s)`));
|
|
500
|
+
}
|
|
501
|
+
};
|
|
502
|
+
let failure;
|
|
503
|
+
if (!resolved && !password) {
|
|
504
|
+
report.error = 'Not signed in';
|
|
505
|
+
}
|
|
506
|
+
else {
|
|
507
|
+
const spinner = options.json ? null : ora('Checking session...').start();
|
|
508
|
+
try {
|
|
509
|
+
const client = resolved
|
|
510
|
+
? await OverleafClient.fromSessionCookie(resolved.value, baseUrl, cookieName)
|
|
511
|
+
: await OverleafClient.fromPasswordLogin(password.email, password.password, baseUrl);
|
|
512
|
+
const timeout = program.opts().timeout || getTimeout();
|
|
513
|
+
client.setGlobalTimeout(timeout);
|
|
514
|
+
const projects = await client.listProjects();
|
|
515
|
+
report.signedIn = true;
|
|
516
|
+
report.projects = projects.length;
|
|
517
|
+
report.account = client.accountEmail ?? (password ? password.email : null);
|
|
518
|
+
spinner?.stop();
|
|
519
|
+
}
|
|
520
|
+
catch (error) {
|
|
521
|
+
spinner?.stop();
|
|
522
|
+
failure = error;
|
|
523
|
+
report.error = formatError(error, { verbose: true })[0];
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
print();
|
|
527
|
+
if (!report.signedIn) {
|
|
265
528
|
process.exitCode = 1;
|
|
529
|
+
if (options.json)
|
|
530
|
+
return;
|
|
531
|
+
console.log();
|
|
532
|
+
if (!resolved && !password) {
|
|
533
|
+
console.log(chalk.yellow('Not signed in.'));
|
|
534
|
+
console.log(`Run: ${chalk.cyan('olcli auth')}`);
|
|
535
|
+
}
|
|
536
|
+
else {
|
|
537
|
+
const [first, ...rest] = formatError(failure, { verbose: Boolean(program.opts().verbose) });
|
|
538
|
+
console.error(chalk.red(first));
|
|
539
|
+
for (const line of rest)
|
|
540
|
+
console.error(chalk.dim(` ${line}`));
|
|
541
|
+
}
|
|
266
542
|
}
|
|
267
|
-
}
|
|
543
|
+
}
|
|
544
|
+
/** Hidden alias for a command folded into `auth status`. */
|
|
545
|
+
function deprecatedAlias(name, description) {
|
|
546
|
+
program
|
|
547
|
+
.command(name, { hidden: true })
|
|
548
|
+
.description(description)
|
|
549
|
+
.option('--json', 'Output as JSON')
|
|
550
|
+
.action(async (options) => {
|
|
551
|
+
console.error(chalk.dim(`'olcli ${name}' is now 'olcli auth status'.`));
|
|
552
|
+
await authStatus(options);
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
authCmd
|
|
556
|
+
.command('status')
|
|
557
|
+
.description('Show instance, account, credential source and a masked cookie')
|
|
558
|
+
.option('--json', 'Output as JSON')
|
|
559
|
+
.addHelpText('after', `
|
|
560
|
+
Exits 1 when no credential is found or Overleaf rejects the session, so
|
|
561
|
+
'olcli auth status || olcli auth' signs in only when needed. The cookie is
|
|
562
|
+
shown masked: its last four characters and its length.
|
|
563
|
+
|
|
564
|
+
Credential sources, first match wins:
|
|
565
|
+
1. OVERLEAF_SESSION environment variable
|
|
566
|
+
2. .olauth file in the current directory
|
|
567
|
+
3. Global config file
|
|
568
|
+
4. OVERLEAF_EMAIL/OVERLEAF_PASSWORD, or a password saved with --save-password`)
|
|
569
|
+
.action(authStatus);
|
|
570
|
+
deprecatedAlias('whoami', 'Alias of: auth status');
|
|
268
571
|
program
|
|
269
572
|
.command('logout')
|
|
270
573
|
.description('Clear stored credentials')
|
|
@@ -342,8 +645,7 @@ program
|
|
|
342
645
|
}
|
|
343
646
|
}
|
|
344
647
|
catch (error) {
|
|
345
|
-
spinner
|
|
346
|
-
process.exit(1);
|
|
648
|
+
failWith(spinner, error);
|
|
347
649
|
}
|
|
348
650
|
});
|
|
349
651
|
program
|
|
@@ -371,14 +673,13 @@ program
|
|
|
371
673
|
// Sort entities by path for nice display
|
|
372
674
|
const sorted = entities.sort((a, b) => a.path.localeCompare(b.path));
|
|
373
675
|
for (const entity of sorted) {
|
|
374
|
-
const
|
|
375
|
-
console.log(` ${
|
|
676
|
+
const marker = entity.type === 'doc' ? icon('📄', 'doc ') : icon('📎', 'file');
|
|
677
|
+
console.log(` ${marker} ${entity.path}`);
|
|
376
678
|
}
|
|
377
679
|
setLastProject(proj.id);
|
|
378
680
|
}
|
|
379
681
|
catch (error) {
|
|
380
|
-
spinner
|
|
381
|
-
process.exit(1);
|
|
682
|
+
failWith(spinner, error);
|
|
382
683
|
}
|
|
383
684
|
});
|
|
384
685
|
const commentsCmd = program
|
|
@@ -478,8 +779,7 @@ commentsCmd
|
|
|
478
779
|
setLastProject(proj.id);
|
|
479
780
|
}
|
|
480
781
|
catch (error) {
|
|
481
|
-
spinner
|
|
482
|
-
process.exit(1);
|
|
782
|
+
failWith(spinner, error);
|
|
483
783
|
}
|
|
484
784
|
});
|
|
485
785
|
commentsCmd
|
|
@@ -502,8 +802,7 @@ commentsCmd
|
|
|
502
802
|
setLastProject(proj.id);
|
|
503
803
|
}
|
|
504
804
|
catch (error) {
|
|
505
|
-
spinner
|
|
506
|
-
process.exit(1);
|
|
805
|
+
failWith(spinner, error);
|
|
507
806
|
}
|
|
508
807
|
});
|
|
509
808
|
commentsCmd
|
|
@@ -526,8 +825,7 @@ commentsCmd
|
|
|
526
825
|
setLastProject(proj.id);
|
|
527
826
|
}
|
|
528
827
|
catch (error) {
|
|
529
|
-
spinner
|
|
530
|
-
process.exit(1);
|
|
828
|
+
failWith(spinner, error);
|
|
531
829
|
}
|
|
532
830
|
});
|
|
533
831
|
commentsCmd
|
|
@@ -550,8 +848,7 @@ commentsCmd
|
|
|
550
848
|
setLastProject(proj.id);
|
|
551
849
|
}
|
|
552
850
|
catch (error) {
|
|
553
|
-
spinner
|
|
554
|
-
process.exit(1);
|
|
851
|
+
failWith(spinner, error);
|
|
555
852
|
}
|
|
556
853
|
});
|
|
557
854
|
commentsCmd
|
|
@@ -574,8 +871,7 @@ commentsCmd
|
|
|
574
871
|
setLastProject(proj.id);
|
|
575
872
|
}
|
|
576
873
|
catch (error) {
|
|
577
|
-
spinner
|
|
578
|
-
process.exit(1);
|
|
874
|
+
failWith(spinner, error);
|
|
579
875
|
}
|
|
580
876
|
});
|
|
581
877
|
commentsCmd
|
|
@@ -613,8 +909,7 @@ commentsCmd
|
|
|
613
909
|
setLastProject(proj.id);
|
|
614
910
|
}
|
|
615
911
|
catch (error) {
|
|
616
|
-
spinner
|
|
617
|
-
process.exit(1);
|
|
912
|
+
failWith(spinner, error);
|
|
618
913
|
}
|
|
619
914
|
});
|
|
620
915
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -622,7 +917,10 @@ commentsCmd
|
|
|
622
917
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
623
918
|
const changesCmd = program
|
|
624
919
|
.command('changes')
|
|
625
|
-
.description('
|
|
920
|
+
.description('Accept or reject tracked changes (needs a premium project owner)')
|
|
921
|
+
.addHelpText('after', `
|
|
922
|
+
Tracked changes need a project whose owner has an Overleaf premium plan;
|
|
923
|
+
collaborators on any plan can then accept and reject them.`);
|
|
626
924
|
changesCmd
|
|
627
925
|
.command('accept <ids...>')
|
|
628
926
|
.description('Accept tracked changes by id (ids from: olcli comments list --changes)')
|
|
@@ -648,8 +946,7 @@ changesCmd
|
|
|
648
946
|
setLastProject(proj.id);
|
|
649
947
|
}
|
|
650
948
|
catch (error) {
|
|
651
|
-
spinner
|
|
652
|
-
process.exit(1);
|
|
949
|
+
failWith(spinner, error);
|
|
653
950
|
}
|
|
654
951
|
});
|
|
655
952
|
changesCmd
|
|
@@ -688,8 +985,7 @@ changesCmd
|
|
|
688
985
|
setLastProject(proj.id);
|
|
689
986
|
}
|
|
690
987
|
catch (error) {
|
|
691
|
-
spinner
|
|
692
|
-
process.exit(1);
|
|
988
|
+
failWith(spinner, error);
|
|
693
989
|
}
|
|
694
990
|
});
|
|
695
991
|
program
|
|
@@ -709,8 +1005,7 @@ program
|
|
|
709
1005
|
setLastProject(proj.id);
|
|
710
1006
|
}
|
|
711
1007
|
catch (error) {
|
|
712
|
-
spinner
|
|
713
|
-
process.exit(1);
|
|
1008
|
+
failWith(spinner, error);
|
|
714
1009
|
}
|
|
715
1010
|
});
|
|
716
1011
|
program
|
|
@@ -724,14 +1019,13 @@ program
|
|
|
724
1019
|
const client = await getClient(options.cookie);
|
|
725
1020
|
const proj = await resolveProject(client, project);
|
|
726
1021
|
const zip = await client.downloadProject(proj.id);
|
|
727
|
-
const outputPath = options.output || `${proj.name
|
|
1022
|
+
const outputPath = options.output || `${safeLocalName(proj.name)}.zip`;
|
|
728
1023
|
writeFileSync(outputPath, zip);
|
|
729
1024
|
spinner.succeed(`Downloaded: ${outputPath} (${(zip.length / 1024).toFixed(1)} KB)`);
|
|
730
1025
|
setLastProject(proj.id);
|
|
731
1026
|
}
|
|
732
1027
|
catch (error) {
|
|
733
|
-
spinner
|
|
734
|
-
process.exit(1);
|
|
1028
|
+
failWith(spinner, error);
|
|
735
1029
|
}
|
|
736
1030
|
});
|
|
737
1031
|
program
|
|
@@ -747,14 +1041,13 @@ program
|
|
|
747
1041
|
const proj = await resolveProject(client, project);
|
|
748
1042
|
spinner.text = 'Compiling...';
|
|
749
1043
|
const pdf = await client.downloadPdf(proj.id, undefined, options.resource);
|
|
750
|
-
const outputPath = options.output || `${proj.name
|
|
1044
|
+
const outputPath = options.output || `${safeLocalName(proj.name)}.pdf`;
|
|
751
1045
|
writeFileSync(outputPath, pdf);
|
|
752
1046
|
spinner.succeed(`Downloaded PDF: ${outputPath} (${(pdf.length / 1024).toFixed(1)} KB)`);
|
|
753
1047
|
setLastProject(proj.id);
|
|
754
1048
|
}
|
|
755
1049
|
catch (error) {
|
|
756
|
-
spinner
|
|
757
|
-
process.exit(1);
|
|
1050
|
+
failWith(spinner, error);
|
|
758
1051
|
}
|
|
759
1052
|
});
|
|
760
1053
|
program
|
|
@@ -812,8 +1105,7 @@ program
|
|
|
812
1105
|
setLastProject(proj.id);
|
|
813
1106
|
}
|
|
814
1107
|
catch (error) {
|
|
815
|
-
spinner
|
|
816
|
-
process.exit(1);
|
|
1108
|
+
failWith(spinner, error);
|
|
817
1109
|
}
|
|
818
1110
|
});
|
|
819
1111
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -853,8 +1145,7 @@ program
|
|
|
853
1145
|
setLastProject(proj.id);
|
|
854
1146
|
}
|
|
855
1147
|
catch (error) {
|
|
856
|
-
spinner
|
|
857
|
-
process.exit(1);
|
|
1148
|
+
failWith(spinner, error);
|
|
858
1149
|
}
|
|
859
1150
|
});
|
|
860
1151
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -877,27 +1168,48 @@ program
|
|
|
877
1168
|
setLastProject(proj.id);
|
|
878
1169
|
}
|
|
879
1170
|
catch (error) {
|
|
880
|
-
spinner
|
|
881
|
-
process.exit(1);
|
|
1171
|
+
failWith(spinner, error);
|
|
882
1172
|
}
|
|
883
1173
|
});
|
|
884
1174
|
program
|
|
885
1175
|
.command('rename <oldname> <newname> [project]')
|
|
886
1176
|
.alias('mv')
|
|
887
|
-
.description('Rename a file or folder in a project')
|
|
1177
|
+
.description('Rename or move a file or folder in a project')
|
|
888
1178
|
.option('--cookie <session>', 'Session cookie override')
|
|
1179
|
+
.addHelpText('after', `
|
|
1180
|
+
The destination decides what happens:
|
|
1181
|
+
intro.tex rename in place, the folder stays the same
|
|
1182
|
+
chapters/intro.tex move into chapters/ and rename; missing folders are created
|
|
1183
|
+
chapters/ move into chapters/ and keep the name
|
|
1184
|
+
/ or /intro.tex move to the project root
|
|
1185
|
+
|
|
1186
|
+
Nothing is written when the destination already exists, the source is not
|
|
1187
|
+
found, or a folder would be moved into itself or one of its subfolders.
|
|
1188
|
+
|
|
1189
|
+
Examples:
|
|
1190
|
+
olcli mv draft.tex intro.tex
|
|
1191
|
+
olcli mv chapters/draft.tex chapters/intro/intro.tex
|
|
1192
|
+
olcli mv figures/old.png archive/
|
|
1193
|
+
olcli mv sub/notes.txt /`)
|
|
889
1194
|
.action(async (oldname, newname, project, options) => {
|
|
890
1195
|
const spinner = ora('Renaming file...').start();
|
|
891
1196
|
try {
|
|
892
1197
|
const client = await getClient(options.cookie);
|
|
893
1198
|
const proj = await resolveProject(client, project);
|
|
894
|
-
await client.
|
|
895
|
-
|
|
1199
|
+
const plan = await client.planMove(proj.id, oldname, newname);
|
|
1200
|
+
if (plan.noop) {
|
|
1201
|
+
spinner.info(`Nothing to do: ${plan.from} is already at ${plan.to}`);
|
|
1202
|
+
setLastProject(proj.id);
|
|
1203
|
+
return;
|
|
1204
|
+
}
|
|
1205
|
+
const result = await client.applyMovePlan(proj.id, plan);
|
|
1206
|
+
const verb = plan.move ? (plan.rename ? 'Moved and renamed' : 'Moved') : 'Renamed';
|
|
1207
|
+
const created = result.createdFolders.length ? ` (created ${result.createdFolders.join(', ')})` : '';
|
|
1208
|
+
spinner.succeed(`${verb}: ${result.from} → ${result.to || '/'}${created}`);
|
|
896
1209
|
setLastProject(proj.id);
|
|
897
1210
|
}
|
|
898
1211
|
catch (error) {
|
|
899
|
-
spinner
|
|
900
|
-
process.exit(1);
|
|
1212
|
+
failWith(spinner, error);
|
|
901
1213
|
}
|
|
902
1214
|
});
|
|
903
1215
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -934,8 +1246,7 @@ projectCmd
|
|
|
934
1246
|
console.log(` ${chalk.cyan(created.url)}`);
|
|
935
1247
|
}
|
|
936
1248
|
catch (error) {
|
|
937
|
-
spinner
|
|
938
|
-
process.exit(1);
|
|
1249
|
+
failWith(spinner, error);
|
|
939
1250
|
}
|
|
940
1251
|
});
|
|
941
1252
|
projectCmd
|
|
@@ -963,8 +1274,7 @@ projectCmd
|
|
|
963
1274
|
setLastProject(proj.id);
|
|
964
1275
|
}
|
|
965
1276
|
catch (error) {
|
|
966
|
-
spinner
|
|
967
|
-
process.exit(1);
|
|
1277
|
+
failWith(spinner, error);
|
|
968
1278
|
}
|
|
969
1279
|
});
|
|
970
1280
|
projectCmd
|
|
@@ -1062,8 +1372,7 @@ projectCmd
|
|
|
1062
1372
|
}
|
|
1063
1373
|
}
|
|
1064
1374
|
catch (error) {
|
|
1065
|
-
spinner
|
|
1066
|
-
process.exit(1);
|
|
1375
|
+
failWith(spinner, error);
|
|
1067
1376
|
}
|
|
1068
1377
|
});
|
|
1069
1378
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -1085,8 +1394,7 @@ program
|
|
|
1085
1394
|
setLastProject(proj.id);
|
|
1086
1395
|
}
|
|
1087
1396
|
catch (error) {
|
|
1088
|
-
spinner
|
|
1089
|
-
process.exit(1);
|
|
1397
|
+
failWith(spinner, error);
|
|
1090
1398
|
}
|
|
1091
1399
|
});
|
|
1092
1400
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -1119,19 +1427,16 @@ program
|
|
|
1119
1427
|
const client = await getClient(options.cookie);
|
|
1120
1428
|
// Resolve project if needed
|
|
1121
1429
|
if (!projectId) {
|
|
1122
|
-
|
|
1123
|
-
if (!proj) {
|
|
1124
|
-
proj = await client.getProject(project);
|
|
1125
|
-
}
|
|
1126
|
-
if (!proj) {
|
|
1127
|
-
spinner.fail(`Project not found: ${project}`);
|
|
1128
|
-
process.exit(1);
|
|
1129
|
-
}
|
|
1430
|
+
const proj = await client.findProject(project);
|
|
1130
1431
|
projectId = proj.id;
|
|
1131
1432
|
projectName = proj.name;
|
|
1132
|
-
// Default directory is project name
|
|
1433
|
+
// Default directory is the project name, made safe for a path
|
|
1133
1434
|
if (!dir) {
|
|
1134
|
-
targetDir = proj.
|
|
1435
|
+
targetDir = defaultProjectDir(proj.id, proj.name);
|
|
1436
|
+
if (targetDir === legacyLocalName(proj.name) && targetDir !== safeLocalName(proj.name)) {
|
|
1437
|
+
spinner.info(`Using existing directory ${targetDir}/ (created by an earlier olcli)`);
|
|
1438
|
+
spinner.start('Downloading project...');
|
|
1439
|
+
}
|
|
1135
1440
|
}
|
|
1136
1441
|
}
|
|
1137
1442
|
spinner.text = 'Downloading project...';
|
|
@@ -1225,8 +1530,7 @@ program
|
|
|
1225
1530
|
setLastProject(projectId);
|
|
1226
1531
|
}
|
|
1227
1532
|
catch (error) {
|
|
1228
|
-
spinner
|
|
1229
|
-
process.exit(1);
|
|
1533
|
+
failWith(spinner, error);
|
|
1230
1534
|
}
|
|
1231
1535
|
});
|
|
1232
1536
|
program
|
|
@@ -1280,14 +1584,7 @@ program
|
|
|
1280
1584
|
const client = await getClient(options.cookie);
|
|
1281
1585
|
// Resolve project if needed
|
|
1282
1586
|
if (!projectId) {
|
|
1283
|
-
|
|
1284
|
-
if (!proj) {
|
|
1285
|
-
proj = await client.getProject(projectName);
|
|
1286
|
-
}
|
|
1287
|
-
if (!proj) {
|
|
1288
|
-
spinner.fail(`Project not found: ${projectName}`);
|
|
1289
|
-
process.exit(1);
|
|
1290
|
-
}
|
|
1587
|
+
const proj = await client.findProject(projectName);
|
|
1291
1588
|
projectId = proj.id;
|
|
1292
1589
|
projectName = proj.name;
|
|
1293
1590
|
}
|
|
@@ -1450,7 +1747,7 @@ program
|
|
|
1450
1747
|
spinner.succeed(`Uploaded ${uploaded} file(s) to "${projectName}"`);
|
|
1451
1748
|
}
|
|
1452
1749
|
if (deleted > 0) {
|
|
1453
|
-
console.log(chalk.dim(` ✖ ${deleted} deleted on remote`));
|
|
1750
|
+
console.log(chalk.dim(` ${icon('✖', '-')} ${deleted} deleted on remote`));
|
|
1454
1751
|
}
|
|
1455
1752
|
if (deleteSkipped.length > 0) {
|
|
1456
1753
|
console.log(chalk.yellow(` ${deleteSkipped.length} deletion(s) skipped:`));
|
|
@@ -1464,8 +1761,7 @@ program
|
|
|
1464
1761
|
setLastProject(projectId);
|
|
1465
1762
|
}
|
|
1466
1763
|
catch (error) {
|
|
1467
|
-
spinner
|
|
1468
|
-
process.exit(1);
|
|
1764
|
+
failWith(spinner, error);
|
|
1469
1765
|
}
|
|
1470
1766
|
});
|
|
1471
1767
|
program
|
|
@@ -1507,14 +1803,7 @@ skipped, or pushed. Combine it with --dry-run to preview deletions:
|
|
|
1507
1803
|
const client = await getClient(options.cookie);
|
|
1508
1804
|
// Resolve project
|
|
1509
1805
|
if (!projectId) {
|
|
1510
|
-
|
|
1511
|
-
if (!proj) {
|
|
1512
|
-
proj = await client.getProject(projectName);
|
|
1513
|
-
}
|
|
1514
|
-
if (!proj) {
|
|
1515
|
-
spinner.fail(`Project not found: ${projectName}`);
|
|
1516
|
-
process.exit(1);
|
|
1517
|
-
}
|
|
1806
|
+
const proj = await client.findProject(projectName);
|
|
1518
1807
|
projectId = proj.id;
|
|
1519
1808
|
projectName = proj.name;
|
|
1520
1809
|
}
|
|
@@ -1688,13 +1977,13 @@ skipped, or pushed. Combine it with --dry-run to preview deletions:
|
|
|
1688
1977
|
spinner.succeed(`Synced "${projectName}"`);
|
|
1689
1978
|
}
|
|
1690
1979
|
// Summary
|
|
1691
|
-
console.log(chalk.dim(` ↓ ${filesUpdatedLocally.length} pulled from remote`));
|
|
1692
|
-
console.log(chalk.dim(` ↑ ${filesToUpload.length} pushed to remote`));
|
|
1980
|
+
console.log(chalk.dim(` ${icon('↓', '<')} ${filesUpdatedLocally.length} pulled from remote`));
|
|
1981
|
+
console.log(chalk.dim(` ${icon('↑', '>')} ${filesToUpload.length} pushed to remote`));
|
|
1693
1982
|
if (filesDeletedRemote.length > 0) {
|
|
1694
|
-
console.log(chalk.dim(` ✖ ${filesDeletedRemote.length} deleted on remote`));
|
|
1983
|
+
console.log(chalk.dim(` ${icon('✖', '-')} ${filesDeletedRemote.length} deleted on remote`));
|
|
1695
1984
|
}
|
|
1696
1985
|
if (filesDeleteSkipped.length > 0) {
|
|
1697
|
-
console.log(chalk.yellow(` ⚠ ${filesDeleteSkipped.length} deletion(s) failed (kept remote)`));
|
|
1986
|
+
console.log(chalk.yellow(` ${icon('⚠', '!')} ${filesDeleteSkipped.length} deletion(s) failed (kept remote)`));
|
|
1698
1987
|
}
|
|
1699
1988
|
// The program-level --verbose, not a sync option: commander assigns a
|
|
1700
1989
|
// flag declared on both levels to the program, so a local --verbose here
|
|
@@ -1728,13 +2017,12 @@ skipped, or pushed. Combine it with --dry-run to preview deletions:
|
|
|
1728
2017
|
setLastProject(projectId);
|
|
1729
2018
|
}
|
|
1730
2019
|
catch (error) {
|
|
1731
|
-
spinner
|
|
1732
|
-
process.exit(1);
|
|
2020
|
+
failWith(spinner, error);
|
|
1733
2021
|
}
|
|
1734
2022
|
});
|
|
1735
2023
|
program
|
|
1736
2024
|
.command('diff [project] [dir]')
|
|
1737
|
-
.description('Show content
|
|
2025
|
+
.description('Show content differences between local files and the remote project')
|
|
1738
2026
|
.option('--name-only', 'List changed paths instead of printing patches')
|
|
1739
2027
|
.option('--file <path>', 'Diff a single file')
|
|
1740
2028
|
.option('-U, --unified <n>', 'Lines of context around each hunk (default: 3)', parseInt)
|
|
@@ -1929,8 +2217,7 @@ where the markup only covers the root document.`)
|
|
|
1929
2217
|
process.exitCode = differencesExitCode(entries);
|
|
1930
2218
|
}
|
|
1931
2219
|
catch (error) {
|
|
1932
|
-
spinner
|
|
1933
|
-
process.exit(failureCode);
|
|
2220
|
+
failWith(spinner, error, failureCode);
|
|
1934
2221
|
}
|
|
1935
2222
|
});
|
|
1936
2223
|
/**
|
|
@@ -2235,41 +2522,32 @@ program
|
|
|
2235
2522
|
console.log(chalk.dim('Note: *.pdf is also ignored when a same-named *.tex/.ltx exists in the same folder.'));
|
|
2236
2523
|
}
|
|
2237
2524
|
});
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
//
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
}
|
|
2266
|
-
else if (stored.password) {
|
|
2267
|
-
console.log(chalk.yellow('⚠ Password stored in plaintext in the config file'));
|
|
2268
|
-
console.log(chalk.dim(" Remove it with 'olcli logout', then re-auth without --save-password."));
|
|
2269
|
-
}
|
|
2270
|
-
if (stored.olAuthPath) {
|
|
2271
|
-
console.log(chalk.dim(` .olauth present: ${stored.olAuthPath}`));
|
|
2272
|
-
}
|
|
2273
|
-
});
|
|
2525
|
+
deprecatedAlias('check', 'Alias of: auth status');
|
|
2526
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2527
|
+
// ROOT HELP GROUPS
|
|
2528
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2529
|
+
// Workflow order: sign in, find a project, sync it, touch single files, build,
|
|
2530
|
+
// review, settings. A visible top-level command without a group fails a test.
|
|
2531
|
+
const HELP_GROUPS = [
|
|
2532
|
+
['Account:', ['auth', 'logout']],
|
|
2533
|
+
['Projects:', ['list', 'info', 'project']],
|
|
2534
|
+
['Sync:', ['pull', 'push', 'sync', 'diff']],
|
|
2535
|
+
['Files:', ['download', 'upload', 'delete', 'rename', 'zip']],
|
|
2536
|
+
['Build:', ['compile', 'pdf', 'output']],
|
|
2537
|
+
['Review:', ['comments', 'changes']],
|
|
2538
|
+
['Settings:', ['config', 'ignored']],
|
|
2539
|
+
];
|
|
2540
|
+
{
|
|
2541
|
+
const order = new Map();
|
|
2542
|
+
HELP_GROUPS.forEach(([heading, names], group) => {
|
|
2543
|
+
names.forEach((name, position) => {
|
|
2544
|
+
program.commands.find((c) => c.name() === name)?.helpGroup(heading);
|
|
2545
|
+
order.set(name, group * 100 + position);
|
|
2546
|
+
});
|
|
2547
|
+
});
|
|
2548
|
+
// Commander lists groups in the order their first command appears.
|
|
2549
|
+
program.commands.sort((a, b) => (order.get(a.name()) ?? 9999) - (order.get(b.name()) ?? 9999));
|
|
2550
|
+
program.commandsGroup('Help:').helpCommand(true);
|
|
2551
|
+
}
|
|
2274
2552
|
program.parse(process.argv);
|
|
2275
2553
|
//# sourceMappingURL=cli.js.map
|