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.
@@ -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
+ }
@@ -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
+ }