repo-cloak-cli 1.2.4 → 1.3.2
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/.github/workflows/release.yml +92 -92
- package/LICENSE +21 -21
- package/README.md +138 -118
- package/bin/repo-cloak.js +9 -9
- package/package.json +50 -50
- package/src/cli.js +86 -84
- package/src/commands/pull.js +582 -352
- package/src/commands/push.js +250 -235
- package/src/core/anonymizer.js +128 -128
- package/src/core/copier.js +139 -139
- package/src/core/crypto.js +128 -128
- package/src/core/git.js +61 -0
- package/src/core/mapper.js +235 -235
- package/src/core/scanner.js +137 -137
- package/src/index.js +8 -8
- package/src/ui/banner.js +70 -70
- package/src/ui/fileSelector.js +256 -256
- package/src/ui/prompts.js +165 -165
- package/tests/anonymizer.test.js +127 -127
- package/tests/copier.test.js +94 -94
- package/tests/crypto.test.js +106 -106
- package/tests/git.test.js +103 -0
- package/tests/mapper.test.js +166 -166
- package/tests/scanner.test.js +100 -100
- package/medium.md +0 -319
package/src/commands/pull.js
CHANGED
|
@@ -1,352 +1,582 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pull Command
|
|
3
|
-
* Extract files and anonymize sensitive information
|
|
4
|
-
* Supports quick-add mode when pulling to existing cloaked directory
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import ora from 'ora';
|
|
8
|
-
import chalk from 'chalk';
|
|
9
|
-
import inquirer from 'inquirer';
|
|
10
|
-
import { existsSync, mkdirSync } from 'fs';
|
|
11
|
-
import { resolve, relative } from 'path';
|
|
12
|
-
|
|
13
|
-
import { selectFiles } from '../ui/fileSelector.js';
|
|
14
|
-
import {
|
|
15
|
-
promptSourceDirectory,
|
|
16
|
-
promptDestinationDirectory,
|
|
17
|
-
promptKeywordReplacements,
|
|
18
|
-
showSummaryAndConfirm,
|
|
19
|
-
confirmAction
|
|
20
|
-
} from '../ui/prompts.js';
|
|
21
|
-
import { showSuccess, showError, showInfo } from '../ui/banner.js';
|
|
22
|
-
import { getAllFiles } from '../core/scanner.js';
|
|
23
|
-
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
let
|
|
33
|
-
let
|
|
34
|
-
let
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
if (
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
// Step
|
|
253
|
-
if (!
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Pull Command
|
|
3
|
+
* Extract files and anonymize sensitive information
|
|
4
|
+
* Supports quick-add mode when pulling to existing cloaked directory
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import ora from 'ora';
|
|
8
|
+
import chalk from 'chalk';
|
|
9
|
+
import inquirer from 'inquirer';
|
|
10
|
+
import { existsSync, mkdirSync } from 'fs';
|
|
11
|
+
import { resolve, relative } from 'path';
|
|
12
|
+
|
|
13
|
+
import { selectFiles } from '../ui/fileSelector.js';
|
|
14
|
+
import {
|
|
15
|
+
promptSourceDirectory,
|
|
16
|
+
promptDestinationDirectory,
|
|
17
|
+
promptKeywordReplacements,
|
|
18
|
+
showSummaryAndConfirm,
|
|
19
|
+
confirmAction
|
|
20
|
+
} from '../ui/prompts.js';
|
|
21
|
+
import { showSuccess, showError, showInfo } from '../ui/banner.js';
|
|
22
|
+
import { getAllFiles } from '../core/scanner.js';
|
|
23
|
+
|
|
24
|
+
import { copyFiles } from '../core/copier.js';
|
|
25
|
+
import { createAnonymizer } from '../core/anonymizer.js';
|
|
26
|
+
import { createMapping, saveMapping, loadRawMapping, mergeMapping, hasMapping, decryptMapping } from '../core/mapper.js';
|
|
27
|
+
import { getOrCreateSecret, hasSecret, decryptReplacements } from '../core/crypto.js';
|
|
28
|
+
import { isGitRepo, getChangedFiles } from '../core/git.js';
|
|
29
|
+
|
|
30
|
+
export async function pull(options = {}) {
|
|
31
|
+
try {
|
|
32
|
+
let destDir = null;
|
|
33
|
+
let existingMapping = null;
|
|
34
|
+
let existingReplacements = [];
|
|
35
|
+
let sourceDir = null;
|
|
36
|
+
let isQuickAdd = false;
|
|
37
|
+
|
|
38
|
+
// Step 1: Check if current directory is already a cloaked directory
|
|
39
|
+
const currentDir = process.cwd();
|
|
40
|
+
|
|
41
|
+
if (hasMapping(currentDir) && !options.dest) {
|
|
42
|
+
// Running from inside an existing cloaked directory - auto-detect!
|
|
43
|
+
destDir = currentDir;
|
|
44
|
+
existingMapping = loadRawMapping(destDir);
|
|
45
|
+
|
|
46
|
+
if (!options.force) {
|
|
47
|
+
console.log(chalk.cyan('\n Existing cloaked directory detected'));
|
|
48
|
+
console.log(chalk.dim(` Created: ${existingMapping.timestamp}`));
|
|
49
|
+
console.log(chalk.dim(` Files: ${existingMapping.stats?.totalFiles || existingMapping.files?.length || 0}`));
|
|
50
|
+
console.log(chalk.dim(` Replacements: ${existingMapping.replacements?.length || 0}\n`));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (options.force) {
|
|
54
|
+
isQuickAdd = true;
|
|
55
|
+
console.log(chalk.cyan('\n Restoring missing/outdated files from source...'));
|
|
56
|
+
} else {
|
|
57
|
+
// Ask if they want quick-add mode
|
|
58
|
+
const { mode } = await inquirer.prompt([
|
|
59
|
+
{
|
|
60
|
+
type: 'list',
|
|
61
|
+
name: 'mode',
|
|
62
|
+
message: 'What would you like to do?',
|
|
63
|
+
choices: [
|
|
64
|
+
{
|
|
65
|
+
name: 'Quick Add - Use existing replacements and add more files',
|
|
66
|
+
value: 'quick'
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
name: 'Add More Replacements - Add files with additional anonymization',
|
|
70
|
+
value: 'extend'
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
name: 'Fresh Start - Choose new destination',
|
|
74
|
+
value: 'fresh'
|
|
75
|
+
}
|
|
76
|
+
]
|
|
77
|
+
}
|
|
78
|
+
]);
|
|
79
|
+
|
|
80
|
+
if (mode === 'fresh') {
|
|
81
|
+
// Get a new destination
|
|
82
|
+
destDir = await promptDestinationDirectory();
|
|
83
|
+
existingMapping = null;
|
|
84
|
+
} else {
|
|
85
|
+
isQuickAdd = mode === 'quick';
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (existingMapping) {
|
|
90
|
+
// Decrypt existing replacements
|
|
91
|
+
if (existingMapping.encrypted && hasSecret()) {
|
|
92
|
+
const secret = getOrCreateSecret();
|
|
93
|
+
try {
|
|
94
|
+
const decrypted = decryptReplacements(existingMapping.replacements || [], secret);
|
|
95
|
+
existingReplacements = decrypted.filter(r => !r.decryptFailed);
|
|
96
|
+
|
|
97
|
+
if (existingReplacements.length > 0 && !options.force) {
|
|
98
|
+
console.log(chalk.green(' Existing replacements loaded:\n'));
|
|
99
|
+
existingReplacements.forEach(r => {
|
|
100
|
+
console.log(chalk.dim(` "${r.original}" → "${r.replacement}"`));
|
|
101
|
+
});
|
|
102
|
+
console.log('');
|
|
103
|
+
}
|
|
104
|
+
} catch (err) {
|
|
105
|
+
console.log(chalk.yellow(' Could not decrypt existing replacements'));
|
|
106
|
+
}
|
|
107
|
+
} else if (!existingMapping.encrypted && existingMapping.replacements) {
|
|
108
|
+
// Not encrypted - use directly
|
|
109
|
+
existingReplacements = existingMapping.replacements;
|
|
110
|
+
|
|
111
|
+
if (existingReplacements.length > 0 && !options.force) {
|
|
112
|
+
console.log(chalk.green(' Existing replacements loaded:\n'));
|
|
113
|
+
existingReplacements.forEach(r => {
|
|
114
|
+
console.log(chalk.dim(` "${r.original}" → "${r.replacement}"`));
|
|
115
|
+
});
|
|
116
|
+
console.log('');
|
|
117
|
+
}
|
|
118
|
+
}
|
|
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
|
+
}
|
|
144
|
+
} else {
|
|
145
|
+
// Not running from a cloaked directory - ask for destination
|
|
146
|
+
if (options.force) {
|
|
147
|
+
showError('Force flag can only be used within an existing cloaked directory.');
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
destDir = options.dest
|
|
152
|
+
? resolve(options.dest)
|
|
153
|
+
: await promptDestinationDirectory();
|
|
154
|
+
|
|
155
|
+
// Check if the chosen destination has an existing mapping
|
|
156
|
+
if (existsSync(destDir) && hasMapping(destDir)) {
|
|
157
|
+
existingMapping = loadRawMapping(destDir);
|
|
158
|
+
|
|
159
|
+
if (!options.force) {
|
|
160
|
+
console.log(chalk.cyan('\n Existing cloaked directory detected'));
|
|
161
|
+
console.log(chalk.dim(` Created: ${existingMapping.timestamp}`));
|
|
162
|
+
console.log(chalk.dim(` Files: ${existingMapping.stats?.totalFiles || existingMapping.files?.length || 0}`));
|
|
163
|
+
console.log(chalk.dim(` Replacements: ${existingMapping.replacements?.length || 0}\n`));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (options.force) {
|
|
167
|
+
isQuickAdd = true;
|
|
168
|
+
console.log(chalk.cyan('\n Restoring missing/outdated files from source...'));
|
|
169
|
+
} else {
|
|
170
|
+
// Ask if they want quick-add mode
|
|
171
|
+
const { mode } = await inquirer.prompt([
|
|
172
|
+
{
|
|
173
|
+
type: 'list',
|
|
174
|
+
name: 'mode',
|
|
175
|
+
message: 'What would you like to do?',
|
|
176
|
+
choices: [
|
|
177
|
+
{ name: 'Quick Add - Use existing replacements and add more files', value: 'quick' },
|
|
178
|
+
{ name: 'Add More Replacements - Add files with additional anonymization', value: 'extend' },
|
|
179
|
+
{ name: 'Fresh Start - Choose new destination', value: 'fresh' }
|
|
180
|
+
]
|
|
181
|
+
}
|
|
182
|
+
]);
|
|
183
|
+
|
|
184
|
+
if (mode === 'fresh') {
|
|
185
|
+
destDir = await promptDestinationDirectory();
|
|
186
|
+
existingMapping = null;
|
|
187
|
+
} else {
|
|
188
|
+
isQuickAdd = mode === 'quick';
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (existingMapping) {
|
|
193
|
+
// Decrypt existing replacements
|
|
194
|
+
if (existingMapping.encrypted && hasSecret()) {
|
|
195
|
+
const secret = getOrCreateSecret();
|
|
196
|
+
try {
|
|
197
|
+
const decrypted = decryptReplacements(existingMapping.replacements || [], secret);
|
|
198
|
+
existingReplacements = decrypted.filter(r => !r.decryptFailed);
|
|
199
|
+
|
|
200
|
+
if (existingReplacements.length > 0 && !options.force) {
|
|
201
|
+
console.log(chalk.green(' Existing replacements loaded:\n'));
|
|
202
|
+
existingReplacements.forEach(r => {
|
|
203
|
+
console.log(chalk.dim(` "${r.original}" → "${r.replacement}"`));
|
|
204
|
+
});
|
|
205
|
+
console.log('');
|
|
206
|
+
}
|
|
207
|
+
} catch (err) {
|
|
208
|
+
console.log(chalk.yellow(' Could not decrypt existing replacements'));
|
|
209
|
+
}
|
|
210
|
+
} else if (!existingMapping.encrypted && existingMapping.replacements) {
|
|
211
|
+
// Not encrypted - use directly
|
|
212
|
+
existingReplacements = existingMapping.replacements;
|
|
213
|
+
if (existingReplacements.length > 0 && !options.force) {
|
|
214
|
+
console.log(chalk.green(' Existing replacements loaded:\n'));
|
|
215
|
+
existingReplacements.forEach(r => {
|
|
216
|
+
console.log(chalk.dim(` "${r.original}" → "${r.replacement}"`));
|
|
217
|
+
});
|
|
218
|
+
console.log('');
|
|
219
|
+
}
|
|
220
|
+
}
|
|
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
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
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
|
+
|
|
268
|
+
// Step 4: Select files (Check for Git integration first)
|
|
269
|
+
let selectedFiles = [];
|
|
270
|
+
let useGitFiles = false;
|
|
271
|
+
|
|
272
|
+
if (options.force) {
|
|
273
|
+
// In force mode, we just re-pull all files that are in the mapping
|
|
274
|
+
if (existingMapping && existingMapping.files) {
|
|
275
|
+
// We need to reconstruct the absolute paths to the source files
|
|
276
|
+
// The mapping stores 'original' as relative path to sourceDir
|
|
277
|
+
// But we need to check if we can decrypt them
|
|
278
|
+
let filesToPull = [];
|
|
279
|
+
|
|
280
|
+
if (existingMapping.encrypted) {
|
|
281
|
+
// We already decrypted the files list earlier if possible
|
|
282
|
+
// But let's be sure we have the decrypted file list
|
|
283
|
+
if (existingMapping.files[0]?.original.iv) {
|
|
284
|
+
// Still encrypted - we need the secret
|
|
285
|
+
const secret = getOrCreateSecret();
|
|
286
|
+
try {
|
|
287
|
+
const decryptedFiles = existingMapping.files.map(f => ({
|
|
288
|
+
original: decrypt(f.original, secret),
|
|
289
|
+
cloaked: f.cloaked
|
|
290
|
+
}));
|
|
291
|
+
filesToPull = decryptedFiles;
|
|
292
|
+
} catch (e) {
|
|
293
|
+
showError('Could not decrypt file list. Cannot force pull.');
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
} else {
|
|
297
|
+
// Already decrypted or never encrypted
|
|
298
|
+
filesToPull = existingMapping.files;
|
|
299
|
+
}
|
|
300
|
+
} else {
|
|
301
|
+
filesToPull = existingMapping.files;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
selectedFiles = filesToPull.map(f => resolve(sourceDir, f.original));
|
|
305
|
+
|
|
306
|
+
// NEW: Also scan local directory for files that might exist here but not in mapping
|
|
307
|
+
try {
|
|
308
|
+
const localFiles = getAllFiles(destDir);
|
|
309
|
+
// Create reverse replacements for deanonymizing paths
|
|
310
|
+
const reverseReplacements = existingReplacements.map(r => ({
|
|
311
|
+
original: r.replacement,
|
|
312
|
+
replacement: r.original
|
|
313
|
+
}));
|
|
314
|
+
|
|
315
|
+
// Helper to reverse anonymize path
|
|
316
|
+
const reverseAnonymize = (path) => {
|
|
317
|
+
let result = path;
|
|
318
|
+
for (const { original, replacement } of reverseReplacements) {
|
|
319
|
+
const regex = new RegExp(original.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi');
|
|
320
|
+
result = result.replace(regex, replacement);
|
|
321
|
+
}
|
|
322
|
+
return result;
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
let addedCount = 0;
|
|
326
|
+
localFiles.forEach(file => {
|
|
327
|
+
// Get path relative to destDir
|
|
328
|
+
const relativeDestPath = relative(destDir, file.absolutePath);
|
|
329
|
+
|
|
330
|
+
// Reverse anonymize to get potential source path
|
|
331
|
+
const relativeSourcePath = reverseAnonymize(relativeDestPath);
|
|
332
|
+
const absoluteSourcePath = resolve(sourceDir, relativeSourcePath);
|
|
333
|
+
|
|
334
|
+
// Check if this file exists in source
|
|
335
|
+
if (existsSync(absoluteSourcePath)) {
|
|
336
|
+
// Add to selectedFiles if not already there
|
|
337
|
+
if (!selectedFiles.includes(absoluteSourcePath)) {
|
|
338
|
+
selectedFiles.push(absoluteSourcePath);
|
|
339
|
+
addedCount++;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
if (addedCount > 0) {
|
|
345
|
+
console.log(chalk.cyan(` Found ${addedCount} additional local files to sync from source.`));
|
|
346
|
+
}
|
|
347
|
+
} catch (err) {
|
|
348
|
+
// Ignore scanning errors
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (selectedFiles.length > 0) {
|
|
352
|
+
console.log(chalk.cyan(` Force pulling ${selectedFiles.length} files...`));
|
|
353
|
+
} else {
|
|
354
|
+
showError('No files found to pull (checked mapping and local directory).');
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
} else {
|
|
358
|
+
showError('No files found in existing mapping.');
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
} else if (isGitRepo(sourceDir)) {
|
|
362
|
+
const { useGit } = await inquirer.prompt([
|
|
363
|
+
{
|
|
364
|
+
type: 'confirm',
|
|
365
|
+
name: 'useGit',
|
|
366
|
+
message: 'Git repository detected. Do you want to select changed/added files?',
|
|
367
|
+
default: false
|
|
368
|
+
}
|
|
369
|
+
]);
|
|
370
|
+
|
|
371
|
+
if (useGit) {
|
|
372
|
+
const spinner = ora('Scanning changed files...').start();
|
|
373
|
+
const gitFiles = await getChangedFiles(sourceDir);
|
|
374
|
+
spinner.stop();
|
|
375
|
+
|
|
376
|
+
if (gitFiles.length === 0) {
|
|
377
|
+
console.log(chalk.yellow(' No changed or added files found in Git status.'));
|
|
378
|
+
const { fallback } = await inquirer.prompt([
|
|
379
|
+
{
|
|
380
|
+
type: 'confirm',
|
|
381
|
+
name: 'fallback',
|
|
382
|
+
message: 'Do you want to manually select files instead?',
|
|
383
|
+
default: true
|
|
384
|
+
}
|
|
385
|
+
]);
|
|
386
|
+
|
|
387
|
+
if (!fallback) {
|
|
388
|
+
return;
|
|
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));
|
|
395
|
+
|
|
396
|
+
if (validGitFiles.length > 0) {
|
|
397
|
+
console.log(chalk.green(` Found ${validGitFiles.length} changed files.`));
|
|
398
|
+
|
|
399
|
+
// Let user confirm/deselect git files
|
|
400
|
+
const { confirmGitFiles } = await inquirer.prompt([
|
|
401
|
+
{
|
|
402
|
+
type: 'checkbox',
|
|
403
|
+
name: 'confirmGitFiles',
|
|
404
|
+
message: 'Select changed files to extract:',
|
|
405
|
+
choices: validGitFiles.map(f => ({
|
|
406
|
+
name: relative(sourceDir, f),
|
|
407
|
+
value: f,
|
|
408
|
+
checked: true
|
|
409
|
+
}))
|
|
410
|
+
}
|
|
411
|
+
]);
|
|
412
|
+
|
|
413
|
+
selectedFiles = confirmGitFiles;
|
|
414
|
+
useGitFiles = true;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
if (!options.force && (!useGitFiles || selectedFiles.length === 0)) {
|
|
421
|
+
selectedFiles = await selectFiles(sourceDir);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
if (selectedFiles.length === 0) {
|
|
425
|
+
showError('No files selected. Aborting.');
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (!options.force) {
|
|
430
|
+
console.log(chalk.green(`\n✓ Selected ${selectedFiles.length} files\n`));
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// Step 5: Handle replacements based on mode
|
|
434
|
+
let replacements = [...existingReplacements];
|
|
435
|
+
|
|
436
|
+
if (!options.force) {
|
|
437
|
+
if (isQuickAdd) {
|
|
438
|
+
// Quick add mode - just use existing replacements
|
|
439
|
+
if (replacements.length > 0) {
|
|
440
|
+
console.log(chalk.cyan(' Using existing replacements (quick-add mode)\n'));
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Ask if they want to add more
|
|
444
|
+
const { addMore } = await inquirer.prompt([
|
|
445
|
+
{
|
|
446
|
+
type: 'confirm',
|
|
447
|
+
name: 'addMore',
|
|
448
|
+
message: 'Add additional replacements?',
|
|
449
|
+
default: false
|
|
450
|
+
}
|
|
451
|
+
]);
|
|
452
|
+
|
|
453
|
+
if (addMore) {
|
|
454
|
+
const additionalReplacements = await promptKeywordReplacements();
|
|
455
|
+
replacements = [...replacements, ...additionalReplacements];
|
|
456
|
+
}
|
|
457
|
+
} else if (existingMapping) {
|
|
458
|
+
// Extend mode - prompt for more replacements to add to existing
|
|
459
|
+
console.log(chalk.cyan('\n Add more replacements (existing will be preserved):\n'));
|
|
460
|
+
const additionalReplacements = await promptKeywordReplacements();
|
|
461
|
+
replacements = [...replacements, ...additionalReplacements];
|
|
462
|
+
} else {
|
|
463
|
+
// Fresh start - prompt for all replacements
|
|
464
|
+
replacements = await promptKeywordReplacements();
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Step 6: Confirm
|
|
469
|
+
if (!options.force) {
|
|
470
|
+
const confirmed = await showSummaryAndConfirm(
|
|
471
|
+
selectedFiles.length,
|
|
472
|
+
destDir,
|
|
473
|
+
replacements
|
|
474
|
+
);
|
|
475
|
+
|
|
476
|
+
if (!confirmed) {
|
|
477
|
+
showInfo('Operation cancelled.');
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
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
|
+
// Step 8: Copy and anonymize files
|
|
489
|
+
const spinner = ora('Copying and anonymizing files...').start();
|
|
490
|
+
|
|
491
|
+
const anonymizer = createAnonymizer(replacements);
|
|
492
|
+
let lastFile = '';
|
|
493
|
+
|
|
494
|
+
const results = await copyFiles(
|
|
495
|
+
selectedFiles,
|
|
496
|
+
sourceDir,
|
|
497
|
+
destDir,
|
|
498
|
+
anonymizer,
|
|
499
|
+
(current, total, file) => {
|
|
500
|
+
lastFile = file;
|
|
501
|
+
spinner.text = `Copying files... ${current}/${total} - ${file}`;
|
|
502
|
+
},
|
|
503
|
+
replacements // Pass replacements for path anonymization
|
|
504
|
+
);
|
|
505
|
+
|
|
506
|
+
spinner.succeed(`Copied ${results.copied} files`);
|
|
507
|
+
|
|
508
|
+
if (results.pathsRenamed > 0) {
|
|
509
|
+
console.log(chalk.cyan(` 📁 ${results.pathsRenamed} paths renamed`));
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
if (results.transformed > 0) {
|
|
513
|
+
console.log(chalk.cyan(` 📝 ${results.transformed} files had content replaced`));
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
if (results.errors.length > 0) {
|
|
517
|
+
console.log(chalk.yellow(` ⚠️ ${results.errors.length} files had errors`));
|
|
518
|
+
results.errors.forEach(e => {
|
|
519
|
+
console.log(chalk.dim(` - ${e.file}: ${e.error}`));
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// Step 9: Prepare new file mappings
|
|
524
|
+
const newFiles = selectedFiles.map(f => {
|
|
525
|
+
const originalPath = relative(sourceDir, f);
|
|
526
|
+
let anonymizedPath = originalPath;
|
|
527
|
+
for (const { original, replacement } of replacements) {
|
|
528
|
+
const regex = new RegExp(original.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi');
|
|
529
|
+
anonymizedPath = anonymizedPath.replace(regex, replacement);
|
|
530
|
+
}
|
|
531
|
+
return {
|
|
532
|
+
original: originalPath,
|
|
533
|
+
cloaked: anonymizedPath
|
|
534
|
+
};
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
// Step 10: Check for existing mapping and merge if found
|
|
538
|
+
let mapping;
|
|
539
|
+
let isIncremental = false;
|
|
540
|
+
|
|
541
|
+
if (existingMapping) {
|
|
542
|
+
// Merge with existing
|
|
543
|
+
mapping = mergeMapping(existingMapping, newFiles);
|
|
544
|
+
isIncremental = true;
|
|
545
|
+
console.log(chalk.cyan(` 🔄 Merged with existing mapping`));
|
|
546
|
+
} else {
|
|
547
|
+
// Create new mapping
|
|
548
|
+
mapping = createMapping({
|
|
549
|
+
sourceDir,
|
|
550
|
+
destDir,
|
|
551
|
+
replacements,
|
|
552
|
+
files: newFiles
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
const mapPath = saveMapping(destDir, mapping);
|
|
557
|
+
|
|
558
|
+
if (isIncremental) {
|
|
559
|
+
const history = mapping.pullHistory || [];
|
|
560
|
+
const lastPull = history[history.length - 1];
|
|
561
|
+
console.log(chalk.dim(` 📋 Mapping updated: ${lastPull?.filesAdded || 0} new files added (total: ${mapping.stats?.totalFiles})`));
|
|
562
|
+
} else {
|
|
563
|
+
console.log(chalk.dim(` 📋 Mapping saved: ${mapPath}`));
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// Done!
|
|
567
|
+
showSuccess('Extraction complete!');
|
|
568
|
+
console.log(chalk.white(` 📂 Files extracted to: ${chalk.cyan.bold(destDir)}`));
|
|
569
|
+
|
|
570
|
+
if (isQuickAdd || isIncremental) {
|
|
571
|
+
console.log(chalk.dim(`\n Tip: Run again to add more files quickly\n`));
|
|
572
|
+
} else {
|
|
573
|
+
console.log(chalk.dim(`\n To restore later, run: ${chalk.white('repo-cloak push')}\n`));
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
} catch (error) {
|
|
577
|
+
showError(`Pull failed: ${error.message}`);
|
|
578
|
+
if (process.env.DEBUG) {
|
|
579
|
+
console.error(error);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
}
|