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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "repo-cloak-cli",
3
- "version": "1.2.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",
@@ -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
- const selectedFiles = await selectFiles(sourceDir);
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.');
@@ -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: Get source (cloaked) directory
28
- const cloakedDir = options.source
29
- ? resolve(options.source)
30
- : await promptBackupFolder();
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}`);
@@ -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
+ });