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.
@@ -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
- const useOriginal = await confirmAction(
145
- `Restore to original location? (${sourcePath})`
146
- );
147
-
148
- if (useOriginal) {
149
- destDir = sourcePath;
150
- } else {
151
- destDir = await promptDestinationDirectory();
152
- }
153
- } else {
154
- if (sourcePath) {
155
- console.log(chalk.yellow(` Original path no longer exists: ${sourcePath}`));
156
- }
157
- destDir = await promptDestinationDirectory();
158
- }
159
-
160
- // Step 6: Confirm
161
- const confirmed = await confirmAction(
162
- `Restore ${mapping.files?.length || 0} files to ${destDir}?`
163
- );
164
-
165
- if (!confirmed) {
166
- showInfo('Operation cancelled.');
167
- return;
168
- }
169
-
170
- // Step 7: Get all files from cloaked directory
171
- const files = getAllFiles(cloakedDir);
172
-
173
- if (files.length === 0) {
174
- showWarning('No files found in the cloaked directory.');
175
- return;
176
- }
177
-
178
- // Step 8: Create destination if needed
179
- if (!existsSync(destDir)) {
180
- mkdirSync(destDir, { recursive: true });
181
- console.log(chalk.dim(` Created directory: ${destDir}`));
182
- }
183
-
184
- // Step 9: Copy and de-anonymize files
185
- const restoreSpinner = ora('Restoring files...').start();
186
-
187
- // Filter out any replacements where decryption failed
188
- const validReplacements = (mapping.replacements || []).filter(r => r.original);
189
- const deanonymizer = createDeanonymizer(validReplacements);
190
-
191
- // Also pass reversed replacements for path restoration
192
- const reversedReplacements = validReplacements.map(r => ({
193
- original: r.replacement,
194
- replacement: r.original
195
- }));
196
-
197
- const results = await copyFiles(
198
- files,
199
- cloakedDir,
200
- destDir,
201
- deanonymizer,
202
- (current, total, file) => {
203
- restoreSpinner.text = `Restoring files... ${current}/${total} - ${file}`;
204
- },
205
- reversedReplacements // Pass for path de-anonymization
206
- );
207
-
208
- restoreSpinner.succeed(`Restored ${results.copied} files`);
209
-
210
- if (results.pathsRenamed > 0) {
211
- console.log(chalk.cyan(` 📁 ${results.pathsRenamed} paths restored`));
212
- }
213
-
214
- if (results.transformed > 0) {
215
- console.log(chalk.cyan(` 📝 ${results.transformed} files had content restored`));
216
- }
217
-
218
- if (results.errors.length > 0) {
219
- console.log(chalk.yellow(` ⚠️ ${results.errors.length} files had errors`));
220
- results.errors.forEach(e => {
221
- console.log(chalk.dim(` - ${e.file}: ${e.error}`));
222
- });
223
- }
224
-
225
- // Done!
226
- showSuccess('Restoration complete!');
227
- console.log(chalk.white(` 📂 Files restored to: ${chalk.cyan.bold(destDir)}\n`));
228
-
229
- } catch (error) {
230
- showError(`Push failed: ${error.message}`);
231
- if (process.env.DEBUG) {
232
- console.error(error);
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
+ }