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 +38 -0
- package/README.md +54 -22
- package/dist/cli.js +151 -33
- package/dist/index.js +315 -17
- package/dist/services/drive.js +164 -40
- package/dist/utils/subprocess.js +49 -27
- package/dist/utils/validation.js +13 -0
- package/glama.json +36 -0
- package/package.json +1 -1
- package/smithery.yaml +9 -0
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
|
|
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 #
|
|
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
|
|
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
|
|
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
|
|
189
|
-
proton-drive-cli trash list
|
|
190
|
-
proton-drive-cli restore /my-files/old-draft.pdf
|
|
191
|
-
proton-drive-cli trash empty
|
|
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
|
|
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
|
|
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/
|
|
250
|
-
| `drive_download` | Download to local path | `remotePath`, `localPath` |
|
|
251
|
-
| `
|
|
252
|
-
| `
|
|
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`
|
|
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
|
|
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
|
-
*
|
|
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/
|
|
45
|
-
download <remote> <local>
|
|
46
|
-
|
|
47
|
-
|
|
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
|
|
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
|
|
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", "
|
|
164
|
-
console.error(`Invalid --conflict value: ${conflictRaw}. Must be skip,
|
|
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
|
-
|
|
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
|
|
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();
|