repo-cloak-cli 1.2.1 → 1.3.1
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 +1 -1
- package/src/commands/pull.js +69 -2
- package/src/commands/push.js +14 -4
- package/src/core/git.js +61 -0
- package/tests/git.test.js +103 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "repo-cloak-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.1",
|
|
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",
|
package/src/commands/pull.js
CHANGED
|
@@ -24,6 +24,7 @@ import { copyFiles } from '../core/copier.js';
|
|
|
24
24
|
import { createAnonymizer } from '../core/anonymizer.js';
|
|
25
25
|
import { createMapping, saveMapping, loadRawMapping, mergeMapping, hasMapping, decryptMapping } from '../core/mapper.js';
|
|
26
26
|
import { getOrCreateSecret, hasSecret, decryptReplacements } from '../core/crypto.js';
|
|
27
|
+
import { isGitRepo, getChangedFiles } from '../core/git.js';
|
|
27
28
|
|
|
28
29
|
export async function pull(options = {}) {
|
|
29
30
|
try {
|
|
@@ -180,6 +181,8 @@ export async function pull(options = {}) {
|
|
|
180
181
|
}
|
|
181
182
|
}
|
|
182
183
|
|
|
184
|
+
// ...
|
|
185
|
+
|
|
183
186
|
// Step 3: Get source directory if not already determined
|
|
184
187
|
if (!sourceDir) {
|
|
185
188
|
sourceDir = options.source
|
|
@@ -194,8 +197,72 @@ export async function pull(options = {}) {
|
|
|
194
197
|
|
|
195
198
|
console.log(chalk.dim(` Source: ${sourceDir}\n`));
|
|
196
199
|
|
|
197
|
-
// Step 4: Select files
|
|
198
|
-
|
|
200
|
+
// Step 4: Select files (Check for Git integration first)
|
|
201
|
+
let selectedFiles = [];
|
|
202
|
+
let useGitFiles = false;
|
|
203
|
+
|
|
204
|
+
if (isGitRepo(sourceDir)) {
|
|
205
|
+
const { useGit } = await inquirer.prompt([
|
|
206
|
+
{
|
|
207
|
+
type: 'confirm',
|
|
208
|
+
name: 'useGit',
|
|
209
|
+
message: 'Git repository detected. Do you want to select changed/added files?',
|
|
210
|
+
default: false
|
|
211
|
+
}
|
|
212
|
+
]);
|
|
213
|
+
|
|
214
|
+
if (useGit) {
|
|
215
|
+
const spinner = ora('Scanning changed files...').start();
|
|
216
|
+
const gitFiles = await getChangedFiles(sourceDir);
|
|
217
|
+
spinner.stop();
|
|
218
|
+
|
|
219
|
+
if (gitFiles.length === 0) {
|
|
220
|
+
console.log(chalk.yellow(' No changed or added files found in Git status.'));
|
|
221
|
+
const { fallback } = await inquirer.prompt([
|
|
222
|
+
{
|
|
223
|
+
type: 'confirm',
|
|
224
|
+
name: 'fallback',
|
|
225
|
+
message: 'Do you want to manually select files instead?',
|
|
226
|
+
default: true
|
|
227
|
+
}
|
|
228
|
+
]);
|
|
229
|
+
|
|
230
|
+
if (!fallback) {
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
} else {
|
|
234
|
+
// Filter to absolute paths and exist check
|
|
235
|
+
const validGitFiles = gitFiles
|
|
236
|
+
.map(f => resolve(sourceDir, f))
|
|
237
|
+
.filter(f => existsSync(f));
|
|
238
|
+
|
|
239
|
+
if (validGitFiles.length > 0) {
|
|
240
|
+
console.log(chalk.green(` Found ${validGitFiles.length} changed files.`));
|
|
241
|
+
|
|
242
|
+
// Let user confirm/deselect git files
|
|
243
|
+
const { confirmGitFiles } = await inquirer.prompt([
|
|
244
|
+
{
|
|
245
|
+
type: 'checkbox',
|
|
246
|
+
name: 'confirmGitFiles',
|
|
247
|
+
message: 'Select changed files to extract:',
|
|
248
|
+
choices: validGitFiles.map(f => ({
|
|
249
|
+
name: relative(sourceDir, f),
|
|
250
|
+
value: f,
|
|
251
|
+
checked: true
|
|
252
|
+
}))
|
|
253
|
+
}
|
|
254
|
+
]);
|
|
255
|
+
|
|
256
|
+
selectedFiles = confirmGitFiles;
|
|
257
|
+
useGitFiles = true;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (!useGitFiles || selectedFiles.length === 0) {
|
|
264
|
+
selectedFiles = await selectFiles(sourceDir);
|
|
265
|
+
}
|
|
199
266
|
|
|
200
267
|
if (selectedFiles.length === 0) {
|
|
201
268
|
showError('No files selected. Aborting.');
|
package/src/commands/push.js
CHANGED
|
@@ -24,10 +24,20 @@ import { getOrCreateSecret, hasSecret, decrypt, getConfigDir } from '../core/cry
|
|
|
24
24
|
|
|
25
25
|
export async function push(options = {}) {
|
|
26
26
|
try {
|
|
27
|
-
// Step 1:
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
// Step 1: Check if current directory has a mapping (auto-detect)
|
|
28
|
+
const currentDir = process.cwd();
|
|
29
|
+
let cloakedDir;
|
|
30
|
+
|
|
31
|
+
if (hasMapping(currentDir) && !options.source) {
|
|
32
|
+
// Running from inside a cloaked directory - use it directly
|
|
33
|
+
cloakedDir = currentDir;
|
|
34
|
+
console.log(chalk.cyan('\n Cloaked directory detected in current folder'));
|
|
35
|
+
} else {
|
|
36
|
+
// Ask for the source directory
|
|
37
|
+
cloakedDir = options.source
|
|
38
|
+
? resolve(options.source)
|
|
39
|
+
: await promptBackupFolder();
|
|
40
|
+
}
|
|
31
41
|
|
|
32
42
|
if (!existsSync(cloakedDir)) {
|
|
33
43
|
showError(`Directory does not exist: ${cloakedDir}`);
|
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
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git Module Tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
6
|
+
import { isGitRepo, getChangedFiles } from '../src/core/git.js';
|
|
7
|
+
import { existsSync, mkdirSync, writeFileSync, rmSync } from 'fs';
|
|
8
|
+
import { join } from 'path';
|
|
9
|
+
import { tmpdir } from 'os';
|
|
10
|
+
import { execSync } from 'child_process';
|
|
11
|
+
|
|
12
|
+
describe('Git Module', () => {
|
|
13
|
+
let testDir;
|
|
14
|
+
|
|
15
|
+
function initGitRepo(dir) {
|
|
16
|
+
execSync('git init', { cwd: dir });
|
|
17
|
+
execSync('git config user.name "Test User"', { cwd: dir });
|
|
18
|
+
execSync('git config user.email "test@example.com"', { cwd: dir });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
testDir = join(tmpdir(), `repo-cloak-git-test-${Date.now()}`);
|
|
23
|
+
mkdirSync(testDir, { recursive: true });
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
afterEach(() => {
|
|
27
|
+
if (existsSync(testDir)) {
|
|
28
|
+
try {
|
|
29
|
+
rmSync(testDir, { recursive: true, force: true });
|
|
30
|
+
} catch (e) {
|
|
31
|
+
// Ignore cleanup errors
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe('isGitRepo', () => {
|
|
37
|
+
it('should return true for git repository', () => {
|
|
38
|
+
initGitRepo(testDir);
|
|
39
|
+
expect(isGitRepo(testDir)).toBe(true);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should return false for non-git directory', () => {
|
|
43
|
+
expect(isGitRepo(testDir)).toBe(false);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe('getChangedFiles', () => {
|
|
48
|
+
it('should return empty list for clean repo', async () => {
|
|
49
|
+
initGitRepo(testDir);
|
|
50
|
+
const files = await getChangedFiles(testDir);
|
|
51
|
+
expect(files).toEqual([]);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should detect untracked files', async () => {
|
|
55
|
+
initGitRepo(testDir);
|
|
56
|
+
writeFileSync(join(testDir, 'newfile.txt'), 'content');
|
|
57
|
+
|
|
58
|
+
const files = await getChangedFiles(testDir);
|
|
59
|
+
expect(files).toContain('newfile.txt');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should detect modified files', async () => {
|
|
63
|
+
initGitRepo(testDir);
|
|
64
|
+
writeFileSync(join(testDir, 'test.txt'), 'initial');
|
|
65
|
+
execSync('git add test.txt', { cwd: testDir });
|
|
66
|
+
execSync('git commit -m "initial"', { cwd: testDir });
|
|
67
|
+
|
|
68
|
+
writeFileSync(join(testDir, 'test.txt'), 'changed');
|
|
69
|
+
|
|
70
|
+
const files = await getChangedFiles(testDir);
|
|
71
|
+
expect(files).toContain('test.txt');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('should expand untracked directories', async () => {
|
|
75
|
+
initGitRepo(testDir);
|
|
76
|
+
const deployDir = join(testDir, 'deploy');
|
|
77
|
+
mkdirSync(deployDir);
|
|
78
|
+
writeFileSync(join(deployDir, 'config.yml'), 'config');
|
|
79
|
+
writeFileSync(join(deployDir, 'script.sh'), 'script');
|
|
80
|
+
|
|
81
|
+
// git status --porcelain shows "?? deploy/"
|
|
82
|
+
// We want ["deploy/config.yml", "deploy/script.sh"]
|
|
83
|
+
|
|
84
|
+
const files = await getChangedFiles(testDir);
|
|
85
|
+
expect(files).toContain('deploy/config.yml'); // Using forward slash for cross-platform expectation in test
|
|
86
|
+
expect(files).toContain('deploy/script.sh');
|
|
87
|
+
expect(files).toHaveLength(2);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('should handle renamed files', async () => {
|
|
91
|
+
initGitRepo(testDir);
|
|
92
|
+
writeFileSync(join(testDir, 'old.txt'), 'content');
|
|
93
|
+
execSync('git add old.txt', { cwd: testDir });
|
|
94
|
+
execSync('git commit -m "initial"', { cwd: testDir });
|
|
95
|
+
|
|
96
|
+
execSync('git mv old.txt new.txt', { cwd: testDir });
|
|
97
|
+
|
|
98
|
+
const files = await getChangedFiles(testDir);
|
|
99
|
+
expect(files).toContain('new.txt');
|
|
100
|
+
expect(files).not.toContain('old.txt');
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
});
|