repo-cloak-cli 1.2.4 → 1.3.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/.github/workflows/release.yml +92 -92
- package/LICENSE +21 -21
- package/README.md +138 -118
- package/bin/repo-cloak.js +9 -9
- package/package.json +50 -50
- package/src/cli.js +86 -84
- package/src/commands/pull.js +582 -352
- package/src/commands/push.js +250 -235
- package/src/core/anonymizer.js +128 -128
- package/src/core/copier.js +139 -139
- package/src/core/crypto.js +128 -128
- package/src/core/git.js +61 -0
- package/src/core/mapper.js +235 -235
- package/src/core/scanner.js +137 -137
- package/src/index.js +8 -8
- package/src/ui/banner.js +70 -70
- package/src/ui/fileSelector.js +256 -256
- package/src/ui/prompts.js +165 -165
- package/tests/anonymizer.test.js +127 -127
- package/tests/copier.test.js +94 -94
- package/tests/crypto.test.js +106 -106
- package/tests/git.test.js +103 -0
- package/tests/mapper.test.js +166 -166
- package/tests/scanner.test.js +100 -100
- package/medium.md +0 -319
package/src/core/crypto.js
CHANGED
|
@@ -1,128 +1,128 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Crypto Module
|
|
3
|
-
* Handles encryption/decryption of sensitive data using user-specific secret
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from 'crypto';
|
|
7
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
8
|
-
import { join } from 'path';
|
|
9
|
-
import { homedir } from 'os';
|
|
10
|
-
|
|
11
|
-
const CONFIG_DIR = join(homedir(), '.repo-cloak');
|
|
12
|
-
const SECRET_FILE = join(CONFIG_DIR, 'secret.key');
|
|
13
|
-
const ALGORITHM = 'aes-256-gcm';
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Get or create user's secret key
|
|
17
|
-
*/
|
|
18
|
-
export function getOrCreateSecret() {
|
|
19
|
-
// Ensure config directory exists
|
|
20
|
-
if (!existsSync(CONFIG_DIR)) {
|
|
21
|
-
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// Check if secret exists
|
|
25
|
-
if (existsSync(SECRET_FILE)) {
|
|
26
|
-
return readFileSync(SECRET_FILE, 'utf-8').trim();
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// Generate new secret
|
|
30
|
-
const secret = randomBytes(32).toString('hex');
|
|
31
|
-
writeFileSync(SECRET_FILE, secret, { mode: 0o600 }); // Read/write only for owner
|
|
32
|
-
|
|
33
|
-
return secret;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Check if user has a secret key
|
|
38
|
-
*/
|
|
39
|
-
export function hasSecret() {
|
|
40
|
-
return existsSync(SECRET_FILE);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Encrypt a string using user's secret
|
|
45
|
-
*/
|
|
46
|
-
export function encrypt(text, secret) {
|
|
47
|
-
const key = scryptSync(secret, 'repo-cloak-salt', 32);
|
|
48
|
-
const iv = randomBytes(16);
|
|
49
|
-
const cipher = createCipheriv(ALGORITHM, key, iv);
|
|
50
|
-
|
|
51
|
-
let encrypted = cipher.update(text, 'utf8', 'hex');
|
|
52
|
-
encrypted += cipher.final('hex');
|
|
53
|
-
const authTag = cipher.getAuthTag();
|
|
54
|
-
|
|
55
|
-
// Return iv:authTag:encrypted
|
|
56
|
-
return `${iv.toString('hex')}:${authTag.toString('hex')}:${encrypted}`;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Decrypt a string using user's secret
|
|
61
|
-
*/
|
|
62
|
-
export function decrypt(encryptedData, secret) {
|
|
63
|
-
try {
|
|
64
|
-
const [ivHex, authTagHex, encrypted] = encryptedData.split(':');
|
|
65
|
-
|
|
66
|
-
if (!ivHex || !authTagHex || !encrypted) {
|
|
67
|
-
throw new Error('Invalid encrypted data format');
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const key = scryptSync(secret, 'repo-cloak-salt', 32);
|
|
71
|
-
const iv = Buffer.from(ivHex, 'hex');
|
|
72
|
-
const authTag = Buffer.from(authTagHex, 'hex');
|
|
73
|
-
const decipher = createDecipheriv(ALGORITHM, key, iv);
|
|
74
|
-
|
|
75
|
-
decipher.setAuthTag(authTag);
|
|
76
|
-
|
|
77
|
-
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
|
|
78
|
-
decrypted += decipher.final('utf8');
|
|
79
|
-
|
|
80
|
-
return decrypted;
|
|
81
|
-
} catch (error) {
|
|
82
|
-
return null; // Decryption failed
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Encrypt replacements for storage
|
|
88
|
-
*/
|
|
89
|
-
export function encryptReplacements(replacements, secret) {
|
|
90
|
-
return replacements.map(r => ({
|
|
91
|
-
original: encrypt(r.original, secret),
|
|
92
|
-
replacement: r.replacement, // Keep replacement visible (it's the safe version)
|
|
93
|
-
encrypted: true
|
|
94
|
-
}));
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* Decrypt replacements from storage
|
|
99
|
-
*/
|
|
100
|
-
export function decryptReplacements(replacements, secret) {
|
|
101
|
-
return replacements.map(r => {
|
|
102
|
-
if (!r.encrypted) {
|
|
103
|
-
return r; // Already decrypted or legacy format
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const original = decrypt(r.original, secret);
|
|
107
|
-
|
|
108
|
-
if (original === null) {
|
|
109
|
-
return {
|
|
110
|
-
...r,
|
|
111
|
-
original: null, // Failed to decrypt
|
|
112
|
-
decryptFailed: true
|
|
113
|
-
};
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
return {
|
|
117
|
-
original,
|
|
118
|
-
replacement: r.replacement
|
|
119
|
-
};
|
|
120
|
-
});
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* Get the config directory path
|
|
125
|
-
*/
|
|
126
|
-
export function getConfigDir() {
|
|
127
|
-
return CONFIG_DIR;
|
|
128
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Crypto Module
|
|
3
|
+
* Handles encryption/decryption of sensitive data using user-specific secret
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from 'crypto';
|
|
7
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
8
|
+
import { join } from 'path';
|
|
9
|
+
import { homedir } from 'os';
|
|
10
|
+
|
|
11
|
+
const CONFIG_DIR = join(homedir(), '.repo-cloak');
|
|
12
|
+
const SECRET_FILE = join(CONFIG_DIR, 'secret.key');
|
|
13
|
+
const ALGORITHM = 'aes-256-gcm';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Get or create user's secret key
|
|
17
|
+
*/
|
|
18
|
+
export function getOrCreateSecret() {
|
|
19
|
+
// Ensure config directory exists
|
|
20
|
+
if (!existsSync(CONFIG_DIR)) {
|
|
21
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Check if secret exists
|
|
25
|
+
if (existsSync(SECRET_FILE)) {
|
|
26
|
+
return readFileSync(SECRET_FILE, 'utf-8').trim();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Generate new secret
|
|
30
|
+
const secret = randomBytes(32).toString('hex');
|
|
31
|
+
writeFileSync(SECRET_FILE, secret, { mode: 0o600 }); // Read/write only for owner
|
|
32
|
+
|
|
33
|
+
return secret;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Check if user has a secret key
|
|
38
|
+
*/
|
|
39
|
+
export function hasSecret() {
|
|
40
|
+
return existsSync(SECRET_FILE);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Encrypt a string using user's secret
|
|
45
|
+
*/
|
|
46
|
+
export function encrypt(text, secret) {
|
|
47
|
+
const key = scryptSync(secret, 'repo-cloak-salt', 32);
|
|
48
|
+
const iv = randomBytes(16);
|
|
49
|
+
const cipher = createCipheriv(ALGORITHM, key, iv);
|
|
50
|
+
|
|
51
|
+
let encrypted = cipher.update(text, 'utf8', 'hex');
|
|
52
|
+
encrypted += cipher.final('hex');
|
|
53
|
+
const authTag = cipher.getAuthTag();
|
|
54
|
+
|
|
55
|
+
// Return iv:authTag:encrypted
|
|
56
|
+
return `${iv.toString('hex')}:${authTag.toString('hex')}:${encrypted}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Decrypt a string using user's secret
|
|
61
|
+
*/
|
|
62
|
+
export function decrypt(encryptedData, secret) {
|
|
63
|
+
try {
|
|
64
|
+
const [ivHex, authTagHex, encrypted] = encryptedData.split(':');
|
|
65
|
+
|
|
66
|
+
if (!ivHex || !authTagHex || !encrypted) {
|
|
67
|
+
throw new Error('Invalid encrypted data format');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const key = scryptSync(secret, 'repo-cloak-salt', 32);
|
|
71
|
+
const iv = Buffer.from(ivHex, 'hex');
|
|
72
|
+
const authTag = Buffer.from(authTagHex, 'hex');
|
|
73
|
+
const decipher = createDecipheriv(ALGORITHM, key, iv);
|
|
74
|
+
|
|
75
|
+
decipher.setAuthTag(authTag);
|
|
76
|
+
|
|
77
|
+
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
|
|
78
|
+
decrypted += decipher.final('utf8');
|
|
79
|
+
|
|
80
|
+
return decrypted;
|
|
81
|
+
} catch (error) {
|
|
82
|
+
return null; // Decryption failed
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Encrypt replacements for storage
|
|
88
|
+
*/
|
|
89
|
+
export function encryptReplacements(replacements, secret) {
|
|
90
|
+
return replacements.map(r => ({
|
|
91
|
+
original: encrypt(r.original, secret),
|
|
92
|
+
replacement: r.replacement, // Keep replacement visible (it's the safe version)
|
|
93
|
+
encrypted: true
|
|
94
|
+
}));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Decrypt replacements from storage
|
|
99
|
+
*/
|
|
100
|
+
export function decryptReplacements(replacements, secret) {
|
|
101
|
+
return replacements.map(r => {
|
|
102
|
+
if (!r.encrypted) {
|
|
103
|
+
return r; // Already decrypted or legacy format
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const original = decrypt(r.original, secret);
|
|
107
|
+
|
|
108
|
+
if (original === null) {
|
|
109
|
+
return {
|
|
110
|
+
...r,
|
|
111
|
+
original: null, // Failed to decrypt
|
|
112
|
+
decryptFailed: true
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
original,
|
|
118
|
+
replacement: r.replacement
|
|
119
|
+
};
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Get the config directory path
|
|
125
|
+
*/
|
|
126
|
+
export function getConfigDir() {
|
|
127
|
+
return CONFIG_DIR;
|
|
128
|
+
}
|
package/src/core/git.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git Integration
|
|
3
|
+
* Utilities to interact with Git repositories
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { exec } from 'child_process';
|
|
7
|
+
import { promisify } from 'util';
|
|
8
|
+
import { existsSync } from 'fs';
|
|
9
|
+
import { join } from 'path';
|
|
10
|
+
|
|
11
|
+
const execAsync = promisify(exec);
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Check if a directory is a Git repository
|
|
15
|
+
* @param {string} dirPath - Directory to check
|
|
16
|
+
* @returns {boolean} True if .git folder exists
|
|
17
|
+
*/
|
|
18
|
+
export function isGitRepo(dirPath) {
|
|
19
|
+
return existsSync(join(dirPath, '.git'));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Get list of changed/added/untracked files
|
|
24
|
+
* @param {string} dirPath - Repository root
|
|
25
|
+
* @returns {Promise<string[]>} List of relative file paths
|
|
26
|
+
*/
|
|
27
|
+
export async function getChangedFiles(dirPath) {
|
|
28
|
+
try {
|
|
29
|
+
// -u option shows individual files in untracked directories
|
|
30
|
+
const { stdout } = await execAsync('git status --porcelain -u', { cwd: dirPath });
|
|
31
|
+
|
|
32
|
+
if (!stdout) return [];
|
|
33
|
+
|
|
34
|
+
const files = stdout
|
|
35
|
+
.split('\n')
|
|
36
|
+
.filter(line => line.trim() !== '')
|
|
37
|
+
.map(line => {
|
|
38
|
+
// Format: "MM file.txt" or "?? file.txt" or "R old -> new"
|
|
39
|
+
const status = line.substring(0, 2);
|
|
40
|
+
let file = line.substring(3).trim();
|
|
41
|
+
|
|
42
|
+
// Handle renamed files: "old -> new"
|
|
43
|
+
if (file.includes(' -> ')) {
|
|
44
|
+
file = file.split(' -> ')[1];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Remove quotes if present (git status quotes files with spaces)
|
|
48
|
+
const cleanFile = file.replace(/^"|"$/g, '');
|
|
49
|
+
|
|
50
|
+
return { status, file: cleanFile };
|
|
51
|
+
})
|
|
52
|
+
// Filter deleted files (D) as we can't pull them
|
|
53
|
+
.filter(item => !item.status.includes('D'))
|
|
54
|
+
.map(item => item.file);
|
|
55
|
+
|
|
56
|
+
return files;
|
|
57
|
+
} catch (error) {
|
|
58
|
+
// If git command fails, return empty list
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
}
|