synced-folder-guard 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Abdelrahman Farag
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,127 @@
1
+ # synced-folder-guard
2
+
3
+ > Stop wasting an hour on `EPERM` / `EBADF` / `TAR_BAD_ARCHIVE` errors. This tool warns you **before** you install packages inside a cloud-synced folder.
4
+
5
+ Running `npm install` (or `pip`, `cargo`, `yarn`, `pnpm`) inside **Google Drive**, **OneDrive**, **Dropbox**, or **iCloud Drive** is one of the most confusing failures in dev tooling. The sync client fights the package manager for the same files mid-write, and you get a wall of errors like:
6
+
7
+ ```
8
+ npm warn tar TAR_BAD_ARCHIVE: Unrecognized archive format
9
+ npm warn tar TAR_ENTRY_ERROR EBADF: bad file descriptor, write
10
+ npm warn tar TAR_ENTRY_ERROR EPERM: operation not permitted, write
11
+ Error: Cannot find module 'ajv'
12
+ ```
13
+
14
+ Every guide tells you the same thing — *after* you've already lost the time: "clear your cache, delete `node_modules`, move the project." `synced-folder-guard` flips that: it tells you **before** the install, in one second.
15
+
16
+ ## Install
17
+
18
+ ```bash
19
+ npm install -g synced-folder-guard
20
+ ```
21
+
22
+ Or run it once with no install:
23
+
24
+ ```bash
25
+ npx synced-folder-guard
26
+ ```
27
+
28
+ ## Usage
29
+
30
+ Check the current folder:
31
+
32
+ ```bash
33
+ synced-folder-guard # or the short alias: sfg
34
+ ```
35
+
36
+ If you're safe:
37
+
38
+ ```
39
+ OK not in a cloud-synced folder: C:\Users\you\repos\my-app
40
+ ```
41
+
42
+ If you're not:
43
+
44
+ ```
45
+ WARNING this folder is inside a cloud-synced location.
46
+
47
+ Path: G:\My Drive\repos\my-app
48
+ Provider: Google Drive
49
+ - Google Drive (folder name "My Drive")
50
+
51
+ Installing packages here often fails or silently corrupts, with errors like:
52
+ EPERM, EBADF, ENOENT, TAR_BAD_ARCHIVE, "Unrecognized archive format"
53
+
54
+ Fix: move this project to a non-synced folder (e.g. a local repos dir),
55
+ or pause syncing for the duration of the install.
56
+ ```
57
+
58
+ Check a specific path:
59
+
60
+ ```bash
61
+ synced-folder-guard "G:\My Drive\repos\my-app"
62
+ ```
63
+
64
+ ## Guard your installs automatically
65
+
66
+ Add it as a `preinstall` script so a bad location can never bite you again:
67
+
68
+ ```json
69
+ {
70
+ "scripts": {
71
+ "preinstall": "synced-folder-guard --fail"
72
+ }
73
+ }
74
+ ```
75
+
76
+ With `--fail`, the install aborts (exit code `1`) when the project lives in a synced folder.
77
+
78
+ ## Options
79
+
80
+ | Flag | Description |
81
+ |------|-------------|
82
+ | `-f`, `--fail` | Exit with code `1` if the folder is synced (use in `preinstall`) |
83
+ | `--json` | Machine-readable JSON output |
84
+ | `-q`, `--quiet` | Print nothing when the folder is safe |
85
+ | `--no-color` | Disable colored output |
86
+ | `-h`, `--help` | Show help |
87
+ | `-v`, `--version` | Show version |
88
+
89
+ ## Use as a library
90
+
91
+ ```js
92
+ const { detect } = require('synced-folder-guard');
93
+
94
+ const result = detect(process.cwd());
95
+ // {
96
+ // path: 'G:\\My Drive\\repos\\my-app',
97
+ // synced: true,
98
+ // matches: [{ provider: 'Google Drive', via: 'folder name "My Drive"' }]
99
+ // }
100
+
101
+ if (result.synced) {
102
+ console.warn(`Heads up: ${result.matches[0].provider} folder detected.`);
103
+ }
104
+ ```
105
+
106
+ ## What it detects
107
+
108
+ | Provider | How |
109
+ |----------|-----|
110
+ | **Google Drive** | `My Drive`, `Shared drives`, `Google Drive`, `GoogleDrive-*` (macOS CloudStorage) folder names |
111
+ | **OneDrive** | `$OneDrive` / `$OneDriveCommercial` / `$OneDriveConsumer` env vars, and `OneDrive` / `OneDrive - Company` folder names |
112
+ | **Dropbox** | Real folder location read from Dropbox's own `info.json`, plus the `Dropbox` folder name |
113
+ | **iCloud Drive** | `Mobile Documents` / `com~apple~CloudDocs` folder names |
114
+
115
+ Detection is segment-boundary aware, so a folder like `MyDropboxNotes` is **not** a false positive.
116
+
117
+ ## Why this happens
118
+
119
+ Cloud sync clients use placeholder/virtual files and aggressively lock files to upload them. Package managers extract thousands of small files and rename them rapidly. When both touch the same file at the same moment, the write fails — sometimes loudly (`EPERM`), sometimes silently (a half-extracted tarball that breaks `require` later). The only reliable fix is to keep `node_modules` out of the synced folder entirely.
120
+
121
+ ## Zero dependencies
122
+
123
+ Pure Node, no runtime dependencies. Works on Windows, macOS, and Linux. Node `>=16`.
124
+
125
+ ## License
126
+
127
+ MIT
package/bin/cli.js ADDED
@@ -0,0 +1,119 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const { detect } = require('../src/detect');
5
+ const pkg = require('../package.json');
6
+
7
+ const ESC = String.fromCharCode(27);
8
+
9
+ // ASCII-only output: Windows terminals (the main audience here) mangle emoji
10
+ // and box-drawing glyphs, so we deliberately avoid them.
11
+ function makeColor(enabled) {
12
+ const wrap = (code) => (s) => (enabled ? `${ESC}[${code}m${s}${ESC}[0m` : s);
13
+ return {
14
+ bold: wrap('1'),
15
+ red: wrap('31'),
16
+ yellow: wrap('33'),
17
+ green: wrap('32'),
18
+ dim: wrap('2'),
19
+ };
20
+ }
21
+
22
+ function parseArgs(argv) {
23
+ const opts = { fail: false, json: false, quiet: false, color: null, path: null };
24
+ for (let i = 0; i < argv.length; i++) {
25
+ const a = argv[i];
26
+ if (a === 'check') continue;
27
+ else if (a === '--fail' || a === '-f') opts.fail = true;
28
+ else if (a === '--json') opts.json = true;
29
+ else if (a === '--quiet' || a === '-q') opts.quiet = true;
30
+ else if (a === '--no-color') opts.color = false;
31
+ else if (a === '--color') opts.color = true;
32
+ else if (a === '--help' || a === '-h') opts.help = true;
33
+ else if (a === '--version' || a === '-v') opts.version = true;
34
+ else if (a.startsWith('-')) {
35
+ process.stderr.write(`Unknown option: ${a}\n`);
36
+ opts.help = true;
37
+ } else if (opts.path === null) {
38
+ opts.path = a;
39
+ }
40
+ }
41
+ return opts;
42
+ }
43
+
44
+ const HELP = `synced-folder-guard v${pkg.version}
45
+
46
+ Warns BEFORE a package install corrupts inside a cloud-synced folder
47
+ (Google Drive, OneDrive, Dropbox, iCloud).
48
+
49
+ Usage:
50
+ synced-folder-guard [check] [path] Check a path (default: current folder)
51
+ sfg [check] [path] Short alias
52
+
53
+ Options:
54
+ -f, --fail Exit with code 1 if the folder is synced (use in preinstall)
55
+ --json Output machine-readable JSON
56
+ -q, --quiet Print nothing when the folder is safe
57
+ --no-color Disable colored output
58
+ -h, --help Show this help
59
+ -v, --version Show version
60
+
61
+ Use as a guard in package.json:
62
+ "scripts": { "preinstall": "synced-folder-guard --fail" }
63
+ `;
64
+
65
+ function main() {
66
+ const opts = parseArgs(process.argv.slice(2));
67
+
68
+ if (opts.help) {
69
+ process.stdout.write(HELP);
70
+ process.exit(0);
71
+ }
72
+ if (opts.version) {
73
+ process.stdout.write(`${pkg.version}\n`);
74
+ process.exit(0);
75
+ }
76
+
77
+ const colorEnabled =
78
+ opts.color !== null ? opts.color : Boolean(process.stdout.isTTY) && !process.env.NO_COLOR;
79
+ const c = makeColor(colorEnabled);
80
+
81
+ const result = detect(opts.path || process.cwd());
82
+
83
+ if (opts.json) {
84
+ process.stdout.write(JSON.stringify(result, null, 2) + '\n');
85
+ process.exit(opts.fail && result.synced ? 1 : 0);
86
+ }
87
+
88
+ if (!result.synced) {
89
+ if (!opts.quiet) {
90
+ process.stdout.write(`${c.green('OK')} not in a cloud-synced folder: ${result.path}\n`);
91
+ }
92
+ process.exit(0);
93
+ }
94
+
95
+ const providers = result.matches.map((m) => m.provider).join(', ');
96
+ const lines = [];
97
+ lines.push('');
98
+ lines.push(`${c.bold(c.yellow('WARNING'))} this folder is inside a cloud-synced location.`);
99
+ lines.push('');
100
+ lines.push(` ${c.bold('Path:')} ${result.path}`);
101
+ lines.push(` ${c.bold('Provider:')} ${providers}`);
102
+ for (const m of result.matches) {
103
+ const root = m.root ? `, root: ${m.root}` : '';
104
+ lines.push(` ${c.dim(`- ${m.provider} (${m.via}${root})`)}`);
105
+ }
106
+ lines.push('');
107
+ lines.push(' Installing packages here often fails or silently corrupts, with errors like:');
108
+ lines.push(` ${c.dim('EPERM, EBADF, ENOENT, TAR_BAD_ARCHIVE, "Unrecognized archive format"')}`);
109
+ lines.push('');
110
+ lines.push(` ${c.bold('Fix:')} move this project to a non-synced folder (e.g. a local repos dir),`);
111
+ lines.push(' or pause syncing for the duration of the install.');
112
+ lines.push('');
113
+
114
+ process.stdout.write(lines.join('\n') + '\n');
115
+
116
+ process.exit(opts.fail ? 1 : 0);
117
+ }
118
+
119
+ main();
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "synced-folder-guard",
3
+ "version": "0.1.0",
4
+ "description": "Warns you BEFORE npm/pip/cargo installs corrupt inside a cloud-synced folder (Google Drive, OneDrive, Dropbox, iCloud). Stops EPERM/EBADF/TAR_BAD_ARCHIVE errors before they cost you an hour.",
5
+ "type": "commonjs",
6
+ "main": "src/detect.js",
7
+ "bin": {
8
+ "synced-folder-guard": "bin/cli.js",
9
+ "sfg": "bin/cli.js"
10
+ },
11
+ "files": [
12
+ "src/",
13
+ "bin/",
14
+ "README.md",
15
+ "LICENSE"
16
+ ],
17
+ "scripts": {
18
+ "test": "node --test"
19
+ },
20
+ "keywords": [
21
+ "npm",
22
+ "onedrive",
23
+ "google-drive",
24
+ "dropbox",
25
+ "icloud",
26
+ "cloud-sync",
27
+ "synced-folder",
28
+ "eperm",
29
+ "ebadf",
30
+ "tar_bad_archive",
31
+ "preinstall",
32
+ "node_modules",
33
+ "guard",
34
+ "cli"
35
+ ],
36
+ "engines": {
37
+ "node": ">=16"
38
+ },
39
+ "author": "Abdelrahman Farag",
40
+ "license": "MIT",
41
+ "repository": {
42
+ "type": "git",
43
+ "url": "git+https://github.com/ahafarag/synced-folder-guard.git"
44
+ },
45
+ "bugs": {
46
+ "url": "https://github.com/ahafarag/synced-folder-guard/issues"
47
+ },
48
+ "homepage": "https://github.com/ahafarag/synced-folder-guard#readme"
49
+ }
package/src/detect.js ADDED
@@ -0,0 +1,122 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+ const fs = require('fs');
5
+ const os = require('os');
6
+
7
+ // Split a path into segments, tolerant of both / and \ separators so the
8
+ // same logic works regardless of host platform.
9
+ function segs(p) {
10
+ return String(p).split(/[\\/]+/).filter(Boolean);
11
+ }
12
+
13
+ // True when childSegs is contained within parentSegs (segment-boundary safe,
14
+ // so "C:\\MyDropboxNotes" is NOT treated as inside "C:\\...\\Dropbox").
15
+ function isInsideSegs(childSegs, parentSegs, caseInsensitive) {
16
+ if (parentSegs.length === 0 || childSegs.length < parentSegs.length) return false;
17
+ for (let i = 0; i < parentSegs.length; i++) {
18
+ const a = caseInsensitive ? childSegs[i].toLowerCase() : childSegs[i];
19
+ const b = caseInsensitive ? parentSegs[i].toLowerCase() : parentSegs[i];
20
+ if (a !== b) return false;
21
+ }
22
+ return true;
23
+ }
24
+
25
+ // Detection based purely on folder names that cloud providers use. This is the
26
+ // catch-all that works cross-platform without needing the provider installed.
27
+ function heuristicMatches(segments) {
28
+ const matches = [];
29
+ for (const seg of segments) {
30
+ const low = seg.toLowerCase();
31
+ if (low === 'my drive' || low === 'shared drives' || low === 'google drive' || low.startsWith('googledrive-')) {
32
+ matches.push({ provider: 'Google Drive', via: `folder name "${seg}"` });
33
+ } else if (low === 'onedrive' || low.startsWith('onedrive -') || low.startsWith('onedrive-')) {
34
+ matches.push({ provider: 'OneDrive', via: `folder name "${seg}"` });
35
+ } else if (low === 'dropbox') {
36
+ matches.push({ provider: 'Dropbox', via: `folder name "${seg}"` });
37
+ } else if (low === 'mobile documents' || low.startsWith('com~apple~clouddocs')) {
38
+ matches.push({ provider: 'iCloud Drive', via: `folder name "${seg}"` });
39
+ }
40
+ }
41
+ return matches;
42
+ }
43
+
44
+ // OneDrive exports its synced root(s) as environment variables on Windows.
45
+ function envRoots(env) {
46
+ const roots = [];
47
+ for (const key of ['OneDrive', 'OneDriveConsumer', 'OneDriveCommercial']) {
48
+ if (env[key]) roots.push({ provider: 'OneDrive', root: env[key], via: `env $${key}` });
49
+ }
50
+ return roots;
51
+ }
52
+
53
+ // Dropbox records its real folder location(s) in info.json. Reading it is the
54
+ // most accurate way to detect a Dropbox folder regardless of where it lives.
55
+ function dropboxRoots(env, home, readFile) {
56
+ const candidates = [];
57
+ if (env.APPDATA) candidates.push(path.join(env.APPDATA, 'Dropbox', 'info.json'));
58
+ if (env.LOCALAPPDATA) candidates.push(path.join(env.LOCALAPPDATA, 'Dropbox', 'info.json'));
59
+ candidates.push(path.join(home, '.dropbox', 'info.json'));
60
+
61
+ const roots = [];
62
+ for (const file of candidates) {
63
+ let raw;
64
+ try {
65
+ raw = readFile(file);
66
+ } catch {
67
+ continue;
68
+ }
69
+ if (!raw) continue;
70
+ let json;
71
+ try {
72
+ json = JSON.parse(raw);
73
+ } catch {
74
+ continue;
75
+ }
76
+ for (const account of Object.keys(json)) {
77
+ const root = json[account] && json[account].path;
78
+ if (root) roots.push({ provider: 'Dropbox', root, via: `Dropbox config (${account})` });
79
+ }
80
+ }
81
+ return roots;
82
+ }
83
+
84
+ // Inspect a path and report whether it sits inside a known cloud-synced folder.
85
+ // Dependencies (env, home, platform, file reader) are injectable for testing.
86
+ function detect(targetPath, opts = {}) {
87
+ const platform = opts.platform || process.platform;
88
+ const env = opts.env || process.env;
89
+ const home = opts.home || os.homedir();
90
+ const readFile = opts.readFile || ((f) => fs.readFileSync(f, 'utf8'));
91
+ const caseInsensitive = platform === 'win32';
92
+
93
+ const resolved = path.resolve(targetPath || opts.cwd || process.cwd());
94
+ const targetSegs = segs(resolved);
95
+
96
+ const matches = [];
97
+
98
+ const concreteRoots = [...envRoots(env), ...dropboxRoots(env, home, readFile)];
99
+ for (const r of concreteRoots) {
100
+ if (isInsideSegs(targetSegs, segs(path.resolve(r.root)), caseInsensitive)) {
101
+ matches.push({ provider: r.provider, root: path.resolve(r.root), via: r.via });
102
+ }
103
+ }
104
+
105
+ for (const m of heuristicMatches(targetSegs)) matches.push(m);
106
+
107
+ const seen = new Set();
108
+ const deduped = [];
109
+ for (const m of matches) {
110
+ if (seen.has(m.provider)) continue;
111
+ seen.add(m.provider);
112
+ deduped.push(m);
113
+ }
114
+
115
+ return {
116
+ path: resolved,
117
+ synced: deduped.length > 0,
118
+ matches: deduped,
119
+ };
120
+ }
121
+
122
+ module.exports = { detect, segs, isInsideSegs, heuristicMatches, envRoots, dropboxRoots };