gdrive-syncer 2.2.0 → 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,14 +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 |
42
81
  | `filesync:register` | Register local config to global registry |
43
82
  | `filesync:unregister` | Remove from global registry |
83
+ | `filesync:migrate` | Migrate legacy syncs to Files Sync format |
44
84
 
45
85
  ### Drive Operations
46
86
 
@@ -51,7 +91,9 @@ Run without arguments for interactive menu, or use direct commands.
51
91
  | `drive:mkdir` | Create a directory in Drive |
52
92
  | `drive:delete` | Delete a file/folder from Drive |
53
93
 
54
- ### 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.
55
97
 
56
98
  | Command | Description |
57
99
  |---------|-------------|
@@ -213,7 +255,9 @@ gdrive-syncer filesync:diff
213
255
  4. Multi-select which projects to sync
214
256
  5. Operation runs on all selected projects
215
257
 
216
- ## Legacy Sync Configuration
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.
217
261
 
218
262
  Legacy sync uses the `gdrive sync upload` command under the hood. Configuration is stored globally at:
219
263
 
@@ -287,10 +331,30 @@ gdrive-syncer sync:upload
287
331
  gdrive-syncer sync:list
288
332
  ```
289
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
+
290
353
  ## Files Sync vs Legacy Sync
291
354
 
292
355
  | Feature | Files Sync | Legacy Sync |
293
356
  |---------|----------|-------------|
357
+ | **gdrive version** | gdrive@2 and gdrive@3 | gdrive@2 only |
294
358
  | Config location | Local (`.gdrive-sync.json`) or Global (`~/.gdrive_syncer/env-sync.json`) | `~/.gdrive_syncer/gdrive.config.json` (global only) |
295
359
  | Direction | Two-way (upload & download) | One-way (upload only) |
296
360
  | File patterns | Supported (`.env.*`, `*`) | All files in folder |
@@ -318,6 +382,9 @@ gdrive-syncer filesync:diff local
318
382
  # Show differences for global config only
319
383
  gdrive-syncer filesync:diff global
320
384
 
385
+ # Show differences for registered local configs
386
+ gdrive-syncer filesync:diff registered
387
+
321
388
  # Download from Drive (with backup)
322
389
  gdrive-syncer filesync:download
323
390
  gdrive-syncer filesync:download local
@@ -326,6 +393,9 @@ gdrive-syncer filesync:download local
326
393
  gdrive-syncer filesync:upload
327
394
  gdrive-syncer filesync:upload global
328
395
 
396
+ # Upload from all registered configs
397
+ gdrive-syncer filesync:upload registered
398
+
329
399
  # Initialize config (choose Local or Global)
330
400
  gdrive-syncer filesync:init
331
401
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdrive-syncer",
3
- "version": "2.2.0",
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, envRegister, envUnregister } = 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
 
@@ -29,6 +29,7 @@ const commands = {
29
29
  'filesync:remove': { handler: envRemove, desc: 'Remove sync config' },
30
30
  'filesync:register': { handler: envRegister, desc: 'Register local config to global' },
31
31
  'filesync:unregister': { handler: envUnregister, desc: 'Unregister local config' },
32
+ 'filesync:migrate': { handler: envMigrate, desc: 'Migrate legacy syncs to Files Sync' },
32
33
 
33
34
  // Drive Operations
34
35
  'drive:search': { handler: runSearch, desc: 'Search files in Drive' },
@@ -49,14 +50,15 @@ const showHelp = () => {
49
50
  const lines = [
50
51
  `${color.bold('Files Sync')} ${color.dim('(two-way sync with .gdrive-sync.json)')}`,
51
52
  ` filesync ${color.dim('Interactive sync menu')}`,
52
- ` filesync:diff [local|global] ${color.dim('Show differences')}`,
53
- ` filesync:download [local|global] ${color.dim('Download from Drive')}`,
54
- ` 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')}`,
55
56
  ` filesync:init ${color.dim('Create/add sync config')}`,
56
57
  ` filesync:show ${color.dim('Show configurations')}`,
57
58
  ` filesync:remove ${color.dim('Remove sync config')}`,
58
59
  ` filesync:register ${color.dim('Register local config to global')}`,
59
60
  ` filesync:unregister ${color.dim('Unregister local config')}`,
61
+ ` filesync:migrate ${color.dim('Migrate legacy syncs to Files Sync')}`,
60
62
  ``,
61
63
  `${color.bold('Drive Operations')}`,
62
64
  ` drive:search ${color.dim('Search files')}`,
@@ -136,6 +138,7 @@ const showHelp = () => {
136
138
  { value: 'filesync:remove', label: 'Remove', hint: 'Remove a sync from config' },
137
139
  { value: 'filesync:register', label: 'Register', hint: 'Register local config to global' },
138
140
  { value: 'filesync:unregister', label: 'Unregister', hint: 'Unregister local config' },
141
+ { value: 'filesync:migrate', label: 'Migrate', hint: 'Migrate legacy syncs to Files Sync' },
139
142
  ],
140
143
  });
141
144
  } else if (category === 'drive') {
@@ -158,6 +161,7 @@ const showHelp = () => {
158
161
  { value: 'sync:show', label: 'Show Config', hint: 'View sync configurations' },
159
162
  { value: 'sync:add', label: 'Add Config', hint: 'Add a new sync configuration' },
160
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' },
161
165
  ],
162
166
  });
163
167
  }
package/src/envSync.js CHANGED
@@ -2,6 +2,7 @@
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
8
  const { select, multiselect, text, isCancel, cancel, spinner, confirm, note, log } = require('@clack/prompts');
@@ -739,6 +740,10 @@ const envRun = async (presetAction, presetConfigType) => {
739
740
  log.error('No syncs in global config.');
740
741
  return;
741
742
  }
743
+ if (presetConfigType === 'registered' && registeredRefs.length === 0) {
744
+ log.error('No registered local configs found.');
745
+ return;
746
+ }
742
747
  configType = presetConfigType;
743
748
  log.info(`Using ${configType} config`);
744
749
  } else {
@@ -907,8 +912,9 @@ const runSyncOperation = async (syncConfig, action, projectRoot, backupPath, con
907
912
  const s = spinner();
908
913
  s.start('Fetching files from Google Drive...');
909
914
 
910
- const listResult = shell.exec(`gdrive list -q "'${folderId}' in parents" --no-header`, {
911
- silent: true,
915
+ const listResult = gdrive.list({
916
+ query: `'${folderId}' in parents`,
917
+ noHeader: true,
912
918
  });
913
919
 
914
920
  if (listResult.code !== 0) {
@@ -921,24 +927,18 @@ const runSyncOperation = async (syncConfig, action, projectRoot, backupPath, con
921
927
  const driveFiles = [];
922
928
  const stdout = listResult.stdout.trim();
923
929
  if (stdout) {
924
- stdout.split('\n').forEach((line) => {
925
- // gdrive output format: ID NAME TYPE SIZE DATE
926
- // Split by 2+ spaces to handle filenames with single spaces
927
- const parts = line.trim().split(/\s{2,}/);
928
- if (parts.length >= 2) {
929
- const fileId = parts[0].trim();
930
- const fileName = parts[1].trim();
931
- if (matchPattern(fileName, pattern, ignore)) {
932
- driveFiles.push({ id: fileId, name: fileName });
933
- }
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 });
934
934
  }
935
- });
935
+ }
936
936
  }
937
937
 
938
938
  // Download Drive files to temp for comparison
939
939
  s.message('Downloading Drive files for comparison...');
940
940
  for (const file of driveFiles) {
941
- shell.exec(`gdrive download "${file.id}" --path "${tempDir}" --force`, { silent: true });
941
+ gdrive.download(file.id, { destination: tempDir, overwrite: true });
942
942
  }
943
943
  s.stop(color.green(`Found ${driveFiles.length} matching file(s) on Drive`));
944
944
 
@@ -1102,17 +1102,13 @@ const runSyncOperation = async (syncConfig, action, projectRoot, backupPath, con
1102
1102
  for (const filename of changes.modified) {
1103
1103
  const driveFile = driveFiles.find((f) => f.name === filename);
1104
1104
  if (driveFile) {
1105
- shell.exec(`gdrive update "${driveFile.id}" "${path.join(envDir, filename)}"`, {
1106
- silent: true,
1107
- });
1105
+ gdrive.update(driveFile.id, path.join(envDir, filename));
1108
1106
  replaced++;
1109
1107
  }
1110
1108
  }
1111
1109
 
1112
1110
  for (const filename of changes.localOnly) {
1113
- shell.exec(`gdrive upload "${path.join(envDir, filename)}" --parent "${folderId}"`, {
1114
- silent: true,
1115
- });
1111
+ gdrive.upload(path.join(envDir, filename), { parent: folderId });
1116
1112
  uploaded++;
1117
1113
  }
1118
1114
 
@@ -1125,6 +1121,221 @@ const runSyncOperation = async (syncConfig, action, projectRoot, backupPath, con
1125
1121
  }
1126
1122
  };
1127
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
+
1128
1339
  module.exports = {
1129
1340
  envInit,
1130
1341
  envRun,
@@ -1132,6 +1343,7 @@ module.exports = {
1132
1343
  envRemove,
1133
1344
  envRegister,
1134
1345
  envUnregister,
1346
+ envMigrate,
1135
1347
  // Exported for testing
1136
1348
  globToRegex,
1137
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
- }