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 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 and setup [gdrive](https://github.com/prasmussen/gdrive)
7
+ Install the gdrive CLI. This tool supports both gdrive@2 and gdrive@3.
8
8
 
9
- ## Login
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 sync upload)
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
- ## Legacy Sync Configuration
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdrive-syncer",
3
- "version": "2.1.2",
3
+ "version": "3.0.0",
4
4
  "description": "Google Drive Syncer",
5
5
  "main": "./index.js",
6
6
  "bin": "./run.js",
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, envDiff, envDownload, envUpload } = require('./src/envSync');
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
- if (localSyncs.length === 0 && globalSyncs.length === 0) {
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 or Global (if both exist)
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 if (localSyncs.length > 0 && globalSyncs.length > 0) {
606
- configType = await select({
607
- message: 'Which config?',
608
- options: [
609
- {
610
- value: 'local',
611
- label: 'Local',
612
- hint: `${localSyncs.length} sync(s) in .gdrive-sync.json`,
613
- },
614
- {
615
- value: 'global',
616
- label: 'Global',
617
- hint: `${globalSyncs.length} sync(s) in ~/.gdrive_syncer/`,
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(configType)) {
804
+ if (isCancel(selectedRefs)) {
623
805
  cancel('Operation cancelled.');
624
806
  return;
625
807
  }
626
- } else if (localSyncs.length > 0) {
627
- configType = 'local';
628
- log.info(`Using local config`);
629
- } else {
630
- configType = 'global';
631
- log.info(`Using global config`);
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 = shell.exec(`gdrive list -q "'${folderId}' in parents" --no-header`, {
715
- silent: true,
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
- stdout.split('\n').forEach((line) => {
729
- // gdrive output format: ID NAME TYPE SIZE DATE
730
- // Split by 2+ spaces to handle filenames with single spaces
731
- const parts = line.trim().split(/\s{2,}/);
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
- shell.exec(`gdrive download "${file.id}" --path "${tempDir}" --force`, { silent: true });
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
- shell.exec(`gdrive update "${driveFile.id}" "${path.join(envDir, filename)}"`, {
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
- shell.exec(`gdrive upload "${path.join(envDir, filename)}" --parent "${folderId}"`, {
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,
@@ -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 = shell.exec(
42
- `gdrive list --max ${maxResults} --query "name contains '${search.trim()}'" --absolute`,
43
- { silent: true }
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 = shell.exec(
92
- `gdrive list --max ${maxResults} --query "parents in '${driveId.trim()}'" --absolute`,
93
- { silent: true }
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 = shell.exec('gdrive sync list', { silent: true });
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 = shell.exec(`gdrive mkdir -p ${parentId.trim()} ${folder.trim()}`, { silent: true });
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 = shell.exec(`gdrive delete -r ${driveId.trim()}`, { silent: true });
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
- shell.exec(getQuery({ dry: !!dryRun, dId: driveId, pth: fullpath }));
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'
@@ -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
- }