gdrive-syncer 2.1.2 → 3.0.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 +110 -8
- package/package.json +1 -1
- package/run.js +16 -6
- package/src/envSync.js +455 -45
- package/src/gdriveCmd.js +301 -0
- package/src/list.js +20 -11
- package/src/sync.js +19 -4
- package/gdrive.config.json +0 -16
package/Readme.md
CHANGED
|
@@ -4,15 +4,54 @@ A command line tool to manage sync folders with Google Drive. Features two-way s
|
|
|
4
4
|
|
|
5
5
|
## Prerequisites
|
|
6
6
|
|
|
7
|
-
Install
|
|
7
|
+
Install the gdrive CLI. This tool supports both gdrive@2 and gdrive@3.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
### Option 1: gdrive@3 (recommended)
|
|
10
|
+
|
|
11
|
+
Install [gdrive@3](https://github.com/glotlabs/gdrive) via Homebrew:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
brew install gdrive
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Setup requires Google OAuth credentials.
|
|
18
|
+
|
|
19
|
+
> **Before you start:** Ask your team if someone already has OAuth credentials set up for gdrive. Sharing existing credentials avoids creating duplicate Google Cloud projects and simplifies onboarding.
|
|
20
|
+
|
|
21
|
+
#### Setting up OAuth credentials
|
|
22
|
+
|
|
23
|
+
1. Go to [Google Cloud Console](https://console.cloud.google.com/)
|
|
24
|
+
2. Create a new project or select existing
|
|
25
|
+
3. Enable the Google Drive API
|
|
26
|
+
4. Configure OAuth consent screen:
|
|
27
|
+
- **For organizations (Google Workspace):** Select **Internal** user type. This avoids the app review process and allows immediate use by your team.
|
|
28
|
+
- **For personal accounts:** Select **External** and add your email as a test user.
|
|
29
|
+
5. Create OAuth 2.0 credentials (Desktop application)
|
|
30
|
+
6. Add an account:
|
|
31
|
+
```bash
|
|
32
|
+
gdrive account add
|
|
33
|
+
```
|
|
34
|
+
7. Enter your Client ID and Client Secret when prompted
|
|
35
|
+
8. Complete the OAuth flow in your browser
|
|
36
|
+
|
|
37
|
+
> **Tip:** If you're in an organization, using **Internal** OAuth consent means the app is automatically trusted for all users in your domain—no verification required.
|
|
38
|
+
|
|
39
|
+
**Note:** gdrive@3 does not support `gdrive sync` commands. Use the `filesync:*` commands instead.
|
|
40
|
+
|
|
41
|
+
### Option 2: gdrive@2 (legacy)
|
|
42
|
+
|
|
43
|
+
[gdrive@2](https://github.com/prasmussen/gdrive) is deprecated and no longer available via Homebrew. If you have it installed, login with:
|
|
10
44
|
|
|
11
|
-
After [gdrive](https://github.com/prasmussen/gdrive) is installed, login to your Google Drive account:
|
|
12
45
|
```bash
|
|
13
46
|
gdrive about
|
|
14
47
|
```
|
|
15
48
|
|
|
49
|
+
### Verify Installation
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
gdrive version
|
|
53
|
+
```
|
|
54
|
+
|
|
16
55
|
## Installation
|
|
17
56
|
|
|
18
57
|
```bash
|
|
@@ -33,12 +72,15 @@ Run without arguments for interactive menu, or use direct commands.
|
|
|
33
72
|
|
|
34
73
|
| Command | Description |
|
|
35
74
|
|---------|-------------|
|
|
36
|
-
| `filesync:diff [local\|global]` | Show differences between local and Drive |
|
|
37
|
-
| `filesync:download [local\|global]` | Download changed files from Drive |
|
|
38
|
-
| `filesync:upload [local\|global]` | Upload changed files to Drive |
|
|
75
|
+
| `filesync:diff [local\|global\|registered]` | Show differences between local and Drive |
|
|
76
|
+
| `filesync:download [local\|global\|registered]` | Download changed files from Drive |
|
|
77
|
+
| `filesync:upload [local\|global\|registered]` | Upload changed files to Drive |
|
|
39
78
|
| `filesync:init` | Create or add to `.gdrive-sync.json` config |
|
|
40
79
|
| `filesync:show` | Show sync configurations |
|
|
41
80
|
| `filesync:remove` | Remove a sync from config |
|
|
81
|
+
| `filesync:register` | Register local config to global registry |
|
|
82
|
+
| `filesync:unregister` | Remove from global registry |
|
|
83
|
+
| `filesync:migrate` | Migrate legacy syncs to Files Sync format |
|
|
42
84
|
|
|
43
85
|
### Drive Operations
|
|
44
86
|
|
|
@@ -49,7 +91,9 @@ Run without arguments for interactive menu, or use direct commands.
|
|
|
49
91
|
| `drive:mkdir` | Create a directory in Drive |
|
|
50
92
|
| `drive:delete` | Delete a file/folder from Drive |
|
|
51
93
|
|
|
52
|
-
### Legacy Sync (gdrive
|
|
94
|
+
### Legacy Sync (gdrive@2 only)
|
|
95
|
+
|
|
96
|
+
**Note:** These commands require gdrive@2. They are not available with gdrive@3.
|
|
53
97
|
|
|
54
98
|
| Command | Description |
|
|
55
99
|
|---------|-------------|
|
|
@@ -180,8 +224,40 @@ Patterns support glob-style matching with the following features:
|
|
|
180
224
|
- **Automatic backups** - Creates timestamped backups before download
|
|
181
225
|
- **Two-way sync** - Upload local changes or download from Drive
|
|
182
226
|
- **Sync All** - Run operations on all syncs within selected config
|
|
227
|
+
- **Registered Local Configs** - Run sync on multiple projects from anywhere
|
|
183
228
|
|
|
184
|
-
|
|
229
|
+
### Registered Local Configs
|
|
230
|
+
|
|
231
|
+
Register local configs to a global registry, then run sync operations on multiple projects from any directory.
|
|
232
|
+
|
|
233
|
+
```bash
|
|
234
|
+
# Register current project's local config
|
|
235
|
+
cd ~/projects/my-app
|
|
236
|
+
gdrive-syncer filesync:register
|
|
237
|
+
# Suggests name from directory, stores path in global registry
|
|
238
|
+
|
|
239
|
+
# Later, from any directory
|
|
240
|
+
cd ~/random-place
|
|
241
|
+
gdrive-syncer filesync:diff
|
|
242
|
+
# Shows: Local, Global, and "Registered Local Configs" option
|
|
243
|
+
# Select "Registered Local Configs" → multi-select projects → run diff on all
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
**Commands:**
|
|
247
|
+
- `filesync:register` - Register current local config (auto-suggests directory name)
|
|
248
|
+
- `filesync:unregister` - Remove from registry
|
|
249
|
+
- `filesync:show` - Shows registered configs with ✓/✗ status
|
|
250
|
+
|
|
251
|
+
**Workflow:**
|
|
252
|
+
1. Go to each project directory and run `filesync:register`
|
|
253
|
+
2. From any terminal, run `filesync:diff` (or upload/download)
|
|
254
|
+
3. Select "Registered Local Configs"
|
|
255
|
+
4. Multi-select which projects to sync
|
|
256
|
+
5. Operation runs on all selected projects
|
|
257
|
+
|
|
258
|
+
## Legacy Sync Configuration (gdrive@2 only)
|
|
259
|
+
|
|
260
|
+
> **Note:** Legacy sync requires gdrive@2. If you have gdrive@3 installed, use Files Sync (`filesync:*` commands) instead.
|
|
185
261
|
|
|
186
262
|
Legacy sync uses the `gdrive sync upload` command under the hood. Configuration is stored globally at:
|
|
187
263
|
|
|
@@ -255,10 +331,30 @@ gdrive-syncer sync:upload
|
|
|
255
331
|
gdrive-syncer sync:list
|
|
256
332
|
```
|
|
257
333
|
|
|
334
|
+
## Migrating from Legacy Sync to Files Sync
|
|
335
|
+
|
|
336
|
+
If you have existing legacy sync configurations (`~/.gdrive_syncer/gdrive.config.json`), you can migrate them to the new Files Sync format:
|
|
337
|
+
|
|
338
|
+
```bash
|
|
339
|
+
gdrive-syncer filesync:migrate
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
The migration wizard:
|
|
343
|
+
1. Shows all legacy syncs and lets you select which to migrate
|
|
344
|
+
2. For each sync, asks whether to save to **Local** or **Global** config
|
|
345
|
+
3. For local configs, calculates the relative path from the config location
|
|
346
|
+
4. Warns if the path is outside the project directory (suggests using global instead)
|
|
347
|
+
5. Lets you set a file pattern (default: `*` for all files)
|
|
348
|
+
6. Shows a preview before confirming
|
|
349
|
+
7. Optionally removes migrated syncs from the legacy config
|
|
350
|
+
|
|
351
|
+
This is not a one-time migration—you can run it multiple times to gradually migrate syncs as needed.
|
|
352
|
+
|
|
258
353
|
## Files Sync vs Legacy Sync
|
|
259
354
|
|
|
260
355
|
| Feature | Files Sync | Legacy Sync |
|
|
261
356
|
|---------|----------|-------------|
|
|
357
|
+
| **gdrive version** | gdrive@2 and gdrive@3 | gdrive@2 only |
|
|
262
358
|
| Config location | Local (`.gdrive-sync.json`) or Global (`~/.gdrive_syncer/env-sync.json`) | `~/.gdrive_syncer/gdrive.config.json` (global only) |
|
|
263
359
|
| Direction | Two-way (upload & download) | One-way (upload only) |
|
|
264
360
|
| File patterns | Supported (`.env.*`, `*`) | All files in folder |
|
|
@@ -286,6 +382,9 @@ gdrive-syncer filesync:diff local
|
|
|
286
382
|
# Show differences for global config only
|
|
287
383
|
gdrive-syncer filesync:diff global
|
|
288
384
|
|
|
385
|
+
# Show differences for registered local configs
|
|
386
|
+
gdrive-syncer filesync:diff registered
|
|
387
|
+
|
|
289
388
|
# Download from Drive (with backup)
|
|
290
389
|
gdrive-syncer filesync:download
|
|
291
390
|
gdrive-syncer filesync:download local
|
|
@@ -294,6 +393,9 @@ gdrive-syncer filesync:download local
|
|
|
294
393
|
gdrive-syncer filesync:upload
|
|
295
394
|
gdrive-syncer filesync:upload global
|
|
296
395
|
|
|
396
|
+
# Upload from all registered configs
|
|
397
|
+
gdrive-syncer filesync:upload registered
|
|
398
|
+
|
|
297
399
|
# Initialize config (choose Local or Global)
|
|
298
400
|
gdrive-syncer filesync:init
|
|
299
401
|
|
package/package.json
CHANGED
package/run.js
CHANGED
|
@@ -5,15 +5,15 @@ const color = require('picocolors');
|
|
|
5
5
|
const { cfgAdd, cfgRm, cfgShow } = require('./src/cfgManager');
|
|
6
6
|
const { runSync } = require('./src/sync');
|
|
7
7
|
const { runList, runSearch, runDelete, runMkdir, runListSync } = require('./src/list');
|
|
8
|
-
const { envInit, envRun, envShow, envRemove,
|
|
8
|
+
const { envInit, envRun, envShow, envRemove, envRegister, envUnregister, envMigrate } = require('./src/envSync');
|
|
9
9
|
|
|
10
10
|
const [, , ...args] = process.argv;
|
|
11
11
|
const [firstArg] = args;
|
|
12
12
|
|
|
13
|
-
// Get config type from args (local/global)
|
|
13
|
+
// Get config type from args (local/global/registered)
|
|
14
14
|
const getConfigType = () => {
|
|
15
15
|
const arg = args[1]?.toLowerCase();
|
|
16
|
-
if (arg === 'local' || arg === 'global') return arg;
|
|
16
|
+
if (arg === 'local' || arg === 'global' || arg === 'registered') return arg;
|
|
17
17
|
return null;
|
|
18
18
|
};
|
|
19
19
|
|
|
@@ -27,6 +27,9 @@ const commands = {
|
|
|
27
27
|
'filesync:init': { handler: envInit, desc: 'Create/add sync config' },
|
|
28
28
|
'filesync:show': { handler: envShow, desc: 'Show sync configurations' },
|
|
29
29
|
'filesync:remove': { handler: envRemove, desc: 'Remove sync config' },
|
|
30
|
+
'filesync:register': { handler: envRegister, desc: 'Register local config to global' },
|
|
31
|
+
'filesync:unregister': { handler: envUnregister, desc: 'Unregister local config' },
|
|
32
|
+
'filesync:migrate': { handler: envMigrate, desc: 'Migrate legacy syncs to Files Sync' },
|
|
30
33
|
|
|
31
34
|
// Drive Operations
|
|
32
35
|
'drive:search': { handler: runSearch, desc: 'Search files in Drive' },
|
|
@@ -47,12 +50,15 @@ const showHelp = () => {
|
|
|
47
50
|
const lines = [
|
|
48
51
|
`${color.bold('Files Sync')} ${color.dim('(two-way sync with .gdrive-sync.json)')}`,
|
|
49
52
|
` filesync ${color.dim('Interactive sync menu')}`,
|
|
50
|
-
` filesync:diff [local|global] ${color.dim('Show differences')}`,
|
|
51
|
-
` filesync:download [local|global] ${color.dim('Download from Drive')}`,
|
|
52
|
-
` filesync:upload [local|global] ${color.dim('Upload to Drive')}`,
|
|
53
|
+
` filesync:diff [local|global|registered] ${color.dim('Show differences')}`,
|
|
54
|
+
` filesync:download [local|global|registered] ${color.dim('Download from Drive')}`,
|
|
55
|
+
` filesync:upload [local|global|registered] ${color.dim('Upload to Drive')}`,
|
|
53
56
|
` filesync:init ${color.dim('Create/add sync config')}`,
|
|
54
57
|
` filesync:show ${color.dim('Show configurations')}`,
|
|
55
58
|
` filesync:remove ${color.dim('Remove sync config')}`,
|
|
59
|
+
` filesync:register ${color.dim('Register local config to global')}`,
|
|
60
|
+
` filesync:unregister ${color.dim('Unregister local config')}`,
|
|
61
|
+
` filesync:migrate ${color.dim('Migrate legacy syncs to Files Sync')}`,
|
|
56
62
|
``,
|
|
57
63
|
`${color.bold('Drive Operations')}`,
|
|
58
64
|
` drive:search ${color.dim('Search files')}`,
|
|
@@ -130,6 +136,9 @@ const showHelp = () => {
|
|
|
130
136
|
{ value: 'filesync:init', label: 'Init', hint: 'Create/add to .gdrive-sync.json' },
|
|
131
137
|
{ value: 'filesync:show', label: 'Show', hint: 'Show sync configurations' },
|
|
132
138
|
{ value: 'filesync:remove', label: 'Remove', hint: 'Remove a sync from config' },
|
|
139
|
+
{ value: 'filesync:register', label: 'Register', hint: 'Register local config to global' },
|
|
140
|
+
{ value: 'filesync:unregister', label: 'Unregister', hint: 'Unregister local config' },
|
|
141
|
+
{ value: 'filesync:migrate', label: 'Migrate', hint: 'Migrate legacy syncs to Files Sync' },
|
|
133
142
|
],
|
|
134
143
|
});
|
|
135
144
|
} else if (category === 'drive') {
|
|
@@ -152,6 +161,7 @@ const showHelp = () => {
|
|
|
152
161
|
{ value: 'sync:show', label: 'Show Config', hint: 'View sync configurations' },
|
|
153
162
|
{ value: 'sync:add', label: 'Add Config', hint: 'Add a new sync configuration' },
|
|
154
163
|
{ value: 'sync:remove', label: 'Remove Config', hint: 'Remove a sync configuration' },
|
|
164
|
+
{ value: 'filesync:migrate', label: 'Migrate to Files Sync', hint: 'Migrate to new format' },
|
|
155
165
|
],
|
|
156
166
|
});
|
|
157
167
|
}
|
package/src/envSync.js
CHANGED
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
const shell = require('shelljs');
|
|
4
4
|
const fs = require('fs-extra');
|
|
5
|
+
const gdrive = require('./gdriveCmd');
|
|
5
6
|
const path = require('path');
|
|
6
7
|
const os = require('os');
|
|
7
|
-
const { select, text, isCancel, cancel, spinner, confirm, note, log } = require('@clack/prompts');
|
|
8
|
+
const { select, multiselect, text, isCancel, cancel, spinner, confirm, note, log } = require('@clack/prompts');
|
|
8
9
|
const color = require('picocolors');
|
|
9
10
|
|
|
10
11
|
// Config file names
|
|
@@ -18,6 +19,7 @@ const GLOBAL_CONFIG_FILE = path.join(GLOBAL_CONFIG_DIR, 'env-sync.json');
|
|
|
18
19
|
const defaultConfig = {
|
|
19
20
|
syncs: [],
|
|
20
21
|
backupDir: 'gdrive-backups',
|
|
22
|
+
localRefs: [], // Registered local config references
|
|
21
23
|
};
|
|
22
24
|
|
|
23
25
|
/**
|
|
@@ -359,6 +361,120 @@ const envRemove = async () => {
|
|
|
359
361
|
}
|
|
360
362
|
};
|
|
361
363
|
|
|
364
|
+
/**
|
|
365
|
+
* Register current local config to global registry
|
|
366
|
+
*/
|
|
367
|
+
const envRegister = async () => {
|
|
368
|
+
try {
|
|
369
|
+
const localConfig = findLocalConfig();
|
|
370
|
+
|
|
371
|
+
if (!localConfig) {
|
|
372
|
+
log.error('No local config found in current directory tree.');
|
|
373
|
+
log.info('Run "filesync:init" to create one first.');
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Ensure global config exists
|
|
378
|
+
fs.ensureDirSync(GLOBAL_CONFIG_DIR);
|
|
379
|
+
let globalConfig = { ...defaultConfig };
|
|
380
|
+
if (fs.existsSync(GLOBAL_CONFIG_FILE)) {
|
|
381
|
+
globalConfig = loadConfig(GLOBAL_CONFIG_FILE);
|
|
382
|
+
}
|
|
383
|
+
if (!globalConfig.localRefs) {
|
|
384
|
+
globalConfig.localRefs = [];
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Check if already registered
|
|
388
|
+
const existing = globalConfig.localRefs.find((r) => r.configPath === localConfig.configPath);
|
|
389
|
+
if (existing) {
|
|
390
|
+
log.warn(`Already registered as "${existing.name}"`);
|
|
391
|
+
log.info(`Path: ${localConfig.configPath}`);
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Auto-suggest name from directory
|
|
396
|
+
const dirName = path.basename(localConfig.projectRoot);
|
|
397
|
+
const name = await text({
|
|
398
|
+
message: 'Name for this registered config',
|
|
399
|
+
placeholder: dirName,
|
|
400
|
+
defaultValue: dirName,
|
|
401
|
+
validate: (v) => {
|
|
402
|
+
if (!v.trim()) return 'Name is required';
|
|
403
|
+
if (globalConfig.localRefs.find((r) => r.name === v.trim())) return 'Name already exists';
|
|
404
|
+
},
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
if (isCancel(name)) {
|
|
408
|
+
cancel('Registration cancelled.');
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
globalConfig.localRefs.push({
|
|
413
|
+
name: name.trim(),
|
|
414
|
+
configPath: localConfig.configPath,
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
saveConfig(GLOBAL_CONFIG_FILE, globalConfig);
|
|
418
|
+
|
|
419
|
+
log.success(`Registered "${name}" → ${localConfig.configPath}`);
|
|
420
|
+
} catch (e) {
|
|
421
|
+
log.error(e.message);
|
|
422
|
+
}
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Unregister a local config from global registry
|
|
427
|
+
*/
|
|
428
|
+
const envUnregister = async () => {
|
|
429
|
+
try {
|
|
430
|
+
if (!fs.existsSync(GLOBAL_CONFIG_FILE)) {
|
|
431
|
+
log.error('No global config found.');
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const globalConfig = loadConfig(GLOBAL_CONFIG_FILE);
|
|
436
|
+
const refs = globalConfig.localRefs || [];
|
|
437
|
+
|
|
438
|
+
if (refs.length === 0) {
|
|
439
|
+
log.warn('No registered local configs.');
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const options = refs.map((r) => ({
|
|
444
|
+
value: r.name,
|
|
445
|
+
label: r.name,
|
|
446
|
+
hint: r.configPath,
|
|
447
|
+
}));
|
|
448
|
+
|
|
449
|
+
const picked = await select({
|
|
450
|
+
message: 'Select config to unregister',
|
|
451
|
+
options,
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
if (isCancel(picked)) {
|
|
455
|
+
cancel('Cancelled.');
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const shouldRemove = await confirm({
|
|
460
|
+
message: `Unregister "${picked}"?`,
|
|
461
|
+
initialValue: false,
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
if (isCancel(shouldRemove) || !shouldRemove) {
|
|
465
|
+
cancel('Cancelled.');
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
globalConfig.localRefs = refs.filter((r) => r.name !== picked);
|
|
470
|
+
saveConfig(GLOBAL_CONFIG_FILE, globalConfig);
|
|
471
|
+
|
|
472
|
+
log.success(`Unregistered "${picked}"`);
|
|
473
|
+
} catch (e) {
|
|
474
|
+
log.error(e.message);
|
|
475
|
+
}
|
|
476
|
+
};
|
|
477
|
+
|
|
362
478
|
/**
|
|
363
479
|
* Show current config
|
|
364
480
|
*/
|
|
@@ -427,6 +543,23 @@ const envShow = async () => {
|
|
|
427
543
|
if (!globalConfig) {
|
|
428
544
|
log.info(color.dim('No global config found.'));
|
|
429
545
|
}
|
|
546
|
+
|
|
547
|
+
// Show registered local configs
|
|
548
|
+
if (globalConfig) {
|
|
549
|
+
const gc = loadConfig(globalConfig.configPath);
|
|
550
|
+
const refs = gc.localRefs || [];
|
|
551
|
+
if (refs.length > 0) {
|
|
552
|
+
console.log(''); // spacing
|
|
553
|
+
const lines = refs
|
|
554
|
+
.map((r) => {
|
|
555
|
+
const exists = fs.existsSync(r.configPath);
|
|
556
|
+
const status = exists ? color.green('✓') : color.red('✗ missing');
|
|
557
|
+
return `${color.cyan(r.name.padEnd(20))} ${status} ${color.dim(r.configPath)}`;
|
|
558
|
+
})
|
|
559
|
+
.join('\n');
|
|
560
|
+
note(lines, `Registered Local Configs (${refs.length})`);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
430
563
|
} catch (e) {
|
|
431
564
|
log.error(e.message);
|
|
432
565
|
}
|
|
@@ -581,12 +714,19 @@ const envRun = async (presetAction, presetConfigType) => {
|
|
|
581
714
|
}));
|
|
582
715
|
}
|
|
583
716
|
|
|
584
|
-
|
|
717
|
+
// Load registered local configs
|
|
718
|
+
let registeredRefs = [];
|
|
719
|
+
if (globalConfig) {
|
|
720
|
+
const gc = loadConfig(globalConfig.configPath);
|
|
721
|
+
registeredRefs = (gc.localRefs || []).filter((r) => fs.existsSync(r.configPath));
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
if (localSyncs.length === 0 && globalSyncs.length === 0 && registeredRefs.length === 0) {
|
|
585
725
|
log.warn('No syncs configured. Run "env:init" to add one.');
|
|
586
726
|
return;
|
|
587
727
|
}
|
|
588
728
|
|
|
589
|
-
// First: pick Local
|
|
729
|
+
// First: pick Local, Global, or Registered
|
|
590
730
|
let selectedSyncs = [];
|
|
591
731
|
let configType;
|
|
592
732
|
|
|
@@ -600,35 +740,96 @@ const envRun = async (presetAction, presetConfigType) => {
|
|
|
600
740
|
log.error('No syncs in global config.');
|
|
601
741
|
return;
|
|
602
742
|
}
|
|
743
|
+
if (presetConfigType === 'registered' && registeredRefs.length === 0) {
|
|
744
|
+
log.error('No registered local configs found.');
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
603
747
|
configType = presetConfigType;
|
|
604
748
|
log.info(`Using ${configType} config`);
|
|
605
|
-
} else
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
749
|
+
} else {
|
|
750
|
+
// Build options dynamically
|
|
751
|
+
const configOptions = [];
|
|
752
|
+
if (localSyncs.length > 0) {
|
|
753
|
+
configOptions.push({
|
|
754
|
+
value: 'local',
|
|
755
|
+
label: 'Local',
|
|
756
|
+
hint: `${localSyncs.length} sync(s) in .gdrive-sync.json`,
|
|
757
|
+
});
|
|
758
|
+
}
|
|
759
|
+
if (globalSyncs.length > 0) {
|
|
760
|
+
configOptions.push({
|
|
761
|
+
value: 'global',
|
|
762
|
+
label: 'Global',
|
|
763
|
+
hint: `${globalSyncs.length} sync(s) in ~/.gdrive_syncer/`,
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
if (registeredRefs.length > 0) {
|
|
767
|
+
configOptions.push({
|
|
768
|
+
value: 'registered',
|
|
769
|
+
label: 'Registered Local Configs',
|
|
770
|
+
hint: `${registeredRefs.length} registered project(s)`,
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
if (configOptions.length === 1) {
|
|
775
|
+
configType = configOptions[0].value;
|
|
776
|
+
log.info(`Using ${configType} config`);
|
|
777
|
+
} else {
|
|
778
|
+
configType = await select({
|
|
779
|
+
message: 'Which config?',
|
|
780
|
+
options: configOptions,
|
|
781
|
+
});
|
|
782
|
+
|
|
783
|
+
if (isCancel(configType)) {
|
|
784
|
+
cancel('Operation cancelled.');
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
// Handle registered configs with multi-select
|
|
791
|
+
if (configType === 'registered') {
|
|
792
|
+
const refOptions = registeredRefs.map((r) => ({
|
|
793
|
+
value: r.configPath,
|
|
794
|
+
label: r.name,
|
|
795
|
+
hint: path.dirname(r.configPath),
|
|
796
|
+
}));
|
|
797
|
+
|
|
798
|
+
const selectedRefs = await multiselect({
|
|
799
|
+
message: 'Select configs to run (space to toggle, enter to confirm)',
|
|
800
|
+
options: refOptions,
|
|
801
|
+
required: true,
|
|
620
802
|
});
|
|
621
803
|
|
|
622
|
-
if (isCancel(
|
|
804
|
+
if (isCancel(selectedRefs)) {
|
|
623
805
|
cancel('Operation cancelled.');
|
|
624
806
|
return;
|
|
625
807
|
}
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
808
|
+
|
|
809
|
+
// Load syncs from each selected registered config
|
|
810
|
+
for (const refPath of selectedRefs) {
|
|
811
|
+
const ref = registeredRefs.find((r) => r.configPath === refPath);
|
|
812
|
+
const refConfig = loadConfig(refPath);
|
|
813
|
+
const refProjectRoot = path.dirname(refPath);
|
|
814
|
+
const refSyncs = (refConfig.syncs || []).map((s) => ({
|
|
815
|
+
...s,
|
|
816
|
+
_configType: 'registered',
|
|
817
|
+
_configPath: refPath,
|
|
818
|
+
_projectRoot: refProjectRoot,
|
|
819
|
+
_globalBackupDir: refConfig.backupDir,
|
|
820
|
+
_refName: ref.name,
|
|
821
|
+
}));
|
|
822
|
+
selectedSyncs.push(...refSyncs);
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
// Process all selected syncs
|
|
826
|
+
for (const syncConfig of selectedSyncs) {
|
|
827
|
+
const projectRoot = syncConfig._projectRoot;
|
|
828
|
+
const globalBackupDir = syncConfig._globalBackupDir || 'gdrive-backups';
|
|
829
|
+
const backupPath = path.join(projectRoot, syncConfig.backupDir || globalBackupDir);
|
|
830
|
+
await runSyncOperation(syncConfig, action, projectRoot, backupPath, 'registered');
|
|
831
|
+
}
|
|
832
|
+
return;
|
|
632
833
|
}
|
|
633
834
|
|
|
634
835
|
const syncs = configType === 'local' ? localSyncs : globalSyncs;
|
|
@@ -711,8 +912,9 @@ const runSyncOperation = async (syncConfig, action, projectRoot, backupPath, con
|
|
|
711
912
|
const s = spinner();
|
|
712
913
|
s.start('Fetching files from Google Drive...');
|
|
713
914
|
|
|
714
|
-
const listResult =
|
|
715
|
-
|
|
915
|
+
const listResult = gdrive.list({
|
|
916
|
+
query: `'${folderId}' in parents`,
|
|
917
|
+
noHeader: true,
|
|
716
918
|
});
|
|
717
919
|
|
|
718
920
|
if (listResult.code !== 0) {
|
|
@@ -725,24 +927,18 @@ const runSyncOperation = async (syncConfig, action, projectRoot, backupPath, con
|
|
|
725
927
|
const driveFiles = [];
|
|
726
928
|
const stdout = listResult.stdout.trim();
|
|
727
929
|
if (stdout) {
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
if (parts.length >= 2) {
|
|
733
|
-
const fileId = parts[0].trim();
|
|
734
|
-
const fileName = parts[1].trim();
|
|
735
|
-
if (matchPattern(fileName, pattern, ignore)) {
|
|
736
|
-
driveFiles.push({ id: fileId, name: fileName });
|
|
737
|
-
}
|
|
930
|
+
const parsed = gdrive.parseListOutput(stdout);
|
|
931
|
+
for (const file of parsed) {
|
|
932
|
+
if (file.id && file.name && matchPattern(file.name, pattern, ignore)) {
|
|
933
|
+
driveFiles.push({ id: file.id, name: file.name });
|
|
738
934
|
}
|
|
739
|
-
}
|
|
935
|
+
}
|
|
740
936
|
}
|
|
741
937
|
|
|
742
938
|
// Download Drive files to temp for comparison
|
|
743
939
|
s.message('Downloading Drive files for comparison...');
|
|
744
940
|
for (const file of driveFiles) {
|
|
745
|
-
|
|
941
|
+
gdrive.download(file.id, { destination: tempDir, overwrite: true });
|
|
746
942
|
}
|
|
747
943
|
s.stop(color.green(`Found ${driveFiles.length} matching file(s) on Drive`));
|
|
748
944
|
|
|
@@ -906,17 +1102,13 @@ const runSyncOperation = async (syncConfig, action, projectRoot, backupPath, con
|
|
|
906
1102
|
for (const filename of changes.modified) {
|
|
907
1103
|
const driveFile = driveFiles.find((f) => f.name === filename);
|
|
908
1104
|
if (driveFile) {
|
|
909
|
-
|
|
910
|
-
silent: true,
|
|
911
|
-
});
|
|
1105
|
+
gdrive.update(driveFile.id, path.join(envDir, filename));
|
|
912
1106
|
replaced++;
|
|
913
1107
|
}
|
|
914
1108
|
}
|
|
915
1109
|
|
|
916
1110
|
for (const filename of changes.localOnly) {
|
|
917
|
-
|
|
918
|
-
silent: true,
|
|
919
|
-
});
|
|
1111
|
+
gdrive.upload(path.join(envDir, filename), { parent: folderId });
|
|
920
1112
|
uploaded++;
|
|
921
1113
|
}
|
|
922
1114
|
|
|
@@ -929,11 +1121,229 @@ const runSyncOperation = async (syncConfig, action, projectRoot, backupPath, con
|
|
|
929
1121
|
}
|
|
930
1122
|
};
|
|
931
1123
|
|
|
1124
|
+
/**
|
|
1125
|
+
* Migrate legacy syncs to Files Sync config
|
|
1126
|
+
*/
|
|
1127
|
+
const envMigrate = async () => {
|
|
1128
|
+
try {
|
|
1129
|
+
const { paths } = require('./config');
|
|
1130
|
+
const legacyConfigPath = paths.cfgFile;
|
|
1131
|
+
|
|
1132
|
+
// Check if legacy config exists
|
|
1133
|
+
if (!fs.existsSync(legacyConfigPath)) {
|
|
1134
|
+
log.error('No legacy config found at ~/.gdrive_syncer/gdrive.config.json');
|
|
1135
|
+
return;
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
const legacyConfig = JSON.parse(fs.readFileSync(legacyConfigPath, 'utf-8'));
|
|
1139
|
+
const legacySyncs = legacyConfig.syncs || [];
|
|
1140
|
+
|
|
1141
|
+
if (legacySyncs.length === 0) {
|
|
1142
|
+
log.warn('No syncs found in legacy config.');
|
|
1143
|
+
return;
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
// Show legacy syncs for selection
|
|
1147
|
+
const syncOptions = legacySyncs.map((s) => ({
|
|
1148
|
+
value: s.name,
|
|
1149
|
+
label: s.name,
|
|
1150
|
+
hint: `${s.fullpath} → ${s.driveId.slice(0, 12)}...`,
|
|
1151
|
+
}));
|
|
1152
|
+
|
|
1153
|
+
const selectedNames = await multiselect({
|
|
1154
|
+
message: 'Select syncs to migrate (space to toggle, enter to confirm)',
|
|
1155
|
+
options: syncOptions,
|
|
1156
|
+
required: true,
|
|
1157
|
+
});
|
|
1158
|
+
|
|
1159
|
+
if (isCancel(selectedNames)) {
|
|
1160
|
+
cancel('Migration cancelled.');
|
|
1161
|
+
return;
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
const selectedSyncs = legacySyncs.filter((s) => selectedNames.includes(s.name));
|
|
1165
|
+
|
|
1166
|
+
// Process each selected sync
|
|
1167
|
+
for (const legacySync of selectedSyncs) {
|
|
1168
|
+
console.log('');
|
|
1169
|
+
note(
|
|
1170
|
+
`${color.cyan('Name:')} ${legacySync.name}\n${color.cyan('Path:')} ${legacySync.fullpath}\n${color.cyan('Drive ID:')} ${legacySync.driveId}`,
|
|
1171
|
+
`Migrating: ${legacySync.name}`
|
|
1172
|
+
);
|
|
1173
|
+
|
|
1174
|
+
// Ask: Local or Global?
|
|
1175
|
+
const destination = await select({
|
|
1176
|
+
message: `Where to migrate "${legacySync.name}"?`,
|
|
1177
|
+
options: [
|
|
1178
|
+
{ value: 'local', label: 'Local', hint: '.gdrive-sync.json in a project directory' },
|
|
1179
|
+
{ value: 'global', label: 'Global', hint: '~/.gdrive_syncer/env-sync.json' },
|
|
1180
|
+
],
|
|
1181
|
+
});
|
|
1182
|
+
|
|
1183
|
+
if (isCancel(destination)) {
|
|
1184
|
+
cancel('Migration cancelled.');
|
|
1185
|
+
return;
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
// Get pattern
|
|
1189
|
+
const pattern = await text({
|
|
1190
|
+
message: 'File pattern to sync',
|
|
1191
|
+
placeholder: '*',
|
|
1192
|
+
defaultValue: '*',
|
|
1193
|
+
});
|
|
1194
|
+
|
|
1195
|
+
if (isCancel(pattern)) {
|
|
1196
|
+
cancel('Migration cancelled.');
|
|
1197
|
+
return;
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
let configPath;
|
|
1201
|
+
let localDir;
|
|
1202
|
+
let projectRoot;
|
|
1203
|
+
|
|
1204
|
+
if (destination === 'local') {
|
|
1205
|
+
// Ask where to create/use local config
|
|
1206
|
+
const configLocation = await text({
|
|
1207
|
+
message: 'Project root for local config (where .gdrive-sync.json will be)',
|
|
1208
|
+
placeholder: process.cwd(),
|
|
1209
|
+
defaultValue: process.cwd(),
|
|
1210
|
+
validate: (v) => {
|
|
1211
|
+
if (!v.trim()) return 'Path is required';
|
|
1212
|
+
if (!fs.existsSync(v.trim())) return 'Directory does not exist';
|
|
1213
|
+
},
|
|
1214
|
+
});
|
|
1215
|
+
|
|
1216
|
+
if (isCancel(configLocation)) {
|
|
1217
|
+
cancel('Migration cancelled.');
|
|
1218
|
+
return;
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
projectRoot = configLocation.trim();
|
|
1222
|
+
configPath = path.join(projectRoot, LOCAL_CONFIG_FILE);
|
|
1223
|
+
|
|
1224
|
+
// Calculate relative path
|
|
1225
|
+
const relativePath = path.relative(projectRoot, legacySync.fullpath);
|
|
1226
|
+
|
|
1227
|
+
// Check if path goes outside project
|
|
1228
|
+
if (relativePath.startsWith('..')) {
|
|
1229
|
+
log.warn(`Path "${legacySync.fullpath}" is outside the project root.`);
|
|
1230
|
+
log.info(`Relative path would be: ${relativePath}`);
|
|
1231
|
+
|
|
1232
|
+
const proceed = await confirm({
|
|
1233
|
+
message: 'Use this relative path anyway? (No = switch to global)',
|
|
1234
|
+
initialValue: false,
|
|
1235
|
+
});
|
|
1236
|
+
|
|
1237
|
+
if (isCancel(proceed)) {
|
|
1238
|
+
cancel('Migration cancelled.');
|
|
1239
|
+
return;
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
if (!proceed) {
|
|
1243
|
+
// Switch to global
|
|
1244
|
+
log.info('Switching to global config...');
|
|
1245
|
+
configPath = GLOBAL_CONFIG_FILE;
|
|
1246
|
+
localDir = legacySync.fullpath; // Use absolute path for global
|
|
1247
|
+
projectRoot = null;
|
|
1248
|
+
} else {
|
|
1249
|
+
localDir = relativePath;
|
|
1250
|
+
}
|
|
1251
|
+
} else {
|
|
1252
|
+
localDir = relativePath;
|
|
1253
|
+
}
|
|
1254
|
+
} else {
|
|
1255
|
+
// Global config - use absolute path
|
|
1256
|
+
configPath = GLOBAL_CONFIG_FILE;
|
|
1257
|
+
localDir = legacySync.fullpath;
|
|
1258
|
+
projectRoot = null;
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
// Preview the new sync entry
|
|
1262
|
+
const newSync = {
|
|
1263
|
+
name: legacySync.name,
|
|
1264
|
+
localDir: localDir,
|
|
1265
|
+
folderId: legacySync.driveId,
|
|
1266
|
+
pattern: pattern.trim(),
|
|
1267
|
+
};
|
|
1268
|
+
|
|
1269
|
+
note(
|
|
1270
|
+
`${color.cyan('Name:')} ${newSync.name}\n${color.cyan('Local Dir:')} ${newSync.localDir}\n${color.cyan('Folder ID:')} ${newSync.folderId}\n${color.cyan('Pattern:')} ${newSync.pattern}\n${color.cyan('Config:')} ${configPath}`,
|
|
1271
|
+
'Preview'
|
|
1272
|
+
);
|
|
1273
|
+
|
|
1274
|
+
const confirmMigrate = await confirm({
|
|
1275
|
+
message: 'Add this sync to the config?',
|
|
1276
|
+
initialValue: true,
|
|
1277
|
+
});
|
|
1278
|
+
|
|
1279
|
+
if (isCancel(confirmMigrate) || !confirmMigrate) {
|
|
1280
|
+
log.info(`Skipped "${legacySync.name}"`);
|
|
1281
|
+
continue;
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
// Load or create config
|
|
1285
|
+
let config = { ...defaultConfig };
|
|
1286
|
+
if (fs.existsSync(configPath)) {
|
|
1287
|
+
config = loadConfig(configPath);
|
|
1288
|
+
}
|
|
1289
|
+
if (!config.syncs) {
|
|
1290
|
+
config.syncs = [];
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
// Check for duplicate name
|
|
1294
|
+
const existingIndex = config.syncs.findIndex((s) => s.name === newSync.name);
|
|
1295
|
+
if (existingIndex !== -1) {
|
|
1296
|
+
const overwrite = await confirm({
|
|
1297
|
+
message: `Sync "${newSync.name}" already exists in config. Overwrite?`,
|
|
1298
|
+
initialValue: false,
|
|
1299
|
+
});
|
|
1300
|
+
|
|
1301
|
+
if (isCancel(overwrite)) {
|
|
1302
|
+
cancel('Migration cancelled.');
|
|
1303
|
+
return;
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
if (overwrite) {
|
|
1307
|
+
config.syncs[existingIndex] = newSync;
|
|
1308
|
+
} else {
|
|
1309
|
+
log.info(`Skipped "${legacySync.name}" (already exists)`);
|
|
1310
|
+
continue;
|
|
1311
|
+
}
|
|
1312
|
+
} else {
|
|
1313
|
+
config.syncs.push(newSync);
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
saveConfig(configPath, config);
|
|
1317
|
+
log.success(`Migrated "${legacySync.name}" to ${configPath === GLOBAL_CONFIG_FILE ? 'global' : 'local'} config`);
|
|
1318
|
+
|
|
1319
|
+
// Ask if user wants to remove from legacy config
|
|
1320
|
+
const removeFromLegacy = await confirm({
|
|
1321
|
+
message: 'Remove from legacy config?',
|
|
1322
|
+
initialValue: false,
|
|
1323
|
+
});
|
|
1324
|
+
|
|
1325
|
+
if (!isCancel(removeFromLegacy) && removeFromLegacy) {
|
|
1326
|
+
legacyConfig.syncs = legacyConfig.syncs.filter((s) => s.name !== legacySync.name);
|
|
1327
|
+
fs.writeFileSync(legacyConfigPath, JSON.stringify(legacyConfig, null, 2));
|
|
1328
|
+
log.success(`Removed "${legacySync.name}" from legacy config`);
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
console.log('');
|
|
1333
|
+
log.success('Migration complete!');
|
|
1334
|
+
} catch (e) {
|
|
1335
|
+
log.error(e.message);
|
|
1336
|
+
}
|
|
1337
|
+
};
|
|
1338
|
+
|
|
932
1339
|
module.exports = {
|
|
933
1340
|
envInit,
|
|
934
1341
|
envRun,
|
|
935
1342
|
envShow,
|
|
936
1343
|
envRemove,
|
|
1344
|
+
envRegister,
|
|
1345
|
+
envUnregister,
|
|
1346
|
+
envMigrate,
|
|
937
1347
|
// Exported for testing
|
|
938
1348
|
globToRegex,
|
|
939
1349
|
matchPattern,
|
package/src/gdriveCmd.js
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const shell = require('shelljs');
|
|
4
|
+
|
|
5
|
+
let cachedVersion = null;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Detect gdrive version (2 or 3)
|
|
9
|
+
* gdrive@2: "gdrive v2.1.1"
|
|
10
|
+
* gdrive@3: "gdrive 3.9.1 ..."
|
|
11
|
+
*/
|
|
12
|
+
const detectVersion = () => {
|
|
13
|
+
if (cachedVersion !== null) {
|
|
14
|
+
return cachedVersion;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const result = shell.exec('gdrive version', { silent: true });
|
|
18
|
+
if (result.code !== 0) {
|
|
19
|
+
// Try gdrive about as fallback (gdrive@2)
|
|
20
|
+
const aboutResult = shell.exec('gdrive about', { silent: true });
|
|
21
|
+
if (aboutResult.code === 0) {
|
|
22
|
+
cachedVersion = 2;
|
|
23
|
+
return cachedVersion;
|
|
24
|
+
}
|
|
25
|
+
throw new Error('gdrive not found. Please install gdrive CLI.');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const output = result.stdout.toLowerCase();
|
|
29
|
+
// gdrive@3 outputs: "gdrive 3.x.x ..."
|
|
30
|
+
// gdrive@2 outputs: "gdrive v2.x.x"
|
|
31
|
+
if (output.includes('v2.') || output.includes('gdrive 2.')) {
|
|
32
|
+
cachedVersion = 2;
|
|
33
|
+
} else if (output.match(/gdrive\s+3\./)) {
|
|
34
|
+
cachedVersion = 3;
|
|
35
|
+
} else {
|
|
36
|
+
// Default to 3 for newer versions
|
|
37
|
+
cachedVersion = 3;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return cachedVersion;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Get the detected gdrive version
|
|
45
|
+
*/
|
|
46
|
+
const getVersion = () => detectVersion();
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Check if sync commands are available (only in gdrive@2)
|
|
50
|
+
*/
|
|
51
|
+
const hasSyncSupport = () => detectVersion() === 2;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* List files in Google Drive
|
|
55
|
+
* @param {Object} options
|
|
56
|
+
* @param {string} options.query - Search query
|
|
57
|
+
* @param {number} options.max - Max results
|
|
58
|
+
* @param {boolean} options.noHeader - Skip header row
|
|
59
|
+
* @param {boolean} options.absolute - Show absolute paths (v2 only)
|
|
60
|
+
* @param {string} options.parent - Parent folder ID (v3 only, alternative to query)
|
|
61
|
+
*/
|
|
62
|
+
const list = (options = {}) => {
|
|
63
|
+
const version = detectVersion();
|
|
64
|
+
const { query, max = 30, noHeader = false, absolute = false, parent } = options;
|
|
65
|
+
|
|
66
|
+
let cmd;
|
|
67
|
+
if (version === 2) {
|
|
68
|
+
cmd = 'gdrive list';
|
|
69
|
+
if (max) cmd += ` --max ${max}`;
|
|
70
|
+
if (query) cmd += ` --query "${query}"`;
|
|
71
|
+
if (noHeader) cmd += ' --no-header';
|
|
72
|
+
if (absolute) cmd += ' --absolute';
|
|
73
|
+
} else {
|
|
74
|
+
cmd = 'gdrive files list';
|
|
75
|
+
if (max) cmd += ` --max ${max}`;
|
|
76
|
+
if (parent) {
|
|
77
|
+
cmd += ` --parent ${parent}`;
|
|
78
|
+
} else if (query) {
|
|
79
|
+
cmd += ` --query "${query}"`;
|
|
80
|
+
}
|
|
81
|
+
if (noHeader) cmd += ' --skip-header';
|
|
82
|
+
// Note: --absolute not available in v3
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return shell.exec(cmd, { silent: true });
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Download a file from Google Drive
|
|
90
|
+
* @param {string} fileId - File ID to download
|
|
91
|
+
* @param {Object} options
|
|
92
|
+
* @param {string} options.destination - Download destination path
|
|
93
|
+
* @param {boolean} options.overwrite - Overwrite existing files
|
|
94
|
+
* @param {boolean} options.recursive - Download directory recursively
|
|
95
|
+
*/
|
|
96
|
+
const download = (fileId, options = {}) => {
|
|
97
|
+
const version = detectVersion();
|
|
98
|
+
const { destination, overwrite = false, recursive = false } = options;
|
|
99
|
+
|
|
100
|
+
let cmd;
|
|
101
|
+
if (version === 2) {
|
|
102
|
+
cmd = `gdrive download "${fileId}"`;
|
|
103
|
+
if (destination) cmd += ` --path "${destination}"`;
|
|
104
|
+
if (overwrite) cmd += ' --force';
|
|
105
|
+
if (recursive) cmd += ' -r';
|
|
106
|
+
} else {
|
|
107
|
+
cmd = `gdrive files download "${fileId}"`;
|
|
108
|
+
if (destination) cmd += ` --destination "${destination}"`;
|
|
109
|
+
if (overwrite) cmd += ' --overwrite';
|
|
110
|
+
if (recursive) cmd += ' --recursive';
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return shell.exec(cmd, { silent: true });
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Upload a file to Google Drive
|
|
118
|
+
* @param {string} filePath - Local file path to upload
|
|
119
|
+
* @param {Object} options
|
|
120
|
+
* @param {string} options.parent - Parent folder ID
|
|
121
|
+
* @param {boolean} options.recursive - Upload directory recursively
|
|
122
|
+
*/
|
|
123
|
+
const upload = (filePath, options = {}) => {
|
|
124
|
+
const version = detectVersion();
|
|
125
|
+
const { parent, recursive = false } = options;
|
|
126
|
+
|
|
127
|
+
let cmd;
|
|
128
|
+
if (version === 2) {
|
|
129
|
+
cmd = `gdrive upload "${filePath}"`;
|
|
130
|
+
if (parent) cmd += ` --parent "${parent}"`;
|
|
131
|
+
if (recursive) cmd += ' -r';
|
|
132
|
+
} else {
|
|
133
|
+
cmd = `gdrive files upload "${filePath}"`;
|
|
134
|
+
if (parent) cmd += ` --parent "${parent}"`;
|
|
135
|
+
if (recursive) cmd += ' --recursive';
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return shell.exec(cmd, { silent: true });
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Update an existing file on Google Drive
|
|
143
|
+
* @param {string} fileId - File ID to update
|
|
144
|
+
* @param {string} filePath - Local file path with new content
|
|
145
|
+
*/
|
|
146
|
+
const update = (fileId, filePath) => {
|
|
147
|
+
const version = detectVersion();
|
|
148
|
+
|
|
149
|
+
let cmd;
|
|
150
|
+
if (version === 2) {
|
|
151
|
+
cmd = `gdrive update "${fileId}" "${filePath}"`;
|
|
152
|
+
} else {
|
|
153
|
+
cmd = `gdrive files update "${fileId}" "${filePath}"`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return shell.exec(cmd, { silent: true });
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Create a directory on Google Drive
|
|
161
|
+
* @param {string} name - Directory name
|
|
162
|
+
* @param {Object} options
|
|
163
|
+
* @param {string} options.parent - Parent folder ID
|
|
164
|
+
*/
|
|
165
|
+
const mkdir = (name, options = {}) => {
|
|
166
|
+
const version = detectVersion();
|
|
167
|
+
const { parent } = options;
|
|
168
|
+
|
|
169
|
+
let cmd;
|
|
170
|
+
if (version === 2) {
|
|
171
|
+
if (parent) {
|
|
172
|
+
cmd = `gdrive mkdir -p ${parent} "${name}"`;
|
|
173
|
+
} else {
|
|
174
|
+
cmd = `gdrive mkdir "${name}"`;
|
|
175
|
+
}
|
|
176
|
+
} else {
|
|
177
|
+
cmd = `gdrive files mkdir "${name}"`;
|
|
178
|
+
if (parent) cmd += ` --parent ${parent}`;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return shell.exec(cmd, { silent: true });
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Delete a file or directory on Google Drive
|
|
186
|
+
* @param {string} fileId - File/folder ID to delete
|
|
187
|
+
* @param {Object} options
|
|
188
|
+
* @param {boolean} options.recursive - Delete directory recursively
|
|
189
|
+
*/
|
|
190
|
+
const remove = (fileId, options = {}) => {
|
|
191
|
+
const version = detectVersion();
|
|
192
|
+
const { recursive = false } = options;
|
|
193
|
+
|
|
194
|
+
let cmd;
|
|
195
|
+
if (version === 2) {
|
|
196
|
+
cmd = `gdrive delete ${fileId}`;
|
|
197
|
+
if (recursive) cmd += ' -r';
|
|
198
|
+
} else {
|
|
199
|
+
cmd = `gdrive files delete ${fileId}`;
|
|
200
|
+
if (recursive) cmd += ' --recursive';
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return shell.exec(cmd, { silent: true });
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Sync upload (gdrive@2 only) - uploads local to drive
|
|
208
|
+
* @param {string} localPath - Local directory path
|
|
209
|
+
* @param {string} driveId - Drive folder ID
|
|
210
|
+
* @param {Object} options
|
|
211
|
+
* @param {boolean} options.dryRun - Preview changes without applying
|
|
212
|
+
* @param {boolean} options.keepLocal - Don't delete local files
|
|
213
|
+
* @param {boolean} options.deleteExtraneous - Delete files on drive not in local
|
|
214
|
+
*/
|
|
215
|
+
const syncUpload = (localPath, driveId, options = {}) => {
|
|
216
|
+
const version = detectVersion();
|
|
217
|
+
const { dryRun = false, keepLocal = true, deleteExtraneous = false } = options;
|
|
218
|
+
|
|
219
|
+
if (version !== 2) {
|
|
220
|
+
return {
|
|
221
|
+
code: 1,
|
|
222
|
+
stdout: '',
|
|
223
|
+
stderr: 'gdrive sync commands are not available in gdrive@3. Use envSync instead.',
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
let cmd = `gdrive sync upload`;
|
|
228
|
+
if (dryRun) cmd += ' --dry-run';
|
|
229
|
+
if (keepLocal) cmd += ' --keep-local';
|
|
230
|
+
if (deleteExtraneous) cmd += ' --delete-extraneous';
|
|
231
|
+
cmd += ` "${localPath}" ${driveId}`;
|
|
232
|
+
|
|
233
|
+
return shell.exec(cmd, { silent: true });
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* List sync tasks (gdrive@2 only)
|
|
238
|
+
*/
|
|
239
|
+
const syncList = () => {
|
|
240
|
+
const version = detectVersion();
|
|
241
|
+
|
|
242
|
+
if (version !== 2) {
|
|
243
|
+
return {
|
|
244
|
+
code: 1,
|
|
245
|
+
stdout: '',
|
|
246
|
+
stderr: 'gdrive sync commands are not available in gdrive@3.',
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return shell.exec('gdrive sync list', { silent: true });
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Parse list output into structured data
|
|
255
|
+
* Works with both v2 and v3 output formats
|
|
256
|
+
* @param {string} stdout - Raw stdout from list command
|
|
257
|
+
* @returns {Array<{id: string, name: string, type: string, size: string, date: string}>}
|
|
258
|
+
*/
|
|
259
|
+
const parseListOutput = (stdout) => {
|
|
260
|
+
const lines = stdout.trim().split('\n').filter((line) => line.trim());
|
|
261
|
+
|
|
262
|
+
if (lines.length === 0) return [];
|
|
263
|
+
|
|
264
|
+
// Both v2 and v3 use space-padded columns for alignment
|
|
265
|
+
// Split by 2+ whitespace characters to handle this
|
|
266
|
+
const separator = /\s{2,}/;
|
|
267
|
+
|
|
268
|
+
return lines.map((line) => {
|
|
269
|
+
const parts = line.trim().split(separator);
|
|
270
|
+
return {
|
|
271
|
+
id: parts[0] || '',
|
|
272
|
+
name: parts[1] || '',
|
|
273
|
+
type: parts[2] || '',
|
|
274
|
+
size: parts[3] || '',
|
|
275
|
+
date: parts[4] || '',
|
|
276
|
+
};
|
|
277
|
+
});
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Clear the cached version (useful for testing)
|
|
282
|
+
*/
|
|
283
|
+
const clearCache = () => {
|
|
284
|
+
cachedVersion = null;
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
module.exports = {
|
|
288
|
+
detectVersion,
|
|
289
|
+
getVersion,
|
|
290
|
+
hasSyncSupport,
|
|
291
|
+
list,
|
|
292
|
+
download,
|
|
293
|
+
upload,
|
|
294
|
+
update,
|
|
295
|
+
mkdir,
|
|
296
|
+
remove,
|
|
297
|
+
syncUpload,
|
|
298
|
+
syncList,
|
|
299
|
+
parseListOutput,
|
|
300
|
+
clearCache,
|
|
301
|
+
};
|
package/src/list.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const shell = require('shelljs');
|
|
4
4
|
const { text, isCancel, cancel, spinner, confirm, log } = require('@clack/prompts');
|
|
5
5
|
const color = require('picocolors');
|
|
6
|
+
const gdrive = require('./gdriveCmd');
|
|
6
7
|
|
|
7
8
|
const runSearch = async () => {
|
|
8
9
|
try {
|
|
@@ -38,10 +39,11 @@ const runSearch = async () => {
|
|
|
38
39
|
const s = spinner();
|
|
39
40
|
s.start(`Searching for "${search}"...`);
|
|
40
41
|
|
|
41
|
-
const result =
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
const result = gdrive.list({
|
|
43
|
+
max: maxResults,
|
|
44
|
+
query: `name contains '${search.trim()}'`,
|
|
45
|
+
absolute: true, // Only works with gdrive@2
|
|
46
|
+
});
|
|
45
47
|
|
|
46
48
|
s.stop(color.green('Search complete!'));
|
|
47
49
|
console.log(result.stdout);
|
|
@@ -88,10 +90,11 @@ const runList = async () => {
|
|
|
88
90
|
const s = spinner();
|
|
89
91
|
s.start('Fetching folder contents...');
|
|
90
92
|
|
|
91
|
-
const result =
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
93
|
+
const result = gdrive.list({
|
|
94
|
+
max: maxResults,
|
|
95
|
+
query: `parents in '${driveId.trim()}'`,
|
|
96
|
+
absolute: true, // Only works with gdrive@2
|
|
97
|
+
});
|
|
95
98
|
|
|
96
99
|
s.stop(color.green('Done!'));
|
|
97
100
|
console.log(result.stdout);
|
|
@@ -106,10 +109,16 @@ const runList = async () => {
|
|
|
106
109
|
|
|
107
110
|
const runListSync = async () => {
|
|
108
111
|
try {
|
|
112
|
+
if (!gdrive.hasSyncSupport()) {
|
|
113
|
+
log.warn('gdrive sync commands are not available in gdrive@3.');
|
|
114
|
+
log.info('Use "gdrive-syncer env" for file sync functionality instead.');
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
109
118
|
const s = spinner();
|
|
110
119
|
s.start('Fetching sync list...');
|
|
111
120
|
|
|
112
|
-
const result =
|
|
121
|
+
const result = gdrive.syncList();
|
|
113
122
|
|
|
114
123
|
s.stop(color.green('Done!'));
|
|
115
124
|
console.log(result.stdout);
|
|
@@ -153,7 +162,7 @@ const runMkdir = async () => {
|
|
|
153
162
|
const s = spinner();
|
|
154
163
|
s.start(`Creating folder "${folder}"...`);
|
|
155
164
|
|
|
156
|
-
const result =
|
|
165
|
+
const result = gdrive.mkdir(folder.trim(), { parent: parentId.trim() });
|
|
157
166
|
|
|
158
167
|
s.stop(color.green(`Folder "${folder}" created!`));
|
|
159
168
|
console.log(result.stdout);
|
|
@@ -194,7 +203,7 @@ const runDelete = async () => {
|
|
|
194
203
|
const s = spinner();
|
|
195
204
|
s.start('Deleting...');
|
|
196
205
|
|
|
197
|
-
const result =
|
|
206
|
+
const result = gdrive.remove(driveId.trim(), { recursive: true });
|
|
198
207
|
|
|
199
208
|
s.stop(color.green('Deleted successfully!'));
|
|
200
209
|
console.log(result.stdout);
|
package/src/sync.js
CHANGED
|
@@ -6,12 +6,10 @@ const os = require('os');
|
|
|
6
6
|
const { select, isCancel, cancel, spinner, note, log, confirm } = require('@clack/prompts');
|
|
7
7
|
const color = require('picocolors');
|
|
8
8
|
const { getCfgFile } = require('./helpers');
|
|
9
|
+
const gdrive = require('./gdriveCmd');
|
|
9
10
|
|
|
10
11
|
const homedir = os.homedir();
|
|
11
12
|
|
|
12
|
-
const getQuery = ({ dry, pth, dId }) =>
|
|
13
|
-
`gdrive sync upload ${dry ? '--dry-run' : ''} --keep-local --delete-extraneous ${pth} ${dId}`;
|
|
14
|
-
|
|
15
13
|
const syncOne = (cfg, dryRun) => {
|
|
16
14
|
const fullpath = cfg.fullpath;
|
|
17
15
|
const driveId = cfg.driveId;
|
|
@@ -29,7 +27,14 @@ const syncOne = (cfg, dryRun) => {
|
|
|
29
27
|
return;
|
|
30
28
|
}
|
|
31
29
|
|
|
32
|
-
|
|
30
|
+
const result = gdrive.syncUpload(fullpath, driveId, {
|
|
31
|
+
dryRun: !!dryRun,
|
|
32
|
+
keepLocal: true,
|
|
33
|
+
deleteExtraneous: true,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
if (result.stdout) console.log(result.stdout);
|
|
37
|
+
if (result.stderr) log.error(result.stderr);
|
|
33
38
|
};
|
|
34
39
|
|
|
35
40
|
const syncAll = (syncs, dryRun) => {
|
|
@@ -44,6 +49,16 @@ const syncAll = (syncs, dryRun) => {
|
|
|
44
49
|
|
|
45
50
|
const runSync = async (dryRun) => {
|
|
46
51
|
try {
|
|
52
|
+
// Check if sync commands are available (gdrive@2 only)
|
|
53
|
+
if (!gdrive.hasSyncSupport()) {
|
|
54
|
+
log.error('gdrive sync commands are not available in gdrive@3.');
|
|
55
|
+
log.info('');
|
|
56
|
+
log.info('Options:');
|
|
57
|
+
log.info(' 1. Use "gdrive-syncer env" for file sync functionality (recommended)');
|
|
58
|
+
log.info(' 2. Install gdrive@2 if you need legacy sync support');
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
47
62
|
note(
|
|
48
63
|
dryRun ? color.yellow('Running Dry Run') : color.green('Actual Sync'),
|
|
49
64
|
dryRun ? 'Preview Mode' : 'Upload Mode'
|
package/gdrive.config.json
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"syncs": [
|
|
3
|
-
{
|
|
4
|
-
"localpath": "project_envs/php-sayhey",
|
|
5
|
-
"driveId": "1jN1oOZKETwQL4FyoEoCCLlqBwLmYJLRd"
|
|
6
|
-
},
|
|
7
|
-
{
|
|
8
|
-
"localpath": "project_envs/sayhey",
|
|
9
|
-
"driveId": "1movI2Vgu2OMFRUl_aWvJqeQ65c_LZJG8"
|
|
10
|
-
},
|
|
11
|
-
{
|
|
12
|
-
"localpath": "project_envs/pipeline-test",
|
|
13
|
-
"driveId": "19rP3yIsLNQ2V4ViahgGyK-BJu60W6rEp"
|
|
14
|
-
}
|
|
15
|
-
]
|
|
16
|
-
}
|