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/ui/prompts.js
CHANGED
|
@@ -5,53 +5,158 @@
|
|
|
5
5
|
|
|
6
6
|
import inquirer from 'inquirer';
|
|
7
7
|
import chalk from 'chalk';
|
|
8
|
-
import { existsSync } from 'fs';
|
|
8
|
+
import { existsSync, mkdirSync } from 'fs';
|
|
9
9
|
import { resolve, isAbsolute } from 'path';
|
|
10
|
+
import { getSourcePaths, getDestPaths, addSourcePath, addDestPath } from '../core/path-cache.js';
|
|
11
|
+
|
|
12
|
+
const ENTER_DIFFERENT = '__ENTER_DIFFERENT__';
|
|
13
|
+
import { checkboxTreeSelectSingleDir } from './treeCheckboxSelector.js';
|
|
10
14
|
|
|
11
15
|
/**
|
|
12
|
-
*
|
|
16
|
+
* Build a list prompt with cached paths + "Enter a different path" option.
|
|
17
|
+
* Falls back to a plain input prompt when there are no cached paths.
|
|
18
|
+
* @param {object} opts
|
|
19
|
+
* @param {string} opts.message - Question shown to the user
|
|
20
|
+
* @param {string} opts.inputMessage - Follow-up message when they pick "Enter a different path"
|
|
21
|
+
* @param {string[]} opts.cachedPaths - Decrypted cached paths to show
|
|
22
|
+
* @param {(path: string) => string|true} opts.validate - Validator for final resolved path
|
|
23
|
+
* @param {string} [opts.defaultValue] - Default path
|
|
24
|
+
* @param {boolean} [opts.allowNewSubfolder] - Ask to append a subfolder after selection
|
|
25
|
+
* @returns {Promise<string>} Resolved absolute path
|
|
13
26
|
*/
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
27
|
+
async function promptWithCache({ message, cachedPaths, validate, defaultValue, allowNewSubfolder }) {
|
|
28
|
+
// Build the displayed message — append a dim default hint if one is provided
|
|
29
|
+
const displayMessage = defaultValue
|
|
30
|
+
? `${message} ${chalk.dim(`(press Enter to use current folder: ${defaultValue})`)}`
|
|
31
|
+
: message;
|
|
32
|
+
|
|
33
|
+
// With cached paths → show a quick-pick list with the question as its header
|
|
34
|
+
if (cachedPaths.length > 0) {
|
|
35
|
+
const { selected } = await inquirer.prompt([
|
|
36
|
+
{
|
|
37
|
+
type: 'list',
|
|
38
|
+
name: 'selected',
|
|
39
|
+
message: displayMessage,
|
|
40
|
+
choices: [
|
|
41
|
+
...cachedPaths.map(p => ({ name: p, value: p })),
|
|
42
|
+
new inquirer.Separator(),
|
|
43
|
+
{ name: chalk.cyan('↵ Enter a different path'), value: ENTER_DIFFERENT }
|
|
44
|
+
]
|
|
45
|
+
}
|
|
46
|
+
]);
|
|
47
|
+
|
|
48
|
+
if (selected !== ENTER_DIFFERENT) {
|
|
49
|
+
let resolved = resolve(selected);
|
|
50
|
+
|
|
51
|
+
if (allowNewSubfolder) {
|
|
52
|
+
const { subfolder } = await inquirer.prompt([{
|
|
53
|
+
type: 'input',
|
|
54
|
+
name: 'subfolder',
|
|
55
|
+
message: `Subfolder to create inside ${chalk.cyan(resolved)}? (press Enter to use current folder)`
|
|
56
|
+
}]);
|
|
57
|
+
if (subfolder.trim()) {
|
|
58
|
+
resolved = resolve(resolved, subfolder.trim());
|
|
25
59
|
}
|
|
26
|
-
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const result = validate(resolved);
|
|
63
|
+
if (result !== true) {
|
|
64
|
+
console.log(chalk.yellow(` ⚠ ${result}`));
|
|
65
|
+
} else {
|
|
66
|
+
return resolved;
|
|
27
67
|
}
|
|
28
68
|
}
|
|
29
|
-
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// No cache (or user chose "Enter a different path") → use hierarchical tree selector
|
|
72
|
+
console.log(chalk.dim(' (Use arrow keys to navigate, Space to expand/collapse, Enter to select)'));
|
|
73
|
+
let basePath = await checkboxTreeSelectSingleDir({
|
|
74
|
+
message: displayMessage,
|
|
75
|
+
root: process.cwd()
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
if (allowNewSubfolder) {
|
|
79
|
+
const { subfolder } = await inquirer.prompt([{
|
|
80
|
+
type: 'input',
|
|
81
|
+
name: 'subfolder',
|
|
82
|
+
message: `Subfolder to create inside ${chalk.cyan(basePath)}? (press Enter to use current folder)`
|
|
83
|
+
}]);
|
|
84
|
+
if (subfolder.trim()) {
|
|
85
|
+
basePath = resolve(basePath, subfolder.trim());
|
|
86
|
+
}
|
|
87
|
+
}
|
|
30
88
|
|
|
31
|
-
|
|
89
|
+
let finalPath = resolve(basePath);
|
|
90
|
+
// Loop until we get a path that passes validation
|
|
91
|
+
while (true) {
|
|
92
|
+
const result = validate(finalPath);
|
|
93
|
+
if (result === true) break;
|
|
94
|
+
console.log(chalk.yellow(` ⚠ ${result}`));
|
|
95
|
+
|
|
96
|
+
const { retry } = await inquirer.prompt([{
|
|
97
|
+
type: 'input',
|
|
98
|
+
name: 'retry',
|
|
99
|
+
message: 'Please type a valid absolute path:'
|
|
100
|
+
}]);
|
|
101
|
+
finalPath = resolve(retry.trim());
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return finalPath;
|
|
32
105
|
}
|
|
33
106
|
|
|
34
107
|
/**
|
|
35
|
-
* Prompt for
|
|
108
|
+
* Prompt for source directory (the repo to extract files from).
|
|
109
|
+
* Shows cached paths; validates that the directory exists.
|
|
110
|
+
* Saves the chosen path to the cache after selection.
|
|
36
111
|
*/
|
|
37
|
-
export async function
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
47
|
-
return true;
|
|
112
|
+
export async function promptSourceDirectory() {
|
|
113
|
+
const cachedPaths = getSourcePaths();
|
|
114
|
+
|
|
115
|
+
const path = await promptWithCache({
|
|
116
|
+
message: 'Which repo do you want to extract files from?',
|
|
117
|
+
cachedPaths,
|
|
118
|
+
validate: (resolved) => {
|
|
119
|
+
if (!existsSync(resolved)) {
|
|
120
|
+
return `Directory not found: ${resolved}`;
|
|
48
121
|
}
|
|
122
|
+
return true;
|
|
49
123
|
}
|
|
50
|
-
|
|
124
|
+
});
|
|
51
125
|
|
|
52
|
-
|
|
126
|
+
addSourcePath(path);
|
|
127
|
+
return path;
|
|
53
128
|
}
|
|
54
129
|
|
|
130
|
+
/**
|
|
131
|
+
* Prompt for destination directory (where cloaked files will be saved).
|
|
132
|
+
* Shows cached paths; creates the folder automatically if it doesn't exist.
|
|
133
|
+
* Saves the chosen path to the cache after selection.
|
|
134
|
+
*/
|
|
135
|
+
export async function promptDestinationDirectory() {
|
|
136
|
+
const cachedPaths = getDestPaths();
|
|
137
|
+
|
|
138
|
+
const path = await promptWithCache({
|
|
139
|
+
message: 'Where should the cloaked (anonymized) files be saved?',
|
|
140
|
+
defaultValue: process.cwd(),
|
|
141
|
+
allowNewSubfolder: true,
|
|
142
|
+
cachedPaths,
|
|
143
|
+
validate: (resolved) => {
|
|
144
|
+
if (!resolved.trim()) return 'Please enter a destination path.';
|
|
145
|
+
return true; // Destination may not exist yet – we will create it
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// Auto-create the destination if it doesn't exist
|
|
150
|
+
if (!existsSync(path)) {
|
|
151
|
+
mkdirSync(path, { recursive: true });
|
|
152
|
+
console.log(chalk.dim(` Created directory: ${path}`));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
addDestPath(path);
|
|
156
|
+
return path;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
|
|
55
160
|
/**
|
|
56
161
|
* Prompt for keyword replacements
|
|
57
162
|
*/
|
|
@@ -68,12 +173,12 @@ export async function promptKeywordReplacements() {
|
|
|
68
173
|
{
|
|
69
174
|
type: 'input',
|
|
70
175
|
name: 'original',
|
|
71
|
-
message: '
|
|
176
|
+
message: 'What do you want to replace? (leave empty to skip / finish):',
|
|
72
177
|
},
|
|
73
178
|
{
|
|
74
179
|
type: 'input',
|
|
75
180
|
name: 'replacement',
|
|
76
|
-
message: 'Replace with:',
|
|
181
|
+
message: 'Replace it with:',
|
|
77
182
|
when: (answers) => answers.original.trim() !== '',
|
|
78
183
|
validate: (input) => {
|
|
79
184
|
if (!input.trim()) {
|
|
@@ -139,7 +244,7 @@ export async function showSummaryAndConfirm(fileCount, destination, replacements
|
|
|
139
244
|
|
|
140
245
|
console.log(chalk.cyan('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'));
|
|
141
246
|
|
|
142
|
-
return confirmAction('
|
|
247
|
+
return confirmAction('Everything looks good — go ahead?');
|
|
143
248
|
}
|
|
144
249
|
|
|
145
250
|
/**
|
|
@@ -150,7 +255,7 @@ export async function promptBackupFolder() {
|
|
|
150
255
|
{
|
|
151
256
|
type: 'input',
|
|
152
257
|
name: 'folderPath',
|
|
153
|
-
message: '
|
|
258
|
+
message: 'Where is the cloaked folder you want to restore from?',
|
|
154
259
|
validate: (input) => {
|
|
155
260
|
const path = resolve(input);
|
|
156
261
|
if (!existsSync(path)) {
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import inquirer from 'inquirer';
|
|
4
|
+
import checkbox from '@inquirer/checkbox';
|
|
5
|
+
|
|
6
|
+
function buildTree(root, options) {
|
|
7
|
+
const { maxDepth, ignore } = options;
|
|
8
|
+
const nodes = new Map();
|
|
9
|
+
const checked = new Set();
|
|
10
|
+
|
|
11
|
+
function walk(currentPath, depth, parentId) {
|
|
12
|
+
if (depth > maxDepth) return;
|
|
13
|
+
try {
|
|
14
|
+
const entries = fs.readdirSync(currentPath, { withFileTypes: true });
|
|
15
|
+
|
|
16
|
+
for (const entry of entries) {
|
|
17
|
+
const fullPath = path.join(currentPath, entry.name);
|
|
18
|
+
if (ignore(fullPath, entry.name)) continue;
|
|
19
|
+
|
|
20
|
+
const id = fullPath;
|
|
21
|
+
const node = {
|
|
22
|
+
id,
|
|
23
|
+
name: entry.name,
|
|
24
|
+
fullPath,
|
|
25
|
+
parentId,
|
|
26
|
+
children: [],
|
|
27
|
+
isDirectory: entry.isDirectory(),
|
|
28
|
+
};
|
|
29
|
+
nodes.set(id, node);
|
|
30
|
+
|
|
31
|
+
if (parentId) {
|
|
32
|
+
const parent = nodes.get(parentId);
|
|
33
|
+
if (parent) parent.children.push(id);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (entry.isDirectory()) {
|
|
37
|
+
walk(fullPath, depth + 1, id);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
} catch (e) {
|
|
41
|
+
// ignore read errors
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
walk(root, 0, undefined);
|
|
46
|
+
return { nodes, checked };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function setSubtreeChecked(state, nodeId, checkedStatus) {
|
|
50
|
+
const queue = [nodeId];
|
|
51
|
+
while (queue.length) {
|
|
52
|
+
const id = queue.pop();
|
|
53
|
+
if (checkedStatus) state.checked.add(id);
|
|
54
|
+
else state.checked.delete(id);
|
|
55
|
+
const node = state.nodes.get(id);
|
|
56
|
+
if (node) queue.push(...node.children);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function recomputeParentFromChildren(state, nodeId) {
|
|
61
|
+
let current = state.nodes.get(nodeId);
|
|
62
|
+
while (current?.parentId) {
|
|
63
|
+
const parent = state.nodes.get(current.parentId);
|
|
64
|
+
if (!parent) break;
|
|
65
|
+
const allChildrenChecked = parent.children.length
|
|
66
|
+
? parent.children.every((cid) => state.checked.has(cid))
|
|
67
|
+
: false;
|
|
68
|
+
|
|
69
|
+
if (allChildrenChecked) state.checked.add(parent.id);
|
|
70
|
+
else state.checked.delete(parent.id);
|
|
71
|
+
|
|
72
|
+
current = parent;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function applyExplicitNodeState(state, nodeId, targetState) {
|
|
77
|
+
setSubtreeChecked(state, nodeId, targetState);
|
|
78
|
+
recomputeParentFromChildren(state, nodeId);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function flattenNodesHierarchically(nodes) {
|
|
82
|
+
const roots = Array.from(nodes.values()).filter((n) => !n.parentId);
|
|
83
|
+
roots.sort((a, b) => a.name.localeCompare(b.name));
|
|
84
|
+
|
|
85
|
+
const flatList = [];
|
|
86
|
+
|
|
87
|
+
function visit(node, prefix, isLast) {
|
|
88
|
+
const connector = prefix ? (isLast ? '└─ ' : '├─ ') : '';
|
|
89
|
+
const icon = node.isDirectory ? '[d] ' : ' ';
|
|
90
|
+
|
|
91
|
+
flatList.push({
|
|
92
|
+
node,
|
|
93
|
+
displayName: `${prefix}${connector}${icon}${node.name}`
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const childPrefix = prefix + (isLast ? ' ' : '| ');
|
|
97
|
+
const children = node.children
|
|
98
|
+
.map((id) => nodes.get(id))
|
|
99
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
100
|
+
|
|
101
|
+
children.forEach((child, index) => {
|
|
102
|
+
const last = index === children.length - 1;
|
|
103
|
+
visit(child, childPrefix, last);
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
roots.forEach((root, index) => {
|
|
108
|
+
const last = index === roots.length - 1;
|
|
109
|
+
visit(root, '', last);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
return flatList;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function printSelectionSummary(root, state, hierarchicalNodes) {
|
|
116
|
+
const selectedFiles = hierarchicalNodes.filter(n => !n.node.isDirectory && state.checked.has(n.node.id));
|
|
117
|
+
if (selectedFiles.length === 0) {
|
|
118
|
+
console.log('\x1b[90m (no files selected yet)\x1b[0m\n');
|
|
119
|
+
} else {
|
|
120
|
+
console.log(`\x1b[32m ${selectedFiles.length} file(s) selected:\x1b[0m`);
|
|
121
|
+
const maxDisplay = 10;
|
|
122
|
+
selectedFiles.slice(0, maxDisplay).forEach(n => {
|
|
123
|
+
const rel = path.relative(root, n.node.id);
|
|
124
|
+
console.log(` \x1b[32m+\x1b[0m ${rel}`);
|
|
125
|
+
});
|
|
126
|
+
if (selectedFiles.length > maxDisplay) {
|
|
127
|
+
console.log(`\x1b[90m ... and ${selectedFiles.length - maxDisplay} more\x1b[0m`);
|
|
128
|
+
}
|
|
129
|
+
console.log('');
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export async function checkboxTreeSelect(options = {}) {
|
|
134
|
+
const {
|
|
135
|
+
root = process.cwd(),
|
|
136
|
+
maxDepth = 5,
|
|
137
|
+
ignore = (fullPath, name) => name === 'node_modules' || name.startsWith('.git'),
|
|
138
|
+
message = 'Select files',
|
|
139
|
+
pageSize = 15,
|
|
140
|
+
precheck = [],
|
|
141
|
+
} = options;
|
|
142
|
+
|
|
143
|
+
const state = buildTree(root, { maxDepth, ignore });
|
|
144
|
+
|
|
145
|
+
if (precheck && precheck.length > 0) {
|
|
146
|
+
for (const p of precheck) {
|
|
147
|
+
if (state.nodes.has(p)) applyExplicitNodeState(state, p, true);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const hierarchicalNodes = flattenNodesHierarchically(state.nodes);
|
|
152
|
+
const allNodes = hierarchicalNodes.map(item => item.node);
|
|
153
|
+
|
|
154
|
+
if (hierarchicalNodes.length === 0) {
|
|
155
|
+
console.log(`No items found in ${root}`);
|
|
156
|
+
return [];
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Search loop: user types a query, sees filtered results, selects with space.
|
|
160
|
+
// Empty search + Enter = done.
|
|
161
|
+
while (true) {
|
|
162
|
+
console.clear();
|
|
163
|
+
console.log(`\nRoot: \x1b[1m${root}\x1b[0m`);
|
|
164
|
+
console.log('\x1b[90mType a filename to search. Space to select, Enter to confirm. Empty search + Enter to finish.\x1b[0m\n');
|
|
165
|
+
printSelectionSummary(root, state, hierarchicalNodes);
|
|
166
|
+
|
|
167
|
+
const { query } = await inquirer.prompt([{
|
|
168
|
+
type: 'input',
|
|
169
|
+
name: 'query',
|
|
170
|
+
message: `${message} — search:`,
|
|
171
|
+
}]);
|
|
172
|
+
|
|
173
|
+
const trimmed = query.trim().toLowerCase();
|
|
174
|
+
|
|
175
|
+
// Empty query = user is done selecting
|
|
176
|
+
if (!trimmed) break;
|
|
177
|
+
|
|
178
|
+
// Filter nodes whose relative path matches the query
|
|
179
|
+
const matched = hierarchicalNodes.filter(({ node }) => {
|
|
180
|
+
const rel = path.relative(root, node.id).toLowerCase();
|
|
181
|
+
return rel.includes(trimmed);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
if (matched.length === 0) {
|
|
185
|
+
console.log(`\x1b[33m No results for "${query}". Try a different term.\x1b[0m`);
|
|
186
|
+
await new Promise(r => setTimeout(r, 1200));
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Build choices for the checkbox prompt from search results
|
|
191
|
+
const choices = matched.map(({ node }) => {
|
|
192
|
+
const rel = path.relative(root, node.id);
|
|
193
|
+
const icon = node.isDirectory ? '[dir] ' : '';
|
|
194
|
+
return {
|
|
195
|
+
name: `${icon}${rel}`,
|
|
196
|
+
value: node.id,
|
|
197
|
+
checked: state.checked.has(node.id),
|
|
198
|
+
};
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
const prevChecked = new Set(state.checked);
|
|
202
|
+
|
|
203
|
+
let selected;
|
|
204
|
+
try {
|
|
205
|
+
selected = await checkbox({
|
|
206
|
+
message: `Results for "${query}" — Space to toggle, Enter to go back to search`,
|
|
207
|
+
choices,
|
|
208
|
+
pageSize,
|
|
209
|
+
loop: false,
|
|
210
|
+
});
|
|
211
|
+
} catch {
|
|
212
|
+
// User force-quit (ctrl+c) — treat as done
|
|
213
|
+
break;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Apply changes: determine what was toggled from the matched set
|
|
217
|
+
const nextChecked = new Set(selected);
|
|
218
|
+
for (const { node } of matched) {
|
|
219
|
+
const was = prevChecked.has(node.id);
|
|
220
|
+
const now = nextChecked.has(node.id);
|
|
221
|
+
if (was !== now) {
|
|
222
|
+
applyExplicitNodeState(state, node.id, now);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return Array.from(state.checked);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export async function checkboxTreeSelectFilesOnly(options = {}) {
|
|
231
|
+
const paths = await checkboxTreeSelect(options);
|
|
232
|
+
return paths.filter((p) => {
|
|
233
|
+
try {
|
|
234
|
+
return fs.statSync(p).isFile();
|
|
235
|
+
} catch {
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export async function checkboxTreeSelectSingleDir(options = {}) {
|
|
242
|
+
while (true) {
|
|
243
|
+
const paths = await checkboxTreeSelect({ ...options, message: options.message + ' (Select MAXIMUM ONE directory)' });
|
|
244
|
+
const dirs = paths.filter(p => {
|
|
245
|
+
try { return fs.statSync(p).isDirectory(); } catch { return false; }
|
|
246
|
+
});
|
|
247
|
+
if (dirs.length > 1) {
|
|
248
|
+
console.log('\nPlease select ONLY ONE directory.\n');
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
return dirs[0] || '';
|
|
252
|
+
}
|
|
253
|
+
}
|
package/test-tree.js
ADDED
package/tests/anonymizer.test.js
CHANGED
|
@@ -9,37 +9,37 @@ describe('Anonymizer', () => {
|
|
|
9
9
|
describe('createAnonymizer', () => {
|
|
10
10
|
it('should replace exact matches', () => {
|
|
11
11
|
const anonymizer = createAnonymizer([
|
|
12
|
-
{ original: '
|
|
12
|
+
{ original: 'Microsoft', replacement: 'ABCCompany' }
|
|
13
13
|
]);
|
|
14
14
|
|
|
15
15
|
// Title case input -> Title case output (first letter upper, rest lower)
|
|
16
|
-
expect(anonymizer('Hello
|
|
16
|
+
expect(anonymizer('Hello Microsoft world')).toBe('Hello Abccompany world');
|
|
17
17
|
});
|
|
18
18
|
|
|
19
19
|
it('should handle all uppercase', () => {
|
|
20
20
|
const anonymizer = createAnonymizer([
|
|
21
|
-
{ original: '
|
|
21
|
+
{ original: 'Microsoft', replacement: 'ABCCompany' }
|
|
22
22
|
]);
|
|
23
23
|
|
|
24
|
-
expect(anonymizer('
|
|
24
|
+
expect(anonymizer('MICROSOFT is great')).toBe('ABCCOMPANY is great');
|
|
25
25
|
});
|
|
26
26
|
|
|
27
27
|
it('should handle all lowercase', () => {
|
|
28
28
|
const anonymizer = createAnonymizer([
|
|
29
|
-
{ original: '
|
|
29
|
+
{ original: 'Microsoft', replacement: 'ABCCompany' }
|
|
30
30
|
]);
|
|
31
31
|
|
|
32
|
-
expect(anonymizer('
|
|
32
|
+
expect(anonymizer('microsoft is lower')).toBe('abccompany is lower');
|
|
33
33
|
});
|
|
34
34
|
|
|
35
35
|
it('should handle multiple replacements', () => {
|
|
36
36
|
const anonymizer = createAnonymizer([
|
|
37
|
-
{ original: '
|
|
37
|
+
{ original: 'Microsoft', replacement: 'ABCCompany' },
|
|
38
38
|
{ original: 'Frontend', replacement: 'Client' }
|
|
39
39
|
]);
|
|
40
40
|
|
|
41
41
|
// Both are Title case -> first upper + rest lower
|
|
42
|
-
expect(anonymizer('
|
|
42
|
+
expect(anonymizer('Microsoft Frontend API')).toBe('Abccompany Client API');
|
|
43
43
|
});
|
|
44
44
|
|
|
45
45
|
it('should handle empty replacements', () => {
|
|
@@ -65,45 +65,45 @@ describe('Anonymizer', () => {
|
|
|
65
65
|
describe('createDeanonymizer', () => {
|
|
66
66
|
it('should reverse the anonymization', () => {
|
|
67
67
|
const replacements = [
|
|
68
|
-
{ original: '
|
|
68
|
+
{ original: 'Microsoft', replacement: 'ABCCompany' }
|
|
69
69
|
];
|
|
70
70
|
|
|
71
71
|
const deanonymizer = createDeanonymizer(replacements);
|
|
72
72
|
|
|
73
|
-
// ABCCompany (Title case) ->
|
|
74
|
-
expect(deanonymizer('Hello ABCCompany world')).toBe('Hello
|
|
73
|
+
// ABCCompany (Title case) -> Microsoft (Title case: first upper + rest lower)
|
|
74
|
+
expect(deanonymizer('Hello ABCCompany world')).toBe('Hello Microsoft world');
|
|
75
75
|
});
|
|
76
76
|
|
|
77
77
|
it('should handle multiple replacements in reverse', () => {
|
|
78
78
|
const replacements = [
|
|
79
|
-
{ original: '
|
|
79
|
+
{ original: 'Microsoft', replacement: 'ABCCompany' },
|
|
80
80
|
{ original: 'API', replacement: 'Service' }
|
|
81
81
|
];
|
|
82
82
|
|
|
83
83
|
const deanonymizer = createDeanonymizer(replacements);
|
|
84
84
|
|
|
85
85
|
// Title case -> Title case for both
|
|
86
|
-
expect(deanonymizer('ABCCompany Service')).toBe('
|
|
86
|
+
expect(deanonymizer('ABCCompany Service')).toBe('Microsoft Api');
|
|
87
87
|
});
|
|
88
88
|
|
|
89
89
|
it('should handle uppercase in reverse', () => {
|
|
90
90
|
const replacements = [
|
|
91
|
-
{ original: '
|
|
91
|
+
{ original: 'Microsoft', replacement: 'ABCCompany' }
|
|
92
92
|
];
|
|
93
93
|
|
|
94
94
|
const deanonymizer = createDeanonymizer(replacements);
|
|
95
95
|
|
|
96
|
-
expect(deanonymizer('ABCCOMPANY')).toBe('
|
|
96
|
+
expect(deanonymizer('ABCCOMPANY')).toBe('MICROSOFT');
|
|
97
97
|
});
|
|
98
98
|
|
|
99
99
|
it('should handle lowercase in reverse', () => {
|
|
100
100
|
const replacements = [
|
|
101
|
-
{ original: '
|
|
101
|
+
{ original: 'Microsoft', replacement: 'ABCCompany' }
|
|
102
102
|
];
|
|
103
103
|
|
|
104
104
|
const deanonymizer = createDeanonymizer(replacements);
|
|
105
105
|
|
|
106
|
-
expect(deanonymizer('abccompany')).toBe('
|
|
106
|
+
expect(deanonymizer('abccompany')).toBe('microsoft');
|
|
107
107
|
});
|
|
108
108
|
});
|
|
109
109
|
});
|
package/tests/copier.test.js
CHANGED
|
@@ -55,9 +55,9 @@ describe('Copier Module', () => {
|
|
|
55
55
|
const sourceFile = join(sourceDir, 'code.js');
|
|
56
56
|
const destFile = join(destDir, 'code.js');
|
|
57
57
|
|
|
58
|
-
writeFileSync(sourceFile, 'const company = "
|
|
58
|
+
writeFileSync(sourceFile, 'const company = "Microsoft";');
|
|
59
59
|
|
|
60
|
-
const transform = (content) => content.replace(/
|
|
60
|
+
const transform = (content) => content.replace(/Microsoft/g, 'ABCCompany');
|
|
61
61
|
const result = copyFileWithTransform(sourceFile, destFile, transform);
|
|
62
62
|
|
|
63
63
|
expect(result.transformed).toBe(true);
|
|
@@ -70,7 +70,7 @@ describe('Copier Module', () => {
|
|
|
70
70
|
|
|
71
71
|
writeFileSync(sourceFile, 'const x = 1;');
|
|
72
72
|
|
|
73
|
-
const transform = (content) => content.replace(/
|
|
73
|
+
const transform = (content) => content.replace(/Microsoft/g, 'ABCCompany');
|
|
74
74
|
const result = copyFileWithTransform(sourceFile, destFile, transform);
|
|
75
75
|
|
|
76
76
|
expect(result.transformed).toBe(false);
|
package/tests/crypto.test.js
CHANGED
|
@@ -65,7 +65,7 @@ describe('Crypto Module', () => {
|
|
|
65
65
|
describe('encryptReplacements/decryptReplacements', () => {
|
|
66
66
|
it('should encrypt only the original field', () => {
|
|
67
67
|
const replacements = [
|
|
68
|
-
{ original: '
|
|
68
|
+
{ original: 'Microsoft', replacement: 'ABCCompany' },
|
|
69
69
|
{ original: 'Secret', replacement: 'Public' }
|
|
70
70
|
];
|
|
71
71
|
|
|
@@ -82,13 +82,13 @@ describe('Crypto Module', () => {
|
|
|
82
82
|
|
|
83
83
|
it('should decrypt back to original', () => {
|
|
84
84
|
const replacements = [
|
|
85
|
-
{ original: '
|
|
85
|
+
{ original: 'Microsoft', replacement: 'ABCCompany' }
|
|
86
86
|
];
|
|
87
87
|
|
|
88
88
|
const encrypted = encryptReplacements(replacements, testSecret);
|
|
89
89
|
const decrypted = decryptReplacements(encrypted, testSecret);
|
|
90
90
|
|
|
91
|
-
expect(decrypted[0].original).toBe('
|
|
91
|
+
expect(decrypted[0].original).toBe('Microsoft');
|
|
92
92
|
expect(decrypted[0].replacement).toBe('ABCCompany');
|
|
93
93
|
});
|
|
94
94
|
|