repo-cloak-cli 1.0.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/.github/workflows/publish.yml +36 -0
- package/.github/workflows/version-bump.yml +49 -0
- package/LICENSE +21 -0
- package/README.md +118 -0
- package/bin/repo-cloak.js +10 -0
- package/package.json +48 -0
- package/src/cli.js +84 -0
- package/src/commands/pull.js +128 -0
- package/src/commands/push.js +145 -0
- package/src/core/anonymizer.js +128 -0
- package/src/core/copier.js +96 -0
- package/src/core/mapper.js +120 -0
- package/src/core/scanner.js +137 -0
- package/src/index.js +8 -0
- package/src/ui/banner.js +70 -0
- package/src/ui/fileSelector.js +182 -0
- package/src/ui/prompts.js +165 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
name: Publish to npm
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
- master
|
|
8
|
+
paths-ignore:
|
|
9
|
+
- '*.md'
|
|
10
|
+
- '.gitignore'
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
publish:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
|
|
16
|
+
steps:
|
|
17
|
+
- name: ๐ญ Checkout repository
|
|
18
|
+
uses: actions/checkout@v4
|
|
19
|
+
|
|
20
|
+
- name: ๐ฆ Setup Node.js
|
|
21
|
+
uses: actions/setup-node@v4
|
|
22
|
+
with:
|
|
23
|
+
node-version: '20'
|
|
24
|
+
registry-url: 'https://registry.npmjs.org'
|
|
25
|
+
|
|
26
|
+
- name: ๐ฅ Install dependencies
|
|
27
|
+
run: npm ci
|
|
28
|
+
|
|
29
|
+
- name: ๐งช Run tests (if any)
|
|
30
|
+
run: npm test --if-present
|
|
31
|
+
continue-on-error: true
|
|
32
|
+
|
|
33
|
+
- name: ๐ Publish to npm
|
|
34
|
+
run: npm publish --access public
|
|
35
|
+
env:
|
|
36
|
+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
name: Version Bump & Publish
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_dispatch:
|
|
5
|
+
inputs:
|
|
6
|
+
version_type:
|
|
7
|
+
description: 'Version bump type'
|
|
8
|
+
required: true
|
|
9
|
+
default: 'patch'
|
|
10
|
+
type: choice
|
|
11
|
+
options:
|
|
12
|
+
- patch
|
|
13
|
+
- minor
|
|
14
|
+
- major
|
|
15
|
+
|
|
16
|
+
jobs:
|
|
17
|
+
version-and-publish:
|
|
18
|
+
runs-on: ubuntu-latest
|
|
19
|
+
|
|
20
|
+
steps:
|
|
21
|
+
- name: ๐ญ Checkout repository
|
|
22
|
+
uses: actions/checkout@v4
|
|
23
|
+
with:
|
|
24
|
+
token: ${{ secrets.GITHUB_TOKEN }}
|
|
25
|
+
|
|
26
|
+
- name: ๐ฆ Setup Node.js
|
|
27
|
+
uses: actions/setup-node@v4
|
|
28
|
+
with:
|
|
29
|
+
node-version: '20'
|
|
30
|
+
registry-url: 'https://registry.npmjs.org'
|
|
31
|
+
|
|
32
|
+
- name: ๐ง Configure Git
|
|
33
|
+
run: |
|
|
34
|
+
git config user.name "github-actions[bot]"
|
|
35
|
+
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
36
|
+
|
|
37
|
+
- name: ๐ฅ Install dependencies
|
|
38
|
+
run: npm ci
|
|
39
|
+
|
|
40
|
+
- name: ๐ Bump version
|
|
41
|
+
run: npm version ${{ inputs.version_type }} -m "chore: bump version to %s"
|
|
42
|
+
|
|
43
|
+
- name: ๐ค Push changes
|
|
44
|
+
run: git push --follow-tags
|
|
45
|
+
|
|
46
|
+
- name: ๐ Publish to npm
|
|
47
|
+
run: npm publish --access public
|
|
48
|
+
env:
|
|
49
|
+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# repo-cloak ๐ญ
|
|
2
|
+
|
|
3
|
+
> **Selectively extract and anonymize files from repositories**
|
|
4
|
+
|
|
5
|
+
Perfect for sharing code with AI agents without exposing proprietary details. Extract specific files, replace sensitive information (company names, project names, etc.), and restore them later.
|
|
6
|
+
|
|
7
|
+
[](https://www.npmjs.com/package/repo-cloak)
|
|
8
|
+
[](https://opensource.org/licenses/MIT)
|
|
9
|
+
|
|
10
|
+
## โจ Features
|
|
11
|
+
|
|
12
|
+
- ๐ **Interactive file browser** - Navigate and select files with a beautiful TUI
|
|
13
|
+
- ๐ญ **Smart anonymization** - Replace sensitive keywords while preserving case
|
|
14
|
+
- ๐ **Structure preservation** - Maintains original folder hierarchy
|
|
15
|
+
- ๐ **Push/Pull workflow** - Extract files, work on them, push back with original names
|
|
16
|
+
- ๐พ **Mapping file** - Tracks all replacements for seamless restoration
|
|
17
|
+
- ๐ **Cross-platform** - Works on Windows, macOS, and Linux
|
|
18
|
+
|
|
19
|
+
## ๐ฆ Installation
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install -g repo-cloak-cli
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Or use directly with npx:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npx repo-cloak-cli
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## ๐ Quick Start
|
|
32
|
+
|
|
33
|
+
### Pull (Extract & Anonymize)
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
# Interactive mode
|
|
37
|
+
repo-cloak pull
|
|
38
|
+
|
|
39
|
+
# With options
|
|
40
|
+
repo-cloak pull --source ./my-project --dest ./extracted
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
1. Select files/folders to extract
|
|
44
|
+
2. Enter destination path
|
|
45
|
+
3. Add keyword replacements (e.g., "Microsoft Corp" โ "ACME Inc")
|
|
46
|
+
4. Confirm and extract!
|
|
47
|
+
|
|
48
|
+
### Push (Restore)
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
# Interactive mode
|
|
52
|
+
repo-cloak push
|
|
53
|
+
|
|
54
|
+
# With options
|
|
55
|
+
repo-cloak push --source ./extracted --dest ./my-project
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Restores all files with original keywords replaced back.
|
|
59
|
+
|
|
60
|
+
## ๐ฏ Use Cases
|
|
61
|
+
|
|
62
|
+
- **AI Code Review** - Share proprietary code with AI tools by anonymizing company/project names
|
|
63
|
+
- **Open Source Templates** - Extract project templates while removing internal references
|
|
64
|
+
- **Code Samples** - Create sanitized examples from production code
|
|
65
|
+
- **Compliance** - Remove sensitive identifiers before sharing code externally
|
|
66
|
+
|
|
67
|
+
## ๐ Commands
|
|
68
|
+
|
|
69
|
+
| Command | Description |
|
|
70
|
+
|---------|-------------|
|
|
71
|
+
| `repo-cloak` | Interactive menu to choose pull or push |
|
|
72
|
+
| `repo-cloak pull` | Extract and anonymize files |
|
|
73
|
+
| `repo-cloak push` | Restore files with original names |
|
|
74
|
+
| `repo-cloak --help` | Show help |
|
|
75
|
+
| `repo-cloak --version` | Show version |
|
|
76
|
+
|
|
77
|
+
## ๐ง Options
|
|
78
|
+
|
|
79
|
+
### Pull Options
|
|
80
|
+
- `-s, --source <path>` - Source directory (default: current directory)
|
|
81
|
+
- `-d, --dest <path>` - Destination directory
|
|
82
|
+
- `-q, --quiet` - Minimal output
|
|
83
|
+
|
|
84
|
+
### Push Options
|
|
85
|
+
- `-s, --source <path>` - Cloaked backup directory
|
|
86
|
+
- `-d, --dest <path>` - Destination directory
|
|
87
|
+
- `-q, --quiet` - Minimal output
|
|
88
|
+
|
|
89
|
+
## ๐ How It Works
|
|
90
|
+
|
|
91
|
+
1. **Pull** creates a `.repo-cloak-map.json` file in the destination that stores:
|
|
92
|
+
- Original source path
|
|
93
|
+
- All keyword replacements
|
|
94
|
+
- File list with mappings
|
|
95
|
+
- Timestamp
|
|
96
|
+
|
|
97
|
+
2. **Push** reads this mapping file to:
|
|
98
|
+
- Reverse all keyword replacements
|
|
99
|
+
- Restore files to original or new location
|
|
100
|
+
|
|
101
|
+
## ๐ Privacy by Design
|
|
102
|
+
|
|
103
|
+
- No data is sent to any external servers
|
|
104
|
+
- All processing happens locally
|
|
105
|
+
- Binary files are copied without modification
|
|
106
|
+
- Hidden files and common ignored directories (node_modules, .git) are skipped
|
|
107
|
+
|
|
108
|
+
## ๐ค Contributing
|
|
109
|
+
|
|
110
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
111
|
+
|
|
112
|
+
## ๐ License
|
|
113
|
+
|
|
114
|
+
MIT ยฉ Shazni Shiraz
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
Made with โค๏ธ for developers who need to share code safely
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "repo-cloak-cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "๐ญ Selectively extract and anonymize files from repositories. Perfect for sharing code with AI agents without exposing proprietary details.",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"repo-cloak": "./bin/repo-cloak.js",
|
|
9
|
+
"cloak": "./bin/repo-cloak.js"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"start": "node bin/repo-cloak.js",
|
|
13
|
+
"test": "node --test",
|
|
14
|
+
"lint": "eslint src/"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"cli",
|
|
18
|
+
"anonymize",
|
|
19
|
+
"extract",
|
|
20
|
+
"repository",
|
|
21
|
+
"code",
|
|
22
|
+
"privacy",
|
|
23
|
+
"ai",
|
|
24
|
+
"llm",
|
|
25
|
+
"obfuscate",
|
|
26
|
+
"cloak",
|
|
27
|
+
"mask",
|
|
28
|
+
"sanitize"
|
|
29
|
+
],
|
|
30
|
+
"author": "",
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": ""
|
|
35
|
+
},
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=18.0.0"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"chalk": "^5.3.0",
|
|
41
|
+
"commander": "^12.1.0",
|
|
42
|
+
"figlet": "^1.7.0",
|
|
43
|
+
"glob": "^10.3.10",
|
|
44
|
+
"inquirer": "^9.2.12",
|
|
45
|
+
"ora": "^8.0.1"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {}
|
|
48
|
+
}
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI Command Router
|
|
3
|
+
* Handles command-line arguments and routes to appropriate handlers
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Command } from 'commander';
|
|
7
|
+
import { showBanner } from './ui/banner.js';
|
|
8
|
+
import { pull } from './commands/pull.js';
|
|
9
|
+
import { push } from './commands/push.js';
|
|
10
|
+
import { readFileSync } from 'fs';
|
|
11
|
+
import { fileURLToPath } from 'url';
|
|
12
|
+
import { dirname, join } from 'path';
|
|
13
|
+
|
|
14
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
15
|
+
const __dirname = dirname(__filename);
|
|
16
|
+
|
|
17
|
+
// Read version from package.json
|
|
18
|
+
const packageJson = JSON.parse(
|
|
19
|
+
readFileSync(join(__dirname, '..', 'package.json'), 'utf-8')
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
const program = new Command();
|
|
23
|
+
|
|
24
|
+
program
|
|
25
|
+
.name('repo-cloak')
|
|
26
|
+
.description('๐ญ Selectively extract and anonymize files from repositories')
|
|
27
|
+
.version(packageJson.version);
|
|
28
|
+
|
|
29
|
+
program
|
|
30
|
+
.command('pull')
|
|
31
|
+
.description('Extract files and anonymize sensitive information')
|
|
32
|
+
.option('-s, --source <path>', 'Source directory (default: current directory)')
|
|
33
|
+
.option('-d, --dest <path>', 'Destination directory')
|
|
34
|
+
.option('-q, --quiet', 'Minimal output')
|
|
35
|
+
.action(async (options) => {
|
|
36
|
+
await showBanner();
|
|
37
|
+
await pull(options);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
program
|
|
41
|
+
.command('push')
|
|
42
|
+
.description('Restore files with original names from a cloaked backup')
|
|
43
|
+
.option('-s, --source <path>', 'Source cloaked directory')
|
|
44
|
+
.option('-d, --dest <path>', 'Destination directory (original location)')
|
|
45
|
+
.option('-q, --quiet', 'Minimal output')
|
|
46
|
+
.action(async (options) => {
|
|
47
|
+
await showBanner();
|
|
48
|
+
await push(options);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Default command (no subcommand) - show interactive menu
|
|
52
|
+
program
|
|
53
|
+
.action(async () => {
|
|
54
|
+
await showBanner();
|
|
55
|
+
const { default: inquirer } = await import('inquirer');
|
|
56
|
+
|
|
57
|
+
const { action } = await inquirer.prompt([
|
|
58
|
+
{
|
|
59
|
+
type: 'list',
|
|
60
|
+
name: 'action',
|
|
61
|
+
message: 'What would you like to do?',
|
|
62
|
+
choices: [
|
|
63
|
+
{ name: '๐ค Pull - Extract & anonymize files', value: 'pull' },
|
|
64
|
+
{ name: '๐ฅ Push - Restore files with original names', value: 'push' },
|
|
65
|
+
{ name: 'โ Exit', value: 'exit' }
|
|
66
|
+
]
|
|
67
|
+
}
|
|
68
|
+
]);
|
|
69
|
+
|
|
70
|
+
if (action === 'pull') {
|
|
71
|
+
await pull({});
|
|
72
|
+
} else if (action === 'push') {
|
|
73
|
+
await push({});
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
export async function run() {
|
|
78
|
+
try {
|
|
79
|
+
await program.parseAsync(process.argv);
|
|
80
|
+
} catch (error) {
|
|
81
|
+
console.error('Error:', error.message);
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pull Command
|
|
3
|
+
* Extract files and anonymize sensitive information
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import ora from 'ora';
|
|
7
|
+
import chalk from 'chalk';
|
|
8
|
+
import { existsSync, mkdirSync } from 'fs';
|
|
9
|
+
import { resolve, relative } from 'path';
|
|
10
|
+
|
|
11
|
+
import { selectFiles } from '../ui/fileSelector.js';
|
|
12
|
+
import {
|
|
13
|
+
promptSourceDirectory,
|
|
14
|
+
promptDestinationDirectory,
|
|
15
|
+
promptKeywordReplacements,
|
|
16
|
+
showSummaryAndConfirm
|
|
17
|
+
} from '../ui/prompts.js';
|
|
18
|
+
import { showSuccess, showError, showInfo } from '../ui/banner.js';
|
|
19
|
+
import { getAllFiles } from '../core/scanner.js';
|
|
20
|
+
import { copyFiles } from '../core/copier.js';
|
|
21
|
+
import { createAnonymizer } from '../core/anonymizer.js';
|
|
22
|
+
import { createMapping, saveMapping } from '../core/mapper.js';
|
|
23
|
+
|
|
24
|
+
export async function pull(options = {}) {
|
|
25
|
+
try {
|
|
26
|
+
// Step 1: Get source directory
|
|
27
|
+
const sourceDir = options.source
|
|
28
|
+
? resolve(options.source)
|
|
29
|
+
: await promptSourceDirectory();
|
|
30
|
+
|
|
31
|
+
if (!existsSync(sourceDir)) {
|
|
32
|
+
showError(`Source directory does not exist: ${sourceDir}`);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
console.log(chalk.dim(`\n Source: ${sourceDir}\n`));
|
|
37
|
+
|
|
38
|
+
// Step 2: Select files
|
|
39
|
+
const selectedFiles = await selectFiles(sourceDir);
|
|
40
|
+
|
|
41
|
+
if (selectedFiles.length === 0) {
|
|
42
|
+
showError('No files selected. Aborting.');
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
console.log(chalk.green(`\nโ Selected ${selectedFiles.length} files\n`));
|
|
47
|
+
|
|
48
|
+
// Step 3: Get destination directory
|
|
49
|
+
const destDir = options.dest
|
|
50
|
+
? resolve(options.dest)
|
|
51
|
+
: await promptDestinationDirectory();
|
|
52
|
+
|
|
53
|
+
// Step 4: Get keyword replacements
|
|
54
|
+
const replacements = await promptKeywordReplacements();
|
|
55
|
+
|
|
56
|
+
// Step 5: Confirm
|
|
57
|
+
const confirmed = await showSummaryAndConfirm(
|
|
58
|
+
selectedFiles.length,
|
|
59
|
+
destDir,
|
|
60
|
+
replacements
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
if (!confirmed) {
|
|
64
|
+
showInfo('Operation cancelled.');
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Step 6: Create destination directory
|
|
69
|
+
if (!existsSync(destDir)) {
|
|
70
|
+
mkdirSync(destDir, { recursive: true });
|
|
71
|
+
console.log(chalk.dim(` Created directory: ${destDir}`));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Step 7: Copy and anonymize files
|
|
75
|
+
const spinner = ora('Copying and anonymizing files...').start();
|
|
76
|
+
|
|
77
|
+
const anonymizer = createAnonymizer(replacements);
|
|
78
|
+
let lastFile = '';
|
|
79
|
+
|
|
80
|
+
const results = await copyFiles(
|
|
81
|
+
selectedFiles,
|
|
82
|
+
sourceDir,
|
|
83
|
+
destDir,
|
|
84
|
+
anonymizer,
|
|
85
|
+
(current, total, file) => {
|
|
86
|
+
lastFile = file;
|
|
87
|
+
spinner.text = `Copying files... ${current}/${total} - ${file}`;
|
|
88
|
+
}
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
spinner.succeed(`Copied ${results.copied} files`);
|
|
92
|
+
|
|
93
|
+
if (results.transformed > 0) {
|
|
94
|
+
console.log(chalk.cyan(` ๐ ${results.transformed} files had content replaced`));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (results.errors.length > 0) {
|
|
98
|
+
console.log(chalk.yellow(` โ ๏ธ ${results.errors.length} files had errors`));
|
|
99
|
+
results.errors.forEach(e => {
|
|
100
|
+
console.log(chalk.dim(` - ${e.file}: ${e.error}`));
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Step 8: Save mapping file
|
|
105
|
+
const mapping = createMapping({
|
|
106
|
+
sourceDir,
|
|
107
|
+
destDir,
|
|
108
|
+
replacements,
|
|
109
|
+
files: selectedFiles.map(f => ({
|
|
110
|
+
relativePath: relative(sourceDir, f)
|
|
111
|
+
}))
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const mapPath = saveMapping(destDir, mapping);
|
|
115
|
+
console.log(chalk.dim(` ๐ Mapping saved: ${mapPath}`));
|
|
116
|
+
|
|
117
|
+
// Done!
|
|
118
|
+
showSuccess('Extraction complete!');
|
|
119
|
+
console.log(chalk.white(` ๐ Files extracted to: ${chalk.cyan.bold(destDir)}`));
|
|
120
|
+
console.log(chalk.dim(`\n To restore later, run: ${chalk.white('repo-cloak push')}\n`));
|
|
121
|
+
|
|
122
|
+
} catch (error) {
|
|
123
|
+
showError(`Pull failed: ${error.message}`);
|
|
124
|
+
if (process.env.DEBUG) {
|
|
125
|
+
console.error(error);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Push Command
|
|
3
|
+
* Restore files with original names from a cloaked backup
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import ora from 'ora';
|
|
7
|
+
import chalk from 'chalk';
|
|
8
|
+
import { existsSync, mkdirSync, readdirSync } from 'fs';
|
|
9
|
+
import { resolve, join, relative } from 'path';
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
promptBackupFolder,
|
|
13
|
+
promptDestinationDirectory,
|
|
14
|
+
confirmAction
|
|
15
|
+
} from '../ui/prompts.js';
|
|
16
|
+
import { showSuccess, showError, showInfo, showWarning } from '../ui/banner.js';
|
|
17
|
+
import { getAllFiles } from '../core/scanner.js';
|
|
18
|
+
import { copyFiles } from '../core/copier.js';
|
|
19
|
+
import { createDeanonymizer } from '../core/anonymizer.js';
|
|
20
|
+
import { loadMapping, hasMapping, getReplacements, getOriginalSource } from '../core/mapper.js';
|
|
21
|
+
|
|
22
|
+
export async function push(options = {}) {
|
|
23
|
+
try {
|
|
24
|
+
// Step 1: Get source (cloaked) directory
|
|
25
|
+
const cloakedDir = options.source
|
|
26
|
+
? resolve(options.source)
|
|
27
|
+
: await promptBackupFolder();
|
|
28
|
+
|
|
29
|
+
if (!existsSync(cloakedDir)) {
|
|
30
|
+
showError(`Directory does not exist: ${cloakedDir}`);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Step 2: Check for mapping file
|
|
35
|
+
if (!hasMapping(cloakedDir)) {
|
|
36
|
+
showError('No repo-cloak mapping file found in this directory.');
|
|
37
|
+
console.log(chalk.dim(' Make sure you selected a directory created by "repo-cloak pull"'));
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Step 3: Load mapping
|
|
42
|
+
const spinner = ora('Loading mapping file...').start();
|
|
43
|
+
const mapping = loadMapping(cloakedDir);
|
|
44
|
+
spinner.succeed('Mapping file loaded');
|
|
45
|
+
|
|
46
|
+
console.log(chalk.dim(`\n Original source: ${mapping.source?.path || 'Unknown'}`));
|
|
47
|
+
console.log(chalk.dim(` Extracted on: ${mapping.timestamp}`));
|
|
48
|
+
console.log(chalk.dim(` Replacements: ${mapping.replacements?.length || 0}`));
|
|
49
|
+
console.log(chalk.dim(` Files: ${mapping.files?.length || 0}\n`));
|
|
50
|
+
|
|
51
|
+
// Show replacements that will be reversed
|
|
52
|
+
if (mapping.replacements && mapping.replacements.length > 0) {
|
|
53
|
+
console.log(chalk.cyan(' Replacements to reverse:'));
|
|
54
|
+
mapping.replacements.forEach(r => {
|
|
55
|
+
console.log(chalk.dim(` "${r.replacement}" โ "${r.original}"`));
|
|
56
|
+
});
|
|
57
|
+
console.log('');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Step 4: Get destination directory
|
|
61
|
+
const originalPath = getOriginalSource(mapping);
|
|
62
|
+
let destDir;
|
|
63
|
+
|
|
64
|
+
if (options.dest) {
|
|
65
|
+
destDir = resolve(options.dest);
|
|
66
|
+
} else if (originalPath && existsSync(originalPath)) {
|
|
67
|
+
const useOriginal = await confirmAction(
|
|
68
|
+
`Restore to original location? (${originalPath})`
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
if (useOriginal) {
|
|
72
|
+
destDir = originalPath;
|
|
73
|
+
} else {
|
|
74
|
+
destDir = await promptDestinationDirectory();
|
|
75
|
+
}
|
|
76
|
+
} else {
|
|
77
|
+
if (originalPath) {
|
|
78
|
+
console.log(chalk.yellow(` Original path no longer exists: ${originalPath}`));
|
|
79
|
+
}
|
|
80
|
+
destDir = await promptDestinationDirectory();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Step 5: Confirm
|
|
84
|
+
const confirmed = await confirmAction(
|
|
85
|
+
`Restore ${mapping.files?.length || 0} files to ${destDir}?`
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
if (!confirmed) {
|
|
89
|
+
showInfo('Operation cancelled.');
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Step 6: Get all files from cloaked directory
|
|
94
|
+
const files = getAllFiles(cloakedDir);
|
|
95
|
+
|
|
96
|
+
if (files.length === 0) {
|
|
97
|
+
showWarning('No files found in the cloaked directory.');
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Step 7: Create destination if needed
|
|
102
|
+
if (!existsSync(destDir)) {
|
|
103
|
+
mkdirSync(destDir, { recursive: true });
|
|
104
|
+
console.log(chalk.dim(` Created directory: ${destDir}`));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Step 8: Copy and de-anonymize files
|
|
108
|
+
const restoreSpinner = ora('Restoring files...').start();
|
|
109
|
+
|
|
110
|
+
const deanonymizer = createDeanonymizer(getReplacements(mapping));
|
|
111
|
+
|
|
112
|
+
const results = await copyFiles(
|
|
113
|
+
files,
|
|
114
|
+
cloakedDir,
|
|
115
|
+
destDir,
|
|
116
|
+
deanonymizer,
|
|
117
|
+
(current, total, file) => {
|
|
118
|
+
restoreSpinner.text = `Restoring files... ${current}/${total} - ${file}`;
|
|
119
|
+
}
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
restoreSpinner.succeed(`Restored ${results.copied} files`);
|
|
123
|
+
|
|
124
|
+
if (results.transformed > 0) {
|
|
125
|
+
console.log(chalk.cyan(` ๐ ${results.transformed} files had content restored`));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (results.errors.length > 0) {
|
|
129
|
+
console.log(chalk.yellow(` โ ๏ธ ${results.errors.length} files had errors`));
|
|
130
|
+
results.errors.forEach(e => {
|
|
131
|
+
console.log(chalk.dim(` - ${e.file}: ${e.error}`));
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Done!
|
|
136
|
+
showSuccess('Restoration complete!');
|
|
137
|
+
console.log(chalk.white(` ๐ Files restored to: ${chalk.cyan.bold(destDir)}\n`));
|
|
138
|
+
|
|
139
|
+
} catch (error) {
|
|
140
|
+
showError(`Push failed: ${error.message}`);
|
|
141
|
+
if (process.env.DEBUG) {
|
|
142
|
+
console.error(error);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|