repo-cloak-cli 1.3.2 ā 1.3.4
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/DEVELOPMENT.md +74 -0
- package/LINKEDIN.md +14 -0
- package/MEDIUM.md +335 -0
- package/README.md +65 -23
- package/SLIDES-TO-GENERATE-BY-NOTEBOOK-LLM.md +52 -0
- package/package.json +6 -5
- package/src/cli.js +2 -0
- package/src/commands/pull.js +305 -125
- package/src/core/agents-template.js +37 -0
- package/src/core/anonymizer.js +14 -4
- package/src/core/git.js +68 -0
- package/src/core/path-cache.js +131 -0
- package/src/core/secrets.js +169 -0
- package/src/ui/banner.js +32 -33
- package/src/ui/prompts.js +139 -34
- package/src/ui/treeCheckboxSelector.js +253 -0
- package/test-tree.js +2 -0
- package/tests/anonymizer.test.js +17 -17
- package/tests/copier.test.js +3 -3
- package/tests/crypto.test.js +3 -3
- package/tests/git.test.js +59 -1
- package/tests/path-cache.test.js +164 -0
- package/tests/secrets.test.js +93 -0
- package/.github/workflows/release.yml +0 -92
- package/src/ui/fileSelector.js +0 -256
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "repo-cloak-cli",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.4",
|
|
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",
|
|
7
7
|
"bin": {
|
|
8
|
-
"repo-cloak": "
|
|
9
|
-
"cloak": "
|
|
8
|
+
"repo-cloak": "bin/repo-cloak.js",
|
|
9
|
+
"cloak": "bin/repo-cloak.js"
|
|
10
10
|
},
|
|
11
11
|
"scripts": {
|
|
12
12
|
"start": "node bin/repo-cloak.js",
|
|
@@ -32,12 +32,13 @@
|
|
|
32
32
|
"license": "MIT",
|
|
33
33
|
"repository": {
|
|
34
34
|
"type": "git",
|
|
35
|
-
"url": "https://github.com/iamshz97/repo-cloak.git"
|
|
35
|
+
"url": "git+https://github.com/iamshz97/repo-cloak.git"
|
|
36
36
|
},
|
|
37
37
|
"engines": {
|
|
38
38
|
"node": ">=18.0.0"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
+
"@inquirer/checkbox": "^5.1.2",
|
|
41
42
|
"chalk": "^5.3.0",
|
|
42
43
|
"commander": "^12.1.0",
|
|
43
44
|
"figlet": "^1.7.0",
|
|
@@ -48,4 +49,4 @@
|
|
|
48
49
|
"devDependencies": {
|
|
49
50
|
"vitest": "^4.0.18"
|
|
50
51
|
}
|
|
51
|
-
}
|
|
52
|
+
}
|
package/src/cli.js
CHANGED
|
@@ -32,6 +32,8 @@ program
|
|
|
32
32
|
.option('-s, --source <path>', 'Source directory (default: current directory)')
|
|
33
33
|
.option('-d, --dest <path>', 'Destination directory')
|
|
34
34
|
.option('-f, --force', 'Force pull all files (skip prompts, requires existing mapping)')
|
|
35
|
+
.option('-c, --commit <hash...>', 'Extract files changed in specific commits')
|
|
36
|
+
.option('-l, --list-commits [count]', 'List and select from recent commits (default: 10)')
|
|
35
37
|
.option('-q, --quiet', 'Minimal output')
|
|
36
38
|
.action(async (options) => {
|
|
37
39
|
await showBanner();
|
package/src/commands/pull.js
CHANGED
|
@@ -7,10 +7,10 @@
|
|
|
7
7
|
import ora from 'ora';
|
|
8
8
|
import chalk from 'chalk';
|
|
9
9
|
import inquirer from 'inquirer';
|
|
10
|
-
import { existsSync,
|
|
11
|
-
import { resolve, relative } from 'path';
|
|
10
|
+
import { existsSync, writeFileSync } from 'fs';
|
|
11
|
+
import { resolve, relative, join } from 'path';
|
|
12
12
|
|
|
13
|
-
import {
|
|
13
|
+
import { checkboxTreeSelectFilesOnly } from '../ui/treeCheckboxSelector.js';
|
|
14
14
|
import {
|
|
15
15
|
promptSourceDirectory,
|
|
16
16
|
promptDestinationDirectory,
|
|
@@ -20,12 +20,15 @@ import {
|
|
|
20
20
|
} from '../ui/prompts.js';
|
|
21
21
|
import { showSuccess, showError, showInfo } from '../ui/banner.js';
|
|
22
22
|
import { getAllFiles } from '../core/scanner.js';
|
|
23
|
+
import { scanFilesForSecrets } from '../core/secrets.js';
|
|
24
|
+
import { getAgentsMarkdown } from '../core/agents-template.js';
|
|
23
25
|
|
|
24
26
|
import { copyFiles } from '../core/copier.js';
|
|
25
27
|
import { createAnonymizer } from '../core/anonymizer.js';
|
|
26
28
|
import { createMapping, saveMapping, loadRawMapping, mergeMapping, hasMapping, decryptMapping } from '../core/mapper.js';
|
|
27
29
|
import { getOrCreateSecret, hasSecret, decryptReplacements } from '../core/crypto.js';
|
|
28
|
-
import { isGitRepo, getChangedFiles } from '../core/git.js';
|
|
30
|
+
import { isGitRepo, getChangedFiles, getRecentCommits, getFilesChangedInCommits } from '../core/git.js';
|
|
31
|
+
|
|
29
32
|
|
|
30
33
|
export async function pull(options = {}) {
|
|
31
34
|
try {
|
|
@@ -38,6 +41,23 @@ export async function pull(options = {}) {
|
|
|
38
41
|
// Step 1: Check if current directory is already a cloaked directory
|
|
39
42
|
const currentDir = process.cwd();
|
|
40
43
|
|
|
44
|
+
// āā Step 2: Resolve sourceDir first (always ask source before destination) āā
|
|
45
|
+
if (!sourceDir) {
|
|
46
|
+
sourceDir = options.source
|
|
47
|
+
? resolve(options.source)
|
|
48
|
+
: await promptSourceDirectory();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!existsSync(sourceDir)) {
|
|
52
|
+
showError(`Source directory does not exist: ${sourceDir}`);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (!options.force) {
|
|
57
|
+
console.log(chalk.dim(` Source: ${sourceDir}\n`));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// āā Step 3: Resolve destDir āā
|
|
41
61
|
if (hasMapping(currentDir) && !options.dest) {
|
|
42
62
|
// Running from inside an existing cloaked directory - auto-detect!
|
|
43
63
|
destDir = currentDir;
|
|
@@ -59,18 +79,18 @@ export async function pull(options = {}) {
|
|
|
59
79
|
{
|
|
60
80
|
type: 'list',
|
|
61
81
|
name: 'mode',
|
|
62
|
-
message: '
|
|
82
|
+
message: 'This looks like a folder you already used ā what do you want to do?',
|
|
63
83
|
choices: [
|
|
64
84
|
{
|
|
65
|
-
name: 'Quick Add
|
|
85
|
+
name: 'Quick Add ā keep existing settings and add more files',
|
|
66
86
|
value: 'quick'
|
|
67
87
|
},
|
|
68
88
|
{
|
|
69
|
-
name: 'Add More Replacements
|
|
89
|
+
name: 'Add More Replacements ā add files with extra anonymization',
|
|
70
90
|
value: 'extend'
|
|
71
91
|
},
|
|
72
92
|
{
|
|
73
|
-
name: 'Fresh Start
|
|
93
|
+
name: 'Fresh Start ā pick a new output folder',
|
|
74
94
|
value: 'fresh'
|
|
75
95
|
}
|
|
76
96
|
]
|
|
@@ -116,33 +136,9 @@ export async function pull(options = {}) {
|
|
|
116
136
|
console.log('');
|
|
117
137
|
}
|
|
118
138
|
}
|
|
119
|
-
|
|
120
|
-
// Try to get original source path
|
|
121
|
-
if (existingMapping.encrypted && hasSecret()) {
|
|
122
|
-
const secret = getOrCreateSecret();
|
|
123
|
-
try {
|
|
124
|
-
const decrypted = decryptMapping(existingMapping, secret);
|
|
125
|
-
if (decrypted.source?.path && existsSync(decrypted.source.path)) {
|
|
126
|
-
sourceDir = decrypted.source.path;
|
|
127
|
-
if (!options.force) {
|
|
128
|
-
console.log(chalk.dim(` Source: ${sourceDir}\n`));
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
} catch (err) {
|
|
132
|
-
// Source path couldn't be decrypted, will prompt
|
|
133
|
-
}
|
|
134
|
-
} else if (!existingMapping.encrypted && existingMapping.source?.path) {
|
|
135
|
-
// Not encrypted - use directly
|
|
136
|
-
if (existsSync(existingMapping.source.path)) {
|
|
137
|
-
sourceDir = existingMapping.source.path;
|
|
138
|
-
if (!options.force) {
|
|
139
|
-
console.log(chalk.dim(` Source: ${sourceDir}\n`));
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
139
|
}
|
|
144
140
|
} else {
|
|
145
|
-
// Not running from a cloaked directory
|
|
141
|
+
// Not running from a cloaked directory
|
|
146
142
|
if (options.force) {
|
|
147
143
|
showError('Force flag can only be used within an existing cloaked directory.');
|
|
148
144
|
return;
|
|
@@ -172,11 +168,11 @@ export async function pull(options = {}) {
|
|
|
172
168
|
{
|
|
173
169
|
type: 'list',
|
|
174
170
|
name: 'mode',
|
|
175
|
-
message: '
|
|
171
|
+
message: 'This looks like a folder you already used ā what do you want to do?',
|
|
176
172
|
choices: [
|
|
177
|
-
{ name: 'Quick Add
|
|
178
|
-
{ name: 'Add More Replacements
|
|
179
|
-
{ name: 'Fresh Start
|
|
173
|
+
{ name: 'Quick Add ā keep existing settings and add more files', value: 'quick' },
|
|
174
|
+
{ name: 'Add More Replacements ā add files with extra anonymization', value: 'extend' },
|
|
175
|
+
{ name: 'Fresh Start ā pick a new output folder', value: 'fresh' }
|
|
180
176
|
]
|
|
181
177
|
}
|
|
182
178
|
]);
|
|
@@ -218,53 +214,11 @@ export async function pull(options = {}) {
|
|
|
218
214
|
console.log('');
|
|
219
215
|
}
|
|
220
216
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
// Try to get original source path
|
|
225
|
-
if (existingMapping.encrypted && hasSecret()) {
|
|
226
|
-
const secret = getOrCreateSecret();
|
|
227
|
-
try {
|
|
228
|
-
const decrypted = decryptMapping(existingMapping, secret);
|
|
229
|
-
if (decrypted.source?.path && existsSync(decrypted.source.path)) {
|
|
230
|
-
sourceDir = decrypted.source.path;
|
|
231
|
-
if (!options.force) {
|
|
232
|
-
console.log(chalk.dim(` Source: ${sourceDir}\n`));
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
} catch (err) {
|
|
236
|
-
// Source path couldn't be decrypted, will prompt
|
|
237
|
-
}
|
|
238
|
-
} else if (!existingMapping.encrypted && existingMapping.source?.path) {
|
|
239
|
-
if (existsSync(existingMapping.source.path)) {
|
|
240
|
-
sourceDir = existingMapping.source.path;
|
|
241
|
-
if (!options.force) {
|
|
242
|
-
console.log(chalk.dim(` Source: ${sourceDir}\n`));
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
217
|
}
|
|
247
218
|
}
|
|
248
219
|
}
|
|
249
220
|
|
|
250
|
-
//
|
|
251
|
-
|
|
252
|
-
// Step 3: Get source directory if not already determined
|
|
253
|
-
if (!sourceDir) {
|
|
254
|
-
sourceDir = options.source
|
|
255
|
-
? resolve(options.source)
|
|
256
|
-
: await promptSourceDirectory();
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
if (!existsSync(sourceDir)) {
|
|
260
|
-
showError(`Source directory does not exist: ${sourceDir}`);
|
|
261
|
-
return;
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
if (!options.force) {
|
|
265
|
-
console.log(chalk.dim(` Source: ${sourceDir}\n`));
|
|
266
|
-
}
|
|
267
|
-
|
|
221
|
+
// ...\n
|
|
268
222
|
// Step 4: Select files (Check for Git integration first)
|
|
269
223
|
let selectedFiles = [];
|
|
270
224
|
let useGitFiles = false;
|
|
@@ -358,67 +312,250 @@ export async function pull(options = {}) {
|
|
|
358
312
|
showError('No files found in existing mapping.');
|
|
359
313
|
return;
|
|
360
314
|
}
|
|
315
|
+
} else if (!isGitRepo(sourceDir) && (options.commit || options.listCommits !== undefined)) {
|
|
316
|
+
showError('Source directory is not a Git repository. Cannot use commit flags.');
|
|
317
|
+
return;
|
|
361
318
|
} else if (isGitRepo(sourceDir)) {
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
319
|
+
if (options.commit) {
|
|
320
|
+
const spinner = ora('Fetching files from commits...').start();
|
|
321
|
+
const commitFiles = await getFilesChangedInCommits(sourceDir, options.commit);
|
|
322
|
+
spinner.stop();
|
|
323
|
+
|
|
324
|
+
if (commitFiles.length === 0) {
|
|
325
|
+
showError('No files found in the specified commits.');
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Filter to absolute paths and exist check
|
|
330
|
+
const validCommitFiles = commitFiles
|
|
331
|
+
.map(f => resolve(sourceDir, f))
|
|
332
|
+
.filter(f => existsSync(f));
|
|
333
|
+
|
|
334
|
+
if (validCommitFiles.length > 0) {
|
|
335
|
+
console.log(chalk.green(` Found ${validCommitFiles.length} files in specified commits.`));
|
|
336
|
+
selectedFiles = validCommitFiles;
|
|
337
|
+
useGitFiles = true;
|
|
338
|
+
} else {
|
|
339
|
+
showError('None of the files from the specified commits exist locally.');
|
|
340
|
+
return;
|
|
368
341
|
}
|
|
369
|
-
|
|
342
|
+
} else if (options.listCommits !== undefined) {
|
|
343
|
+
const count = options.listCommits === true ? 10 : parseInt(options.listCommits, 10) || 10;
|
|
344
|
+
const commits = await getRecentCommits(sourceDir, count);
|
|
370
345
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
346
|
+
if (commits.length === 0) {
|
|
347
|
+
showError('No commits found in the repository.');
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const { selectedCommits } = await inquirer.prompt([
|
|
352
|
+
{
|
|
353
|
+
type: 'checkbox',
|
|
354
|
+
name: 'selectedCommits',
|
|
355
|
+
message: 'Which commits do you want to pull files from? (space to select, enter to confirm):',
|
|
356
|
+
choices: commits.map(c => ({
|
|
357
|
+
name: `${c.hash} - ${c.message}`,
|
|
358
|
+
value: c.hash
|
|
359
|
+
}))
|
|
360
|
+
}
|
|
361
|
+
]);
|
|
362
|
+
|
|
363
|
+
if (selectedCommits.length === 0) {
|
|
364
|
+
showError('No commits selected. Aborting.');
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const spinner = ora('Fetching files from selected commits...').start();
|
|
369
|
+
const commitFiles = await getFilesChangedInCommits(sourceDir, selectedCommits);
|
|
374
370
|
spinner.stop();
|
|
371
|
+
|
|
372
|
+
if (commitFiles.length === 0) {
|
|
373
|
+
showError('No files found in the selected commits.');
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const validCommitFiles = commitFiles
|
|
378
|
+
.map(f => resolve(sourceDir, f))
|
|
379
|
+
.filter(f => existsSync(f));
|
|
380
|
+
|
|
381
|
+
if (validCommitFiles.length > 0) {
|
|
382
|
+
console.log(chalk.green(` Found ${validCommitFiles.length} files in selected commits.`));
|
|
383
|
+
selectedFiles = validCommitFiles;
|
|
384
|
+
useGitFiles = true;
|
|
385
|
+
} else {
|
|
386
|
+
showError('None of the files from the selected commits exist locally.');
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
} else {
|
|
390
|
+
const { gitAction } = await inquirer.prompt([
|
|
391
|
+
{
|
|
392
|
+
type: 'list',
|
|
393
|
+
name: 'gitAction',
|
|
394
|
+
message: 'This is a Git repo ā how do you want to pick files?',
|
|
395
|
+
choices: [
|
|
396
|
+
{ name: 'Uncommitted changes (working directory)', value: 'uncommitted' },
|
|
397
|
+
{ name: 'Files from recent commits', value: 'commits' },
|
|
398
|
+
{ name: 'Specific commit ID', value: 'commit_id' },
|
|
399
|
+
{ name: 'Manual selection (bypasses git)', value: 'manual' }
|
|
400
|
+
]
|
|
401
|
+
}
|
|
402
|
+
]);
|
|
375
403
|
|
|
376
|
-
if (
|
|
377
|
-
|
|
378
|
-
const { fallback } = await inquirer.prompt([
|
|
404
|
+
if (gitAction === 'commit_id') {
|
|
405
|
+
const { commitHash } = await inquirer.prompt([
|
|
379
406
|
{
|
|
380
|
-
type: '
|
|
381
|
-
name: '
|
|
382
|
-
message: '
|
|
383
|
-
|
|
407
|
+
type: 'input',
|
|
408
|
+
name: 'commitHash',
|
|
409
|
+
message: 'Paste or type the commit hash you want to pull files from:',
|
|
410
|
+
validate: input => input.trim() !== '' ? true : 'Commit hash cannot be empty.'
|
|
384
411
|
}
|
|
385
412
|
]);
|
|
386
413
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
} else {
|
|
391
|
-
// Filter to absolute paths and exist check
|
|
392
|
-
const validGitFiles = gitFiles
|
|
393
|
-
.map(f => resolve(sourceDir, f))
|
|
394
|
-
.filter(f => existsSync(f));
|
|
414
|
+
const spinner = ora(`Fetching files from commit ${commitHash}...`).start();
|
|
415
|
+
const commitFiles = await getFilesChangedInCommits(sourceDir, [commitHash]);
|
|
416
|
+
spinner.stop();
|
|
395
417
|
|
|
396
|
-
if (
|
|
397
|
-
console.log(chalk.
|
|
418
|
+
if (commitFiles.length === 0) {
|
|
419
|
+
console.log(chalk.yellow(` No files found in commit ${commitHash} or invalid commit ID.`));
|
|
420
|
+
} else {
|
|
421
|
+
const validCommitFiles = commitFiles
|
|
422
|
+
.map(f => resolve(sourceDir, f))
|
|
423
|
+
.filter(f => existsSync(f));
|
|
424
|
+
|
|
425
|
+
if (validCommitFiles.length > 0) {
|
|
426
|
+
const allowedPaths = new Set(validCommitFiles);
|
|
427
|
+
const allowedDirs = new Set();
|
|
428
|
+
for (const file of validCommitFiles) {
|
|
429
|
+
let dir = resolve(file, '..');
|
|
430
|
+
while (dir && dir !== sourceDir && dir !== '/' && dir !== resolve(dir, '..')) {
|
|
431
|
+
allowedDirs.add(dir);
|
|
432
|
+
dir = resolve(dir, '..');
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
allowedDirs.add(sourceDir);
|
|
436
|
+
|
|
437
|
+
const pickedFiles = await checkboxTreeSelectFilesOnly({
|
|
438
|
+
root: sourceDir,
|
|
439
|
+
message: `Found ${validCommitFiles.length} files in commit ${commitHash} ā uncheck any you want to skip:`,
|
|
440
|
+
precheck: validCommitFiles,
|
|
441
|
+
ignore: (fullPath, name) => {
|
|
442
|
+
return !(allowedPaths.has(fullPath) || allowedDirs.has(fullPath));
|
|
443
|
+
}
|
|
444
|
+
});
|
|
398
445
|
|
|
399
|
-
|
|
400
|
-
|
|
446
|
+
if (pickedFiles.length > 0) {
|
|
447
|
+
selectedFiles = pickedFiles;
|
|
448
|
+
useGitFiles = true;
|
|
449
|
+
} else {
|
|
450
|
+
console.log(chalk.yellow(' No files selected.'));
|
|
451
|
+
}
|
|
452
|
+
} else {
|
|
453
|
+
console.log(chalk.yellow(` None of the files from the specified commit exist locally.`));
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
} else if (gitAction === 'commits') {
|
|
457
|
+
const commits = await getRecentCommits(sourceDir, 10);
|
|
458
|
+
if (commits.length === 0) {
|
|
459
|
+
console.log(chalk.yellow(' No commits found in the repository.'));
|
|
460
|
+
} else {
|
|
461
|
+
const { selectedCommits } = await inquirer.prompt([
|
|
401
462
|
{
|
|
402
463
|
type: 'checkbox',
|
|
403
|
-
name: '
|
|
404
|
-
message: '
|
|
405
|
-
choices:
|
|
406
|
-
name:
|
|
407
|
-
value:
|
|
408
|
-
checked: true
|
|
464
|
+
name: 'selectedCommits',
|
|
465
|
+
message: 'Which commits do you want to pull files from? (space to select, enter to confirm):',
|
|
466
|
+
choices: commits.map(c => ({
|
|
467
|
+
name: `${c.hash} - ${c.message}`,
|
|
468
|
+
value: c.hash
|
|
409
469
|
}))
|
|
410
470
|
}
|
|
411
471
|
]);
|
|
412
472
|
|
|
413
|
-
|
|
414
|
-
|
|
473
|
+
if (selectedCommits.length > 0) {
|
|
474
|
+
const spinner = ora('Fetching files from selected commits...').start();
|
|
475
|
+
const commitFiles = await getFilesChangedInCommits(sourceDir, selectedCommits);
|
|
476
|
+
spinner.stop();
|
|
477
|
+
|
|
478
|
+
const validCommitFiles = commitFiles
|
|
479
|
+
.map(f => resolve(sourceDir, f))
|
|
480
|
+
.filter(f => existsSync(f));
|
|
481
|
+
|
|
482
|
+
if (validCommitFiles.length > 0) {
|
|
483
|
+
const allowedPaths = new Set(validCommitFiles);
|
|
484
|
+
const allowedDirs = new Set();
|
|
485
|
+
for (const file of validCommitFiles) {
|
|
486
|
+
let dir = resolve(file, '..');
|
|
487
|
+
while (dir && dir !== sourceDir && dir !== '/' && dir !== resolve(dir, '..')) {
|
|
488
|
+
allowedDirs.add(dir);
|
|
489
|
+
dir = resolve(dir, '..');
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
allowedDirs.add(sourceDir);
|
|
493
|
+
|
|
494
|
+
const pickedFiles = await checkboxTreeSelectFilesOnly({
|
|
495
|
+
root: sourceDir,
|
|
496
|
+
message: `Found ${validCommitFiles.length} files across selected commits ā uncheck any you want to skip:`,
|
|
497
|
+
precheck: validCommitFiles,
|
|
498
|
+
ignore: (fullPath, name) => {
|
|
499
|
+
return !(allowedPaths.has(fullPath) || allowedDirs.has(fullPath));
|
|
500
|
+
}
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
if (pickedFiles.length > 0) {
|
|
504
|
+
selectedFiles = pickedFiles;
|
|
505
|
+
useGitFiles = true;
|
|
506
|
+
} else {
|
|
507
|
+
console.log(chalk.yellow(' No files selected.'));
|
|
508
|
+
}
|
|
509
|
+
} else {
|
|
510
|
+
console.log(chalk.yellow(' None of the files from the selected commits exist locally.'));
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
} else if (gitAction === 'uncommitted') {
|
|
515
|
+
const spinner = ora('Scanning uncommitted files...').start();
|
|
516
|
+
const gitFiles = await getChangedFiles(sourceDir);
|
|
517
|
+
spinner.stop();
|
|
518
|
+
|
|
519
|
+
if (gitFiles.length === 0) {
|
|
520
|
+
console.log(chalk.yellow(' No uncommitted files found in Git status.'));
|
|
521
|
+
} else {
|
|
522
|
+
const validGitFiles = gitFiles
|
|
523
|
+
.map(f => resolve(sourceDir, f))
|
|
524
|
+
.filter(f => existsSync(f));
|
|
525
|
+
|
|
526
|
+
if (validGitFiles.length > 0) {
|
|
527
|
+
console.log(chalk.green(` Found ${validGitFiles.length} changed files.`));
|
|
528
|
+
|
|
529
|
+
const allowedPaths = new Set(validGitFiles);
|
|
530
|
+
const allowedDirs = new Set();
|
|
531
|
+
for (const file of validGitFiles) {
|
|
532
|
+
let dir = resolve(file, '..');
|
|
533
|
+
while (dir && dir !== sourceDir && dir !== '/' && dir !== resolve(dir, '..')) {
|
|
534
|
+
allowedDirs.add(dir);
|
|
535
|
+
dir = resolve(dir, '..');
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
allowedDirs.add(sourceDir);
|
|
539
|
+
|
|
540
|
+
const pickedFiles = await checkboxTreeSelectFilesOnly({
|
|
541
|
+
root: sourceDir,
|
|
542
|
+
message: 'Which changed files do you want to extract? (space to deselect):',
|
|
543
|
+
precheck: validGitFiles,
|
|
544
|
+
ignore: (fullPath, name) => {
|
|
545
|
+
return !(allowedPaths.has(fullPath) || allowedDirs.has(fullPath));
|
|
546
|
+
}
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
selectedFiles = pickedFiles;
|
|
550
|
+
useGitFiles = true;
|
|
551
|
+
}
|
|
415
552
|
}
|
|
416
553
|
}
|
|
417
554
|
}
|
|
418
555
|
}
|
|
419
556
|
|
|
420
557
|
if (!options.force && (!useGitFiles || selectedFiles.length === 0)) {
|
|
421
|
-
selectedFiles = await
|
|
558
|
+
selectedFiles = await checkboxTreeSelectFilesOnly({ root: sourceDir });
|
|
422
559
|
}
|
|
423
560
|
|
|
424
561
|
if (selectedFiles.length === 0) {
|
|
@@ -430,6 +567,48 @@ export async function pull(options = {}) {
|
|
|
430
567
|
console.log(chalk.green(`\nā Selected ${selectedFiles.length} files\n`));
|
|
431
568
|
}
|
|
432
569
|
|
|
570
|
+
// Step 4.5: Scan for Secrets
|
|
571
|
+
if (!options.force) {
|
|
572
|
+
const scanSpinner = ora('Scanning selected files for sensitive data...').start();
|
|
573
|
+
const secretFindings = await scanFilesForSecrets(selectedFiles);
|
|
574
|
+
scanSpinner.stop();
|
|
575
|
+
|
|
576
|
+
if (secretFindings.length > 0) {
|
|
577
|
+
console.log(chalk.red.bold('\nā ļø WARNING: POTENTIAL SENSITIVE DATA DETECTED ā ļø\n'));
|
|
578
|
+
|
|
579
|
+
// Group by file to present a cleaner list
|
|
580
|
+
const findingsByFile = secretFindings.reduce((acc, finding) => {
|
|
581
|
+
const relPath = relative(sourceDir, finding.file);
|
|
582
|
+
if (!acc[relPath]) acc[relPath] = new Set();
|
|
583
|
+
acc[relPath].add(`${finding.type} (Line ${finding.line})`);
|
|
584
|
+
return acc;
|
|
585
|
+
}, {});
|
|
586
|
+
|
|
587
|
+
for (const [file, secrets] of Object.entries(findingsByFile)) {
|
|
588
|
+
console.log(chalk.yellow(` ${file}:`));
|
|
589
|
+
for (const secret of secrets) {
|
|
590
|
+
console.log(chalk.dim(` - ${secret}`));
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
console.log('');
|
|
594
|
+
|
|
595
|
+
const { proceedWithSecrets } = await inquirer.prompt([
|
|
596
|
+
{
|
|
597
|
+
type: 'confirm',
|
|
598
|
+
name: 'proceedWithSecrets',
|
|
599
|
+
message: 'Some sensitive data was detected ā are you sure you want to continue?',
|
|
600
|
+
default: false
|
|
601
|
+
}
|
|
602
|
+
]);
|
|
603
|
+
|
|
604
|
+
if (!proceedWithSecrets) {
|
|
605
|
+
showInfo('Operation cancelled to protect sensitive data.');
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
console.log('');
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
433
612
|
// Step 5: Handle replacements based on mode
|
|
434
613
|
let replacements = [...existingReplacements];
|
|
435
614
|
|
|
@@ -445,7 +624,7 @@ export async function pull(options = {}) {
|
|
|
445
624
|
{
|
|
446
625
|
type: 'confirm',
|
|
447
626
|
name: 'addMore',
|
|
448
|
-
message: '
|
|
627
|
+
message: 'Want to add any more replacements?',
|
|
449
628
|
default: false
|
|
450
629
|
}
|
|
451
630
|
]);
|
|
@@ -479,12 +658,6 @@ export async function pull(options = {}) {
|
|
|
479
658
|
}
|
|
480
659
|
}
|
|
481
660
|
|
|
482
|
-
// Step 7: Create destination directory
|
|
483
|
-
if (!existsSync(destDir)) {
|
|
484
|
-
mkdirSync(destDir, { recursive: true });
|
|
485
|
-
console.log(chalk.dim(` Created directory: ${destDir}`));
|
|
486
|
-
}
|
|
487
|
-
|
|
488
661
|
// Step 8: Copy and anonymize files
|
|
489
662
|
const spinner = ora('Copying and anonymizing files...').start();
|
|
490
663
|
|
|
@@ -520,6 +693,13 @@ export async function pull(options = {}) {
|
|
|
520
693
|
});
|
|
521
694
|
}
|
|
522
695
|
|
|
696
|
+
// Step 8.5: Write AGENTS.md into the cloaked workspace
|
|
697
|
+
if (!existingMapping) {
|
|
698
|
+
const agentsPath = join(destDir, 'AGENTS.md');
|
|
699
|
+
writeFileSync(agentsPath, getAgentsMarkdown(), 'utf-8');
|
|
700
|
+
console.log(chalk.cyan(` š¤ AGENTS.md created for AI agent context`));
|
|
701
|
+
}
|
|
702
|
+
|
|
523
703
|
// Step 9: Prepare new file mappings
|
|
524
704
|
const newFiles = selectedFiles.map(f => {
|
|
525
705
|
const originalPath = relative(sourceDir, f);
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AGENTS.md Template
|
|
3
|
+
* Auto-generated file placed inside cloaked workspaces to instruct AI agents
|
|
4
|
+
* about the nature of the isolated, anonymized repository.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export function getAgentsMarkdown() {
|
|
8
|
+
return `# AI Agent Guidelines for This Repository
|
|
9
|
+
|
|
10
|
+
## Important Context
|
|
11
|
+
|
|
12
|
+
This is an **isolated, partially cloned repository** created using [Repo-Cloak](https://www.npmjs.com/package/repo-cloak-cli). It contains only a subset of files selectively pulled from a larger enterprise codebase for the purpose of working with AI coding tools in a safe, anonymized environment.
|
|
13
|
+
|
|
14
|
+
## What You Need to Know
|
|
15
|
+
|
|
16
|
+
- **Partial repository.** This workspace does NOT contain the full codebase. Only specific files were selected and pulled by the developer. Missing files, references, or imports pointing to modules not present in this workspace are expected.
|
|
17
|
+
- **Anonymized identifiers.** Company names, project names, and other proprietary identifiers have been systematically replaced with anonymized alternatives. Do not attempt to guess or restore the original names.
|
|
18
|
+
- **Preserve the existing structure.** All file paths, folder hierarchies, and naming conventions in this workspace mirror the original repository structure. Maintain this structure in any changes you make.
|
|
19
|
+
- **Only modify files present in this workspace.** Do not create files outside the directories already present unless explicitly asked to by the user. Your changes will be pushed back into the original repository, so they must align with the existing structure.
|
|
20
|
+
|
|
21
|
+
## If You Need More Context
|
|
22
|
+
|
|
23
|
+
If you encounter a situation where:
|
|
24
|
+
- A referenced file, module, or dependency is missing
|
|
25
|
+
- You cannot determine the correct interface, type, or contract
|
|
26
|
+
- The available code is insufficient to complete the task confidently
|
|
27
|
+
|
|
28
|
+
**Ask the user to pull additional files** using Repo-Cloak. They can selectively add more files to this workspace without starting over. Do not guess or fabricate missing implementations.
|
|
29
|
+
|
|
30
|
+
## Working With This Repository
|
|
31
|
+
|
|
32
|
+
1. Treat this workspace as a real project. The structure and patterns are genuine.
|
|
33
|
+
2. Write code that follows the conventions and patterns you observe in the existing files.
|
|
34
|
+
3. Your changes will be de-anonymized and merged back into the original codebase automatically.
|
|
35
|
+
4. Focus on the task the user has given you. The files present are the files they have chosen to expose for this purpose.
|
|
36
|
+
`;
|
|
37
|
+
}
|