repo-cloak-cli 1.0.1 → 1.0.2
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/package.json +6 -2
- package/src/commands/pull.js +41 -20
- package/src/core/mapper.js +56 -0
- package/tests/anonymizer.test.js +127 -0
- package/tests/copier.test.js +94 -0
- package/tests/crypto.test.js +106 -0
- package/tests/mapper.test.js +166 -0
- package/tests/scanner.test.js +100 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "repo-cloak-cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "🎭 Selectively extract and anonymize files from repositories. Perfect for sharing code with AI agents without exposing proprietary details.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
},
|
|
11
11
|
"scripts": {
|
|
12
12
|
"start": "node bin/repo-cloak.js",
|
|
13
|
-
"test": "
|
|
13
|
+
"test": "vitest run",
|
|
14
|
+
"test:watch": "vitest",
|
|
14
15
|
"lint": "eslint src/"
|
|
15
16
|
},
|
|
16
17
|
"keywords": [
|
|
@@ -43,5 +44,8 @@
|
|
|
43
44
|
"glob": "^10.3.10",
|
|
44
45
|
"inquirer": "^9.2.12",
|
|
45
46
|
"ora": "^8.0.1"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"vitest": "^4.0.18"
|
|
46
50
|
}
|
|
47
51
|
}
|
package/src/commands/pull.js
CHANGED
|
@@ -19,7 +19,7 @@ import { showSuccess, showError, showInfo } from '../ui/banner.js';
|
|
|
19
19
|
import { getAllFiles } from '../core/scanner.js';
|
|
20
20
|
import { copyFiles } from '../core/copier.js';
|
|
21
21
|
import { createAnonymizer } from '../core/anonymizer.js';
|
|
22
|
-
import { createMapping, saveMapping } from '../core/mapper.js';
|
|
22
|
+
import { createMapping, saveMapping, loadRawMapping, mergeMapping, hasMapping } from '../core/mapper.js';
|
|
23
23
|
|
|
24
24
|
export async function pull(options = {}) {
|
|
25
25
|
try {
|
|
@@ -106,28 +106,49 @@ export async function pull(options = {}) {
|
|
|
106
106
|
});
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
-
// Step 8:
|
|
110
|
-
const
|
|
111
|
-
sourceDir,
|
|
112
|
-
|
|
113
|
-
replacements
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
}
|
|
122
|
-
return {
|
|
123
|
-
original: originalPath,
|
|
124
|
-
cloaked: anonymizedPath
|
|
125
|
-
};
|
|
126
|
-
})
|
|
109
|
+
// Step 8: Prepare new file mappings
|
|
110
|
+
const newFiles = selectedFiles.map(f => {
|
|
111
|
+
const originalPath = relative(sourceDir, f);
|
|
112
|
+
let anonymizedPath = originalPath;
|
|
113
|
+
for (const { original, replacement } of replacements) {
|
|
114
|
+
const regex = new RegExp(original.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi');
|
|
115
|
+
anonymizedPath = anonymizedPath.replace(regex, replacement);
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
original: originalPath,
|
|
119
|
+
cloaked: anonymizedPath
|
|
120
|
+
};
|
|
127
121
|
});
|
|
128
122
|
|
|
123
|
+
// Step 9: Check for existing mapping and merge if found
|
|
124
|
+
const existingMapping = loadRawMapping(destDir);
|
|
125
|
+
let mapping;
|
|
126
|
+
let isIncremental = false;
|
|
127
|
+
|
|
128
|
+
if (existingMapping) {
|
|
129
|
+
// Merge with existing
|
|
130
|
+
mapping = mergeMapping(existingMapping, newFiles);
|
|
131
|
+
isIncremental = true;
|
|
132
|
+
console.log(chalk.cyan(` 🔄 Merged with existing mapping (incremental pull)`));
|
|
133
|
+
} else {
|
|
134
|
+
// Create new mapping
|
|
135
|
+
mapping = createMapping({
|
|
136
|
+
sourceDir,
|
|
137
|
+
destDir,
|
|
138
|
+
replacements,
|
|
139
|
+
files: newFiles
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
129
143
|
const mapPath = saveMapping(destDir, mapping);
|
|
130
|
-
|
|
144
|
+
|
|
145
|
+
if (isIncremental) {
|
|
146
|
+
const history = mapping.pullHistory || [];
|
|
147
|
+
const lastPull = history[history.length - 1];
|
|
148
|
+
console.log(chalk.dim(` 📋 Mapping updated: ${lastPull?.filesAdded || 0} new files added (total: ${mapping.stats?.totalFiles})`));
|
|
149
|
+
} else {
|
|
150
|
+
console.log(chalk.dim(` 📋 Mapping saved: ${mapPath}`));
|
|
151
|
+
}
|
|
131
152
|
|
|
132
153
|
// Done!
|
|
133
154
|
showSuccess('Extraction complete!');
|
package/src/core/mapper.js
CHANGED
|
@@ -177,3 +177,59 @@ export function updateMapping(destDir, updates) {
|
|
|
177
177
|
writeFileSync(mapPath, JSON.stringify(updated, null, 2), 'utf-8');
|
|
178
178
|
return updated;
|
|
179
179
|
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Load existing raw mapping (without decryption) for merging
|
|
183
|
+
*/
|
|
184
|
+
export function loadRawMapping(cloakedDir) {
|
|
185
|
+
const mapPath = join(cloakedDir, MAP_FILENAME);
|
|
186
|
+
|
|
187
|
+
if (!existsSync(mapPath)) {
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
try {
|
|
192
|
+
const content = readFileSync(mapPath, 'utf-8');
|
|
193
|
+
return JSON.parse(content);
|
|
194
|
+
} catch (error) {
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Merge new files into existing mapping (for incremental pulls)
|
|
201
|
+
*/
|
|
202
|
+
export function mergeMapping(existingMapping, newFiles) {
|
|
203
|
+
// Get existing file paths for deduplication
|
|
204
|
+
const existingPaths = new Set(
|
|
205
|
+
(existingMapping.files || []).map(f => f.cloaked)
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
// Filter out files that already exist (avoid duplicates)
|
|
209
|
+
const uniqueNewFiles = newFiles.filter(f => !existingPaths.has(f.cloaked));
|
|
210
|
+
|
|
211
|
+
// Merge files
|
|
212
|
+
const mergedFiles = [
|
|
213
|
+
...(existingMapping.files || []),
|
|
214
|
+
...uniqueNewFiles
|
|
215
|
+
];
|
|
216
|
+
|
|
217
|
+
// Track pull history
|
|
218
|
+
const pullHistory = existingMapping.pullHistory || [];
|
|
219
|
+
pullHistory.push({
|
|
220
|
+
timestamp: new Date().toISOString(),
|
|
221
|
+
filesAdded: uniqueNewFiles.length,
|
|
222
|
+
totalFiles: mergedFiles.length
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
...existingMapping,
|
|
227
|
+
files: mergedFiles,
|
|
228
|
+
pullHistory,
|
|
229
|
+
stats: {
|
|
230
|
+
...existingMapping.stats,
|
|
231
|
+
totalFiles: mergedFiles.length
|
|
232
|
+
},
|
|
233
|
+
updatedAt: new Date().toISOString()
|
|
234
|
+
};
|
|
235
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Anonymizer Tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect } from 'vitest';
|
|
6
|
+
import { createAnonymizer, createDeanonymizer } from '../src/core/anonymizer.js';
|
|
7
|
+
|
|
8
|
+
describe('Anonymizer', () => {
|
|
9
|
+
describe('createAnonymizer', () => {
|
|
10
|
+
it('should replace exact matches', () => {
|
|
11
|
+
const anonymizer = createAnonymizer([
|
|
12
|
+
{ original: 'Cuviva', replacement: 'ABCCompany' }
|
|
13
|
+
]);
|
|
14
|
+
|
|
15
|
+
// Title case input -> Title case output (first letter upper, rest lower)
|
|
16
|
+
expect(anonymizer('Hello Cuviva world')).toBe('Hello Abccompany world');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('should handle all uppercase', () => {
|
|
20
|
+
const anonymizer = createAnonymizer([
|
|
21
|
+
{ original: 'Cuviva', replacement: 'ABCCompany' }
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
expect(anonymizer('CUVIVA is great')).toBe('ABCCOMPANY is great');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should handle all lowercase', () => {
|
|
28
|
+
const anonymizer = createAnonymizer([
|
|
29
|
+
{ original: 'Cuviva', replacement: 'ABCCompany' }
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
expect(anonymizer('cuviva is lower')).toBe('abccompany is lower');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('should handle multiple replacements', () => {
|
|
36
|
+
const anonymizer = createAnonymizer([
|
|
37
|
+
{ original: 'Cuviva', replacement: 'ABCCompany' },
|
|
38
|
+
{ original: 'Frontend', replacement: 'Client' }
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
// Both are Title case -> first upper + rest lower
|
|
42
|
+
expect(anonymizer('Cuviva Frontend API')).toBe('Abccompany Client API');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should handle empty replacements', () => {
|
|
46
|
+
const anonymizer = createAnonymizer([]);
|
|
47
|
+
expect(anonymizer('Hello world')).toBe('Hello world');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should handle special regex characters in original', () => {
|
|
51
|
+
const anonymizer = createAnonymizer([
|
|
52
|
+
{ original: 'test.value', replacement: 'replaced' }
|
|
53
|
+
]);
|
|
54
|
+
|
|
55
|
+
// Title case preservation
|
|
56
|
+
expect(anonymizer('This is Test.value here')).toBe('This is Replaced here');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should handle null or undefined replacements', () => {
|
|
60
|
+
const anonymizer = createAnonymizer(null);
|
|
61
|
+
expect(anonymizer('Hello world')).toBe('Hello world');
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe('createDeanonymizer', () => {
|
|
66
|
+
it('should reverse the anonymization', () => {
|
|
67
|
+
const replacements = [
|
|
68
|
+
{ original: 'Cuviva', replacement: 'ABCCompany' }
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
const deanonymizer = createDeanonymizer(replacements);
|
|
72
|
+
|
|
73
|
+
// ABCCompany (Title case) -> Cuviva (Title case: first upper + rest lower)
|
|
74
|
+
expect(deanonymizer('Hello ABCCompany world')).toBe('Hello Cuviva world');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('should handle multiple replacements in reverse', () => {
|
|
78
|
+
const replacements = [
|
|
79
|
+
{ original: 'Cuviva', replacement: 'ABCCompany' },
|
|
80
|
+
{ original: 'API', replacement: 'Service' }
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
const deanonymizer = createDeanonymizer(replacements);
|
|
84
|
+
|
|
85
|
+
// Title case -> Title case for both
|
|
86
|
+
expect(deanonymizer('ABCCompany Service')).toBe('Cuviva Api');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('should handle uppercase in reverse', () => {
|
|
90
|
+
const replacements = [
|
|
91
|
+
{ original: 'Cuviva', replacement: 'ABCCompany' }
|
|
92
|
+
];
|
|
93
|
+
|
|
94
|
+
const deanonymizer = createDeanonymizer(replacements);
|
|
95
|
+
|
|
96
|
+
expect(deanonymizer('ABCCOMPANY')).toBe('CUVIVA');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('should handle lowercase in reverse', () => {
|
|
100
|
+
const replacements = [
|
|
101
|
+
{ original: 'Cuviva', replacement: 'ABCCompany' }
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
const deanonymizer = createDeanonymizer(replacements);
|
|
105
|
+
|
|
106
|
+
expect(deanonymizer('abccompany')).toBe('cuviva');
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe('Case transformation', () => {
|
|
112
|
+
it('should preserve all uppercase', () => {
|
|
113
|
+
const anonymizer = createAnonymizer([
|
|
114
|
+
{ original: 'SECRET', replacement: 'PUBLIC' }
|
|
115
|
+
]);
|
|
116
|
+
|
|
117
|
+
expect(anonymizer('This is SECRET data')).toBe('This is PUBLIC data');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('should preserve all lowercase', () => {
|
|
121
|
+
const anonymizer = createAnonymizer([
|
|
122
|
+
{ original: 'secret', replacement: 'public' }
|
|
123
|
+
]);
|
|
124
|
+
|
|
125
|
+
expect(anonymizer('this is secret data')).toBe('this is public data');
|
|
126
|
+
});
|
|
127
|
+
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copier Tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
6
|
+
import { copyFile, copyFileWithTransform } from '../src/core/copier.js';
|
|
7
|
+
import { existsSync, mkdirSync, rmSync, writeFileSync, readFileSync } from 'fs';
|
|
8
|
+
import { join } from 'path';
|
|
9
|
+
import { tmpdir } from 'os';
|
|
10
|
+
|
|
11
|
+
describe('Copier Module', () => {
|
|
12
|
+
let testDir;
|
|
13
|
+
let sourceDir;
|
|
14
|
+
let destDir;
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
testDir = join(tmpdir(), `repo-cloak-copier-test-${Date.now()}`);
|
|
18
|
+
sourceDir = join(testDir, 'source');
|
|
19
|
+
destDir = join(testDir, 'dest');
|
|
20
|
+
mkdirSync(sourceDir, { recursive: true });
|
|
21
|
+
mkdirSync(destDir, { recursive: true });
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
if (existsSync(testDir)) {
|
|
26
|
+
rmSync(testDir, { recursive: true, force: true });
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe('copyFile', () => {
|
|
31
|
+
it('should copy a file to destination', () => {
|
|
32
|
+
const sourceFile = join(sourceDir, 'test.txt');
|
|
33
|
+
const destFile = join(destDir, 'test.txt');
|
|
34
|
+
|
|
35
|
+
writeFileSync(sourceFile, 'Hello World');
|
|
36
|
+
copyFile(sourceFile, destFile);
|
|
37
|
+
|
|
38
|
+
expect(existsSync(destFile)).toBe(true);
|
|
39
|
+
expect(readFileSync(destFile, 'utf-8')).toBe('Hello World');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should create nested directories', () => {
|
|
43
|
+
const sourceFile = join(sourceDir, 'test.txt');
|
|
44
|
+
const destFile = join(destDir, 'nested', 'deep', 'test.txt');
|
|
45
|
+
|
|
46
|
+
writeFileSync(sourceFile, 'Content');
|
|
47
|
+
copyFile(sourceFile, destFile);
|
|
48
|
+
|
|
49
|
+
expect(existsSync(destFile)).toBe(true);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe('copyFileWithTransform', () => {
|
|
54
|
+
it('should transform content during copy', () => {
|
|
55
|
+
const sourceFile = join(sourceDir, 'code.js');
|
|
56
|
+
const destFile = join(destDir, 'code.js');
|
|
57
|
+
|
|
58
|
+
writeFileSync(sourceFile, 'const company = "Cuviva";');
|
|
59
|
+
|
|
60
|
+
const transform = (content) => content.replace(/Cuviva/g, 'ABCCompany');
|
|
61
|
+
const result = copyFileWithTransform(sourceFile, destFile, transform);
|
|
62
|
+
|
|
63
|
+
expect(result.transformed).toBe(true);
|
|
64
|
+
expect(readFileSync(destFile, 'utf-8')).toBe('const company = "ABCCompany";');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should report no transformation when content unchanged', () => {
|
|
68
|
+
const sourceFile = join(sourceDir, 'code.js');
|
|
69
|
+
const destFile = join(destDir, 'code.js');
|
|
70
|
+
|
|
71
|
+
writeFileSync(sourceFile, 'const x = 1;');
|
|
72
|
+
|
|
73
|
+
const transform = (content) => content.replace(/Cuviva/g, 'ABCCompany');
|
|
74
|
+
const result = copyFileWithTransform(sourceFile, destFile, transform);
|
|
75
|
+
|
|
76
|
+
expect(result.transformed).toBe(false);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should handle binary files by copying as-is', () => {
|
|
80
|
+
const sourceFile = join(sourceDir, 'image.png');
|
|
81
|
+
const destFile = join(destDir, 'image.png');
|
|
82
|
+
|
|
83
|
+
// Create a simple binary-like file
|
|
84
|
+
const buffer = Buffer.from([0x89, 0x50, 0x4E, 0x47, 0x00]);
|
|
85
|
+
writeFileSync(sourceFile, buffer);
|
|
86
|
+
|
|
87
|
+
const transform = (content) => content.replace(/test/g, 'replaced');
|
|
88
|
+
const result = copyFileWithTransform(sourceFile, destFile, transform);
|
|
89
|
+
|
|
90
|
+
expect(result.transformed).toBe(false);
|
|
91
|
+
expect(existsSync(destFile)).toBe(true);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
});
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Crypto Tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
6
|
+
import { encrypt, decrypt, encryptReplacements, decryptReplacements } from '../src/core/crypto.js';
|
|
7
|
+
import { existsSync, rmSync, mkdirSync } from 'fs';
|
|
8
|
+
import { join } from 'path';
|
|
9
|
+
import { homedir } from 'os';
|
|
10
|
+
|
|
11
|
+
describe('Crypto Module', () => {
|
|
12
|
+
const testSecret = 'test-secret-key-for-unit-tests-1234567890';
|
|
13
|
+
|
|
14
|
+
describe('encrypt/decrypt', () => {
|
|
15
|
+
it('should encrypt and decrypt a string', () => {
|
|
16
|
+
const original = 'Hello World';
|
|
17
|
+
const encrypted = encrypt(original, testSecret);
|
|
18
|
+
const decrypted = decrypt(encrypted, testSecret);
|
|
19
|
+
|
|
20
|
+
expect(decrypted).toBe(original);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('should produce different encrypted output each time (random IV)', () => {
|
|
24
|
+
const original = 'Same input';
|
|
25
|
+
const encrypted1 = encrypt(original, testSecret);
|
|
26
|
+
const encrypted2 = encrypt(original, testSecret);
|
|
27
|
+
|
|
28
|
+
expect(encrypted1).not.toBe(encrypted2);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('should return null for wrong secret', () => {
|
|
32
|
+
const original = 'Secret message';
|
|
33
|
+
const encrypted = encrypt(original, testSecret);
|
|
34
|
+
const decrypted = decrypt(encrypted, 'wrong-secret');
|
|
35
|
+
|
|
36
|
+
expect(decrypted).toBeNull();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should handle special characters', () => {
|
|
40
|
+
const original = 'Special: @#$%^&*()_+ 日本語 🎭';
|
|
41
|
+
const encrypted = encrypt(original, testSecret);
|
|
42
|
+
const decrypted = decrypt(encrypted, testSecret);
|
|
43
|
+
|
|
44
|
+
expect(decrypted).toBe(original);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should handle empty string or return null for empty', () => {
|
|
48
|
+
const original = '';
|
|
49
|
+
const encrypted = encrypt(original, testSecret);
|
|
50
|
+
const decrypted = decrypt(encrypted, testSecret);
|
|
51
|
+
|
|
52
|
+
// Empty string may return empty or null depending on cipher
|
|
53
|
+
expect(decrypted === '' || decrypted === null).toBe(true);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should handle long strings', () => {
|
|
57
|
+
const original = 'A'.repeat(10000);
|
|
58
|
+
const encrypted = encrypt(original, testSecret);
|
|
59
|
+
const decrypted = decrypt(encrypted, testSecret);
|
|
60
|
+
|
|
61
|
+
expect(decrypted).toBe(original);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe('encryptReplacements/decryptReplacements', () => {
|
|
66
|
+
it('should encrypt only the original field', () => {
|
|
67
|
+
const replacements = [
|
|
68
|
+
{ original: 'Cuviva', replacement: 'ABCCompany' },
|
|
69
|
+
{ original: 'Secret', replacement: 'Public' }
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
const encrypted = encryptReplacements(replacements, testSecret);
|
|
73
|
+
|
|
74
|
+
// Replacement should still be visible
|
|
75
|
+
expect(encrypted[0].replacement).toBe('ABCCompany');
|
|
76
|
+
expect(encrypted[1].replacement).toBe('Public');
|
|
77
|
+
|
|
78
|
+
// Original should be encrypted (contains colons from format)
|
|
79
|
+
expect(encrypted[0].original).toContain(':');
|
|
80
|
+
expect(encrypted[0].encrypted).toBe(true);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('should decrypt back to original', () => {
|
|
84
|
+
const replacements = [
|
|
85
|
+
{ original: 'Cuviva', replacement: 'ABCCompany' }
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
const encrypted = encryptReplacements(replacements, testSecret);
|
|
89
|
+
const decrypted = decryptReplacements(encrypted, testSecret);
|
|
90
|
+
|
|
91
|
+
expect(decrypted[0].original).toBe('Cuviva');
|
|
92
|
+
expect(decrypted[0].replacement).toBe('ABCCompany');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should mark failed decryptions', () => {
|
|
96
|
+
const replacements = [
|
|
97
|
+
{ original: 'Test', replacement: 'Demo' }
|
|
98
|
+
];
|
|
99
|
+
|
|
100
|
+
const encrypted = encryptReplacements(replacements, testSecret);
|
|
101
|
+
const decrypted = decryptReplacements(encrypted, 'wrong-secret');
|
|
102
|
+
|
|
103
|
+
expect(decrypted[0].decryptFailed).toBe(true);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
});
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mapper Tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
6
|
+
import {
|
|
7
|
+
createMapping,
|
|
8
|
+
saveMapping,
|
|
9
|
+
loadRawMapping,
|
|
10
|
+
mergeMapping,
|
|
11
|
+
hasMapping
|
|
12
|
+
} from '../src/core/mapper.js';
|
|
13
|
+
import { existsSync, mkdirSync, rmSync, writeFileSync, readFileSync } from 'fs';
|
|
14
|
+
import { join } from 'path';
|
|
15
|
+
import { tmpdir } from 'os';
|
|
16
|
+
|
|
17
|
+
describe('Mapper Module', () => {
|
|
18
|
+
let testDir;
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
testDir = join(tmpdir(), `repo-cloak-test-${Date.now()}`);
|
|
22
|
+
mkdirSync(testDir, { recursive: true });
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
if (existsSync(testDir)) {
|
|
27
|
+
rmSync(testDir, { recursive: true, force: true });
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe('createMapping', () => {
|
|
32
|
+
it('should create a valid mapping object', () => {
|
|
33
|
+
const mapping = createMapping({
|
|
34
|
+
sourceDir: '/path/to/source',
|
|
35
|
+
destDir: '/path/to/dest',
|
|
36
|
+
replacements: [{ original: 'Test', replacement: 'Demo' }],
|
|
37
|
+
files: [{ original: 'file.js', cloaked: 'file.js' }]
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
expect(mapping.version).toBe('1.1.0');
|
|
41
|
+
expect(mapping.tool).toBe('repo-cloak');
|
|
42
|
+
expect(mapping.encrypted).toBe(true);
|
|
43
|
+
expect(mapping.files).toHaveLength(1);
|
|
44
|
+
expect(mapping.stats.totalFiles).toBe(1);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should encrypt sensitive data', () => {
|
|
48
|
+
const mapping = createMapping({
|
|
49
|
+
sourceDir: '/secret/path',
|
|
50
|
+
destDir: '/dest/path',
|
|
51
|
+
replacements: [{ original: 'SecretWord', replacement: 'Public' }],
|
|
52
|
+
files: [{ original: 'secret.js', cloaked: 'public.js' }]
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Source path should be encrypted (contains :)
|
|
56
|
+
expect(mapping.source.path).toContain(':');
|
|
57
|
+
// Replacements original should be encrypted
|
|
58
|
+
expect(mapping.replacements[0].original).toContain(':');
|
|
59
|
+
expect(mapping.replacements[0].encrypted).toBe(true);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe('saveMapping / loadRawMapping', () => {
|
|
64
|
+
it('should save and load mapping file', () => {
|
|
65
|
+
const mapping = {
|
|
66
|
+
version: '1.0.0',
|
|
67
|
+
files: [{ original: 'a.js', cloaked: 'b.js' }]
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
saveMapping(testDir, mapping);
|
|
71
|
+
|
|
72
|
+
expect(hasMapping(testDir)).toBe(true);
|
|
73
|
+
|
|
74
|
+
const loaded = loadRawMapping(testDir);
|
|
75
|
+
expect(loaded.version).toBe('1.0.0');
|
|
76
|
+
expect(loaded.files).toHaveLength(1);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should return null for non-existent mapping', () => {
|
|
80
|
+
const loaded = loadRawMapping('/non/existent/path');
|
|
81
|
+
expect(loaded).toBeNull();
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe('hasMapping', () => {
|
|
86
|
+
it('should return true when mapping exists', () => {
|
|
87
|
+
writeFileSync(join(testDir, '.repo-cloak-map.json'), '{}');
|
|
88
|
+
expect(hasMapping(testDir)).toBe(true);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('should return false when mapping does not exist', () => {
|
|
92
|
+
expect(hasMapping(testDir)).toBe(false);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe('mergeMapping', () => {
|
|
97
|
+
it('should merge new files with existing mapping', () => {
|
|
98
|
+
const existing = {
|
|
99
|
+
version: '1.0.0',
|
|
100
|
+
files: [
|
|
101
|
+
{ original: 'a.js', cloaked: 'a.js' },
|
|
102
|
+
{ original: 'b.js', cloaked: 'b.js' }
|
|
103
|
+
],
|
|
104
|
+
stats: { totalFiles: 2 }
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const newFiles = [
|
|
108
|
+
{ original: 'c.js', cloaked: 'c.js' },
|
|
109
|
+
{ original: 'd.js', cloaked: 'd.js' }
|
|
110
|
+
];
|
|
111
|
+
|
|
112
|
+
const merged = mergeMapping(existing, newFiles);
|
|
113
|
+
|
|
114
|
+
expect(merged.files).toHaveLength(4);
|
|
115
|
+
expect(merged.stats.totalFiles).toBe(4);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('should avoid duplicate files', () => {
|
|
119
|
+
const existing = {
|
|
120
|
+
version: '1.0.0',
|
|
121
|
+
files: [
|
|
122
|
+
{ original: 'a.js', cloaked: 'a.js' },
|
|
123
|
+
{ original: 'b.js', cloaked: 'b.js' }
|
|
124
|
+
],
|
|
125
|
+
stats: { totalFiles: 2 }
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const newFiles = [
|
|
129
|
+
{ original: 'b.js', cloaked: 'b.js' }, // Duplicate
|
|
130
|
+
{ original: 'c.js', cloaked: 'c.js' } // New
|
|
131
|
+
];
|
|
132
|
+
|
|
133
|
+
const merged = mergeMapping(existing, newFiles);
|
|
134
|
+
|
|
135
|
+
expect(merged.files).toHaveLength(3); // Not 4
|
|
136
|
+
expect(merged.stats.totalFiles).toBe(3);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('should track pull history', () => {
|
|
140
|
+
const existing = {
|
|
141
|
+
version: '1.0.0',
|
|
142
|
+
files: [],
|
|
143
|
+
stats: { totalFiles: 0 }
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const merged = mergeMapping(existing, [{ original: 'a.js', cloaked: 'a.js' }]);
|
|
147
|
+
|
|
148
|
+
expect(merged.pullHistory).toHaveLength(1);
|
|
149
|
+
expect(merged.pullHistory[0].filesAdded).toBe(1);
|
|
150
|
+
expect(merged.pullHistory[0].timestamp).toBeDefined();
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('should append to existing pull history', () => {
|
|
154
|
+
const existing = {
|
|
155
|
+
version: '1.0.0',
|
|
156
|
+
files: [{ original: 'a.js', cloaked: 'a.js' }],
|
|
157
|
+
stats: { totalFiles: 1 },
|
|
158
|
+
pullHistory: [{ timestamp: '2024-01-01', filesAdded: 1, totalFiles: 1 }]
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const merged = mergeMapping(existing, [{ original: 'b.js', cloaked: 'b.js' }]);
|
|
162
|
+
|
|
163
|
+
expect(merged.pullHistory).toHaveLength(2);
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
});
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scanner Tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
6
|
+
import { getAllFiles, isBinaryFile } from '../src/core/scanner.js';
|
|
7
|
+
import { existsSync, mkdirSync, rmSync, writeFileSync } from 'fs';
|
|
8
|
+
import { join } from 'path';
|
|
9
|
+
import { tmpdir } from 'os';
|
|
10
|
+
|
|
11
|
+
describe('Scanner Module', () => {
|
|
12
|
+
let testDir;
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
testDir = join(tmpdir(), `repo-cloak-scanner-test-${Date.now()}`);
|
|
16
|
+
mkdirSync(testDir, { recursive: true });
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
if (existsSync(testDir)) {
|
|
21
|
+
rmSync(testDir, { recursive: true, force: true });
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe('getAllFiles', () => {
|
|
26
|
+
it('should find all files in directory', () => {
|
|
27
|
+
writeFileSync(join(testDir, 'a.js'), 'content');
|
|
28
|
+
writeFileSync(join(testDir, 'b.js'), 'content');
|
|
29
|
+
|
|
30
|
+
const files = getAllFiles(testDir);
|
|
31
|
+
|
|
32
|
+
expect(files).toHaveLength(2);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('should find files in nested directories', () => {
|
|
36
|
+
const nestedDir = join(testDir, 'src', 'components');
|
|
37
|
+
mkdirSync(nestedDir, { recursive: true });
|
|
38
|
+
|
|
39
|
+
writeFileSync(join(testDir, 'index.js'), 'content');
|
|
40
|
+
writeFileSync(join(nestedDir, 'Button.js'), 'content');
|
|
41
|
+
|
|
42
|
+
const files = getAllFiles(testDir);
|
|
43
|
+
|
|
44
|
+
expect(files).toHaveLength(2);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should ignore node_modules', () => {
|
|
48
|
+
const nodeModules = join(testDir, 'node_modules', 'package');
|
|
49
|
+
mkdirSync(nodeModules, { recursive: true });
|
|
50
|
+
|
|
51
|
+
writeFileSync(join(testDir, 'index.js'), 'content');
|
|
52
|
+
writeFileSync(join(nodeModules, 'index.js'), 'content');
|
|
53
|
+
|
|
54
|
+
const files = getAllFiles(testDir);
|
|
55
|
+
|
|
56
|
+
expect(files).toHaveLength(1);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should ignore .git directories', () => {
|
|
60
|
+
const gitDir = join(testDir, '.git', 'objects');
|
|
61
|
+
mkdirSync(gitDir, { recursive: true });
|
|
62
|
+
|
|
63
|
+
writeFileSync(join(testDir, 'index.js'), 'content');
|
|
64
|
+
writeFileSync(join(gitDir, 'abc123'), 'content');
|
|
65
|
+
|
|
66
|
+
const files = getAllFiles(testDir);
|
|
67
|
+
|
|
68
|
+
expect(files).toHaveLength(1);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('should return empty array for empty directory', () => {
|
|
72
|
+
const files = getAllFiles(testDir);
|
|
73
|
+
expect(files).toHaveLength(0);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe('isBinaryFile', () => {
|
|
78
|
+
it('should detect common binary extensions', () => {
|
|
79
|
+
expect(isBinaryFile('image.png')).toBe(true);
|
|
80
|
+
expect(isBinaryFile('image.jpg')).toBe(true);
|
|
81
|
+
expect(isBinaryFile('archive.zip')).toBe(true);
|
|
82
|
+
expect(isBinaryFile('doc.pdf')).toBe(true);
|
|
83
|
+
expect(isBinaryFile('lib.dll')).toBe(true);
|
|
84
|
+
expect(isBinaryFile('app.exe')).toBe(true);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('should detect text files', () => {
|
|
88
|
+
expect(isBinaryFile('code.js')).toBe(false);
|
|
89
|
+
expect(isBinaryFile('style.css')).toBe(false);
|
|
90
|
+
expect(isBinaryFile('data.json')).toBe(false);
|
|
91
|
+
expect(isBinaryFile('README.md')).toBe(false);
|
|
92
|
+
expect(isBinaryFile('code.ts')).toBe(false);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should handle paths with directories', () => {
|
|
96
|
+
expect(isBinaryFile('/path/to/image.png')).toBe(true);
|
|
97
|
+
expect(isBinaryFile('src/components/Button.js')).toBe(false);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
});
|