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/src/core/anonymizer.js
CHANGED
|
@@ -69,18 +69,28 @@ function escapeRegex(string) {
|
|
|
69
69
|
* Match the case pattern of the original to the replacement
|
|
70
70
|
*/
|
|
71
71
|
function matchCase(original, replacement) {
|
|
72
|
-
//
|
|
72
|
+
// Helper to check if string contains any letters
|
|
73
|
+
const hasLetters = /[a-zA-Z]/.test(original);
|
|
74
|
+
|
|
75
|
+
if (!hasLetters) {
|
|
76
|
+
return replacement;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Check if original is entirely uppercase
|
|
73
80
|
if (original === original.toUpperCase()) {
|
|
74
81
|
return replacement.toUpperCase();
|
|
75
82
|
}
|
|
76
83
|
|
|
77
|
-
//
|
|
84
|
+
// Check if original is entirely lowercase
|
|
78
85
|
if (original === original.toLowerCase()) {
|
|
79
86
|
return replacement.toLowerCase();
|
|
80
87
|
}
|
|
81
88
|
|
|
82
|
-
//
|
|
83
|
-
|
|
89
|
+
// Check if original is Title Case (first letter uppercase, rest lowercase)
|
|
90
|
+
const firstLetter = original.charAt(0);
|
|
91
|
+
const restOfStr = original.slice(1);
|
|
92
|
+
|
|
93
|
+
if (firstLetter === firstLetter.toUpperCase() && restOfStr === restOfStr.toLowerCase()) {
|
|
84
94
|
return replacement.charAt(0).toUpperCase() + replacement.slice(1).toLowerCase();
|
|
85
95
|
}
|
|
86
96
|
|
package/src/core/git.js
CHANGED
|
@@ -59,3 +59,71 @@ export async function getChangedFiles(dirPath) {
|
|
|
59
59
|
return [];
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Get recent commits
|
|
65
|
+
* @param {string} dirPath - Repository root
|
|
66
|
+
* @param {number} count - Number of commits to retrieve
|
|
67
|
+
* @returns {Promise<Array<{hash: string, message: string}>>} List of commit objects
|
|
68
|
+
*/
|
|
69
|
+
export async function getRecentCommits(dirPath, count = 10) {
|
|
70
|
+
try {
|
|
71
|
+
const { stdout } = await execAsync(`git log -n ${count} --pretty=format:"%h - %s"`, { cwd: dirPath });
|
|
72
|
+
if (!stdout) return [];
|
|
73
|
+
|
|
74
|
+
return stdout
|
|
75
|
+
.split(/\r?\n/)
|
|
76
|
+
.filter(line => line.trim() !== '')
|
|
77
|
+
.map(line => {
|
|
78
|
+
const sepIndex = line.indexOf(' - ');
|
|
79
|
+
if (sepIndex === -1) return { hash: line.trim(), message: '' };
|
|
80
|
+
const hash = line.substring(0, sepIndex).trim();
|
|
81
|
+
const message = line.substring(sepIndex + 3).trim();
|
|
82
|
+
return { hash, message };
|
|
83
|
+
});
|
|
84
|
+
} catch (error) {
|
|
85
|
+
return [];
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Get list of files changed in specific commits
|
|
91
|
+
* @param {string} dirPath - Repository root
|
|
92
|
+
* @param {string[]} commits - List of commit hashes
|
|
93
|
+
* @returns {Promise<string[]>} List of relative file paths
|
|
94
|
+
*/
|
|
95
|
+
export async function getFilesChangedInCommits(dirPath, commits) {
|
|
96
|
+
if (!commits || commits.length === 0) return [];
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
const filesSet = new Set();
|
|
100
|
+
|
|
101
|
+
for (const commit of commits) {
|
|
102
|
+
// --name-status outputs lines like "status\tfilePath" (e.g., "M\tfile.txt")
|
|
103
|
+
const { stdout } = await execAsync(`git show --name-status --pretty="" ${commit}`, { cwd: dirPath });
|
|
104
|
+
if (stdout) {
|
|
105
|
+
const lines = stdout.split(/\r?\n/).filter(line => line.trim() !== '');
|
|
106
|
+
for (const line of lines) {
|
|
107
|
+
const parts = line.split('\t');
|
|
108
|
+
if (parts.length < 2) continue;
|
|
109
|
+
|
|
110
|
+
const status = parts[0];
|
|
111
|
+
// Skip deleted files since we cannot pull them
|
|
112
|
+
if (!status.startsWith('D')) {
|
|
113
|
+
// For renamed files (Rxxx or Cxxx), parts[2] might be the new file name
|
|
114
|
+
let file = parts.length > 2 ? parts[2] : parts[1];
|
|
115
|
+
|
|
116
|
+
// Remove quotes if present (git status/show quotes files with spaces)
|
|
117
|
+
const cleanFile = file.replace(/^"|"$/g, '');
|
|
118
|
+
|
|
119
|
+
filesSet.add(cleanFile);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return Array.from(filesSet);
|
|
126
|
+
} catch (error) {
|
|
127
|
+
return [];
|
|
128
|
+
}
|
|
129
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Path Cache Module
|
|
3
|
+
* Stores recently used source and destination paths, encrypted with the user's secret key.
|
|
4
|
+
* Persisted to ~/.repo-cloak/path-cache.json
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
8
|
+
import { join } from 'path';
|
|
9
|
+
import { homedir } from 'os';
|
|
10
|
+
import { encrypt, decrypt, getOrCreateSecret, hasSecret } from './crypto.js';
|
|
11
|
+
|
|
12
|
+
const CONFIG_DIR = join(homedir(), '.repo-cloak');
|
|
13
|
+
const CACHE_FILE = join(CONFIG_DIR, 'path-cache.json');
|
|
14
|
+
const MAX_PATHS = 10;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Load the raw cache file (entries are stored encrypted)
|
|
18
|
+
* @returns {{ sources: string[], destinations: string[] }}
|
|
19
|
+
*/
|
|
20
|
+
function loadRawCache() {
|
|
21
|
+
try {
|
|
22
|
+
if (!existsSync(CACHE_FILE)) {
|
|
23
|
+
return { sources: [], destinations: [] };
|
|
24
|
+
}
|
|
25
|
+
const raw = readFileSync(CACHE_FILE, 'utf-8');
|
|
26
|
+
const parsed = JSON.parse(raw);
|
|
27
|
+
return {
|
|
28
|
+
sources: Array.isArray(parsed.sources) ? parsed.sources : [],
|
|
29
|
+
destinations: Array.isArray(parsed.destinations) ? parsed.destinations : []
|
|
30
|
+
};
|
|
31
|
+
} catch {
|
|
32
|
+
return { sources: [], destinations: [] };
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Save entries back to the cache file
|
|
38
|
+
* @param {{ sources: string[], destinations: string[] }} cache
|
|
39
|
+
*/
|
|
40
|
+
function saveRawCache(cache) {
|
|
41
|
+
try {
|
|
42
|
+
if (!existsSync(CONFIG_DIR)) {
|
|
43
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
44
|
+
}
|
|
45
|
+
writeFileSync(CACHE_FILE, JSON.stringify(cache, null, 2), { mode: 0o600 });
|
|
46
|
+
} catch {
|
|
47
|
+
// Silently ignore write errors ā caching is best-effort
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Decrypt a list of encrypted path strings. Returns only successfully decrypted values.
|
|
53
|
+
* @param {string[]} encrypted
|
|
54
|
+
* @param {string} secret
|
|
55
|
+
* @returns {string[]}
|
|
56
|
+
*/
|
|
57
|
+
function decryptPaths(encrypted, secret) {
|
|
58
|
+
return encrypted
|
|
59
|
+
.map(e => {
|
|
60
|
+
try {
|
|
61
|
+
return decrypt(e, secret);
|
|
62
|
+
} catch {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
})
|
|
66
|
+
.filter(Boolean);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// āāā Public API āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Get recently used source paths (decrypted). Returns empty array if no secret exists yet.
|
|
73
|
+
* @returns {string[]}
|
|
74
|
+
*/
|
|
75
|
+
export function getSourcePaths() {
|
|
76
|
+
if (!hasSecret()) return [];
|
|
77
|
+
const secret = getOrCreateSecret();
|
|
78
|
+
const cache = loadRawCache();
|
|
79
|
+
return decryptPaths(cache.sources, secret);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Get recently used destination paths (decrypted). Returns empty array if no secret exists yet.
|
|
84
|
+
* @returns {string[]}
|
|
85
|
+
*/
|
|
86
|
+
export function getDestPaths() {
|
|
87
|
+
if (!hasSecret()) return [];
|
|
88
|
+
const secret = getOrCreateSecret();
|
|
89
|
+
const cache = loadRawCache();
|
|
90
|
+
return decryptPaths(cache.destinations, secret);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Persist a source path to the cache (encrypted). Moves it to the front and deduplicates.
|
|
95
|
+
* @param {string} path - Absolute path
|
|
96
|
+
*/
|
|
97
|
+
export function addSourcePath(path) {
|
|
98
|
+
try {
|
|
99
|
+
const secret = getOrCreateSecret();
|
|
100
|
+
const cache = loadRawCache();
|
|
101
|
+
|
|
102
|
+
// Decrypt existing to deduplicate
|
|
103
|
+
const existing = decryptPaths(cache.sources, secret);
|
|
104
|
+
const deduped = [path, ...existing.filter(p => p !== path)].slice(0, MAX_PATHS);
|
|
105
|
+
|
|
106
|
+
cache.sources = deduped.map(p => encrypt(p, secret));
|
|
107
|
+
saveRawCache(cache);
|
|
108
|
+
} catch {
|
|
109
|
+
// Silently ignore
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Persist a destination path to the cache (encrypted). Moves it to the front and deduplicates.
|
|
115
|
+
* @param {string} path - Absolute path
|
|
116
|
+
*/
|
|
117
|
+
export function addDestPath(path) {
|
|
118
|
+
try {
|
|
119
|
+
const secret = getOrCreateSecret();
|
|
120
|
+
const cache = loadRawCache();
|
|
121
|
+
|
|
122
|
+
// Decrypt existing to deduplicate
|
|
123
|
+
const existing = decryptPaths(cache.destinations, secret);
|
|
124
|
+
const deduped = [path, ...existing.filter(p => p !== path)].slice(0, MAX_PATHS);
|
|
125
|
+
|
|
126
|
+
cache.destinations = deduped.map(p => encrypt(p, secret));
|
|
127
|
+
saveRawCache(cache);
|
|
128
|
+
} catch {
|
|
129
|
+
// Silently ignore
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Secret Data Scanner
|
|
3
|
+
* Scans files for common sensitive data patterns (e.g. AWS keys, private keys, generic tokens)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { readFileSync, statSync } from 'fs';
|
|
7
|
+
import { isBinaryFile } from './scanner.js';
|
|
8
|
+
|
|
9
|
+
// Max file size to scan (2MB). Prevents blocking on massive files like minified bundles.
|
|
10
|
+
const MAX_SCAN_SIZE = 2 * 1024 * 1024;
|
|
11
|
+
|
|
12
|
+
// Common regular expressions for sensitive data detection
|
|
13
|
+
export const SECRET_PATTERNS = [
|
|
14
|
+
{
|
|
15
|
+
name: 'AWS Access Key ID',
|
|
16
|
+
regex: /(A3T[A-Z0-9]|AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)[A-Z0-9]{16}/g
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
name: 'AWS Secret Access Key',
|
|
20
|
+
regex: /aws_(?:secret_)?(?:access_)?key(?:\s*=?\s*["']?)(?!<)[a-zA-Z0-9/+=]{40}(?!>)/gi
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
name: 'RSA Private Key',
|
|
24
|
+
regex: /-----BEGIN RSA PRIVATE KEY-----/g
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
name: 'Generic Private Key',
|
|
28
|
+
regex: /-----BEGIN PRIVATE KEY-----/g
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
name: 'DSA Private Key',
|
|
32
|
+
regex: /-----BEGIN DSA PRIVATE KEY-----/g
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: 'OpenSSH Private Key',
|
|
36
|
+
regex: /-----BEGIN OPENSSH PRIVATE KEY-----/g
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
name: 'Generic API Key / Token',
|
|
40
|
+
regex: /(?:api_?key|auth_?token|access_?token|secret_?key|bearer_?token)(?:\s*[:=]\s*["']?)(?!<)[a-zA-Z0-9\-_]{20,}(?!>)/gi
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: 'Generic Password / Secret',
|
|
44
|
+
regex: /(?:password|passwd|pwd|secret|pass)(?:\s*[:=]\s*["']?)(?!<)[a-zA-Z0-9\-_@!#$%^&*]{8,}(?!>)/gi
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: 'JSON Web Token (JWT)',
|
|
48
|
+
regex: /eyJ[a-zA-Z0-9_-]{10,}\.eyJ[a-zA-Z0-9_-]{10,}\.[a-zA-Z0-9_-]{10,}/g
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: 'GitHub Token',
|
|
52
|
+
regex: /(?:ghp|gho|ghu|ghs|ghr)_[a-zA-Z0-9]{36}/g
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: 'GitHub OAuth App Token',
|
|
56
|
+
regex: /gho_[a-zA-Z0-9]{36}/g
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
name: 'Slack Token',
|
|
60
|
+
regex: /(xox[pboar]-[0-9]{12}-[0-9]{12}-[0-9]{12}-[a-z0-9]{32})/g
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
name: 'Slack Webhook',
|
|
64
|
+
regex: /https:\/\/hooks\.slack\.com\/services\/T[a-zA-Z0-9_]{8,}\/B[a-zA-Z0-9_]{8,}\/[a-zA-Z0-9_]{24}/g
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
name: 'Stripe API Key',
|
|
68
|
+
regex: /(?:sk_live|rk_live|sk_test|rk_test)_[0-9a-zA-Z]{24}/g
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
name: 'Google API Key',
|
|
72
|
+
regex: /AIza[0-9A-Za-z\-_]{35}/g
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
name: 'Google OAuth Access Token',
|
|
76
|
+
regex: /ya29\.[0-9A-Za-z\-_]+/g
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
name: 'Discord Bot Token',
|
|
80
|
+
regex: /[M|N][a-zA-Z0-9_-]{23,}\.[a-zA-Z0-9_-]{6,}\.[a-zA-Z0-9_-]{27,}/g
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
name: 'Discord Webhook',
|
|
84
|
+
regex: /https:\/\/discord\.com\/api\/webhooks\/[0-9]{18,19}\/[a-zA-Z0-9_-]{68}/g
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
name: 'Database Connection String',
|
|
88
|
+
regex: /(?:mysql|postgresql|mongodb|mssql|sqlite|redis|amqp):\/\/[a-zA-Z0-9_-]+:[a-zA-Z0-9_-]+@[a-zA-Z0-9_.-]+:[0-9]{1,5}\/[a-zA-Z0-9_-]+/gi
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
name: 'Heroku API Key',
|
|
92
|
+
regex: /[h|H]eroku[0-9a-zA-Z_-]{8}-[0-9a-zA-Z_-]{4}-[0-9a-zA-Z_-]{4}-[0-9a-zA-Z_-]{4}-[0-9a-zA-Z_-]{12}/g
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
name: 'Mailgun API Key',
|
|
96
|
+
regex: /key-[0-9a-zA-Z]{32}/g
|
|
97
|
+
}
|
|
98
|
+
];
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Scan a single file for secrets
|
|
102
|
+
* @param {string} filePath - Absolute path to the file
|
|
103
|
+
* @returns {Array<{type: string, file: string, line: number}>} Array of found secrets
|
|
104
|
+
*/
|
|
105
|
+
export function scanFileForSecrets(filePath) {
|
|
106
|
+
const findings = [];
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
if (isBinaryFile(filePath)) {
|
|
110
|
+
return findings;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const stats = statSync(filePath);
|
|
114
|
+
if (stats.size > MAX_SCAN_SIZE) {
|
|
115
|
+
return findings; // Skip huge files
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
119
|
+
const lines = content.split(/\r?\n/);
|
|
120
|
+
|
|
121
|
+
// Keep track of which patterns found what to avoid duplicate reports per line
|
|
122
|
+
const seenOnLine = new Set();
|
|
123
|
+
|
|
124
|
+
for (let i = 0; i < lines.length; i++) {
|
|
125
|
+
const lineContent = lines[i];
|
|
126
|
+
|
|
127
|
+
for (const pattern of SECRET_PATTERNS) {
|
|
128
|
+
// Reset regex state since it has the 'g' flag
|
|
129
|
+
pattern.regex.lastIndex = 0;
|
|
130
|
+
|
|
131
|
+
if (pattern.regex.test(lineContent)) {
|
|
132
|
+
const uniqueKey = `${i}-${pattern.name}`;
|
|
133
|
+
if (!seenOnLine.has(uniqueKey)) {
|
|
134
|
+
findings.push({
|
|
135
|
+
type: pattern.name,
|
|
136
|
+
file: filePath,
|
|
137
|
+
line: i + 1
|
|
138
|
+
});
|
|
139
|
+
seenOnLine.add(uniqueKey);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
} catch (error) {
|
|
145
|
+
// Ignore read errors or permission issues for scanning
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return findings;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Scan multiple files for secrets
|
|
153
|
+
* @param {string[]} filePaths - Array of absolute file paths
|
|
154
|
+
* @returns {Array<{type: string, file: string, line: number}>} Flattened array of all findings
|
|
155
|
+
*/
|
|
156
|
+
export async function scanFilesForSecrets(filePaths) {
|
|
157
|
+
if (!filePaths || filePaths.length === 0) return [];
|
|
158
|
+
|
|
159
|
+
let allFindings = [];
|
|
160
|
+
|
|
161
|
+
for (const filePath of filePaths) {
|
|
162
|
+
const fileFindings = scanFileForSecrets(filePath);
|
|
163
|
+
if (fileFindings.length > 0) {
|
|
164
|
+
allFindings = allFindings.concat(fileFindings);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return allFindings;
|
|
169
|
+
}
|
package/src/ui/banner.js
CHANGED
|
@@ -1,51 +1,50 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* Banner
|
|
3
|
+
* Clean, elegant CLI header
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import chalk from 'chalk';
|
|
7
7
|
import figlet from 'figlet';
|
|
8
|
+
import { readFileSync } from 'fs';
|
|
9
|
+
import { join, dirname } from 'path';
|
|
10
|
+
import { fileURLToPath } from 'url';
|
|
11
|
+
|
|
12
|
+
function getVersion() {
|
|
13
|
+
try {
|
|
14
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, '../../package.json'), 'utf-8'));
|
|
16
|
+
return pkg.version || '1.0.0';
|
|
17
|
+
} catch {
|
|
18
|
+
return '1.0.0';
|
|
19
|
+
}
|
|
20
|
+
}
|
|
8
21
|
|
|
9
22
|
export async function showBanner() {
|
|
10
23
|
return new Promise((resolve) => {
|
|
11
24
|
figlet.text('repo-cloak', {
|
|
12
|
-
font: '
|
|
25
|
+
font: 'Slant',
|
|
13
26
|
horizontalLayout: 'default',
|
|
14
27
|
verticalLayout: 'default'
|
|
15
28
|
}, (err, data) => {
|
|
16
|
-
|
|
17
|
-
|
|
29
|
+
const version = getVersion();
|
|
30
|
+
|
|
31
|
+
if (err || !data) {
|
|
32
|
+
console.log('\n' + chalk.bold.white(' repo-cloak') + chalk.dim(` v${version}\n`));
|
|
18
33
|
resolve();
|
|
19
34
|
return;
|
|
20
35
|
}
|
|
21
36
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
chalk.
|
|
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));
|
|
37
|
+
console.log('');
|
|
38
|
+
// Render the ASCII art in a single elegant dim-white color
|
|
39
|
+
data.split('\n').forEach(line => {
|
|
40
|
+
console.log(chalk.white.dim(' ' + line));
|
|
38
41
|
});
|
|
39
42
|
|
|
40
|
-
// Tagline box
|
|
41
|
-
const tagline = 'š Selectively extract & anonymize repository files';
|
|
42
|
-
const version = 'v1.0.0';
|
|
43
|
-
|
|
44
43
|
console.log('');
|
|
45
|
-
console.log(
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
44
|
+
console.log(
|
|
45
|
+
chalk.dim(' Selectively extract & anonymize repository files') +
|
|
46
|
+
chalk.dim(` v${version}`)
|
|
47
|
+
);
|
|
49
48
|
console.log('');
|
|
50
49
|
|
|
51
50
|
resolve();
|
|
@@ -54,17 +53,17 @@ export async function showBanner() {
|
|
|
54
53
|
}
|
|
55
54
|
|
|
56
55
|
export function showSuccess(message) {
|
|
57
|
-
console.log(chalk.green
|
|
56
|
+
console.log(chalk.green(`\n ā ${message}\n`));
|
|
58
57
|
}
|
|
59
58
|
|
|
60
59
|
export function showError(message) {
|
|
61
|
-
console.log(chalk.red
|
|
60
|
+
console.log(chalk.red(`\n ā ${message}\n`));
|
|
62
61
|
}
|
|
63
62
|
|
|
64
63
|
export function showWarning(message) {
|
|
65
|
-
console.log(chalk.yellow
|
|
64
|
+
console.log(chalk.yellow(`\n ! ${message}\n`));
|
|
66
65
|
}
|
|
67
66
|
|
|
68
67
|
export function showInfo(message) {
|
|
69
|
-
console.log(chalk.
|
|
68
|
+
console.log(chalk.dim(`\n ${message}\n`));
|
|
70
69
|
}
|