repo-cloak-cli 1.0.0

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.
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Anonymizer
3
+ * Handles keyword replacement for anonymizing content
4
+ */
5
+
6
+ /**
7
+ * Create a replacement function for a list of replacements
8
+ * @param {Array<{original: string, replacement: string}>} replacements
9
+ * @param {Object} options
10
+ * @returns {Function} Transform function
11
+ */
12
+ export function createAnonymizer(replacements, options = {}) {
13
+ const { caseSensitive = false } = options;
14
+
15
+ if (!replacements || replacements.length === 0) {
16
+ return (content) => content;
17
+ }
18
+
19
+ return (content) => {
20
+ let result = content;
21
+
22
+ for (const { original, replacement } of replacements) {
23
+ if (caseSensitive) {
24
+ // Case-sensitive replacement
25
+ result = result.split(original).join(replacement);
26
+ } else {
27
+ // Case-insensitive replacement using regex
28
+ const regex = new RegExp(escapeRegex(original), 'gi');
29
+ result = result.replace(regex, (match) => {
30
+ // Preserve case pattern
31
+ return matchCase(match, replacement);
32
+ });
33
+ }
34
+ }
35
+
36
+ return result;
37
+ };
38
+ }
39
+
40
+ /**
41
+ * Create reverse anonymizer (for push operation)
42
+ * @param {Array<{original: string, replacement: string}>} replacements
43
+ * @returns {Function} Transform function
44
+ */
45
+ export function createDeanonymizer(replacements, options = {}) {
46
+ const { caseSensitive = false } = options;
47
+
48
+ if (!replacements || replacements.length === 0) {
49
+ return (content) => content;
50
+ }
51
+
52
+ // Reverse the replacements
53
+ const reversed = replacements.map(r => ({
54
+ original: r.replacement,
55
+ replacement: r.original
56
+ }));
57
+
58
+ return createAnonymizer(reversed, options);
59
+ }
60
+
61
+ /**
62
+ * Escape special regex characters
63
+ */
64
+ function escapeRegex(string) {
65
+ return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
66
+ }
67
+
68
+ /**
69
+ * Match the case pattern of the original to the replacement
70
+ */
71
+ function matchCase(original, replacement) {
72
+ // If original is all uppercase, make replacement uppercase
73
+ if (original === original.toUpperCase()) {
74
+ return replacement.toUpperCase();
75
+ }
76
+
77
+ // If original is all lowercase, make replacement lowercase
78
+ if (original === original.toLowerCase()) {
79
+ return replacement.toLowerCase();
80
+ }
81
+
82
+ // If original is Title Case, make replacement Title Case
83
+ if (original[0] === original[0].toUpperCase()) {
84
+ return replacement.charAt(0).toUpperCase() + replacement.slice(1).toLowerCase();
85
+ }
86
+
87
+ // Default: return replacement as-is
88
+ return replacement;
89
+ }
90
+
91
+ /**
92
+ * Apply replacements to a file path (for renaming files/folders)
93
+ */
94
+ export function anonymizePath(filePath, replacements) {
95
+ if (!replacements || replacements.length === 0) {
96
+ return filePath;
97
+ }
98
+
99
+ let result = filePath;
100
+
101
+ for (const { original, replacement } of replacements) {
102
+ const regex = new RegExp(escapeRegex(original), 'gi');
103
+ result = result.replace(regex, replacement);
104
+ }
105
+
106
+ return result;
107
+ }
108
+
109
+ /**
110
+ * Count replacements in content
111
+ */
112
+ export function countReplacements(content, replacements) {
113
+ if (!replacements || replacements.length === 0) {
114
+ return 0;
115
+ }
116
+
117
+ let count = 0;
118
+
119
+ for (const { original } of replacements) {
120
+ const regex = new RegExp(escapeRegex(original), 'gi');
121
+ const matches = content.match(regex);
122
+ if (matches) {
123
+ count += matches.length;
124
+ }
125
+ }
126
+
127
+ return count;
128
+ }
@@ -0,0 +1,96 @@
1
+ /**
2
+ * File Copier
3
+ * Copies files while preserving directory structure
4
+ */
5
+
6
+ import { copyFileSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
7
+ import { dirname, join, relative } from 'path';
8
+ import { isBinaryFile } from './scanner.js';
9
+
10
+ /**
11
+ * Copy a single file, creating directories as needed
12
+ */
13
+ export function copyFile(sourcePath, destPath) {
14
+ // Ensure destination directory exists
15
+ const destDir = dirname(destPath);
16
+ mkdirSync(destDir, { recursive: true });
17
+
18
+ // Copy the file
19
+ copyFileSync(sourcePath, destPath);
20
+ }
21
+
22
+ /**
23
+ * Copy a file with content transformation
24
+ */
25
+ export function copyFileWithTransform(sourcePath, destPath, transformFn) {
26
+ // Ensure destination directory exists
27
+ const destDir = dirname(destPath);
28
+ mkdirSync(destDir, { recursive: true });
29
+
30
+ // Check if binary - copy as-is
31
+ if (isBinaryFile(sourcePath)) {
32
+ copyFileSync(sourcePath, destPath);
33
+ return { transformed: false };
34
+ }
35
+
36
+ // Read, transform, write
37
+ try {
38
+ let content = readFileSync(sourcePath, 'utf-8');
39
+ const originalContent = content;
40
+ content = transformFn(content);
41
+ writeFileSync(destPath, content, 'utf-8');
42
+ return {
43
+ transformed: content !== originalContent,
44
+ originalLength: originalContent.length,
45
+ newLength: content.length
46
+ };
47
+ } catch (error) {
48
+ // If reading as text fails, copy as binary
49
+ copyFileSync(sourcePath, destPath);
50
+ return { transformed: false, error: error.message };
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Copy multiple files with progress callback
56
+ */
57
+ export async function copyFiles(files, sourceBase, destBase, transformFn, onProgress) {
58
+ const results = {
59
+ total: files.length,
60
+ copied: 0,
61
+ transformed: 0,
62
+ errors: []
63
+ };
64
+
65
+ for (let i = 0; i < files.length; i++) {
66
+ const file = files[i];
67
+ const relativePath = typeof file === 'string'
68
+ ? relative(sourceBase, file)
69
+ : file.relativePath;
70
+ const sourcePath = typeof file === 'string' ? file : file.absolutePath;
71
+ const destPath = join(destBase, relativePath);
72
+
73
+ try {
74
+ if (transformFn) {
75
+ const result = copyFileWithTransform(sourcePath, destPath, transformFn);
76
+ if (result.transformed) {
77
+ results.transformed++;
78
+ }
79
+ } else {
80
+ copyFile(sourcePath, destPath);
81
+ }
82
+ results.copied++;
83
+ } catch (error) {
84
+ results.errors.push({
85
+ file: relativePath,
86
+ error: error.message
87
+ });
88
+ }
89
+
90
+ if (onProgress) {
91
+ onProgress(i + 1, files.length, relativePath);
92
+ }
93
+ }
94
+
95
+ return results;
96
+ }
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Mapping File Manager
3
+ * Tracks replacements and file mappings for push/pull operations
4
+ */
5
+
6
+ import { readFileSync, writeFileSync, existsSync } from 'fs';
7
+ import { join } from 'path';
8
+
9
+ const MAP_FILENAME = '.repo-cloak-map.json';
10
+
11
+ /**
12
+ * Create a mapping object
13
+ */
14
+ export function createMapping(options) {
15
+ const {
16
+ sourceDir,
17
+ destDir,
18
+ replacements,
19
+ files,
20
+ timestamp = new Date().toISOString()
21
+ } = options;
22
+
23
+ return {
24
+ version: '1.0.0',
25
+ tool: 'repo-cloak',
26
+ timestamp,
27
+ source: {
28
+ path: sourceDir,
29
+ platform: process.platform
30
+ },
31
+ destination: {
32
+ path: destDir
33
+ },
34
+ replacements: replacements.map(r => ({
35
+ original: r.original,
36
+ replacement: r.replacement
37
+ })),
38
+ files: files.map(f => ({
39
+ original: typeof f === 'string' ? f : f.relativePath,
40
+ cloaked: typeof f === 'string' ? f : f.relativePath
41
+ })),
42
+ stats: {
43
+ totalFiles: files.length,
44
+ replacementsCount: replacements.length
45
+ }
46
+ };
47
+ }
48
+
49
+ /**
50
+ * Save mapping to destination directory
51
+ */
52
+ export function saveMapping(destDir, mapping) {
53
+ const mapPath = join(destDir, MAP_FILENAME);
54
+ writeFileSync(mapPath, JSON.stringify(mapping, null, 2), 'utf-8');
55
+ return mapPath;
56
+ }
57
+
58
+ /**
59
+ * Load mapping from a cloaked directory
60
+ */
61
+ export function loadMapping(cloakedDir) {
62
+ const mapPath = join(cloakedDir, MAP_FILENAME);
63
+
64
+ if (!existsSync(mapPath)) {
65
+ throw new Error(`No mapping file found in ${cloakedDir}. Is this a repo-cloak backup?`);
66
+ }
67
+
68
+ try {
69
+ const content = readFileSync(mapPath, 'utf-8');
70
+ return JSON.parse(content);
71
+ } catch (error) {
72
+ throw new Error(`Failed to read mapping file: ${error.message}`);
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Check if a directory has a mapping file
78
+ */
79
+ export function hasMapping(dir) {
80
+ const mapPath = join(dir, MAP_FILENAME);
81
+ return existsSync(mapPath);
82
+ }
83
+
84
+ /**
85
+ * Get the original source path from mapping
86
+ */
87
+ export function getOriginalSource(mapping) {
88
+ return mapping.source?.path || null;
89
+ }
90
+
91
+ /**
92
+ * Get replacements from mapping
93
+ */
94
+ export function getReplacements(mapping) {
95
+ return mapping.replacements || [];
96
+ }
97
+
98
+ /**
99
+ * Get file list from mapping
100
+ */
101
+ export function getFiles(mapping) {
102
+ return mapping.files || [];
103
+ }
104
+
105
+ /**
106
+ * Update mapping with additional info
107
+ */
108
+ export function updateMapping(destDir, updates) {
109
+ const mapPath = join(destDir, MAP_FILENAME);
110
+
111
+ if (!existsSync(mapPath)) {
112
+ throw new Error('No mapping file found');
113
+ }
114
+
115
+ const mapping = loadMapping(destDir);
116
+ const updated = { ...mapping, ...updates, updatedAt: new Date().toISOString() };
117
+
118
+ writeFileSync(mapPath, JSON.stringify(updated, null, 2), 'utf-8');
119
+ return updated;
120
+ }
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Directory Scanner
3
+ * Scans and builds file tree structure
4
+ */
5
+
6
+ import { readdirSync, statSync } from 'fs';
7
+ import { join, relative, sep, extname } from 'path';
8
+
9
+ // Binary file extensions to copy without modification
10
+ const BINARY_EXTENSIONS = new Set([
11
+ '.png', '.jpg', '.jpeg', '.gif', '.bmp', '.ico', '.webp', '.svg',
12
+ '.mp3', '.mp4', '.wav', '.avi', '.mov', '.mkv', '.webm',
13
+ '.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx',
14
+ '.zip', '.rar', '.7z', '.tar', '.gz', '.bz2',
15
+ '.exe', '.dll', '.so', '.dylib', '.bin',
16
+ '.ttf', '.otf', '.woff', '.woff2', '.eot',
17
+ '.sqlite', '.db', '.mdb'
18
+ ]);
19
+
20
+ // Directories to always ignore
21
+ const IGNORE_DIRS = new Set([
22
+ 'node_modules',
23
+ '.git',
24
+ '.svn',
25
+ '.hg',
26
+ '.DS_Store',
27
+ 'Thumbs.db',
28
+ '.idea',
29
+ '.vscode',
30
+ '__pycache__',
31
+ '.pytest_cache',
32
+ 'dist',
33
+ 'build',
34
+ '.next',
35
+ '.nuxt',
36
+ 'coverage',
37
+ '.nyc_output',
38
+ '.repo-cloak-map.json',
39
+ '.env',
40
+ '.env.local'
41
+ ]);
42
+
43
+ /**
44
+ * Check if a file is binary based on extension
45
+ */
46
+ export function isBinaryFile(filePath) {
47
+ const ext = extname(filePath).toLowerCase();
48
+ return BINARY_EXTENSIONS.has(ext);
49
+ }
50
+
51
+ /**
52
+ * Check if a file/folder should be ignored
53
+ */
54
+ export function shouldIgnore(name) {
55
+ return IGNORE_DIRS.has(name) || name.startsWith('.');
56
+ }
57
+
58
+ /**
59
+ * Recursively get all files in a directory
60
+ */
61
+ export function getAllFiles(dir, basePath = dir, files = []) {
62
+ try {
63
+ const entries = readdirSync(dir, { withFileTypes: true });
64
+
65
+ for (const entry of entries) {
66
+ const fullPath = join(dir, entry.name);
67
+
68
+ if (shouldIgnore(entry.name)) continue;
69
+
70
+ if (entry.isDirectory()) {
71
+ getAllFiles(fullPath, basePath, files);
72
+ } else {
73
+ files.push({
74
+ absolutePath: fullPath,
75
+ relativePath: relative(basePath, fullPath),
76
+ name: entry.name,
77
+ isBinary: isBinaryFile(fullPath)
78
+ });
79
+ }
80
+ }
81
+ } catch (error) {
82
+ // Permission denied or other errors - skip
83
+ }
84
+
85
+ return files;
86
+ }
87
+
88
+ /**
89
+ * Get directory structure for display
90
+ */
91
+ export function getDirectoryTree(dir, basePath = dir, depth = 0, maxDepth = 5) {
92
+ const tree = [];
93
+
94
+ if (depth > maxDepth) return tree;
95
+
96
+ try {
97
+ const entries = readdirSync(dir, { withFileTypes: true });
98
+
99
+ // Sort: folders first, then files
100
+ entries.sort((a, b) => {
101
+ if (a.isDirectory() && !b.isDirectory()) return -1;
102
+ if (!a.isDirectory() && b.isDirectory()) return 1;
103
+ return a.name.localeCompare(b.name);
104
+ });
105
+
106
+ for (const entry of entries) {
107
+ if (shouldIgnore(entry.name)) continue;
108
+
109
+ const fullPath = join(dir, entry.name);
110
+ const node = {
111
+ name: entry.name,
112
+ path: fullPath,
113
+ relativePath: relative(basePath, fullPath),
114
+ isDirectory: entry.isDirectory(),
115
+ depth
116
+ };
117
+
118
+ tree.push(node);
119
+
120
+ if (entry.isDirectory()) {
121
+ const children = getDirectoryTree(fullPath, basePath, depth + 1, maxDepth);
122
+ tree.push(...children);
123
+ }
124
+ }
125
+ } catch (error) {
126
+ // Skip inaccessible directories
127
+ }
128
+
129
+ return tree;
130
+ }
131
+
132
+ /**
133
+ * Count files in a directory (recursive)
134
+ */
135
+ export function countFiles(dir) {
136
+ return getAllFiles(dir).length;
137
+ }
package/src/index.js ADDED
@@ -0,0 +1,8 @@
1
+ /**
2
+ * repo-cloak - Main exports
3
+ * 🎭 Selectively extract and anonymize files from repositories
4
+ */
5
+
6
+ export { pull } from './commands/pull.js';
7
+ export { push } from './commands/push.js';
8
+ export { showBanner } from './ui/banner.js';
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Fancy ASCII Banner
3
+ * Shows a colorful intro when the CLI starts
4
+ */
5
+
6
+ import chalk from 'chalk';
7
+ import figlet from 'figlet';
8
+
9
+ export async function showBanner() {
10
+ return new Promise((resolve) => {
11
+ figlet.text('repo-cloak', {
12
+ font: 'Standard',
13
+ horizontalLayout: 'default',
14
+ verticalLayout: 'default'
15
+ }, (err, data) => {
16
+ if (err) {
17
+ console.log(chalk.magentaBright.bold('\n🎭 repo-cloak\n'));
18
+ resolve();
19
+ return;
20
+ }
21
+
22
+ // Create gradient effect
23
+ const lines = data.split('\n');
24
+ const colors = [
25
+ chalk.hex('#FF6B6B'), // Coral
26
+ chalk.hex('#FF8E53'), // Orange
27
+ chalk.hex('#FEC89A'), // Peach
28
+ chalk.hex('#A8E6CF'), // Mint
29
+ chalk.hex('#88D8B0'), // Seafoam
30
+ chalk.hex('#7B68EE'), // Medium Slate Blue
31
+ chalk.hex('#9D4EDD'), // Purple
32
+ ];
33
+
34
+ console.log('\n');
35
+ lines.forEach((line, index) => {
36
+ const color = colors[index % colors.length];
37
+ console.log(color(line));
38
+ });
39
+
40
+ // Tagline box
41
+ const tagline = '🎭 Selectively extract & anonymize repository files';
42
+ const version = 'v1.0.0';
43
+
44
+ console.log('');
45
+ console.log(chalk.dim('─'.repeat(55)));
46
+ console.log(chalk.white.bold(` ${tagline}`));
47
+ console.log(chalk.dim(` Compatible with Windows, macOS, and Linux | ${version}`));
48
+ console.log(chalk.dim('─'.repeat(55)));
49
+ console.log('');
50
+
51
+ resolve();
52
+ });
53
+ });
54
+ }
55
+
56
+ export function showSuccess(message) {
57
+ console.log(chalk.green.bold(`\n✅ ${message}\n`));
58
+ }
59
+
60
+ export function showError(message) {
61
+ console.log(chalk.red.bold(`\n❌ ${message}\n`));
62
+ }
63
+
64
+ export function showWarning(message) {
65
+ console.log(chalk.yellow.bold(`\n⚠️ ${message}\n`));
66
+ }
67
+
68
+ export function showInfo(message) {
69
+ console.log(chalk.cyan(`\nℹ️ ${message}\n`));
70
+ }