gdrive-syncer 2.1.2 → 2.2.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
@@ -39,6 +39,8 @@ Run without arguments for interactive menu, or use direct commands.
39
39
  | `filesync:init` | Create or add to `.gdrive-sync.json` config |
40
40
  | `filesync:show` | Show sync configurations |
41
41
  | `filesync:remove` | Remove a sync from config |
42
+ | `filesync:register` | Register local config to global registry |
43
+ | `filesync:unregister` | Remove from global registry |
42
44
 
43
45
  ### Drive Operations
44
46
 
@@ -180,6 +182,36 @@ Patterns support glob-style matching with the following features:
180
182
  - **Automatic backups** - Creates timestamped backups before download
181
183
  - **Two-way sync** - Upload local changes or download from Drive
182
184
  - **Sync All** - Run operations on all syncs within selected config
185
+ - **Registered Local Configs** - Run sync on multiple projects from anywhere
186
+
187
+ ### Registered Local Configs
188
+
189
+ Register local configs to a global registry, then run sync operations on multiple projects from any directory.
190
+
191
+ ```bash
192
+ # Register current project's local config
193
+ cd ~/projects/my-app
194
+ gdrive-syncer filesync:register
195
+ # Suggests name from directory, stores path in global registry
196
+
197
+ # Later, from any directory
198
+ cd ~/random-place
199
+ gdrive-syncer filesync:diff
200
+ # Shows: Local, Global, and "Registered Local Configs" option
201
+ # Select "Registered Local Configs" → multi-select projects → run diff on all
202
+ ```
203
+
204
+ **Commands:**
205
+ - `filesync:register` - Register current local config (auto-suggests directory name)
206
+ - `filesync:unregister` - Remove from registry
207
+ - `filesync:show` - Shows registered configs with ✓/✗ status
208
+
209
+ **Workflow:**
210
+ 1. Go to each project directory and run `filesync:register`
211
+ 2. From any terminal, run `filesync:diff` (or upload/download)
212
+ 3. Select "Registered Local Configs"
213
+ 4. Multi-select which projects to sync
214
+ 5. Operation runs on all selected projects
183
215
 
184
216
  ## Legacy Sync Configuration
185
217
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdrive-syncer",
3
- "version": "2.1.2",
3
+ "version": "2.2.0",
4
4
  "description": "Google Drive Syncer",
5
5
  "main": "./index.js",
6
6
  "bin": "./run.js",
package/run.js CHANGED
@@ -5,7 +5,7 @@ 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 } = require('./src/envSync');
9
9
 
10
10
  const [, , ...args] = process.argv;
11
11
  const [firstArg] = args;
@@ -27,6 +27,8 @@ 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' },
30
32
 
31
33
  // Drive Operations
32
34
  'drive:search': { handler: runSearch, desc: 'Search files in Drive' },
@@ -53,6 +55,8 @@ const showHelp = () => {
53
55
  ` filesync:init ${color.dim('Create/add sync config')}`,
54
56
  ` filesync:show ${color.dim('Show configurations')}`,
55
57
  ` filesync:remove ${color.dim('Remove sync config')}`,
58
+ ` filesync:register ${color.dim('Register local config to global')}`,
59
+ ` filesync:unregister ${color.dim('Unregister local config')}`,
56
60
  ``,
57
61
  `${color.bold('Drive Operations')}`,
58
62
  ` drive:search ${color.dim('Search files')}`,
@@ -130,6 +134,8 @@ const showHelp = () => {
130
134
  { value: 'filesync:init', label: 'Init', hint: 'Create/add to .gdrive-sync.json' },
131
135
  { value: 'filesync:show', label: 'Show', hint: 'Show sync configurations' },
132
136
  { value: 'filesync:remove', label: 'Remove', hint: 'Remove a sync from config' },
137
+ { value: 'filesync:register', label: 'Register', hint: 'Register local config to global' },
138
+ { value: 'filesync:unregister', label: 'Unregister', hint: 'Unregister local config' },
133
139
  ],
134
140
  });
135
141
  } else if (category === 'drive') {
package/src/envSync.js CHANGED
@@ -4,7 +4,7 @@ const shell = require('shelljs');
4
4
  const fs = require('fs-extra');
5
5
  const path = require('path');
6
6
  const os = require('os');
7
- const { select, text, isCancel, cancel, spinner, confirm, note, log } = require('@clack/prompts');
7
+ const { select, multiselect, text, isCancel, cancel, spinner, confirm, note, log } = require('@clack/prompts');
8
8
  const color = require('picocolors');
9
9
 
10
10
  // Config file names
@@ -18,6 +18,7 @@ const GLOBAL_CONFIG_FILE = path.join(GLOBAL_CONFIG_DIR, 'env-sync.json');
18
18
  const defaultConfig = {
19
19
  syncs: [],
20
20
  backupDir: 'gdrive-backups',
21
+ localRefs: [], // Registered local config references
21
22
  };
22
23
 
23
24
  /**
@@ -359,6 +360,120 @@ const envRemove = async () => {
359
360
  }
360
361
  };
361
362
 
363
+ /**
364
+ * Register current local config to global registry
365
+ */
366
+ const envRegister = async () => {
367
+ try {
368
+ const localConfig = findLocalConfig();
369
+
370
+ if (!localConfig) {
371
+ log.error('No local config found in current directory tree.');
372
+ log.info('Run "filesync:init" to create one first.');
373
+ return;
374
+ }
375
+
376
+ // Ensure global config exists
377
+ fs.ensureDirSync(GLOBAL_CONFIG_DIR);
378
+ let globalConfig = { ...defaultConfig };
379
+ if (fs.existsSync(GLOBAL_CONFIG_FILE)) {
380
+ globalConfig = loadConfig(GLOBAL_CONFIG_FILE);
381
+ }
382
+ if (!globalConfig.localRefs) {
383
+ globalConfig.localRefs = [];
384
+ }
385
+
386
+ // Check if already registered
387
+ const existing = globalConfig.localRefs.find((r) => r.configPath === localConfig.configPath);
388
+ if (existing) {
389
+ log.warn(`Already registered as "${existing.name}"`);
390
+ log.info(`Path: ${localConfig.configPath}`);
391
+ return;
392
+ }
393
+
394
+ // Auto-suggest name from directory
395
+ const dirName = path.basename(localConfig.projectRoot);
396
+ const name = await text({
397
+ message: 'Name for this registered config',
398
+ placeholder: dirName,
399
+ defaultValue: dirName,
400
+ validate: (v) => {
401
+ if (!v.trim()) return 'Name is required';
402
+ if (globalConfig.localRefs.find((r) => r.name === v.trim())) return 'Name already exists';
403
+ },
404
+ });
405
+
406
+ if (isCancel(name)) {
407
+ cancel('Registration cancelled.');
408
+ return;
409
+ }
410
+
411
+ globalConfig.localRefs.push({
412
+ name: name.trim(),
413
+ configPath: localConfig.configPath,
414
+ });
415
+
416
+ saveConfig(GLOBAL_CONFIG_FILE, globalConfig);
417
+
418
+ log.success(`Registered "${name}" → ${localConfig.configPath}`);
419
+ } catch (e) {
420
+ log.error(e.message);
421
+ }
422
+ };
423
+
424
+ /**
425
+ * Unregister a local config from global registry
426
+ */
427
+ const envUnregister = async () => {
428
+ try {
429
+ if (!fs.existsSync(GLOBAL_CONFIG_FILE)) {
430
+ log.error('No global config found.');
431
+ return;
432
+ }
433
+
434
+ const globalConfig = loadConfig(GLOBAL_CONFIG_FILE);
435
+ const refs = globalConfig.localRefs || [];
436
+
437
+ if (refs.length === 0) {
438
+ log.warn('No registered local configs.');
439
+ return;
440
+ }
441
+
442
+ const options = refs.map((r) => ({
443
+ value: r.name,
444
+ label: r.name,
445
+ hint: r.configPath,
446
+ }));
447
+
448
+ const picked = await select({
449
+ message: 'Select config to unregister',
450
+ options,
451
+ });
452
+
453
+ if (isCancel(picked)) {
454
+ cancel('Cancelled.');
455
+ return;
456
+ }
457
+
458
+ const shouldRemove = await confirm({
459
+ message: `Unregister "${picked}"?`,
460
+ initialValue: false,
461
+ });
462
+
463
+ if (isCancel(shouldRemove) || !shouldRemove) {
464
+ cancel('Cancelled.');
465
+ return;
466
+ }
467
+
468
+ globalConfig.localRefs = refs.filter((r) => r.name !== picked);
469
+ saveConfig(GLOBAL_CONFIG_FILE, globalConfig);
470
+
471
+ log.success(`Unregistered "${picked}"`);
472
+ } catch (e) {
473
+ log.error(e.message);
474
+ }
475
+ };
476
+
362
477
  /**
363
478
  * Show current config
364
479
  */
@@ -427,6 +542,23 @@ const envShow = async () => {
427
542
  if (!globalConfig) {
428
543
  log.info(color.dim('No global config found.'));
429
544
  }
545
+
546
+ // Show registered local configs
547
+ if (globalConfig) {
548
+ const gc = loadConfig(globalConfig.configPath);
549
+ const refs = gc.localRefs || [];
550
+ if (refs.length > 0) {
551
+ console.log(''); // spacing
552
+ const lines = refs
553
+ .map((r) => {
554
+ const exists = fs.existsSync(r.configPath);
555
+ const status = exists ? color.green('✓') : color.red('✗ missing');
556
+ return `${color.cyan(r.name.padEnd(20))} ${status} ${color.dim(r.configPath)}`;
557
+ })
558
+ .join('\n');
559
+ note(lines, `Registered Local Configs (${refs.length})`);
560
+ }
561
+ }
430
562
  } catch (e) {
431
563
  log.error(e.message);
432
564
  }
@@ -581,12 +713,19 @@ const envRun = async (presetAction, presetConfigType) => {
581
713
  }));
582
714
  }
583
715
 
584
- if (localSyncs.length === 0 && globalSyncs.length === 0) {
716
+ // Load registered local configs
717
+ let registeredRefs = [];
718
+ if (globalConfig) {
719
+ const gc = loadConfig(globalConfig.configPath);
720
+ registeredRefs = (gc.localRefs || []).filter((r) => fs.existsSync(r.configPath));
721
+ }
722
+
723
+ if (localSyncs.length === 0 && globalSyncs.length === 0 && registeredRefs.length === 0) {
585
724
  log.warn('No syncs configured. Run "env:init" to add one.');
586
725
  return;
587
726
  }
588
727
 
589
- // First: pick Local or Global (if both exist)
728
+ // First: pick Local, Global, or Registered
590
729
  let selectedSyncs = [];
591
730
  let configType;
592
731
 
@@ -602,33 +741,90 @@ const envRun = async (presetAction, presetConfigType) => {
602
741
  }
603
742
  configType = presetConfigType;
604
743
  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
- ],
744
+ } else {
745
+ // Build options dynamically
746
+ const configOptions = [];
747
+ if (localSyncs.length > 0) {
748
+ configOptions.push({
749
+ value: 'local',
750
+ label: 'Local',
751
+ hint: `${localSyncs.length} sync(s) in .gdrive-sync.json`,
752
+ });
753
+ }
754
+ if (globalSyncs.length > 0) {
755
+ configOptions.push({
756
+ value: 'global',
757
+ label: 'Global',
758
+ hint: `${globalSyncs.length} sync(s) in ~/.gdrive_syncer/`,
759
+ });
760
+ }
761
+ if (registeredRefs.length > 0) {
762
+ configOptions.push({
763
+ value: 'registered',
764
+ label: 'Registered Local Configs',
765
+ hint: `${registeredRefs.length} registered project(s)`,
766
+ });
767
+ }
768
+
769
+ if (configOptions.length === 1) {
770
+ configType = configOptions[0].value;
771
+ log.info(`Using ${configType} config`);
772
+ } else {
773
+ configType = await select({
774
+ message: 'Which config?',
775
+ options: configOptions,
776
+ });
777
+
778
+ if (isCancel(configType)) {
779
+ cancel('Operation cancelled.');
780
+ return;
781
+ }
782
+ }
783
+ }
784
+
785
+ // Handle registered configs with multi-select
786
+ if (configType === 'registered') {
787
+ const refOptions = registeredRefs.map((r) => ({
788
+ value: r.configPath,
789
+ label: r.name,
790
+ hint: path.dirname(r.configPath),
791
+ }));
792
+
793
+ const selectedRefs = await multiselect({
794
+ message: 'Select configs to run (space to toggle, enter to confirm)',
795
+ options: refOptions,
796
+ required: true,
620
797
  });
621
798
 
622
- if (isCancel(configType)) {
799
+ if (isCancel(selectedRefs)) {
623
800
  cancel('Operation cancelled.');
624
801
  return;
625
802
  }
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`);
803
+
804
+ // Load syncs from each selected registered config
805
+ for (const refPath of selectedRefs) {
806
+ const ref = registeredRefs.find((r) => r.configPath === refPath);
807
+ const refConfig = loadConfig(refPath);
808
+ const refProjectRoot = path.dirname(refPath);
809
+ const refSyncs = (refConfig.syncs || []).map((s) => ({
810
+ ...s,
811
+ _configType: 'registered',
812
+ _configPath: refPath,
813
+ _projectRoot: refProjectRoot,
814
+ _globalBackupDir: refConfig.backupDir,
815
+ _refName: ref.name,
816
+ }));
817
+ selectedSyncs.push(...refSyncs);
818
+ }
819
+
820
+ // Process all selected syncs
821
+ for (const syncConfig of selectedSyncs) {
822
+ const projectRoot = syncConfig._projectRoot;
823
+ const globalBackupDir = syncConfig._globalBackupDir || 'gdrive-backups';
824
+ const backupPath = path.join(projectRoot, syncConfig.backupDir || globalBackupDir);
825
+ await runSyncOperation(syncConfig, action, projectRoot, backupPath, 'registered');
826
+ }
827
+ return;
632
828
  }
633
829
 
634
830
  const syncs = configType === 'local' ? localSyncs : globalSyncs;
@@ -934,6 +1130,8 @@ module.exports = {
934
1130
  envRun,
935
1131
  envShow,
936
1132
  envRemove,
1133
+ envRegister,
1134
+ envUnregister,
937
1135
  // Exported for testing
938
1136
  globToRegex,
939
1137
  matchPattern,