@aloth/olcli 0.10.0 → 0.12.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 CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  **Command-line interface for Overleaf** — Sync, manage, and compile LaTeX projects from your terminal.
4
4
 
5
+ [![CI](https://github.com/aloth/olcli/actions/workflows/ci.yml/badge.svg)](https://github.com/aloth/olcli/actions/workflows/ci.yml)
5
6
  [![npm version](https://img.shields.io/npm/v/@aloth/olcli.svg)](https://www.npmjs.com/package/@aloth/olcli)
6
7
  [![npm downloads](https://img.shields.io/npm/dm/@aloth/olcli.svg)](https://www.npmjs.com/package/@aloth/olcli)
7
8
  [![GitHub stars](https://img.shields.io/github/stars/aloth/olcli)](https://github.com/aloth/olcli)
@@ -17,6 +18,7 @@ Work with Overleaf projects directly from your command line. Edit locally with y
17
18
  ## Features
18
19
 
19
20
  - 📋 **List** all your Overleaf projects
21
+ - ✨ **Create** blank or example projects
20
22
  - ⬇️ **Pull** project files to local directory for offline editing
21
23
  - ⬆️ **Push** local changes back to Overleaf
22
24
  - 🔄 **Sync** bidirectionally with smart conflict detection
@@ -82,9 +84,16 @@ olcli auth --cookie "your_session_cookie_value"
82
84
  **Email/password** (self-hosted without reCAPTCHA):
83
85
 
84
86
  ```bash
85
- olcli auth --email "you@example.com" --password "your_password"
87
+ olcli auth --email "you@example.com"
88
+ # prompts for the password, so it stays out of your shell history
86
89
  ```
87
90
 
91
+ The password is **not stored** unless you pass `--save-password`. A session
92
+ cookie is saved either way and is what later commands use; the password only
93
+ buys an automatic re-login once that cookie expires. For scripts, set
94
+ `OVERLEAF_EMAIL` and `OVERLEAF_PASSWORD` — every command reads them, so a
95
+ scripted run never needs `olcli auth` at all.
96
+
88
97
  ### 2. List Projects
89
98
 
90
99
  ```bash
@@ -127,7 +136,7 @@ All commands auto-detect the project when run from a synced directory (contains
127
136
  |---------|-------------|
128
137
  | `olcli auth` | Set session cookie or login with email/password |
129
138
  | `olcli whoami` | Check authentication status |
130
- | `olcli logout` | Clear stored credentials |
139
+ | `olcli logout` | Clear the global config and the local `.olauth`, reporting each |
131
140
  | `olcli list` | List all projects |
132
141
  | `olcli info [project]` | Show project details and file list |
133
142
  | `olcli pull [project] [dir]` | Download project files to local directory |
@@ -138,6 +147,7 @@ All commands auto-detect the project when run from a synced directory (contains
138
147
  | `olcli download <file> [project]` | Download a single file |
139
148
  | `olcli delete <file> [project]` | Delete a remote file or folder (alias: `rm`) |
140
149
  | `olcli rename <old> <new> [project]` | Rename a remote file or folder (alias: `mv`) |
150
+ | `olcli project create <name>` | Create a blank or example project (`--template blank\|example`) |
141
151
  | `olcli project rename <new> [project]` | Rename the project itself (`--dry-run`) |
142
152
  | `olcli project rename-bulk` | Rename many projects by pattern (dry-run unless `--apply`) |
143
153
  | `olcli compile [project]` | Trigger PDF compilation |
@@ -272,7 +282,32 @@ Credentials are checked in order:
272
282
 
273
283
  1. `OVERLEAF_SESSION` environment variable
274
284
  2. `.olauth` file in current directory
275
- 3. Global config: `~/.config/olcli-nodejs/config.json`
285
+ 3. Global config — run `olcli check` to see the exact path
286
+
287
+ The global config path is platform-dependent (`conf` resolves it), so it is not
288
+ hardcoded here: on macOS it lands under `~/Library/Preferences/`, on Linux under
289
+ `~/.config/`. `olcli auth` prints the path it wrote to.
290
+
291
+ ⚠️ `olcli auth --save-local` writes `.olauth` into the **current directory**,
292
+ which is usually your LaTeX project. Add it to that project's `.gitignore`
293
+ before committing.
294
+
295
+ ### What is stored, and how to clear it
296
+
297
+ Everything is stored in plaintext, so it is worth knowing what is on disk:
298
+
299
+ | Credential | Stored by default | Where |
300
+ |---|---|---|
301
+ | Session cookie | yes | global config, or `.olauth` with `--save-local` |
302
+ | Email + password | **no** — only with `--save-password` | global config |
303
+
304
+ `olcli check` reports what exists without printing any secret.
305
+
306
+ `olcli logout` clears the global config **and** the `.olauth` file in the
307
+ current directory, then lists what it removed. It cannot unset environment
308
+ variables, so if `OVERLEAF_SESSION` or `OVERLEAF_EMAIL`/`OVERLEAF_PASSWORD` are
309
+ set, it says so instead of implying you are logged out — those take precedence
310
+ over anything on disk.
276
311
 
277
312
  ### Self-hosted Overleaf
278
313
 
@@ -341,6 +376,7 @@ import { OverleafClient } from '@aloth/olcli';
341
376
 
342
377
  const client = await OverleafClient.fromSessionCookie(cookie);
343
378
 
379
+ const created = await client.createProject('My Paper');
344
380
  const projects = await client.listProjects();
345
381
  const info = await client.getProjectInfo(projectId);
346
382
  const zipBuf = await client.downloadProject(projectId);
@@ -357,7 +393,8 @@ const comments = await client.listComments(projectId, { status: 'open' });
357
393
  import {
358
394
  OverleafClient,
359
395
  // Types
360
- Project, ProjectInfo, FolderEntry, DocEntry, FileEntry,
396
+ Project, ProjectInfo, ProjectTemplate, CreateProjectOptions, CreatedProject,
397
+ FolderEntry, DocEntry, FileEntry,
361
398
  CommentMessage, ProjectComment, CommentContext, CommentStatus,
362
399
  ListCommentsOptions, AddCommentOptions, Credentials, SessionCookiePair,
363
400
  // Config utilities
@@ -387,6 +424,12 @@ import {
387
424
 
388
425
  Contributions are welcome! Please open an issue or submit a pull request.
389
426
 
427
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for local setup, which tests need a real
428
+ Overleaf account, and what to expect from CI on a pull request.
429
+
430
+ [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) explains how the client works —
431
+ there is no public Overleaf API, so it authenticates as a browser session.
432
+
390
433
  ## License
391
434
 
392
435
  MIT © [Alexander Loth](https://alexloth.com)
package/dist/cli.js CHANGED
@@ -21,7 +21,8 @@ import { loadIgnore } from './ignore.js';
21
21
  const __dirname = dirname(fileURLToPath(import.meta.url));
22
22
  const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf-8'));
23
23
  const VERSION = pkg.version;
24
- import { getSessionCookie, setSessionCookie, setLastProject, getConfigPath, saveOlAuth, clearConfig, getBaseUrl, setBaseUrl, getSessionCookieName, setSessionCookieName, getTimeout, setTimeout, getPasswordCredentials, setPasswordCredentials } from './config.js';
24
+ import { getSessionCookie, setSessionCookie, setLastProject, getConfigPath, saveOlAuth, clearConfig, getBaseUrl, setBaseUrl, getSessionCookieName, setSessionCookieName, getTimeout, setTimeout, getPasswordCredentials, setPasswordCredentials, clearOlAuth, inspectStoredCredentials } from './config.js';
25
+ import { promptHidden, PromptCancelled, NotATerminal } from './prompt.js';
25
26
  const program = new Command();
26
27
  program
27
28
  .name('olcli')
@@ -114,9 +115,18 @@ program
114
115
  .description('Authenticate with Overleaf using a session cookie or email/password')
115
116
  .option('--cookie <session>', 'Session cookie (overleaf_session2 value)')
116
117
  .option('--email <email>', 'Account email for password login')
117
- .option('--password <password>', 'Account password for password login')
118
- .option('--no-save-password', 'Do not persist email/password credentials')
118
+ .option('--password <password>', 'Account password (omit to be prompted; see warning below)')
119
+ .option('--save-password', 'Persist the password in the config file, in plaintext')
120
+ .option('--no-save-password', 'Do not persist the password (the default; kept for existing scripts)')
119
121
  .option('--save-local', 'Save to .olauth in current directory')
122
+ .addHelpText('after', `
123
+ The password is not stored unless you ask for it with --save-password. A
124
+ session cookie is stored either way, and that is what later commands use; the
125
+ password only buys an automatic re-login once the cookie expires.
126
+
127
+ Passing --password puts the password in your shell history. Omit it to be
128
+ prompted instead, or set OVERLEAF_EMAIL and OVERLEAF_PASSWORD, which every
129
+ command reads without needing 'auth' at all.`)
120
130
  .action(async (options) => {
121
131
  if (!options.cookie && !options.email && !options.password) {
122
132
  console.log(chalk.yellow('To authenticate, provide a session cookie:'));
@@ -129,7 +139,8 @@ program
129
139
  console.log(chalk.cyan(' olcli auth --cookie "your_session_cookie_value"'));
130
140
  console.log();
131
141
  console.log('Or log in with email/password:');
132
- console.log(chalk.cyan(' olcli auth --email "you@example.com" --password "your_password"'));
142
+ console.log(chalk.cyan(' olcli auth --email "you@example.com"'));
143
+ console.log(chalk.dim(' (prompts for the password, so it stays out of your shell history)'));
133
144
  console.log();
134
145
  console.log('Or set OVERLEAF_SESSION environment variable');
135
146
  return;
@@ -138,10 +149,41 @@ program
138
149
  console.error(chalk.red('Use either --cookie or --email/--password, not both.'));
139
150
  process.exit(1);
140
151
  }
141
- if (!options.cookie && (!options.email || !options.password)) {
142
- console.error(chalk.red('Both --email and --password are required for password login.'));
152
+ if (!options.cookie && !options.email) {
153
+ console.error(chalk.red('--email is required for password login.'));
143
154
  process.exit(1);
144
155
  }
156
+ // Resolve the password before the spinner starts: a prompt and a spinner
157
+ // both own the terminal, and ora would redraw over the prompt line.
158
+ let password = options.password;
159
+ if (!options.cookie) {
160
+ if (password) {
161
+ console.log(chalk.yellow('⚠ --password is now in your shell history.'));
162
+ console.log(chalk.dim(' Omit it to be prompted, or set OVERLEAF_EMAIL/OVERLEAF_PASSWORD.'));
163
+ }
164
+ else {
165
+ try {
166
+ password = await promptHidden(`Password for ${options.email}: `);
167
+ }
168
+ catch (error) {
169
+ if (error instanceof NotATerminal) {
170
+ console.error(chalk.red('No terminal available to prompt for a password.'));
171
+ console.error('Set OVERLEAF_EMAIL and OVERLEAF_PASSWORD instead — every command reads them,');
172
+ console.error("so a scripted run does not need 'olcli auth' at all.");
173
+ process.exit(1);
174
+ }
175
+ if (error instanceof PromptCancelled) {
176
+ console.error(chalk.red('Cancelled.'));
177
+ process.exit(1);
178
+ }
179
+ throw error;
180
+ }
181
+ if (!password) {
182
+ console.error(chalk.red('Password must not be empty.'));
183
+ process.exit(1);
184
+ }
185
+ }
186
+ }
145
187
  const spinner = ora('Verifying session...').start();
146
188
  try {
147
189
  const baseUrl = program.opts().baseUrl || getBaseUrl();
@@ -160,14 +202,29 @@ program
160
202
  }
161
203
  else {
162
204
  spinner.text = 'Logging in with email/password...';
163
- const client = await OverleafClient.fromPasswordLogin(options.email, options.password, baseUrl);
205
+ const client = await OverleafClient.fromPasswordLogin(options.email, password, baseUrl);
164
206
  const projects = await client.listProjects();
165
207
  persistClientSession(client, cookieName);
166
208
  setBaseUrl(baseUrl);
167
- if (options.savePassword !== false) {
168
- setPasswordCredentials(options.email, options.password);
209
+ // Opt-in, not opt-out. The session cookie persisted just above is what
210
+ // later commands actually use; the password only buys an automatic
211
+ // re-login after that cookie expires, and it is stored in plaintext.
212
+ // A cookie is scoped to olcli and rotates; a password is reusable
213
+ // everywhere and cannot be revoked without changing it. See issue #50.
214
+ const savePassword = options.savePassword === true;
215
+ if (savePassword) {
216
+ setPasswordCredentials(options.email, password);
217
+ }
218
+ // The old message said "Password login saved." unconditionally - even
219
+ // under --no-save-password, which had just prevented exactly that.
220
+ spinner.succeed(`Authenticated! Found ${projects.length} projects.`);
221
+ if (savePassword) {
222
+ console.log(chalk.yellow('Password stored in plaintext in the config file.'));
223
+ }
224
+ else {
225
+ console.log(chalk.dim('Session cookie stored. The password was not saved; re-run'));
226
+ console.log(chalk.dim('olcli auth when the session expires, or use --save-password.'));
169
227
  }
170
- spinner.succeed(`Authenticated! Found ${projects.length} projects. Password login saved.`);
171
228
  }
172
229
  console.log(chalk.dim(`Config saved to: ${getConfigPath()}`));
173
230
  }
@@ -200,9 +257,44 @@ program
200
257
  program
201
258
  .command('logout')
202
259
  .description('Clear stored credentials')
260
+ .addHelpText('after', `
261
+ Clears the global config and the .olauth file in the current directory, and
262
+ reports each one separately. Environment variables cannot be cleared by a
263
+ child process, so OVERLEAF_SESSION and OVERLEAF_EMAIL/OVERLEAF_PASSWORD are
264
+ reported instead of silently ignored - both take precedence over anything on
265
+ disk.`)
203
266
  .action(() => {
267
+ // Read before clearing: afterwards there is nothing left to report on.
268
+ const before = inspectStoredCredentials();
204
269
  clearConfig();
205
- console.log(chalk.green('Credentials cleared'));
270
+ const removedOlAuth = clearOlAuth();
271
+ const cleared = [];
272
+ if (before.sessionCookie)
273
+ cleared.push('session cookie (global config)');
274
+ if (before.password)
275
+ cleared.push('saved password (global config)');
276
+ if (removedOlAuth)
277
+ cleared.push(removedOlAuth);
278
+ if (cleared.length === 0) {
279
+ console.log('Nothing stored to clear.');
280
+ }
281
+ else {
282
+ console.log(chalk.green('Cleared:'));
283
+ for (const item of cleared)
284
+ console.log(` ${item}`);
285
+ }
286
+ // The reason this command was wrong before: it announced success while a
287
+ // higher-precedence source kept the user authenticated. Anything olcli
288
+ // cannot clear has to be said out loud, or the message is a lie again.
289
+ if (before.envSession || before.envPassword) {
290
+ console.log();
291
+ console.log(chalk.yellow('Still authenticated in this shell:'));
292
+ if (before.envSession)
293
+ console.log(' OVERLEAF_SESSION is set');
294
+ if (before.envPassword)
295
+ console.log(' OVERLEAF_EMAIL and OVERLEAF_PASSWORD are set');
296
+ console.log(chalk.dim(' These outrank anything on disk. Unset them to finish logging out.'));
297
+ }
206
298
  });
207
299
  // ─────────────────────────────────────────────────────────────────────────────
208
300
  // PROJECT COMMANDS
@@ -701,7 +793,39 @@ program
701
793
  // ─────────────────────────────────────────────────────────────────────────────
702
794
  const projectCmd = program
703
795
  .command('project')
704
- .description('Operate on projects themselves (rename, bulk rename)');
796
+ .description('Operate on projects themselves (create, rename, bulk rename)');
797
+ projectCmd
798
+ .command('create <name>')
799
+ .description('Create a new project')
800
+ .option('-t, --template <type>', 'Project template: blank or example', 'blank')
801
+ .option('--json', 'Output as JSON')
802
+ .option('--cookie <session>', 'Session cookie override')
803
+ .action(async (name, options) => {
804
+ const template = options.template;
805
+ if (template !== 'blank' && template !== 'example') {
806
+ console.error(chalk.red(`Unsupported project template: ${template}`));
807
+ console.error('Supported templates: blank, example');
808
+ process.exit(1);
809
+ }
810
+ const spinner = ora('Creating project...').start();
811
+ try {
812
+ const client = await getClient(options.cookie);
813
+ const created = await client.createProject(name, { template });
814
+ setLastProject(created.id);
815
+ if (options.json) {
816
+ spinner.stop();
817
+ console.log(JSON.stringify(created, null, 2));
818
+ return;
819
+ }
820
+ spinner.succeed(`Created project: ${created.name}`);
821
+ console.log(` ${chalk.cyan(created.id)}`);
822
+ console.log(` ${chalk.cyan(created.url)}`);
823
+ }
824
+ catch (error) {
825
+ spinner.fail(`Failed: ${error.message}`);
826
+ process.exit(1);
827
+ }
828
+ });
705
829
  projectCmd
706
830
  .command('rename <newname> [project]')
707
831
  .description('Rename a project')
@@ -1732,6 +1856,21 @@ program
1732
1856
  else {
1733
1857
  console.log(chalk.yellow('✗ No session cookie found'));
1734
1858
  }
1859
+ // A stored password is a credential source this command used to omit,
1860
+ // which made it impossible to answer "is my password on disk?" without
1861
+ // opening the config file. Never print the value - only whether it exists
1862
+ // and which source it came from.
1863
+ const stored = inspectStoredCredentials();
1864
+ if (stored.envPassword) {
1865
+ console.log(chalk.yellow('⚠ Password set via OVERLEAF_EMAIL/OVERLEAF_PASSWORD'));
1866
+ }
1867
+ else if (stored.password) {
1868
+ console.log(chalk.yellow('⚠ Password stored in plaintext in the config file'));
1869
+ console.log(chalk.dim(" Remove it with 'olcli logout', then re-auth without --save-password."));
1870
+ }
1871
+ if (stored.olAuthPath) {
1872
+ console.log(chalk.dim(` .olauth present: ${stored.olAuthPath}`));
1873
+ }
1735
1874
  });
1736
1875
  program.parse(process.argv);
1737
1876
  //# sourceMappingURL=cli.js.map