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 +32 -0
- package/package.json +1 -1
- package/run.js +7 -1
- package/src/envSync.js +223 -25
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
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,
|
|
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
|
-
|
|
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
|
|
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
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
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(
|
|
799
|
+
if (isCancel(selectedRefs)) {
|
|
623
800
|
cancel('Operation cancelled.');
|
|
624
801
|
return;
|
|
625
802
|
}
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
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,
|