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/push.js
CHANGED
|
@@ -1,235 +1,250 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Push Command
|
|
3
|
-
* Restore files with original names from a cloaked backup
|
|
4
|
-
* Now with decryption support!
|
|
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, join } from 'path';
|
|
12
|
-
|
|
13
|
-
import {
|
|
14
|
-
promptBackupFolder,
|
|
15
|
-
promptDestinationDirectory,
|
|
16
|
-
confirmAction
|
|
17
|
-
} from '../ui/prompts.js';
|
|
18
|
-
import { showSuccess, showError, showInfo, showWarning } from '../ui/banner.js';
|
|
19
|
-
import { getAllFiles } from '../core/scanner.js';
|
|
20
|
-
import { copyFiles } from '../core/copier.js';
|
|
21
|
-
import { createDeanonymizer } from '../core/anonymizer.js';
|
|
22
|
-
import { loadMapping, hasMapping, getReplacements, getOriginalSource, decryptMapping } from '../core/mapper.js';
|
|
23
|
-
import { getOrCreateSecret, hasSecret, decrypt, getConfigDir } from '../core/crypto.js';
|
|
24
|
-
|
|
25
|
-
export async function push(options = {}) {
|
|
26
|
-
try {
|
|
27
|
-
// Step 1: Check if current directory has a mapping (auto-detect)
|
|
28
|
-
const currentDir = process.cwd();
|
|
29
|
-
let cloakedDir;
|
|
30
|
-
|
|
31
|
-
if (hasMapping(currentDir) && !options.source) {
|
|
32
|
-
// Running from inside a cloaked directory - use it directly
|
|
33
|
-
cloakedDir = currentDir;
|
|
34
|
-
console.log(chalk.cyan('\n Cloaked directory detected in current folder'));
|
|
35
|
-
} else {
|
|
36
|
-
// Ask for the source directory
|
|
37
|
-
cloakedDir = options.source
|
|
38
|
-
? resolve(options.source)
|
|
39
|
-
: await promptBackupFolder();
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
if (!existsSync(cloakedDir)) {
|
|
43
|
-
showError(`Directory does not exist: ${cloakedDir}`);
|
|
44
|
-
return;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// Step 2: Check for mapping file
|
|
48
|
-
if (!hasMapping(cloakedDir)) {
|
|
49
|
-
showError('No repo-cloak mapping file found in this directory.');
|
|
50
|
-
console.log(chalk.dim(' Make sure you selected a directory created by "repo-cloak pull"'));
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// Step 3: Load mapping (without decryption first to check if encrypted)
|
|
55
|
-
const spinner = ora('Loading mapping file...').start();
|
|
56
|
-
let rawMapping = loadMapping(cloakedDir);
|
|
57
|
-
spinner.succeed('Mapping file loaded');
|
|
58
|
-
|
|
59
|
-
let mapping = rawMapping;
|
|
60
|
-
let decryptionFailed = false;
|
|
61
|
-
|
|
62
|
-
// Step 4: Handle encryption
|
|
63
|
-
if (rawMapping.encrypted) {
|
|
64
|
-
console.log(chalk.cyan('\n 🔐 This backup was encrypted'));
|
|
65
|
-
|
|
66
|
-
if (hasSecret()) {
|
|
67
|
-
const secret = getOrCreateSecret();
|
|
68
|
-
try {
|
|
69
|
-
mapping = decryptMapping(rawMapping, secret);
|
|
70
|
-
console.log(chalk.green(' ✓ Decrypted successfully using your secret key'));
|
|
71
|
-
} catch (error) {
|
|
72
|
-
decryptionFailed = true;
|
|
73
|
-
console.log(chalk.yellow(' ⚠️ Decryption failed with your current secret'));
|
|
74
|
-
}
|
|
75
|
-
} else {
|
|
76
|
-
decryptionFailed = true;
|
|
77
|
-
console.log(chalk.yellow(` ⚠️ No secret key found at ${getConfigDir()}`));
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// If decryption failed, ask user for manual input
|
|
81
|
-
if (decryptionFailed) {
|
|
82
|
-
console.log(chalk.yellow('\n Your secret key may have been lost or changed.'));
|
|
83
|
-
console.log(chalk.dim(' You can manually provide the original keywords to restore.\n'));
|
|
84
|
-
|
|
85
|
-
const manualReplacements = [];
|
|
86
|
-
|
|
87
|
-
for (const r of rawMapping.replacements || []) {
|
|
88
|
-
const { original } = await inquirer.prompt([
|
|
89
|
-
{
|
|
90
|
-
type: 'input',
|
|
91
|
-
name: 'original',
|
|
92
|
-
message: `What was the original text for "${r.replacement}"?`,
|
|
93
|
-
prefix: '🔑'
|
|
94
|
-
}
|
|
95
|
-
]);
|
|
96
|
-
|
|
97
|
-
if (original.trim()) {
|
|
98
|
-
manualReplacements.push({
|
|
99
|
-
original: original.trim(),
|
|
100
|
-
replacement: r.replacement
|
|
101
|
-
});
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
mapping = {
|
|
106
|
-
...rawMapping,
|
|
107
|
-
replacements: manualReplacements,
|
|
108
|
-
source: { path: null },
|
|
109
|
-
destination: { path: null },
|
|
110
|
-
files: (rawMapping.files || []).map(f => ({
|
|
111
|
-
original: f.cloaked, // Use cloaked as original if can't decrypt
|
|
112
|
-
cloaked: f.cloaked
|
|
113
|
-
}))
|
|
114
|
-
};
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// Display info
|
|
119
|
-
const sourcePath = getOriginalSource(mapping);
|
|
120
|
-
console.log(chalk.dim(`\n Original source: ${sourcePath || 'Unknown (encrypted)'}`));
|
|
121
|
-
console.log(chalk.dim(` Extracted on: ${mapping.timestamp}`));
|
|
122
|
-
console.log(chalk.dim(` Replacements: ${mapping.replacements?.length || 0}`));
|
|
123
|
-
console.log(chalk.dim(` Files: ${mapping.files?.length || 0}\n`));
|
|
124
|
-
|
|
125
|
-
// Show replacements that will be reversed
|
|
126
|
-
if (mapping.replacements && mapping.replacements.length > 0) {
|
|
127
|
-
console.log(chalk.cyan(' Replacements to reverse:'));
|
|
128
|
-
mapping.replacements.forEach(r => {
|
|
129
|
-
if (r.original) {
|
|
130
|
-
console.log(chalk.dim(` "${r.replacement}" → "${r.original}"`));
|
|
131
|
-
} else {
|
|
132
|
-
console.log(chalk.yellow(` "${r.replacement}" → [ENCRYPTED]`));
|
|
133
|
-
}
|
|
134
|
-
});
|
|
135
|
-
console.log('');
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// Step 5: Get destination directory
|
|
139
|
-
let destDir;
|
|
140
|
-
|
|
141
|
-
if (options.dest) {
|
|
142
|
-
destDir = resolve(options.dest);
|
|
143
|
-
} else if (sourcePath && existsSync(sourcePath)) {
|
|
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
|
-
// Step
|
|
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
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Push Command
|
|
3
|
+
* Restore files with original names from a cloaked backup
|
|
4
|
+
* Now with decryption support!
|
|
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, join } from 'path';
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
promptBackupFolder,
|
|
15
|
+
promptDestinationDirectory,
|
|
16
|
+
confirmAction
|
|
17
|
+
} from '../ui/prompts.js';
|
|
18
|
+
import { showSuccess, showError, showInfo, showWarning } from '../ui/banner.js';
|
|
19
|
+
import { getAllFiles } from '../core/scanner.js';
|
|
20
|
+
import { copyFiles } from '../core/copier.js';
|
|
21
|
+
import { createDeanonymizer } from '../core/anonymizer.js';
|
|
22
|
+
import { loadMapping, hasMapping, getReplacements, getOriginalSource, decryptMapping } from '../core/mapper.js';
|
|
23
|
+
import { getOrCreateSecret, hasSecret, decrypt, getConfigDir } from '../core/crypto.js';
|
|
24
|
+
|
|
25
|
+
export async function push(options = {}) {
|
|
26
|
+
try {
|
|
27
|
+
// Step 1: Check if current directory has a mapping (auto-detect)
|
|
28
|
+
const currentDir = process.cwd();
|
|
29
|
+
let cloakedDir;
|
|
30
|
+
|
|
31
|
+
if (hasMapping(currentDir) && !options.source) {
|
|
32
|
+
// Running from inside a cloaked directory - use it directly
|
|
33
|
+
cloakedDir = currentDir;
|
|
34
|
+
console.log(chalk.cyan('\n Cloaked directory detected in current folder'));
|
|
35
|
+
} else {
|
|
36
|
+
// Ask for the source directory
|
|
37
|
+
cloakedDir = options.source
|
|
38
|
+
? resolve(options.source)
|
|
39
|
+
: await promptBackupFolder();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (!existsSync(cloakedDir)) {
|
|
43
|
+
showError(`Directory does not exist: ${cloakedDir}`);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Step 2: Check for mapping file
|
|
48
|
+
if (!hasMapping(cloakedDir)) {
|
|
49
|
+
showError('No repo-cloak mapping file found in this directory.');
|
|
50
|
+
console.log(chalk.dim(' Make sure you selected a directory created by "repo-cloak pull"'));
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Step 3: Load mapping (without decryption first to check if encrypted)
|
|
55
|
+
const spinner = ora('Loading mapping file...').start();
|
|
56
|
+
let rawMapping = loadMapping(cloakedDir);
|
|
57
|
+
spinner.succeed('Mapping file loaded');
|
|
58
|
+
|
|
59
|
+
let mapping = rawMapping;
|
|
60
|
+
let decryptionFailed = false;
|
|
61
|
+
|
|
62
|
+
// Step 4: Handle encryption
|
|
63
|
+
if (rawMapping.encrypted) {
|
|
64
|
+
console.log(chalk.cyan('\n 🔐 This backup was encrypted'));
|
|
65
|
+
|
|
66
|
+
if (hasSecret()) {
|
|
67
|
+
const secret = getOrCreateSecret();
|
|
68
|
+
try {
|
|
69
|
+
mapping = decryptMapping(rawMapping, secret);
|
|
70
|
+
console.log(chalk.green(' ✓ Decrypted successfully using your secret key'));
|
|
71
|
+
} catch (error) {
|
|
72
|
+
decryptionFailed = true;
|
|
73
|
+
console.log(chalk.yellow(' ⚠️ Decryption failed with your current secret'));
|
|
74
|
+
}
|
|
75
|
+
} else {
|
|
76
|
+
decryptionFailed = true;
|
|
77
|
+
console.log(chalk.yellow(` ⚠️ No secret key found at ${getConfigDir()}`));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// If decryption failed, ask user for manual input
|
|
81
|
+
if (decryptionFailed) {
|
|
82
|
+
console.log(chalk.yellow('\n Your secret key may have been lost or changed.'));
|
|
83
|
+
console.log(chalk.dim(' You can manually provide the original keywords to restore.\n'));
|
|
84
|
+
|
|
85
|
+
const manualReplacements = [];
|
|
86
|
+
|
|
87
|
+
for (const r of rawMapping.replacements || []) {
|
|
88
|
+
const { original } = await inquirer.prompt([
|
|
89
|
+
{
|
|
90
|
+
type: 'input',
|
|
91
|
+
name: 'original',
|
|
92
|
+
message: `What was the original text for "${r.replacement}"?`,
|
|
93
|
+
prefix: '🔑'
|
|
94
|
+
}
|
|
95
|
+
]);
|
|
96
|
+
|
|
97
|
+
if (original.trim()) {
|
|
98
|
+
manualReplacements.push({
|
|
99
|
+
original: original.trim(),
|
|
100
|
+
replacement: r.replacement
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
mapping = {
|
|
106
|
+
...rawMapping,
|
|
107
|
+
replacements: manualReplacements,
|
|
108
|
+
source: { path: null },
|
|
109
|
+
destination: { path: null },
|
|
110
|
+
files: (rawMapping.files || []).map(f => ({
|
|
111
|
+
original: f.cloaked, // Use cloaked as original if can't decrypt
|
|
112
|
+
cloaked: f.cloaked
|
|
113
|
+
}))
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Display info
|
|
119
|
+
const sourcePath = getOriginalSource(mapping);
|
|
120
|
+
console.log(chalk.dim(`\n Original source: ${sourcePath || 'Unknown (encrypted)'}`));
|
|
121
|
+
console.log(chalk.dim(` Extracted on: ${mapping.timestamp}`));
|
|
122
|
+
console.log(chalk.dim(` Replacements: ${mapping.replacements?.length || 0}`));
|
|
123
|
+
console.log(chalk.dim(` Files: ${mapping.files?.length || 0}\n`));
|
|
124
|
+
|
|
125
|
+
// Show replacements that will be reversed
|
|
126
|
+
if (mapping.replacements && mapping.replacements.length > 0) {
|
|
127
|
+
console.log(chalk.cyan(' Replacements to reverse:'));
|
|
128
|
+
mapping.replacements.forEach(r => {
|
|
129
|
+
if (r.original) {
|
|
130
|
+
console.log(chalk.dim(` "${r.replacement}" → "${r.original}"`));
|
|
131
|
+
} else {
|
|
132
|
+
console.log(chalk.yellow(` "${r.replacement}" → [ENCRYPTED]`));
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
console.log('');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Step 5: Get destination directory
|
|
139
|
+
let destDir;
|
|
140
|
+
|
|
141
|
+
if (options.dest) {
|
|
142
|
+
destDir = resolve(options.dest);
|
|
143
|
+
} else if (sourcePath && existsSync(sourcePath)) {
|
|
144
|
+
if (options.force) {
|
|
145
|
+
// If force is used and source path exists, use it automatically
|
|
146
|
+
destDir = sourcePath;
|
|
147
|
+
} else {
|
|
148
|
+
const useOriginal = await confirmAction(
|
|
149
|
+
`Restore to original location? (${sourcePath})`
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
if (useOriginal) {
|
|
153
|
+
destDir = sourcePath;
|
|
154
|
+
} else {
|
|
155
|
+
destDir = await promptDestinationDirectory();
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
} else {
|
|
159
|
+
if (sourcePath) {
|
|
160
|
+
console.log(chalk.yellow(` Original path no longer exists: ${sourcePath}`));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (options.force) {
|
|
164
|
+
showError('Original source path not found and no destination provided. Cannot force push.');
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
destDir = await promptDestinationDirectory();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Step 6: Confirm
|
|
171
|
+
if (!options.force) {
|
|
172
|
+
const confirmed = await confirmAction(
|
|
173
|
+
`Restore ${mapping.files?.length || 0} files to ${destDir}?`
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
if (!confirmed) {
|
|
177
|
+
showInfo('Operation cancelled.');
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
} else {
|
|
181
|
+
// Log what we are doing in force mode
|
|
182
|
+
console.log(chalk.cyan(`\n Force restoring ${mapping.files?.length || 0} files to ${destDir}...`));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Step 7: Get all files from cloaked directory
|
|
186
|
+
const files = getAllFiles(cloakedDir);
|
|
187
|
+
|
|
188
|
+
if (files.length === 0) {
|
|
189
|
+
showWarning('No files found in the cloaked directory.');
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Step 8: Create destination if needed
|
|
194
|
+
if (!existsSync(destDir)) {
|
|
195
|
+
mkdirSync(destDir, { recursive: true });
|
|
196
|
+
console.log(chalk.dim(` Created directory: ${destDir}`));
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Step 9: Copy and de-anonymize files
|
|
200
|
+
const restoreSpinner = ora('Restoring files...').start();
|
|
201
|
+
|
|
202
|
+
// Filter out any replacements where decryption failed
|
|
203
|
+
const validReplacements = (mapping.replacements || []).filter(r => r.original);
|
|
204
|
+
const deanonymizer = createDeanonymizer(validReplacements);
|
|
205
|
+
|
|
206
|
+
// Also pass reversed replacements for path restoration
|
|
207
|
+
const reversedReplacements = validReplacements.map(r => ({
|
|
208
|
+
original: r.replacement,
|
|
209
|
+
replacement: r.original
|
|
210
|
+
}));
|
|
211
|
+
|
|
212
|
+
const results = await copyFiles(
|
|
213
|
+
files,
|
|
214
|
+
cloakedDir,
|
|
215
|
+
destDir,
|
|
216
|
+
deanonymizer,
|
|
217
|
+
(current, total, file) => {
|
|
218
|
+
restoreSpinner.text = `Restoring files... ${current}/${total} - ${file}`;
|
|
219
|
+
},
|
|
220
|
+
reversedReplacements // Pass for path de-anonymization
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
restoreSpinner.succeed(`Restored ${results.copied} files`);
|
|
224
|
+
|
|
225
|
+
if (results.pathsRenamed > 0) {
|
|
226
|
+
console.log(chalk.cyan(` 📁 ${results.pathsRenamed} paths restored`));
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (results.transformed > 0) {
|
|
230
|
+
console.log(chalk.cyan(` 📝 ${results.transformed} files had content restored`));
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (results.errors.length > 0) {
|
|
234
|
+
console.log(chalk.yellow(` ⚠️ ${results.errors.length} files had errors`));
|
|
235
|
+
results.errors.forEach(e => {
|
|
236
|
+
console.log(chalk.dim(` - ${e.file}: ${e.error}`));
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Done!
|
|
241
|
+
showSuccess('Restoration complete!');
|
|
242
|
+
console.log(chalk.white(` 📂 Files restored to: ${chalk.cyan.bold(destDir)}\n`));
|
|
243
|
+
|
|
244
|
+
} catch (error) {
|
|
245
|
+
showError(`Push failed: ${error.message}`);
|
|
246
|
+
if (process.env.DEBUG) {
|
|
247
|
+
console.error(error);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|