mad-pro-cli 1.1.0 ā 1.3.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/index.js +32 -3
- package/lib/commands/add.js +72 -0
- package/lib/commands/create.js +644 -0
- package/lib/commands/doctor/architecture-checker.js +140 -0
- package/lib/commands/doctor/diff-reviewer.js +40 -0
- package/lib/commands/doctor/security-scanner.js +93 -0
- package/lib/commands/doctor.js +140 -0
- package/lib/commands/init.js +145 -52
- package/lib/commands/list.js +42 -0
- package/lib/commands/prompt.js +128 -0
- package/lib/utils/config.js +32 -0
- package/package.json +6 -2
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
|
|
5
|
+
export async function runArchitectureCheck(rootDir, config) {
|
|
6
|
+
const issues = [];
|
|
7
|
+
const arch = config.architecture || 'mvvm';
|
|
8
|
+
const di = config.di || 'hilt';
|
|
9
|
+
|
|
10
|
+
// Helper to scan files
|
|
11
|
+
const ktFiles = [];
|
|
12
|
+
function scanDir(dir) {
|
|
13
|
+
if (!fs.existsSync(dir)) return;
|
|
14
|
+
const files = fs.readdirSync(dir);
|
|
15
|
+
for (const file of files) {
|
|
16
|
+
const fullPath = path.join(dir, file);
|
|
17
|
+
if (fs.statSync(fullPath).isDirectory()) {
|
|
18
|
+
// Skip build and generated dirs
|
|
19
|
+
if (!['build', '.gradle', '.idea', 'node_modules'].includes(file)) {
|
|
20
|
+
scanDir(fullPath);
|
|
21
|
+
}
|
|
22
|
+
} else if (fullPath.endsWith('.kt')) {
|
|
23
|
+
ktFiles.push(fullPath);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
scanDir(rootDir);
|
|
29
|
+
|
|
30
|
+
let hasDomainLayer = false;
|
|
31
|
+
let hasUiLayer = false;
|
|
32
|
+
let hasDataLayer = false;
|
|
33
|
+
|
|
34
|
+
for (const file of ktFiles) {
|
|
35
|
+
const relPath = path.relative(rootDir, file);
|
|
36
|
+
const content = fs.readFileSync(file, 'utf8');
|
|
37
|
+
|
|
38
|
+
// Layer checks
|
|
39
|
+
if (relPath.includes('/domain/') || relPath.includes('/usecase/')) hasDomainLayer = true;
|
|
40
|
+
if (relPath.includes('/ui/') || relPath.includes('/presentation/')) hasUiLayer = true;
|
|
41
|
+
if (relPath.includes('/data/')) hasDataLayer = true;
|
|
42
|
+
|
|
43
|
+
const lines = content.split('\n');
|
|
44
|
+
|
|
45
|
+
// God class check
|
|
46
|
+
if (lines.length > 300) {
|
|
47
|
+
issues.push({
|
|
48
|
+
severity: 'warning',
|
|
49
|
+
file: relPath,
|
|
50
|
+
line: 1,
|
|
51
|
+
title: 'God-class detected',
|
|
52
|
+
message: 'File is over 300 lines. Consider splitting into smaller classes/composables.'
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Specific file type checks based on content
|
|
57
|
+
const isViewModel = content.includes('ViewModel()') || file.endsWith('ViewModel.kt') || content.includes('class ') && content.includes('ViewModel');
|
|
58
|
+
const isActivityOrFragment = content.includes('class ') && (content.includes(' : AppCompatActivity') || content.includes(' : ComponentActivity') || content.includes(' : Fragment'));
|
|
59
|
+
|
|
60
|
+
// Activity logic check
|
|
61
|
+
if (isActivityOrFragment) {
|
|
62
|
+
if (arch !== 'custom' && (content.includes('viewModelScope') || content.includes('Retrofit') || content.includes('Dao'))) {
|
|
63
|
+
issues.push({
|
|
64
|
+
severity: 'critical',
|
|
65
|
+
file: relPath,
|
|
66
|
+
line: lines.findIndex(l => l.includes('Retrofit') || l.includes('Dao') || l.includes('viewModelScope')) + 1 || 1,
|
|
67
|
+
title: `Logic in Activity/Fragment (${arch.toUpperCase()})`,
|
|
68
|
+
message: `Migrate logic to ViewModel or ${arch === 'viper' ? 'Interactor' : 'UseCase'}`,
|
|
69
|
+
refactor: `Ensure setContent { } wraps your NavHost, no logic in Activity`
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (isViewModel) {
|
|
75
|
+
// Missing DI check
|
|
76
|
+
if (di === 'hilt' && !content.includes('@HiltViewModel')) {
|
|
77
|
+
issues.push({ severity: 'warning', file: relPath, line: 1, title: 'Missing @HiltViewModel', message: 'Add @HiltViewModel to use Hilt DI.' });
|
|
78
|
+
} else if (di === 'koin' && !content.includes('@KoinViewModel') && !content.includes('viewModel {')) {
|
|
79
|
+
// Koin usually defined in module, but annotations exist in Koin annotations
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Bypass UseCase check (Directly injecting Repository)
|
|
83
|
+
if (['mvi', 'clean+mvi', 'viper'].includes(arch)) {
|
|
84
|
+
const repoInjectMatch = content.match(/val\s+[a-zA-Z0-9_]+Repository\s*:/);
|
|
85
|
+
if (repoInjectMatch) {
|
|
86
|
+
issues.push({
|
|
87
|
+
severity: 'critical',
|
|
88
|
+
file: relPath,
|
|
89
|
+
line: lines.findIndex(l => l.includes(repoInjectMatch[0])) + 1,
|
|
90
|
+
title: 'Business logic bypass detected',
|
|
91
|
+
message: `Direct usage of Repository in ViewModel. Expected UseCase/Interactor in ${arch.toUpperCase()}`,
|
|
92
|
+
refactor: `class MyUseCase @Inject constructor(private val repo: Repository) { operator fun invoke() = repo.exec() }\nInject MyUseCase instead.`
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// MVI Checks
|
|
98
|
+
if (['mvi', 'clean+mvi'].includes(arch)) {
|
|
99
|
+
if (!content.includes('StateFlow') && !content.includes('UiState')) {
|
|
100
|
+
issues.push({ severity: 'critical', file: relPath, line: 1, title: 'Missing StateFlow/UiState', message: 'MVI requires state management via StateFlow.' });
|
|
101
|
+
}
|
|
102
|
+
if (!content.includes('Intent') && !content.includes('Action') && !content.includes('Event')) {
|
|
103
|
+
issues.push({ severity: 'critical', file: relPath, line: 1, title: 'Missing Intent/Action', message: 'MVI expects explicit Intents or Actions.' });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// `runBlocking` in main thread (generic check)
|
|
109
|
+
lines.forEach((line, index) => {
|
|
110
|
+
if (line.includes('runBlocking')) {
|
|
111
|
+
issues.push({
|
|
112
|
+
severity: 'critical',
|
|
113
|
+
file: relPath,
|
|
114
|
+
line: index + 1,
|
|
115
|
+
title: 'runBlocking detected',
|
|
116
|
+
message: 'Avoid runBlocking in production code as it blocks the thread.',
|
|
117
|
+
refactor: 'Replace with viewModelScope.launch or lifecycleScope.launch'
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Hardcoded string (naive check inside composables / classes)
|
|
122
|
+
if (line.match(/Text\(\s*"[^"]+"\s*\)/)) {
|
|
123
|
+
issues.push({
|
|
124
|
+
severity: 'warning',
|
|
125
|
+
file: relPath,
|
|
126
|
+
line: index + 1,
|
|
127
|
+
title: 'Hardcoded string in UI',
|
|
128
|
+
message: 'Found hardcoded string in Text().',
|
|
129
|
+
refactor: 'Move to res/values/strings.xml and use stringResource(R.string.id)'
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (!hasDomainLayer && ['clean+mvi', 'viper'].includes(arch)) {
|
|
136
|
+
issues.push({ severity: 'warning', file: 'Project Structure', line: 0, title: 'Missing Domain Layer', message: 'Create domain/usecase/ layer.' });
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return issues;
|
|
140
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { execSync } from 'child_process';
|
|
4
|
+
import { runArchitectureCheck } from './architecture-checker.js';
|
|
5
|
+
import { runSecurityScan } from './security-scanner.js';
|
|
6
|
+
|
|
7
|
+
export async function runDiffReview(rootDir, targetBranch, config, doSecurity) {
|
|
8
|
+
let changedFiles = [];
|
|
9
|
+
try {
|
|
10
|
+
const output = execSync(`git diff ${targetBranch}...HEAD --name-only`, { cwd: rootDir, encoding: 'utf8' });
|
|
11
|
+
changedFiles = output.split('\n').map(l => l.trim()).filter(l => l.length > 0 && l.endsWith('.kt'));
|
|
12
|
+
} catch (e) {
|
|
13
|
+
throw new Error(`Failed to get git diff vs ${targetBranch}. Ensure it exists locally and project is a git repo.`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (changedFiles.length === 0) return { files: 0, issues: [] };
|
|
17
|
+
|
|
18
|
+
// Collect all issues from architecture check and security scan
|
|
19
|
+
// Usually they scan the whole dir, but for this we might need to filter.
|
|
20
|
+
// Wait, the arch checker scans the dir recursively.
|
|
21
|
+
// For diff review, we can just run the scanners recursively and then filter the output by the changed files list.
|
|
22
|
+
|
|
23
|
+
const archIssues = await runArchitectureCheck(rootDir, config);
|
|
24
|
+
const securityIssues = doSecurity ? await runSecurityScan(rootDir) : [];
|
|
25
|
+
|
|
26
|
+
const allIssues = [...archIssues, ...securityIssues];
|
|
27
|
+
|
|
28
|
+
// Filter issues to only include changed files
|
|
29
|
+
// arch/security checkers use relative paths from rootDir
|
|
30
|
+
const filteredIssues = allIssues.filter(issue => {
|
|
31
|
+
// Changed files paths are usually relative to git root.
|
|
32
|
+
// Assuming rootDir is git root.
|
|
33
|
+
return changedFiles.includes(issue.file);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
files: changedFiles.length,
|
|
38
|
+
issues: filteredIssues
|
|
39
|
+
};
|
|
40
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
export async function runSecurityScan(rootDir) {
|
|
5
|
+
const issues = [];
|
|
6
|
+
|
|
7
|
+
const ktFiles = [];
|
|
8
|
+
const buildFiles = [];
|
|
9
|
+
|
|
10
|
+
function scanDir(dir) {
|
|
11
|
+
if (!fs.existsSync(dir)) return;
|
|
12
|
+
const files = fs.readdirSync(dir);
|
|
13
|
+
for (const file of files) {
|
|
14
|
+
const fullPath = path.join(dir, file);
|
|
15
|
+
if (fs.statSync(fullPath).isDirectory()) {
|
|
16
|
+
if (!['build', '.gradle', '.idea', 'node_modules'].includes(file)) {
|
|
17
|
+
scanDir(fullPath);
|
|
18
|
+
}
|
|
19
|
+
} else if (fullPath.endsWith('.kt')) {
|
|
20
|
+
ktFiles.push(fullPath);
|
|
21
|
+
} else if (fullPath.endsWith('.gradle.kts') || fullPath.endsWith('.gradle')) {
|
|
22
|
+
buildFiles.push(fullPath);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
scanDir(rootDir);
|
|
28
|
+
|
|
29
|
+
for (const file of buildFiles) {
|
|
30
|
+
const relPath = path.relative(rootDir, file);
|
|
31
|
+
const content = fs.readFileSync(file, 'utf8');
|
|
32
|
+
if (content.includes('debuggable true') && content.includes('release {')) {
|
|
33
|
+
issues.push({ severity: 'critical', file: relPath, line: 0, title: 'Debug flag in Release', message: 'Release buildType has debuggable true.', refactor: 'Remove debuggable true or set to false.' });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
for (const file of ktFiles) {
|
|
38
|
+
const relPath = path.relative(rootDir, file);
|
|
39
|
+
const content = fs.readFileSync(file, 'utf8');
|
|
40
|
+
const lines = content.split('\n');
|
|
41
|
+
|
|
42
|
+
lines.forEach((line, index) => {
|
|
43
|
+
// Hardcoded API Key/Token
|
|
44
|
+
if (line.match(/(val|const val)\s+[A-Z_]+_KEY\s*=\s*"[^"]+"/) || line.match(/=\s*"Bearer\s+[^"]+"/)) {
|
|
45
|
+
issues.push({
|
|
46
|
+
severity: 'critical', file: relPath, line: index + 1,
|
|
47
|
+
title: 'Hardcoded API Key / Secret',
|
|
48
|
+
message: 'Hardcoded secret detected.',
|
|
49
|
+
refactor: 'Use BuildConfig or BuildConfig field from local.properties/CI environment.'
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// HTTP endpoint
|
|
54
|
+
if (line.match(/baseUrl\s*=\s*"http:\/\//)) {
|
|
55
|
+
issues.push({ severity: 'critical', file: relPath, line: index + 1, title: 'HTTP endpoint (bukan HTTPS)', message: 'Insecure HTTP URL in base URL.', refactor: 'Use https:// instead.' });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Unencrypted SharedPreferences
|
|
59
|
+
if (line.includes('getSharedPreferences') && (line.toLowerCase().includes('token') || line.toLowerCase().includes('password'))) {
|
|
60
|
+
issues.push({ severity: 'critical', file: relPath, line: index + 1, title: 'Sensitive Data in SharedPreferences', message: 'Storing sensitive data in regular SharedPreferences.', refactor: 'Use EncryptedSharedPreferences (Security Crypto library).' });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Log expose
|
|
64
|
+
if (line.match(/Log\.[deiwv]\([^,]+,\s*[a-zA-Z]*(token|password|secret)[a-zA-Z]*\)/i)) {
|
|
65
|
+
issues.push({ severity: 'warning', file: relPath, line: index + 1, title: 'Log exposes PII/Token', message: 'Logging sensitive variables.', refactor: 'Remove log statement or mask the value.' });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// GlobalScope warning
|
|
69
|
+
if (line.includes('GlobalScope.launch')) {
|
|
70
|
+
issues.push({ severity: 'warning', file: relPath, line: index + 1, title: 'GlobalScope leak', message: 'GlobalScope coroutine might outlive the component.', refactor: 'Use viewModelScope, lifecycleScope or custom CoroutineScope.' });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// World Readable
|
|
74
|
+
if (line.includes('MODE_WORLD_READABLE')) {
|
|
75
|
+
issues.push({ severity: 'critical', file: relPath, line: index + 1, title: 'World readable file permission', message: 'MODE_WORLD_READABLE is deprecated and insecure.', refactor: 'Use MODE_PRIVATE' });
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// Check BroadcastReceiver
|
|
80
|
+
if (content.includes('registerReceiver(')) {
|
|
81
|
+
if (!content.includes('unregisterReceiver(')) {
|
|
82
|
+
issues.push({ severity: 'critical', file: relPath, line: 0, title: 'BroadcastReceiver leak', message: 'registerReceiver called but no unregisterReceiver found in the same file.', refactor: 'Call unregisterReceiver in onDestroy() / onStop() or use DisposableEffect in Compose.' });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Bitmap recycle warning
|
|
87
|
+
if (content.includes('Bitmap.create') && !content.includes('.recycle()')) {
|
|
88
|
+
issues.push({ severity: 'warning', file: relPath, line: 0, title: 'Bitmap leak possible', message: 'Bitmap created but no recycle() call found.', refactor: 'Ensure bitmaps are recycled when no longer needed.' });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return issues;
|
|
93
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import inquirer from 'inquirer';
|
|
5
|
+
import { program } from 'commander';
|
|
6
|
+
import { loadConfig, updateConfig } from '../utils/config.js';
|
|
7
|
+
import { runArchitectureCheck } from './doctor/architecture-checker.js';
|
|
8
|
+
import { runSecurityScan } from './doctor/security-scanner.js';
|
|
9
|
+
import { runDiffReview } from './doctor/diff-reviewer.js';
|
|
10
|
+
|
|
11
|
+
export default async function doctorCommand(cmdObj) {
|
|
12
|
+
// Setup flags natively (if called directly or parsed from args)
|
|
13
|
+
// In a robust setup this would be registered in index.js for the commander instance,
|
|
14
|
+
// but we can check raw arguments here if not passed.
|
|
15
|
+
const isSecurity = cmdObj?.security || process.argv.includes('--security') || process.argv.includes('-s');
|
|
16
|
+
const isFix = cmdObj?.fix || process.argv.includes('--fix');
|
|
17
|
+
const reviewBranch = cmdObj?.review || getArgValue('--review');
|
|
18
|
+
const outputFile = cmdObj?.output || getArgValue('--output');
|
|
19
|
+
|
|
20
|
+
console.log(chalk.bold.magenta('\n𩺠MAD Pro Doctor - Project Audit'));
|
|
21
|
+
|
|
22
|
+
const rootDir = process.cwd();
|
|
23
|
+
let config = loadConfig(rootDir) || {};
|
|
24
|
+
|
|
25
|
+
// Interactive Architecture Template Selection on first run
|
|
26
|
+
if (!config.architecture) {
|
|
27
|
+
console.log(chalk.cyan('It looks like your project does not have an architecture defined yet.'));
|
|
28
|
+
const archRes = await inquirer.prompt([{
|
|
29
|
+
type: 'list',
|
|
30
|
+
name: 'architecture',
|
|
31
|
+
message: 'Select your project architecture (for accurate anti-pattern scanning):',
|
|
32
|
+
choices: [
|
|
33
|
+
{ name: 'MVVM (ViewModel + StateFlow/LiveData)', value: 'mvvm' },
|
|
34
|
+
{ name: 'MVI (Intent ā State ā Effect)', value: 'mvi' },
|
|
35
|
+
{ name: 'Clean + MVI (Use Case + MVI in ViewModel)', value: 'clean+mvi' },
|
|
36
|
+
{ name: 'MVP (Presenter + View interface)', value: 'mvp' },
|
|
37
|
+
{ name: 'VIPER (View, Interactor, Presenter, Entity, Router)', value: 'viper' },
|
|
38
|
+
{ name: 'Custom (Skip strict architecture checks)', value: 'custom' }
|
|
39
|
+
]
|
|
40
|
+
}]);
|
|
41
|
+
|
|
42
|
+
const diRes = await inquirer.prompt([{
|
|
43
|
+
type: 'list',
|
|
44
|
+
name: 'di',
|
|
45
|
+
message: 'Select your Dependency Injection framework:',
|
|
46
|
+
choices: [
|
|
47
|
+
{ name: 'Hilt', value: 'hilt' },
|
|
48
|
+
{ name: 'Koin', value: 'koin' },
|
|
49
|
+
{ name: 'None/Manual', value: 'none' }
|
|
50
|
+
]
|
|
51
|
+
}]);
|
|
52
|
+
|
|
53
|
+
config = updateConfig({ architecture: archRes.architecture, di: diRes.di }, rootDir);
|
|
54
|
+
console.log(chalk.green(`\nā Configured for ${archRes.architecture.toUpperCase()} + ${diRes.di}. (Saved in .mad-pro.json)\n`));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
console.log(chalk.gray(`Running checks for Architecture: [${config.architecture.toUpperCase()}] and DI: [${config.di}]...\n`));
|
|
58
|
+
|
|
59
|
+
let issues = [];
|
|
60
|
+
let fileCount = 0;
|
|
61
|
+
|
|
62
|
+
if (reviewBranch) {
|
|
63
|
+
console.log(chalk.cyan(`š Diff Code Review against branch: ${reviewBranch}`));
|
|
64
|
+
try {
|
|
65
|
+
const reviewResult = await runDiffReview(rootDir, reviewBranch, config, isSecurity);
|
|
66
|
+
issues = reviewResult.issues;
|
|
67
|
+
fileCount = reviewResult.files;
|
|
68
|
+
console.log(chalk.gray(`Scanned ${fileCount} changed files.\n`));
|
|
69
|
+
} catch (e) {
|
|
70
|
+
console.error(chalk.red(e.message));
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
} else {
|
|
74
|
+
issues = await runArchitectureCheck(rootDir, config);
|
|
75
|
+
if (isSecurity) {
|
|
76
|
+
console.log(chalk.cyan(`š Running Security & Memory Leak Scanners...`));
|
|
77
|
+
const secIssues = await runSecurityScan(rootDir);
|
|
78
|
+
issues = [...issues, ...secIssues];
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Formatting and outputting issues
|
|
83
|
+
if (issues.length === 0) {
|
|
84
|
+
console.log(chalk.bold.green('\nš Your project looks 100% MAD-compliant!'));
|
|
85
|
+
} else {
|
|
86
|
+
let criticals = 0;
|
|
87
|
+
let warnings = 0;
|
|
88
|
+
|
|
89
|
+
issues.forEach(issue => {
|
|
90
|
+
const prefix = issue.severity === 'critical' ? chalk.red('ā [CRITICAL]') : chalk.yellow('ā ļø [WARNING]');
|
|
91
|
+
if (issue.severity === 'critical') criticals++; else warnings++;
|
|
92
|
+
|
|
93
|
+
console.log(`${prefix} ${issue.title}`);
|
|
94
|
+
console.log(chalk.gray(` š File: ${issue.file}:${issue.line}`));
|
|
95
|
+
if (issue.message) console.log(chalk.white(` ā¹ļø ${issue.message}`));
|
|
96
|
+
if (issue.refactor) {
|
|
97
|
+
console.log(chalk.cyan(` š” Refactor:`));
|
|
98
|
+
const refactorLines = issue.refactor.split('\\n');
|
|
99
|
+
refactorLines.forEach(l => console.log(chalk.cyan(` ${l}`)));
|
|
100
|
+
}
|
|
101
|
+
console.log('');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
console.log(chalk.bold('āāāāāāāāāāāāāāāāāāāāāā'));
|
|
105
|
+
console.log(`${chalk.red(`ā ${criticals} Critical`)} | ${chalk.yellow(`ā ļø ${warnings} Warning`)}`);
|
|
106
|
+
console.log(chalk.bold.yellow(`\nā ļø Found ${issues.length} items to improve.`));
|
|
107
|
+
if (!isFix) console.log(chalk.white(`Run with --fix to scaffold recommended fixes (experimental).`));
|
|
108
|
+
|
|
109
|
+
// Simple auto-fix demo based on the flag
|
|
110
|
+
if (isFix) {
|
|
111
|
+
console.log(chalk.green('\nš ļø Scaffolding fixes (Simulated)...'));
|
|
112
|
+
issues.filter(i => i.severity === 'critical' && i.title.includes('Business logic bypass')).forEach(issue => {
|
|
113
|
+
const expectedFile = 'app/src/main/java/com/example/domain/usecase/GeneratedUseCase.kt';
|
|
114
|
+
console.log(` ${chalk.green('ā')} Scaffolded: ${expectedFile}`);
|
|
115
|
+
// In real implementation, this would actually generate the file based on the refactor string.
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (outputFile) {
|
|
120
|
+
let mdOutput = `# MAD Pro Code Review\n\n`;
|
|
121
|
+
if (reviewBranch) mdOutput += `**Target Branch:** \`${reviewBranch}\`\n**Changed Files:** ${fileCount}\n\n`;
|
|
122
|
+
|
|
123
|
+
mdOutput += `## Issues Found: ${issues.length}\n`;
|
|
124
|
+
issues.forEach(issue => {
|
|
125
|
+
mdOutput += `### ${issue.severity === 'critical' ? 'ā' : 'ā ļø'} ${issue.title}\n`;
|
|
126
|
+
mdOutput += `- **File:** \`${issue.file}:${issue.line}\`\n`;
|
|
127
|
+
mdOutput += `- **Message:** ${issue.message}\n`;
|
|
128
|
+
if (issue.refactor) mdOutput += `- **Refactor:** \n\`\`\`kotlin\n${issue.refactor}\n\`\`\`\n\n`;
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
fs.writeFileSync(path.join(rootDir, outputFile), mdOutput);
|
|
132
|
+
console.log(chalk.green(`\nReport saved to ${outputFile}`));
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function getArgValue(flag) {
|
|
138
|
+
const index = process.argv.indexOf(flag);
|
|
139
|
+
return index !== -1 ? process.argv[index + 1] : null;
|
|
140
|
+
}
|
package/lib/commands/init.js
CHANGED
|
@@ -1,63 +1,156 @@
|
|
|
1
|
-
import path from 'path';
|
|
2
1
|
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
3
|
import chalk from 'chalk';
|
|
4
|
+
import inquirer from 'inquirer';
|
|
4
5
|
import { fileURLToPath } from 'url';
|
|
5
6
|
|
|
6
7
|
const __filename = fileURLToPath(import.meta.url);
|
|
7
8
|
const __dirname = path.dirname(__filename);
|
|
8
9
|
|
|
9
10
|
export default async function initCommand(options) {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
console.log(chalk.green('\nā
Success! MAD Skills (Antigravity Bridge) installed.'));
|
|
53
|
-
console.log(chalk.cyan(`\nYour AI agent (Antigravity) can now find these skills at: ${destinationPath}`));
|
|
54
|
-
console.log(chalk.gray('\nTo use these skills, make sure your agent is instructed to read the .agent/skills directory.'));
|
|
55
|
-
|
|
56
|
-
} catch (error) {
|
|
57
|
-
console.error(chalk.red('\nā Error installing skills:'), error.message);
|
|
11
|
+
console.log(chalk.bold.magenta('\nš MAD Pro CLI - Skill Wizard v1.3.0'));
|
|
12
|
+
console.log(chalk.gray('Setting up your Android project with architecture excellence...\n'));
|
|
13
|
+
|
|
14
|
+
// 1. Identify Source & Target
|
|
15
|
+
const rootDir = process.cwd();
|
|
16
|
+
const sourceDir = path.join(__dirname, '../../../references');
|
|
17
|
+
|
|
18
|
+
// Verify source exists
|
|
19
|
+
if (!fs.existsSync(sourceDir)) {
|
|
20
|
+
console.error(chalk.red('Error: Skill library not found. Please reinstall the CLI.'));
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// 2. Interactive Prompts
|
|
25
|
+
const answers = await inquirer.prompt([
|
|
26
|
+
{
|
|
27
|
+
type: 'list',
|
|
28
|
+
name: 'ide',
|
|
29
|
+
message: 'Which IDE are you using?',
|
|
30
|
+
choices: [
|
|
31
|
+
{ name: 'Cursor (Highly Recommended)', value: 'cursor' },
|
|
32
|
+
{ name: 'Windsurf', value: 'windsurf' },
|
|
33
|
+
{ name: 'VS Code', value: 'vscode' },
|
|
34
|
+
{ name: 'Android Studio', value: 'android-studio' },
|
|
35
|
+
{ name: 'Zed', value: 'zed' },
|
|
36
|
+
{ name: 'Others', value: 'default' }
|
|
37
|
+
],
|
|
38
|
+
default: options.ide || 'cursor'
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
type: 'checkbox',
|
|
42
|
+
name: 'categories',
|
|
43
|
+
message: 'Select Skill Categories to include:',
|
|
44
|
+
choices: [
|
|
45
|
+
{ name: 'Core MAD (UI, Domain, Data Layers)', value: 'core', checked: true },
|
|
46
|
+
{ name: 'Platform Capabilities (Camera, Biometrics, etc.)', value: 'platform', checked: true },
|
|
47
|
+
{ name: 'AI & Emerging (Gemini, LLM UI, ARCore)', value: 'ai', checked: false },
|
|
48
|
+
{ name: 'Industry Verticals (Banking, E-commerce, etc.)', value: 'industry', checked: false },
|
|
49
|
+
{ name: 'Monetization & Play (Billing, Subs)', value: 'monetization', checked: false },
|
|
50
|
+
{ name: 'Engineering Excellence (Modularization, CI/CD)', value: 'engineering', checked: false },
|
|
51
|
+
{ name: 'Design Tokens (Colors, Typography, Spacing)', value: 'tokens', checked: true }
|
|
52
|
+
]
|
|
58
53
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
54
|
+
]);
|
|
55
|
+
|
|
56
|
+
// 3. Selective Industry prompt (only if 'industry' is selected)
|
|
57
|
+
let selectedIndustries = [];
|
|
58
|
+
if (answers.categories.includes('industry')) {
|
|
59
|
+
const industryPath = path.join(sourceDir, 'industry');
|
|
60
|
+
const allIndustries = fs.readdirSync(industryPath).filter(f => f.endsWith('.md') && f !== 'google_play_subscriptions.md' && f !== 'in_app_payments.md');
|
|
61
|
+
|
|
62
|
+
const industryChoices = await inquirer.prompt([
|
|
63
|
+
{
|
|
64
|
+
type: 'checkbox',
|
|
65
|
+
name: 'industries',
|
|
66
|
+
message: 'Which Industry Verticals do you need?',
|
|
67
|
+
choices: allIndustries.map(f => ({ name: f.replace('.md', '').replace(/_/g, ' ').toUpperCase(), value: f }))
|
|
68
|
+
}
|
|
69
|
+
]);
|
|
70
|
+
selectedIndustries = industryChoices.industries;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// 4. Create Target Directories
|
|
74
|
+
const targetRefDir = path.join(rootDir, 'references');
|
|
75
|
+
const targetIndustryDir = path.join(targetRefDir, 'industry');
|
|
76
|
+
const targetTokenDir = path.join(targetRefDir, 'design-tokens');
|
|
77
|
+
await fs.ensureDir(targetRefDir);
|
|
78
|
+
if (selectedIndustries.length > 0 || answers.categories.includes('monetization')) {
|
|
79
|
+
await fs.ensureDir(targetIndustryDir);
|
|
80
|
+
}
|
|
81
|
+
if (answers.categories.includes('tokens')) {
|
|
82
|
+
await fs.ensureDir(targetTokenDir);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// 5. Build Mapping & Copy Files
|
|
86
|
+
const skillMapping = {
|
|
87
|
+
'core': [
|
|
88
|
+
'ui_layer_state.md', 'ui_patterns.md', 'ui_layouts.md', 'ui_modifiers.md', 'ui_theming.md', 'ui_navigation.md',
|
|
89
|
+
'domain_layer_use_case.md', 'data_layer_room.md', 'data_layer_networking.md', 'data_layer_serialization.md'
|
|
90
|
+
],
|
|
91
|
+
'platform': [
|
|
92
|
+
'camera_media.md', 'barcode_qr.md', 'image_editing.md', 'voice_speech.md', 'biometric_auth.md', 'security.md',
|
|
93
|
+
'maps_location.md', 'push_notifications.md', 'widget_glance.md', 'app_shortcuts.md'
|
|
94
|
+
],
|
|
95
|
+
'ai': ['gemini_api.md', 'llm_ui_patterns.md', 'ar_core.md', 'on_device_ai.md'],
|
|
96
|
+
'monetization': ['industry/in_app_payments.md', 'industry/google_play_subscriptions.md'],
|
|
97
|
+
'engineering': [
|
|
98
|
+
'modularization.md', 'architecture_di.md', 'concurrency.md', 'performance.md', 'testing.md',
|
|
99
|
+
'design_systems.md', 'observability.md', 'ci_cd.md'
|
|
100
|
+
],
|
|
101
|
+
'tokens': ['design-tokens/color-tokens.md', 'design-tokens/typography-tokens.md', 'design-tokens/spacing-tokens.md']
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
let filesToCopy = [];
|
|
105
|
+
answers.categories.forEach(cat => {
|
|
106
|
+
if (skillMapping[cat]) {
|
|
107
|
+
filesToCopy = [...filesToCopy, ...skillMapping[cat]];
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// Add individual industry files
|
|
112
|
+
selectedIndustries.forEach(industryFile => {
|
|
113
|
+
filesToCopy.push(`industry/${industryFile}`);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// Always include the master index but pruned
|
|
117
|
+
filesToCopy.push('../SKILL.md');
|
|
118
|
+
|
|
119
|
+
// Perform Copying
|
|
120
|
+
console.log(chalk.cyan('\nš¦ Installing selected skills...'));
|
|
121
|
+
|
|
122
|
+
for (const file of filesToCopy) {
|
|
123
|
+
const src = path.join(sourceDir, file);
|
|
124
|
+
const destName = file.includes('industry/') ? file.replace('industry/', 'industry/') : (file.includes('design-tokens/') ? file.replace('design-tokens/', 'design-tokens/') : file);
|
|
125
|
+
const dest = path.join(targetRefDir, destName);
|
|
126
|
+
|
|
127
|
+
if (fs.existsSync(src)) {
|
|
128
|
+
await fs.copy(src, dest);
|
|
129
|
+
console.log(`${chalk.green('ā')} Added ${chalk.gray(destName)}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// 6. Project Configuration (IDE Specific)
|
|
134
|
+
await configureIDE(answers.ide, rootDir);
|
|
135
|
+
|
|
136
|
+
console.log(chalk.bold.green('\nš Setup Complete!'));
|
|
137
|
+
console.log(chalk.white(`Configured IDE: ${chalk.bold.yellow(answers.ide.toUpperCase())}`));
|
|
138
|
+
console.log(chalk.white(`Skills Installed: ${chalk.bold.yellow(filesToCopy.length - 1)} items`));
|
|
139
|
+
console.log(chalk.gray('\nYour AI Agent is now ready with expert context. Happy coding! š\n'));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function configureIDE(ide, rootDir) {
|
|
143
|
+
const configFiles = {
|
|
144
|
+
'cursor': { file: '.cursorrules', content: 'Use the patterns in the /references directory for all Android work.' },
|
|
145
|
+
'windsurf': { file: '.windsurf/rules', content: 'Follow MAD Pro architecture in /references folder.' },
|
|
146
|
+
'vscode': { file: '.vscode/settings.json', content: '{\n "android.skillPath": "./references"\n}' }
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const config = configFiles[ide];
|
|
150
|
+
if (config) {
|
|
151
|
+
const targetFile = path.join(rootDir, config.file);
|
|
152
|
+
await fs.ensureDir(path.dirname(targetFile));
|
|
153
|
+
await fs.writeFile(targetFile, config.content);
|
|
154
|
+
console.log(`${chalk.blue('ā¹')} Created IDE config: ${chalk.gray(config.file)}`);
|
|
62
155
|
}
|
|
63
156
|
}
|