repo-cloak-cli 1.0.2 → 1.2.0

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/medium.md ADDED
@@ -0,0 +1,319 @@
1
+ # Repo-Cloak: A CLI Tool for Safely Using AI Coding Assistants on Proprietary Codebases
2
+
3
+ The rise of AI-powered coding assistants like GitHub Copilot, Cursor, and Claude has fundamentally changed how developers write software. These tools can dramatically accelerate development, debug complex issues, and even architect entire systems. But for engineers working on proprietary or enterprise codebases, there is a persistent concern: *How do you leverage these powerful AI tools without exposing sensitive business logic, customer data, or confidential intellectual property?*
4
+
5
+ This article introduces **Repo-Cloak**, a command-line tool I built to address this exact challenge. It provides a secure, selective approach to sharing code with AI assistants while maintaining complete control over what information leaves your environment.
6
+
7
+ ---
8
+
9
+ ## The Problem: AI Assistance vs Enterprise Security
10
+
11
+ Modern AI coding assistants require context to be effective. The more code they can see, the better their suggestions. But enterprise environments present unique challenges:
12
+
13
+ - **Proprietary Business Logic**: Core algorithms, pricing engines, and competitive differentiators must remain confidential
14
+ - **Customer Data References**: Database schemas, API endpoints, and configuration files often contain sensitive identifiers
15
+ - **Company-Specific Naming**: Project names, internal tools, and organizational structures reveal information about your infrastructure
16
+ - **Compliance Requirements**: Healthcare, finance, and government sectors have strict regulations about data handling
17
+
18
+ The traditional approach forces developers to choose between two suboptimal paths: either manually copy-paste sanitized code snippets (losing context and wasting time), or share entire repositories with AI tools (accepting security risks). Repo-Cloak offers a third path.
19
+
20
+ ---
21
+
22
+ ## The Solution: Selective Extraction with Intelligent Anonymization
23
+
24
+ Repo-Cloak operates on a simple but powerful principle: **extract only what you need, anonymize what you must protect, and maintain a reversible mapping for seamless integration**.
25
+
26
+ ```
27
+ ┌─────────────────────────────────────────────────────────────────────┐
28
+ │ ORIGINAL REPOSITORY │
29
+ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
30
+ │ │ XCorpAPI/ │ │ Payments/ │ │ Analytics/ │ │
31
+ │ │ auth.cs │ │ stripe.cs │ │ metrics.cs │ │
32
+ │ │ users.cs │ │ billing.cs │ │ reports.cs │ │
33
+ │ └──────────────┘ └──────────────┘ └──────────────┘ │
34
+ └─────────────────────────────────────────────────────────────────────┘
35
+
36
+ │ PULL (selective + anonymize)
37
+
38
+ ┌─────────────────────────────────────────────────────────────────────┐
39
+ │ CLOAKED WORKSPACE │
40
+ │ ┌──────────────┐ │
41
+ │ │ AcmeAPI/ │ ← Folder names anonymized │
42
+ │ │ auth.cs │ ← Content: "XCorp" → "Acme" │
43
+ │ │ users.cs │ ← Safe to share with AI assistants │
44
+ │ └──────────────┘ │
45
+ │ │
46
+ │ .repo-cloak-map.json ← Encrypted mapping for restoration │
47
+ └─────────────────────────────────────────────────────────────────────┘
48
+
49
+ │ AI AGENT MAKES MODIFICATIONS
50
+
51
+ ┌─────────────────────────────────────────────────────────────────────┐
52
+ │ PUSH (restore + de-anonymize) │
53
+ │ │
54
+ │ Modified files pushed back to original repository │
55
+ │ All anonymization reversed automatically │
56
+ │ "Acme" → "XCorp" in both content and file paths │
57
+ └─────────────────────────────────────────────────────────────────────┘
58
+ ```
59
+
60
+ ---
61
+
62
+ ## How It Works: A Technical Deep Dive
63
+
64
+ ### 1. Interactive File Selection
65
+
66
+ Rather than extracting entire directories, Repo-Cloak provides an interactive file selector with hierarchical navigation. Developers can search, filter, and selectively choose exactly which files to extract.
67
+
68
+ ```
69
+ ? Select files to extract:
70
+ ◉ 📁 src/Services
71
+ ◉ 📄 BookService.cs
72
+ ◯ 📄 PaymentService.cs ← Excluded: contains billing logic
73
+ ◉ 📄 UserService.cs
74
+ ◯ 📁 src/Infrastructure ← Entire folder excluded
75
+ ```
76
+
77
+ The selector supports pagination for large codebases, folder-level selection (selecting a folder automatically includes all children), and real-time filtering.
78
+
79
+ ### 2. Intelligent Content Anonymization
80
+
81
+ The anonymization engine performs case-preserving replacements across all extracted content. This means your code remains syntactically valid and readable:
82
+
83
+ ```csharp
84
+ // Original
85
+ namespace XCorp.Services {
86
+ public class XCorpBookManager : IXCorpService {
87
+ private const string XCORP_API_KEY = "...";
88
+ }
89
+ }
90
+
91
+ // Anonymized (case preservation maintained)
92
+ namespace Acme.Services {
93
+ public class AcmeBookManager : IAcmeService {
94
+ private const string ACME_API_KEY = "...";
95
+ }
96
+ }
97
+ ```
98
+
99
+ The engine handles:
100
+ - **PascalCase**: `XCorpService` becomes `AcmeService`
101
+ - **camelCase**: `xcorpClient` becomes `acmeClient`
102
+ - **SCREAMING_CASE**: `XCORP_KEY` becomes `ACME_KEY`
103
+ - **lowercase**: `xcorp` becomes `acme`
104
+
105
+ ### 3. Path Anonymization
106
+
107
+ Beyond content, Repo-Cloak also anonymizes folder and file names. This prevents directory structures from revealing organizational patterns:
108
+
109
+ ```
110
+ Original: src/XCorpFrontEnd/XCorpComponents/XCorpButton.tsx
111
+ Cloaked: src/AcmeFrontEnd/AcmeComponents/AcmeButton.tsx
112
+ ```
113
+
114
+ ### 4. Encrypted Mapping with User-Specific Secrets
115
+
116
+ Here is where security becomes critical. The mapping file that tracks original-to-anonymized paths must itself be protected. Repo-Cloak uses AES-256-GCM encryption with a user-specific secret key:
117
+
118
+ ```
119
+ ┌──────────────────────────────────────────────────────────────┐
120
+ │ ENCRYPTION ARCHITECTURE │
121
+ ├──────────────────────────────────────────────────────────────┤
122
+ │ │
123
+ │ ~/.repo-cloak/secret.key │
124
+ │ ├── Generated automatically on first use │
125
+ │ ├── 256-bit cryptographically random key │
126
+ │ ├── Stored with 0600 permissions (owner-only) │
127
+ │ └── Unique per user/machine │
128
+ │ │
129
+ │ .repo-cloak-map.json │
130
+ │ ├── Source paths: ENCRYPTED │
131
+ │ ├── Original keywords: ENCRYPTED │
132
+ │ ├── Replacement keywords: VISIBLE (safe values) │
133
+ │ └── Cloaked file paths: VISIBLE (already anonymized) │
134
+ │ │
135
+ └──────────────────────────────────────────────────────────────┘
136
+ ```
137
+
138
+ The mapping file can be safely committed or shared because:
139
+ - The encrypted fields cannot be decrypted without the user's secret key
140
+ - The visible fields contain only anonymized, non-sensitive values
141
+ - Even if an attacker obtains the mapping, they cannot determine original values
142
+
143
+ If a user loses their secret key (machine reinstall, key deletion), the tool prompts for manual keyword entry during restoration. Since developers know their own codebase, they can provide the original terms when needed.
144
+
145
+ ### 5. Incremental Extraction Support
146
+
147
+ Real-world usage rarely involves a single extraction. As AI assistants request additional context, developers need to pull more files. Repo-Cloak handles this with intelligent merging:
148
+
149
+ ```
150
+ Pull #1: BookService.cs → Mapping tracks 1 file
151
+ Pull #2: UserService.cs → Mapping tracks 2 files (merged)
152
+ Pull #3: Same files again → Deduplication, no duplicates added
153
+ ```
154
+
155
+ Each pull is tracked in a history log:
156
+ ```json
157
+ {
158
+ "pullHistory": [
159
+ { "timestamp": "2024-01-15T10:30:00Z", "filesAdded": 5, "totalFiles": 5 },
160
+ { "timestamp": "2024-01-15T14:22:00Z", "filesAdded": 3, "totalFiles": 8 }
161
+ ]
162
+ }
163
+ ```
164
+
165
+ ---
166
+
167
+ ## The Workflow in Practice
168
+
169
+ ### Step 1: Pull Files from Your Repository
170
+
171
+ ```bash
172
+ repo-cloak pull
173
+ ```
174
+
175
+ The interactive interface guides you through:
176
+ 1. Selecting a source directory (your project)
177
+ 2. Choosing specific files via the tree selector
178
+ 3. Defining keyword replacements (e.g., "XCorp" to "Acme")
179
+ 4. Specifying an output directory
180
+
181
+ ### Step 2: Work with AI Assistants
182
+
183
+ Open the cloaked workspace in your preferred AI-enabled IDE. The anonymized code is syntactically valid and maintains all structural relationships. AI assistants can:
184
+ - Analyze patterns and suggest improvements
185
+ - Debug issues with full context
186
+ - Generate new code that follows your conventions
187
+ - Refactor existing implementations
188
+
189
+ ### Step 3: Push Changes Back
190
+
191
+ ```bash
192
+ repo-cloak push
193
+ ```
194
+
195
+ The tool automatically:
196
+ 1. Locates the mapping file in the cloaked directory
197
+ 2. Decrypts the mapping using your secret key
198
+ 3. Reverses all anonymization in both content and file paths
199
+ 4. Copies modified files back to the original repository
200
+
201
+ ---
202
+
203
+ ## Security Considerations
204
+
205
+ ### What Gets Protected
206
+
207
+ | Element | Protection Method |
208
+ |---------|------------------|
209
+ | File content keywords | Case-preserving replacement |
210
+ | Folder names | Path anonymization |
211
+ | File names | Path anonymization |
212
+ | Original keywords in mapping | AES-256-GCM encryption |
213
+ | Source directory path | AES-256-GCM encryption |
214
+ | Original file paths | AES-256-GCM encryption |
215
+
216
+ ### What Remains Visible
217
+
218
+ | Element | Reason |
219
+ |---------|--------|
220
+ | Replacement keywords | Already anonymized, safe to expose |
221
+ | Cloaked file paths | Already anonymized, safe to expose |
222
+ | Code structure and logic | Required for AI assistance |
223
+
224
+ ### Threat Model
225
+
226
+ Repo-Cloak protects against:
227
+ - **Accidental exposure**: AI tools cannot see original company names, project identifiers, or sensitive naming
228
+ - **Mapping file leakage**: Even if the mapping file is exposed, encryption prevents recovery of original values
229
+ - **Third-party logging**: Cloud-based AI services only receive anonymized content
230
+
231
+ Repo-Cloak does not protect against:
232
+ - **Logic inference**: Sufficiently advanced analysis might infer business purpose from code structure
233
+ - **Unique patterns**: Highly distinctive algorithms may be recognizable regardless of naming
234
+ - **Malicious insiders**: Users with the secret key have full access
235
+
236
+ ### User Responsibility
237
+
238
+ Repo-Cloak is a tool that assists with anonymization, but it does not replace sound judgment. **The responsibility for selecting appropriate files lies entirely with the user.**
239
+
240
+ Before extracting any code, you should:
241
+
242
+ 1. **Review your organization's policies**: Most companies have guidelines about sharing code with external tools or AI services. Ensure you understand and comply with these policies before using Repo-Cloak or any similar tool.
243
+
244
+ 2. **Avoid proprietary algorithms**: Even with anonymized naming, core business logic, patented algorithms, or trade secrets should not be extracted. If an algorithm is proprietary, changing variable names does not make it safe to share.
245
+
246
+ 3. **Verify file contents before extraction**: The selective file picker exists precisely so you can make informed decisions. Do not blindly select entire directories without understanding what they contain.
247
+
248
+ 4. **Cross-check with your team**: When in doubt, consult with your security team, legal department, or engineering leadership. A quick conversation can prevent significant issues.
249
+
250
+ 5. **Use the minimum necessary context**: Extract only what the AI assistant needs to help you. More files means more exposure, even if that exposure is anonymized.
251
+
252
+ This tool provides a layer of protection, but no tool can substitute for thoughtful decision-making about what code should or should not leave your environment.
253
+
254
+ ---
255
+
256
+ ## Installation and Usage
257
+
258
+ ```bash
259
+ # Install globally via npm
260
+ npm install -g repo-cloak-cli
261
+
262
+ # Run the interactive interface
263
+ repo-cloak
264
+
265
+ # Or use specific commands
266
+ repo-cloak pull --source ./my-project --dest ./cloaked-output
267
+ repo-cloak push --source ./cloaked-output --dest ./my-project
268
+ ```
269
+
270
+ ---
271
+
272
+ ## Technical Architecture
273
+
274
+ ```
275
+ repo-cloak/
276
+ ├── bin/
277
+ │ └── repo-cloak.js # CLI entry point
278
+ ├── src/
279
+ │ ├── commands/
280
+ │ │ ├── pull.js # Extraction and anonymization
281
+ │ │ └── push.js # Restoration and de-anonymization
282
+ │ ├── core/
283
+ │ │ ├── anonymizer.js # Case-preserving replacement engine
284
+ │ │ ├── copier.js # File operations with transformation
285
+ │ │ ├── crypto.js # AES-256-GCM encryption
286
+ │ │ ├── mapper.js # Mapping file management
287
+ │ │ └── scanner.js # File discovery and filtering
288
+ │ └── ui/
289
+ │ ├── fileSelector.js # Interactive tree selector
290
+ │ └── prompts.js # User input handling
291
+ └── tests/
292
+ ├── anonymizer.test.js # 13 test cases
293
+ ├── copier.test.js # 5 test cases
294
+ ├── crypto.test.js # 9 test cases
295
+ ├── mapper.test.js # 10 test cases
296
+ └── scanner.test.js # 8 test cases
297
+ ```
298
+
299
+ The project includes 45 unit tests covering all core functionality, ensuring reliability for production use.
300
+
301
+ ---
302
+
303
+ ## Conclusion
304
+
305
+ The tension between leveraging AI tools and maintaining code security is real, but it does not have to be a binary choice. Repo-Cloak provides a practical middle ground: keep your proprietary information private while still benefiting from the productivity gains of modern AI coding assistants.
306
+
307
+ By implementing selective extraction, intelligent anonymization, and encrypted reversible mappings, developers can confidently use tools like Cursor, GitHub Copilot, or any AI coding assistant without exposing sensitive business logic or company-specific information.
308
+
309
+ The tool is open-source and available on npm. Contributions, feedback, and feature requests are welcome.
310
+
311
+ ---
312
+
313
+ **Repository**: [github.com/iamshz97/repo-cloak](https://github.com/iamshz97/repo-cloak)
314
+
315
+ **npm**: `npm install -g repo-cloak-cli`
316
+
317
+ ---
318
+
319
+ *Shazni Shiraz is a software engineer focused on developer tooling and enterprise software architecture.*
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "repo-cloak-cli",
3
- "version": "1.0.2",
3
+ "version": "1.2.0",
4
4
  "description": "🎭 Selectively extract and anonymize files from repositories. Perfect for sharing code with AI agents without exposing proprietary details.",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -1,10 +1,12 @@
1
1
  /**
2
2
  * Pull Command
3
3
  * Extract files and anonymize sensitive information
4
+ * Supports quick-add mode when pulling to existing cloaked directory
4
5
  */
5
6
 
6
7
  import ora from 'ora';
7
8
  import chalk from 'chalk';
9
+ import inquirer from 'inquirer';
8
10
  import { existsSync, mkdirSync } from 'fs';
9
11
  import { resolve, relative } from 'path';
10
12
 
@@ -13,29 +15,117 @@ import {
13
15
  promptSourceDirectory,
14
16
  promptDestinationDirectory,
15
17
  promptKeywordReplacements,
16
- showSummaryAndConfirm
18
+ showSummaryAndConfirm,
19
+ confirmAction
17
20
  } from '../ui/prompts.js';
18
21
  import { showSuccess, showError, showInfo } from '../ui/banner.js';
19
22
  import { getAllFiles } from '../core/scanner.js';
20
23
  import { copyFiles } from '../core/copier.js';
21
24
  import { createAnonymizer } from '../core/anonymizer.js';
22
- import { createMapping, saveMapping, loadRawMapping, mergeMapping, hasMapping } from '../core/mapper.js';
25
+ import { createMapping, saveMapping, loadRawMapping, mergeMapping, hasMapping, decryptMapping } from '../core/mapper.js';
26
+ import { getOrCreateSecret, hasSecret, decryptReplacements } from '../core/crypto.js';
23
27
 
24
28
  export async function pull(options = {}) {
25
29
  try {
26
- // Step 1: Get source directory
27
- const sourceDir = options.source
28
- ? resolve(options.source)
29
- : await promptSourceDirectory();
30
+ // Step 1: Get destination directory first (to check for existing mapping)
31
+ let destDir = options.dest
32
+ ? resolve(options.dest)
33
+ : await promptDestinationDirectory();
34
+
35
+ let existingMapping = null;
36
+ let existingReplacements = [];
37
+ let sourceDir = null;
38
+ let isQuickAdd = false;
39
+
40
+ // Step 2: Check for existing mapping in destination
41
+ if (existsSync(destDir) && hasMapping(destDir)) {
42
+ existingMapping = loadRawMapping(destDir);
43
+
44
+ console.log(chalk.cyan('\n Existing cloaked directory detected'));
45
+ console.log(chalk.dim(` Created: ${existingMapping.timestamp}`));
46
+ console.log(chalk.dim(` Files: ${existingMapping.stats?.totalFiles || existingMapping.files?.length || 0}`));
47
+ console.log(chalk.dim(` Replacements: ${existingMapping.replacements?.length || 0}\n`));
48
+
49
+ // Ask if they want quick-add mode
50
+ const { mode } = await inquirer.prompt([
51
+ {
52
+ type: 'list',
53
+ name: 'mode',
54
+ message: 'What would you like to do?',
55
+ choices: [
56
+ {
57
+ name: 'Quick Add - Use existing replacements and add more files',
58
+ value: 'quick'
59
+ },
60
+ {
61
+ name: 'Add More Replacements - Add files with additional anonymization',
62
+ value: 'extend'
63
+ },
64
+ {
65
+ name: 'Fresh Start - Choose new destination',
66
+ value: 'fresh'
67
+ }
68
+ ]
69
+ }
70
+ ]);
71
+
72
+ if (mode === 'fresh') {
73
+ // Get a new destination
74
+ destDir = await promptDestinationDirectory();
75
+ existingMapping = null;
76
+ } else {
77
+ isQuickAdd = mode === 'quick';
78
+
79
+ // Decrypt existing replacements
80
+ if (existingMapping.encrypted && hasSecret()) {
81
+ const secret = getOrCreateSecret();
82
+ try {
83
+ const decrypted = decryptReplacements(existingMapping.replacements || [], secret);
84
+ existingReplacements = decrypted.filter(r => !r.decryptFailed);
85
+
86
+ if (existingReplacements.length > 0) {
87
+ console.log(chalk.green(' Existing replacements loaded:\n'));
88
+ existingReplacements.forEach(r => {
89
+ console.log(chalk.dim(` "${r.original}" → "${r.replacement}"`));
90
+ });
91
+ console.log('');
92
+ }
93
+ } catch (err) {
94
+ console.log(chalk.yellow(' Could not decrypt existing replacements'));
95
+ }
96
+ }
97
+
98
+ // Try to get original source path
99
+ if (existingMapping.encrypted && hasSecret()) {
100
+ const secret = getOrCreateSecret();
101
+ try {
102
+ const decrypted = decryptMapping(existingMapping, secret);
103
+ if (decrypted.source?.path && existsSync(decrypted.source.path)) {
104
+ sourceDir = decrypted.source.path;
105
+ console.log(chalk.dim(` Source: ${sourceDir}\n`));
106
+ }
107
+ } catch (err) {
108
+ // Source path couldn't be decrypted, will prompt
109
+ }
110
+ }
111
+ }
112
+ }
113
+
114
+ // Step 3: Get source directory if not already determined
115
+ if (!sourceDir) {
116
+ sourceDir = options.source
117
+ ? resolve(options.source)
118
+ : await promptSourceDirectory();
119
+ }
30
120
 
31
121
  if (!existsSync(sourceDir)) {
32
122
  showError(`Source directory does not exist: ${sourceDir}`);
33
123
  return;
34
124
  }
35
125
 
36
- console.log(chalk.dim(`\n Source: ${sourceDir}\n`));
126
+ console.log(chalk.dim(` Source: ${sourceDir}\n`));
37
127
 
38
- // Step 2: Select files
128
+ // Step 4: Select files
39
129
  const selectedFiles = await selectFiles(sourceDir);
40
130
 
41
131
  if (selectedFiles.length === 0) {
@@ -45,15 +135,40 @@ export async function pull(options = {}) {
45
135
 
46
136
  console.log(chalk.green(`\n✓ Selected ${selectedFiles.length} files\n`));
47
137
 
48
- // Step 3: Get destination directory
49
- const destDir = options.dest
50
- ? resolve(options.dest)
51
- : await promptDestinationDirectory();
138
+ // Step 5: Handle replacements based on mode
139
+ let replacements = [...existingReplacements];
140
+
141
+ if (isQuickAdd) {
142
+ // Quick add mode - just use existing replacements
143
+ if (replacements.length > 0) {
144
+ console.log(chalk.cyan(' Using existing replacements (quick-add mode)\n'));
145
+ }
146
+
147
+ // Ask if they want to add more
148
+ const { addMore } = await inquirer.prompt([
149
+ {
150
+ type: 'confirm',
151
+ name: 'addMore',
152
+ message: 'Add additional replacements?',
153
+ default: false
154
+ }
155
+ ]);
52
156
 
53
- // Step 4: Get keyword replacements
54
- const replacements = await promptKeywordReplacements();
157
+ if (addMore) {
158
+ const additionalReplacements = await promptKeywordReplacements();
159
+ replacements = [...replacements, ...additionalReplacements];
160
+ }
161
+ } else if (existingMapping) {
162
+ // Extend mode - prompt for more replacements to add to existing
163
+ console.log(chalk.cyan('\n Add more replacements (existing will be preserved):\n'));
164
+ const additionalReplacements = await promptKeywordReplacements();
165
+ replacements = [...replacements, ...additionalReplacements];
166
+ } else {
167
+ // Fresh start - prompt for all replacements
168
+ replacements = await promptKeywordReplacements();
169
+ }
55
170
 
56
- // Step 5: Confirm
171
+ // Step 6: Confirm
57
172
  const confirmed = await showSummaryAndConfirm(
58
173
  selectedFiles.length,
59
174
  destDir,
@@ -65,13 +180,13 @@ export async function pull(options = {}) {
65
180
  return;
66
181
  }
67
182
 
68
- // Step 6: Create destination directory
183
+ // Step 7: Create destination directory
69
184
  if (!existsSync(destDir)) {
70
185
  mkdirSync(destDir, { recursive: true });
71
186
  console.log(chalk.dim(` Created directory: ${destDir}`));
72
187
  }
73
188
 
74
- // Step 7: Copy and anonymize files
189
+ // Step 8: Copy and anonymize files
75
190
  const spinner = ora('Copying and anonymizing files...').start();
76
191
 
77
192
  const anonymizer = createAnonymizer(replacements);
@@ -106,7 +221,7 @@ export async function pull(options = {}) {
106
221
  });
107
222
  }
108
223
 
109
- // Step 8: Prepare new file mappings
224
+ // Step 9: Prepare new file mappings
110
225
  const newFiles = selectedFiles.map(f => {
111
226
  const originalPath = relative(sourceDir, f);
112
227
  let anonymizedPath = originalPath;
@@ -120,8 +235,7 @@ export async function pull(options = {}) {
120
235
  };
121
236
  });
122
237
 
123
- // Step 9: Check for existing mapping and merge if found
124
- const existingMapping = loadRawMapping(destDir);
238
+ // Step 10: Check for existing mapping and merge if found
125
239
  let mapping;
126
240
  let isIncremental = false;
127
241
 
@@ -129,7 +243,7 @@ export async function pull(options = {}) {
129
243
  // Merge with existing
130
244
  mapping = mergeMapping(existingMapping, newFiles);
131
245
  isIncremental = true;
132
- console.log(chalk.cyan(` 🔄 Merged with existing mapping (incremental pull)`));
246
+ console.log(chalk.cyan(` 🔄 Merged with existing mapping`));
133
247
  } else {
134
248
  // Create new mapping
135
249
  mapping = createMapping({
@@ -153,7 +267,12 @@ export async function pull(options = {}) {
153
267
  // Done!
154
268
  showSuccess('Extraction complete!');
155
269
  console.log(chalk.white(` 📂 Files extracted to: ${chalk.cyan.bold(destDir)}`));
156
- console.log(chalk.dim(`\n To restore later, run: ${chalk.white('repo-cloak push')}\n`));
270
+
271
+ if (isQuickAdd || isIncremental) {
272
+ console.log(chalk.dim(`\n Tip: Run again to add more files quickly\n`));
273
+ } else {
274
+ console.log(chalk.dim(`\n To restore later, run: ${chalk.white('repo-cloak push')}\n`));
275
+ }
157
276
 
158
277
  } catch (error) {
159
278
  showError(`Pull failed: ${error.message}`);