proton-drive-mcp 1.0.26 → 1.0.28

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/CHANGELOG.md CHANGED
@@ -1,5 +1,43 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.0.28 — 2026-08-11
4
+
5
+ Closes the last 2 gaps found in the full CLI command audit — every scriptable Proton Drive CLI command now has a matching tool.
6
+
7
+ ### Added
8
+ - **`drive_info`** — full node metadata for a single file/folder, including latest revision details (`filesystem info`). Raw pass-through of the CLI's node object — richer and less trimmed than `drive_list`'s per-item fields.
9
+ - **`drive_rename`** — rename in place without moving to a different folder (`filesystem rename`). `drive_move` already reached this internally when source/destination share a parent; it's now also directly addressable and documented as the cheaper option for pure renames.
10
+ - **`validateName`** utility — guards bare name/filename CLI arguments (not paths) against flag injection, used by `drive_rename`'s `newName`.
11
+
12
+ ### Coverage
13
+ 33 commands in the CLI's registry (verified against the `cli/v0.7.0` release tag) minus `auth login` (interactive browser flow, deliberately excluded) = 32 scriptable commands, all now covered 1:1 by a tool.
14
+
15
+ ## 1.0.27 — 2026-08-11
16
+
17
+ A full audit against the Proton Drive CLI SDK source found that 9 existing tools called CLI commands or flags that don't exist, or had wrong semantics. This release fixes all of them and adds 7 new tools for public links, bulk access removal, album updates, and Photos timeline transfers.
18
+
19
+ ### Fixed (existing tools were broken — none of these worked correctly before)
20
+ - **`drive_auth_status`** — `auth status` doesn't exist in the CLI at all; every call errored. Now probes by resolving `/my-files` and treats a not-authenticated error as the signal. The `email` field is removed — the CLI has no way to query the signed-in account's email.
21
+ - **`drive_version`** — the CLI's `version` command ignores `--json` and always prints plain text; the tool was trying to JSON-parse it and always threw. Now parsed directly from stdout.
22
+ - **`drive_mkdir`** — the CLI has no `mkdir`; it's `filesystem create-folder <parentPath> <name>`. The tool now splits the given path into parent + name automatically.
23
+ - **`drive_move`** — the CLI's `move` only accepts a target *parent folder*; renaming is a separate `rename <path> <newName>` command. The tool still accepts a full destination path (unchanged external interface) but now composes `move`/`rename` correctly under the hood.
24
+ - **`drive_delete`** — passed a `--confirm` flag that doesn't exist, and the CLI only permits deleting items already inside `/trash` or `/photos-trash` (it rejects live paths). Flag removed; description corrected.
25
+ - **`drive_trash` / `drive_restore` / `drive_list_trash` / `drive_empty_trash`** — called a top-level `trash` command group that doesn't exist; trash operations are subcommands of `filesystem` (`filesystem trash`, `filesystem restore`, `filesystem empty-trash`), and listing trash is `filesystem list /trash`. `empty-trash` also had a nonexistent `--confirm` flag, removed.
26
+ - **`drive_share_revoke`** — called `sharing revoke --user`, which doesn't exist; the real command is `sharing remove --email`.
27
+ - **`drive_upload` / `drive_download`** — conflict strategy values were `skip`/`overwrite`/`rename`; the CLI's actual allowed values are `skip`/`replace`/`keep-both`/`merge` (`merge` is upload/folder-only). `drive_download` also gained a `conflictStrategy` parameter it never had.
28
+
29
+ ### Added
30
+ - **`drive_share_set_url`** — create or update a public share link (`sharing set-url`), with optional role, password, and expiration
31
+ - **`drive_share_remove_url`** — remove a public share link (`sharing remove-url`)
32
+ - **`drive_share_remove_all`** — remove every member and pending invitation from a shared path in one call (`sharing remove --everyone`)
33
+ - **`photos_update_album`** — rename an album or change its cover photo (`album update`)
34
+ - **`photos_list_timeline`** — list photos in your full Photos library timeline, not scoped to an album (`photo timeline`)
35
+ - **`photos_download`** — download one or more photos to a local folder (`photo download`)
36
+ - **`photos_upload`** — upload local photos/videos directly into your Photos library (`photo upload`)
37
+
38
+ ### Not implemented
39
+ - `sharing report` (abuse reporting) exists as source in the SDK but is not registered in the CLI's command registry — invoking it would fail with `CommandNotFoundError`. Excluded as dead code, not a gap.
40
+
3
41
  ## 1.0.25 — 2026-08-04
4
42
 
5
43
  ### Added
package/README.md CHANGED
@@ -30,7 +30,8 @@ Give Claude Desktop (or any MCP client) full access to your Proton Drive and Pro
30
30
 
31
31
  - **Claude manages your Proton Drive** — list, upload, download, move, share, trash, restore
32
32
  - **Proton Photos album management** — list albums, create/delete albums, add and remove photos
33
- - **Full CLI** — same 29 operations, scriptable and pipeable, works in cron and shell scripts
33
+ - **Full CLI** — same 38 operations, scriptable and pipeable, works in cron and shell scripts
34
+ - **100% CLI coverage** — every scriptable Proton Drive CLI command has a matching tool (verified against the CLI's own source; `auth login` is the one command excluded, since it's an interactive browser flow)
34
35
  - **Zero credential exposure** — auth is handled entirely by the official Proton Drive CLI; this MCP never touches your password or session token
35
36
  - **Shell injection safe** — all CLI calls use `execFile` with discrete argument arrays, never string interpolation
36
37
  - **Privacy-native** — end-to-end encryption is handled by Proton's own CLI; this server is just a thin MCP wrapper
@@ -147,7 +148,7 @@ proton-drive-cli <command> [args]
147
148
  ### Auth & info
148
149
 
149
150
  ```bash
150
- proton-drive-cli auth status # check if authenticated
151
+ proton-drive-cli auth status # probes /my-files; the CLI has no dedicated status command
151
152
  proton-drive-cli auth logout # log out (clears OS keychain session)
152
153
  proton-drive-cli version # CLI and SDK version
153
154
  ```
@@ -157,17 +158,20 @@ proton-drive-cli version # CLI and SDK version
157
158
  ```bash
158
159
  proton-drive-cli list /my-files
159
160
  proton-drive-cli list /my-files/Reports
161
+ proton-drive-cli info /my-files/report.pdf # full metadata, incl. revision details
160
162
 
161
163
  proton-drive-cli mkdir /my-files/NewFolder
162
164
 
163
165
  proton-drive-cli upload ./report.pdf /my-files/Reports
164
- proton-drive-cli upload ./dist /my-files/Releases --conflict overwrite
166
+ proton-drive-cli upload ./dist /my-files/Releases --conflict replace
165
167
 
166
168
  proton-drive-cli download /my-files/report.pdf ./local/report.pdf
167
- proton-drive-cli download /my-files/Reports ./local/Reports
169
+ proton-drive-cli download /my-files/Reports ./local/Reports --conflict keep-both
168
170
 
171
+ proton-drive-cli rename /my-files/old-name.pdf new-name.pdf # in place, no move
169
172
  proton-drive-cli move /my-files/old-name.pdf /my-files/new-name.pdf
170
- proton-drive-cli delete /my-files/obsolete-draft.pdf
173
+ proton-drive-cli copy /my-files/report.pdf /my-files/Archive
174
+ proton-drive-cli delete /trash/obsolete-draft.pdf --confirm # only works on items already in trash
171
175
 
172
176
  # Machine-readable output (pipe-friendly)
173
177
  proton-drive-cli list /my-files --json | jq '.[].name'
@@ -180,22 +184,39 @@ proton-drive-cli share status /my-files/Reports
180
184
  proton-drive-cli share invite /my-files/Reports alice@pm.me editor
181
185
  proton-drive-cli share invite /my-files/Reports bob@pm.me viewer --message "FYI"
182
186
  proton-drive-cli share revoke /my-files/Reports alice@pm.me
187
+ proton-drive-cli share remove-all /my-files/Reports --confirm # strip every member + pending invite
188
+
189
+ proton-drive-cli share set-url /my-files/Reports --role viewer --expiration 2026-06-06
190
+ proton-drive-cli share remove-url /my-files/Reports
183
191
  ```
184
192
 
185
193
  ### Trash
186
194
 
187
195
  ```bash
188
- proton-drive-cli trash /my-files/old-draft.pdf # move to trash
189
- proton-drive-cli trash list # see what's in trash
190
- proton-drive-cli restore /my-files/old-draft.pdf # restore from trash
191
- proton-drive-cli trash empty # permanently delete all trashed items
196
+ proton-drive-cli trash /my-files/old-draft.pdf # move to trash
197
+ proton-drive-cli trash list # see what's in trash
198
+ proton-drive-cli restore /my-files/old-draft.pdf # restore from trash
199
+ proton-drive-cli trash empty --confirm # permanently delete all trashed items
200
+ ```
201
+
202
+ ### Photos
203
+
204
+ ```bash
205
+ proton-drive-cli album list
206
+ proton-drive-cli album create "Summer 2026"
207
+ proton-drive-cli album update /albums/Summer2026 --name "Summer Trip"
208
+ proton-drive-cli album add-photo /albums/Summer2026 /photos/IMG_001.jpg
209
+
210
+ proton-drive-cli photo timeline
211
+ proton-drive-cli photo download /photos/IMG_001.jpg ./local/photos --conflict keep-both
212
+ proton-drive-cli photo upload ./camera-roll --conflict skip
192
213
  ```
193
214
 
194
215
  ### Pipe and script
195
216
 
196
217
  ```bash
197
218
  # Backup build output after CI
198
- proton-drive-cli upload ./dist /my-files/Releases/$(date +%Y-%m-%d) --conflict rename
219
+ proton-drive-cli upload ./dist /my-files/Releases/$(date +%Y-%m-%d) --conflict keep-both
199
220
 
200
221
  # Download all contracts for audit
201
222
  proton-drive-cli download /my-files/Contracts ./audit/contracts
@@ -215,10 +236,10 @@ proton-drive-cli share status /my-files/Projects
215
236
  `drive_auth_status` · `drive_auth_logout` · `drive_version`
216
237
 
217
238
  ### Filesystem
218
- `drive_list` · `drive_mkdir` · `drive_upload` · `drive_download` · `drive_move` · `drive_delete`
239
+ `drive_list` · `drive_info` · `drive_mkdir` · `drive_upload` · `drive_download` · `drive_rename` · `drive_move` · `drive_delete`
219
240
 
220
241
  ### Sharing
221
- `drive_share_status` · `drive_share_invite` · `drive_share_revoke`
242
+ `drive_share_status` · `drive_share_invite` · `drive_share_revoke` · `drive_share_remove_all` · `drive_share_set_url` · `drive_share_remove_url`
222
243
 
223
244
  ### Trash
224
245
  `drive_list_trash` · `drive_trash` · `drive_restore` · `drive_empty_trash`
@@ -233,7 +254,7 @@ proton-drive-cli share status /my-files/Projects
233
254
  `drive_list_invitations` · `drive_invitation_accept` · `drive_invitation_reject` · `drive_share_leave`
234
255
 
235
256
  ### Photos
236
- `photos_list_albums` · `photos_create_album` · `photos_delete_album` · `photos_list_album_photos` · `photos_add_to_album` · `photos_remove_from_album`
257
+ `photos_list_albums` · `photos_create_album` · `photos_update_album` · `photos_delete_album` · `photos_list_album_photos` · `photos_add_to_album` · `photos_remove_from_album` · `photos_list_timeline` · `photos_download` · `photos_upload`
237
258
 
238
259
  ---
239
260
 
@@ -241,35 +262,44 @@ proton-drive-cli share status /my-files/Projects
241
262
 
242
263
  | Tool | Description | Key parameters |
243
264
  |------|-------------|----------------|
244
- | `drive_auth_status` | Check if CLI is authenticated | — |
265
+ | `drive_auth_status` | Check if authenticated (probes `/my-files` — no native status command) | — |
245
266
  | `drive_auth_logout` | Log out (clear session) | — |
246
267
  | `drive_version` | CLI and SDK version info | — |
247
268
  | `drive_list` | List files and folders at a path | `path` |
269
+ | `drive_info` | Get full metadata for one file/folder, including revision details | `path` |
248
270
  | `drive_mkdir` | Create a new empty folder | `path` |
249
- | `drive_upload` | Upload local file or folder | `localPath`, `remotePath`, `conflictStrategy` (skip/overwrite/rename) |
250
- | `drive_download` | Download to local path | `remotePath`, `localPath` |
251
- | `drive_move` | Move or rename | `sourcePath`, `destinationPath` |
252
- | `drive_delete` | Permanently delete ⚠️ | `path`, `confirmed: true` |
271
+ | `drive_upload` | Upload local file or folder | `localPath`, `remotePath`, `conflictStrategy` (skip/replace/keep-both/merge) |
272
+ | `drive_download` | Download to local path | `remotePath`, `localPath`, `conflictStrategy` (skip/replace/keep-both) |
273
+ | `drive_rename` | Rename in place, no move | `path`, `newName` |
274
+ | `drive_move` | Move and/or rename | `sourcePath`, `destinationPath` |
275
+ | `drive_copy` | Copy file or folder to another Drive location | `sourcePath`, `destinationPath` |
276
+ | `drive_delete` | Permanently delete an item already in trash ⚠️ | `path`, `confirmed: true` |
253
277
  | `drive_list_trash` | List items currently in trash | — |
254
278
  | `drive_share_status` | Get sharing members and URL | `path` |
255
279
  | `drive_share_invite` | Invite a user | `path`, `email`, `role` (viewer/editor/admin), `message?` |
256
- | `drive_share_revoke` | Revoke access | `path`, `email` |
280
+ | `drive_share_revoke` | Revoke one person's access | `path`, `email` |
281
+ | `drive_share_remove_all` | Remove every member + pending invitation at once ⚠️ | `path`, `confirmed: true` |
282
+ | `drive_share_set_url` | Create/update a public share link | `path`, `role?` (viewer/editor), `password?`, `expiration?` |
283
+ | `drive_share_remove_url` | Remove the public share link ⚠️ | `path` |
257
284
  | `drive_trash` | Move to trash | `path` |
258
285
  | `drive_restore` | Restore from trash | `path` |
259
286
  | `drive_empty_trash` | Permanently delete all trash ⚠️ | `confirmed: true` |
260
287
  | `drive_read_file` | Read text file from local sync folder | `path` |
261
288
  | `drive_write_file` | Write text file to local sync folder ⚠️ | `path`, `content` |
262
- | `drive_copy` | Copy file or folder to another Drive location | `sourcePath`, `destinationPath` |
263
289
  | `drive_list_invitations` | List pending sharing invitations received | — |
264
290
  | `drive_invitation_accept` | Accept a pending invitation | `uid` (from `drive_list_invitations`) |
265
291
  | `drive_invitation_reject` | Reject a pending invitation ⚠️ | `uid` (from `drive_list_invitations`) |
266
292
  | `drive_share_leave` | Leave a shared folder shared with you ⚠️ | `path` |
267
293
  | `photos_list_albums` | List all Proton Photos albums | — |
268
294
  | `photos_create_album` | Create a new empty album | `name` |
295
+ | `photos_update_album` | Rename an album or change its cover photo | `albumPath`, `name?`, `coverPhotoUid?` |
269
296
  | `photos_delete_album` | Delete an album ⚠️ | `albumPath`, `confirmed: true`, `force?`, `save?` |
270
297
  | `photos_list_album_photos` | List photos in an album | `albumPath` |
271
298
  | `photos_add_to_album` | Add a photo from your library to an album | `albumPath`, `photoPath` |
272
299
  | `photos_remove_from_album` | Remove a photo from an album (keeps it in library) ⚠️ | `albumPath`, `photoPath` |
300
+ | `photos_list_timeline` | List photos in your full library timeline | `loadDetails?` |
301
+ | `photos_download` | Download photos to a local folder | `photoPaths`, `localFolder`, `conflictStrategy?` (skip/replace/keep-both) |
302
+ | `photos_upload` | Upload local files directly into your Photos library | `localPaths`, `conflictStrategy?` (skip/keep-both) |
273
303
 
274
304
  > ⚠️ **Destructive tools** require `confirmed: true`. Use `drive_list_trash` first so you know what will be deleted, then pass `confirmed: true` to proceed.
275
305
 
@@ -291,9 +321,11 @@ proton-drive-cli share status /my-files/Projects
291
321
  ## Operational notes
292
322
 
293
323
  - `drive_upload` passes `--skip-thumbnails` by default. Remove it from the subprocess args if you want WebP preview generation (requires Bun 1.3.14+ installed).
294
- - `drive_move` and `drive_delete` are best-effort — the Proton Drive CLI documentation does not guarantee these commands exist in v1. If a command returns a CLI error, the MCP tool surfaces it cleanly.
324
+ - `drive_move` accepts a full destination path (parent + new name) for a familiar interface, but the underlying CLI only has separate `move` (change parent) and `rename` (change name) commands — this MCP translates automatically, issuing one or both as needed.
325
+ - `drive_delete` only works on items already in `/trash` or `/photos-trash` — the CLI rejects live paths. Trash an item first with `drive_trash`, or use `drive_empty_trash` to clear everything at once.
326
+ - `drive_auth_status` has no native CLI equivalent — it probes by resolving `/my-files` and reports authenticated based on whether that succeeds.
295
327
  - Paths are always Drive-absolute: `/my-files/folder/file.pdf`. Relative paths are not supported.
296
- - All calls include `--json` automatically — you never need to pass it manually via the CLI wrapper.
328
+ - All calls include `--json` automatically, except `drive_version`, whose underlying CLI command ignores `--json` and always prints plain text — this MCP parses it directly.
297
329
 
298
330
  ---
299
331
 
package/dist/cli.js CHANGED
@@ -4,30 +4,12 @@
4
4
  * Provides a human-friendly interface to the same DriveService used by the MCP server.
5
5
  *
6
6
  * Usage: proton-drive-cli <command> [args]
7
- * auth status
8
- * auth logout
9
- * version
10
- * list <remote-path>
11
- * mkdir <remote-path>
12
- * upload <local-path> <remote-path> [--conflict skip|overwrite|rename]
13
- * download <remote-path> <local-path>
14
- * move <src> <dst>
15
- * delete <path>
16
- * share status <path>
17
- * share invite <path> <email> <role> [--message "..."]
18
- * share revoke <path> <email>
19
- * trash <path>
20
- * trash list
21
- * trash empty
22
- * restore <path>
23
- *
24
- * Global flags:
25
- * --json Machine-readable JSON output
7
+ * Run with --help (or see usage() below) for the full command list.
26
8
  */
27
9
  import { DriveService } from "./services/drive.js";
28
10
  import { DriveCliError, DriveCliNotFoundError, DriveNotAuthenticatedError, DriveParseError } from "./utils/errors.js";
29
11
  import { checkCliAvailable } from "./utils/subprocess.js";
30
- import { validateRemotePath, validateLocalPath, validateEmail, validateMessage } from "./utils/validation.js";
12
+ import { validateRemotePath, validateLocalPath, validateEmail, validateMessage, validateName } from "./utils/validation.js";
31
13
  const drive = new DriveService();
32
14
  const args = process.argv.slice(2);
33
15
  const jsonMode = args.includes("--json");
@@ -36,28 +18,42 @@ function usage() {
36
18
  proton-drive-cli — Proton Drive companion CLI
37
19
 
38
20
  Commands:
39
- auth status Check authentication status
21
+ auth status Check authentication status (probes /my-files)
40
22
  auth logout Log out
41
23
  version Show CLI/SDK version
42
24
  list <path> List files at path
25
+ info <path> Show full node metadata for one path
43
26
  mkdir <path> Create a new folder
44
- upload <local> <remote> [--conflict X] Upload file/folder (skip/overwrite/rename)
45
- download <remote> <local> Download file/folder
46
- move <src> <dst> Move/rename
47
- delete <path> Delete file/folder permanently
27
+ upload <local> <remote> [--conflict X] Upload file/folder (skip/replace/keep-both/merge)
28
+ download <remote> <local> [--conflict X] Download file/folder (skip/replace/keep-both)
29
+ rename <path> <new-name> Rename in place, no move
30
+ move <src> <dst> Move and/or rename
31
+ delete <path> --confirm Delete a file/folder already in trash, permanently
48
32
  share status <path> Show sharing info
49
33
  share invite <path> <email> <role> Invite user (viewer/editor/admin)
50
- share revoke <path> <email> Revoke access
34
+ share revoke <path> <email> Revoke one user's access
35
+ share remove-all <path> --confirm Remove everyone's access + pending invitations
36
+ share set-url <path> [--role] [--password] [--expiration] Create/update public link
37
+ share remove-url <path> Remove public link
38
+ share leave <path> Leave a folder shared with you
39
+ copy <src> <dst> Copy file/folder
51
40
  trash <path> Move to trash
52
41
  trash list List trash contents
53
- trash empty Permanently delete all trash
42
+ trash empty --confirm Permanently delete all trash
54
43
  restore <path> Restore from trash
44
+ invitation list List pending invitations
45
+ invitation accept <uid> Accept an invitation
46
+ invitation reject <uid> Reject an invitation
55
47
  album list List all photo albums
56
48
  album create <name> Create a new album
57
- album delete <path> [--force] [--save] Delete an album
49
+ album update <path> [--name] [--cover-photo-uid] Rename/update album cover
50
+ album delete <path> --confirm [--force] [--save] Delete an album
58
51
  album photos <path> List photos in an album
59
52
  album add-photo <album> <photo> Add a photo to an album
60
53
  album remove-photo <album> <photo> Remove a photo from an album
54
+ photo timeline [--load-details] List photos in your timeline
55
+ photo download <photo>... <local> [--conflict X] Download photos (skip/replace/keep-both)
56
+ photo upload <local>... [--conflict X] Upload photos to your library (skip/keep-both)
61
57
 
62
58
  Flags:
63
59
  --json Machine-readable JSON output (one line)
@@ -143,6 +139,9 @@ async function run() {
143
139
  case "list":
144
140
  print(await drive.list(requirePath(sub, "list <path>")));
145
141
  break;
142
+ case "info":
143
+ print(await drive.info(requirePath(sub, "info <path>")));
144
+ break;
146
145
  case "upload": {
147
146
  const rawLocal = sub;
148
147
  if (!rawLocal) {
@@ -160,8 +159,8 @@ async function run() {
160
159
  }
161
160
  const remote = requirePath(rest[0], "upload <local> <remote>");
162
161
  const conflictRaw = getFlag("--conflict") ?? "skip";
163
- if (!["skip", "overwrite", "rename"].includes(conflictRaw)) {
164
- console.error(`Invalid --conflict value: ${conflictRaw}. Must be skip, overwrite, or rename.`);
162
+ if (!["skip", "replace", "keep-both", "merge"].includes(conflictRaw)) {
163
+ console.error(`Invalid --conflict value: ${conflictRaw}. Must be skip, replace, keep-both, or merge.`);
165
164
  process.exit(1);
166
165
  }
167
166
  const conflict = conflictRaw;
@@ -185,7 +184,28 @@ async function run() {
185
184
  process.exit(1);
186
185
  return;
187
186
  }
188
- print(await drive.download(remote, local));
187
+ const dConflictRaw = getFlag("--conflict") ?? "skip";
188
+ if (!["skip", "replace", "keep-both"].includes(dConflictRaw)) {
189
+ console.error(`Invalid --conflict value: ${dConflictRaw}. Must be skip, replace, or keep-both.`);
190
+ process.exit(1);
191
+ }
192
+ print(await drive.download(remote, local, dConflictRaw));
193
+ break;
194
+ }
195
+ case "rename": {
196
+ const renamePath = requirePath(sub, "rename <path> <new-name>");
197
+ const rawNewName = rest[0];
198
+ let newName;
199
+ try {
200
+ newName = validateName(rawNewName);
201
+ }
202
+ catch (e) {
203
+ console.error(e instanceof Error ? e.message : String(e));
204
+ process.exit(1);
205
+ return;
206
+ }
207
+ await drive.rename(renamePath, newName);
208
+ console.log(`Renamed: ${renamePath} → ${newName}`);
189
209
  break;
190
210
  }
191
211
  case "move": {
@@ -202,7 +222,7 @@ async function run() {
202
222
  case "delete": {
203
223
  const delPath = requirePath(sub, "delete <path> --confirm");
204
224
  if (!args.includes("--confirm")) {
205
- console.error(`This will permanently delete: ${delPath}\nPass --confirm to proceed.`);
225
+ console.error(`This permanently deletes an item already in trash: ${delPath}\nPass --confirm to proceed.`);
206
226
  process.exit(1);
207
227
  }
208
228
  await drive.delete(delPath);
@@ -241,6 +261,31 @@ async function run() {
241
261
  await drive.shareRevoke(revokePath, revokeEmail);
242
262
  console.log(`Revoked access for ${revokeEmail}.`);
243
263
  }
264
+ else if (sub === "remove-all") {
265
+ const removeAllPath = requirePath(rest[0], "share remove-all <path> --confirm");
266
+ if (!args.includes("--confirm")) {
267
+ console.error(`This removes everyone's access to: ${removeAllPath}\nPass --confirm to proceed.`);
268
+ process.exit(1);
269
+ }
270
+ await drive.shareRemove(removeAllPath, [], true);
271
+ console.log(`Removed all access to: ${removeAllPath}`);
272
+ }
273
+ else if (sub === "set-url") {
274
+ const setUrlPath = requirePath(rest[0], "share set-url <path> [--role] [--password] [--expiration]");
275
+ const roleRaw = getFlag("--role") ?? "viewer";
276
+ if (!["viewer", "editor"].includes(roleRaw)) {
277
+ console.error(`Invalid --role value: ${roleRaw}. Must be viewer or editor.`);
278
+ process.exit(1);
279
+ }
280
+ const password = getFlag("--password");
281
+ const expiration = getFlag("--expiration");
282
+ print(await drive.shareSetUrl(setUrlPath, roleRaw, password, expiration));
283
+ }
284
+ else if (sub === "remove-url") {
285
+ const removeUrlPath = requirePath(rest[0], "share remove-url <path>");
286
+ await drive.shareRemoveUrl(removeUrlPath);
287
+ console.log(`Public link removed: ${removeUrlPath}`);
288
+ }
244
289
  else if (sub === "leave") {
245
290
  const leavePath = requirePath(rest[0], "share leave <path>");
246
291
  await drive.shareLeave(leavePath);
@@ -323,6 +368,17 @@ async function run() {
323
368
  await drive.createAlbum(albumName);
324
369
  console.log(`Album created: ${albumName}`);
325
370
  }
371
+ else if (sub === "update") {
372
+ const albumPath = requirePath(rest[0], "album update <path> [--name] [--cover-photo-uid]");
373
+ const newName = getFlag("--name");
374
+ const coverPhotoUid = getFlag("--cover-photo-uid");
375
+ if (!newName && !coverPhotoUid) {
376
+ console.error("Usage: album update <path> [--name <name>] [--cover-photo-uid <uid>] (at least one required)");
377
+ process.exit(1);
378
+ }
379
+ await drive.updateAlbum(albumPath, newName, coverPhotoUid);
380
+ console.log(`Album updated: ${albumPath}`);
381
+ }
326
382
  else if (sub === "delete") {
327
383
  const albumPath = requirePath(rest[0], "album delete <path> [--force] [--save] --confirm");
328
384
  if (!args.includes("--confirm")) {
@@ -349,10 +405,72 @@ async function run() {
349
405
  console.log(`Removed ${photoPath} from ${albumPath}`);
350
406
  }
351
407
  else {
352
- console.error("Usage: album list | album create <name> | album delete <path> | album photos <path> | album add-photo <album> <photo> | album remove-photo <album> <photo>");
408
+ console.error("Usage: album list | album create <name> | album update <path> | album delete <path> | album photos <path> | album add-photo <album> <photo> | album remove-photo <album> <photo>");
409
+ process.exit(1);
410
+ }
411
+ break;
412
+ case "photo": {
413
+ // Strip --conflict <value> out of a variadic positional list.
414
+ const conflictFlagIdx = rest.indexOf("--conflict");
415
+ const positionals = conflictFlagIdx === -1
416
+ ? rest
417
+ : [...rest.slice(0, conflictFlagIdx), ...rest.slice(conflictFlagIdx + 2)];
418
+ const conflictRaw = getFlag("--conflict") ?? "skip";
419
+ if (sub === "timeline") {
420
+ print(await drive.photoTimeline(args.includes("--load-details")));
421
+ }
422
+ else if (sub === "download") {
423
+ if (positionals.length < 2) {
424
+ console.error("Usage: photo download <photo>... <local-folder> [--conflict X]");
425
+ process.exit(1);
426
+ return;
427
+ }
428
+ const localFolder = positionals[positionals.length - 1];
429
+ const photoPaths = positionals.slice(0, -1).map((p) => requirePath(p, "photo download <photo>... <local-folder>"));
430
+ let localFolderValidated;
431
+ try {
432
+ localFolderValidated = validateLocalPath(localFolder);
433
+ }
434
+ catch (e) {
435
+ console.error(e instanceof Error ? e.message : String(e));
436
+ process.exit(1);
437
+ return;
438
+ }
439
+ if (!["skip", "replace", "keep-both"].includes(conflictRaw)) {
440
+ console.error(`Invalid --conflict value: ${conflictRaw}. Must be skip, replace, or keep-both.`);
441
+ process.exit(1);
442
+ }
443
+ print(await drive.photoDownload(photoPaths, localFolderValidated, conflictRaw));
444
+ }
445
+ else if (sub === "upload") {
446
+ if (positionals.length === 0) {
447
+ console.error("Usage: photo upload <local>... [--conflict X]");
448
+ process.exit(1);
449
+ return;
450
+ }
451
+ const localPaths = [];
452
+ for (const p of positionals) {
453
+ try {
454
+ localPaths.push(validateLocalPath(p));
455
+ }
456
+ catch (e) {
457
+ console.error(e instanceof Error ? e.message : String(e));
458
+ process.exit(1);
459
+ return;
460
+ }
461
+ }
462
+ if (!["skip", "keep-both"].includes(conflictRaw)) {
463
+ console.error(`Invalid --conflict value: ${conflictRaw}. Must be skip or keep-both.`);
464
+ process.exit(1);
465
+ }
466
+ print(await drive.photoUpload(localPaths, conflictRaw));
467
+ }
468
+ else {
469
+ console.error("Usage: photo timeline [--load-details] | photo download <photo>... <local> | photo upload <local>...");
353
470
  process.exit(1);
354
471
  }
355
472
  break;
473
+ }
356
474
  default:
357
475
  console.error(`Unknown command: ${cmd}`);
358
476
  usage();