mad-pro-cli 1.2.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 CHANGED
@@ -11,7 +11,7 @@ import promptCommand from './lib/commands/prompt.js';
11
11
  program
12
12
  .name('mad-pro')
13
13
  .description('MAD Pro CLI - Boost your project with AI Expert Skills')
14
- .version('1.2.0');
14
+ .version('1.3.0');
15
15
 
16
16
  program
17
17
  .command('init')
@@ -22,9 +22,11 @@ export default async function addCommand() {
22
22
  // 1. Get all available skills
23
23
  const coreFiles = fs.readdirSync(sourceDir).filter(f => f.endsWith('.md'));
24
24
  const industryPath = path.join(sourceDir, 'industry');
25
- const industryFiles = fs.readdirSync(industryPath).filter(f => f.endsWith('.md')).map(f => `industry/${f}`);
25
+ const industryFiles = fs.existsSync(industryPath) ? fs.readdirSync(industryPath).filter(f => f.endsWith('.md')).map(f => `industry/${f}`) : [];
26
+ const tokenPath = path.join(sourceDir, 'design-tokens');
27
+ const tokenFiles = fs.existsSync(tokenPath) ? fs.readdirSync(tokenPath).filter(f => f.endsWith('.md')).map(f => `design-tokens/${f}`) : [];
26
28
 
27
- const allSkills = [...coreFiles, ...industryFiles].map(f => ({
29
+ const allSkills = [...coreFiles, ...industryFiles, ...tokenFiles].map(f => ({
28
30
  name: f.replace('.md', '').replace(/_/g, ' ').toUpperCase(),
29
31
  value: f
30
32
  })).sort((a, b) => a.name.localeCompare(b.name));
@@ -48,14 +50,16 @@ export default async function addCommand() {
48
50
  // 3. Perform Copying
49
51
  const targetRefDir = path.join(rootDir, 'references');
50
52
  const targetIndustryDir = path.join(targetRefDir, 'industry');
53
+ const targetTokenDir = path.join(targetRefDir, 'design-tokens');
51
54
  await fs.ensureDir(targetRefDir);
52
55
  await fs.ensureDir(targetIndustryDir);
56
+ await fs.ensureDir(targetTokenDir);
53
57
 
54
58
  console.log(chalk.cyan('\nšŸ“¦ Adding selected skills...'));
55
59
 
56
60
  for (const skill of answers.skills) {
57
61
  const src = path.join(sourceDir, skill);
58
- const dest = path.join(targetRefDir, skill);
62
+ const dest = path.join(targetRefDir, skill.replace('industry/', 'industry/').replace('design-tokens/', 'design-tokens/'));
59
63
 
60
64
  if (fs.existsSync(src)) {
61
65
  await fs.copy(src, dest);
@@ -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
+ }
@@ -1,69 +1,140 @@
1
1
  import fs from 'fs-extra';
2
2
  import path from 'path';
3
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');
4
19
 
5
- export default async function doctorCommand() {
6
20
  console.log(chalk.bold.magenta('\n🩺 MAD Pro Doctor - Project Audit'));
7
- console.log(chalk.gray('Checking your project for architectural health...\n'));
8
21
 
9
22
  const rootDir = process.cwd();
10
-
11
- const checks = [
12
- { name: 'MAD Pro Initialization', path: 'references/', type: 'dir', critical: true, help: 'Run `mad-pro init` to install base skills.' },
13
- { name: 'Skill Master Index', path: 'references/SKILL.md', type: 'file', critical: false, help: 'Master index (SKILL.md) is missing. This weakens AI context.' },
14
- { name: 'Clean Arch: UI Layer', path: 'app/src/main/java/', type: 'dir', contains: 'ui', critical: false, help: 'Missing "ui" package. Conventional MAD apps should have it.' },
15
- { name: 'Clean Arch: Data Layer', path: 'app/src/main/java/', type: 'dir', contains: 'data', critical: false, help: 'Missing "data" package. Offline-first apps need a data source.' },
16
- { name: 'Hilt Dependency Injection', path: 'app/build.gradle.kts', type: 'content', contains: 'dagger.hilt', critical: false, help: 'Consider using Hilt for easier dependency management.' },
17
- ];
18
-
19
- let issues = 0;
20
-
21
- for (const check of checks) {
22
- let passed = false;
23
- const fullPath = path.join(rootDir, check.path);
24
-
25
- if (check.type === 'dir') {
26
- if (fs.existsSync(fullPath) && fs.statSync(fullPath).isDirectory()) {
27
- if (check.contains) {
28
- passed = findFolderRecursive(fullPath, check.contains);
29
- } else {
30
- passed = true;
31
- }
32
- }
33
- } else if (check.type === 'file') {
34
- passed = fs.existsSync(fullPath) && fs.statSync(fullPath).isFile();
35
- } else if (check.type === 'content') {
36
- if (fs.existsSync(fullPath)) {
37
- const content = fs.readFileSync(fullPath, 'utf8');
38
- passed = content.includes(check.contains);
39
- }
40
- }
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
+ }]);
41
52
 
42
- if (passed) {
43
- console.log(`${chalk.green('āœ“')} ${check.name}`);
44
- } else {
45
- issues++;
46
- const prefix = check.critical ? chalk.red('āŒ') : chalk.yellow('āš ļø');
47
- console.log(`${prefix} ${check.name}`);
48
- console.log(chalk.gray(` šŸ’” Recommendation: ${check.help}`));
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];
49
79
  }
50
80
  }
51
81
 
52
- if (issues === 0) {
82
+ // Formatting and outputting issues
83
+ if (issues.length === 0) {
53
84
  console.log(chalk.bold.green('\nšŸŽ‰ Your project looks 100% MAD-compliant!'));
54
85
  } else {
55
- console.log(chalk.bold.yellow(`\nāš ļø Found ${issues} items to improve.`));
56
- }
57
- }
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).`));
58
108
 
59
- function findFolderRecursive(startDir, targetName) {
60
- const files = fs.readdirSync(startDir);
61
- for (const file of files) {
62
- const fullPath = path.join(startDir, file);
63
- if (fs.statSync(fullPath).isDirectory()) {
64
- if (file.toLowerCase() === targetName.toLowerCase()) return true;
65
- if (findFolderRecursive(fullPath, targetName)) return true;
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}`));
66
133
  }
67
134
  }
68
- return false;
135
+ }
136
+
137
+ function getArgValue(flag) {
138
+ const index = process.argv.indexOf(flag);
139
+ return index !== -1 ? process.argv[index + 1] : null;
69
140
  }
@@ -8,7 +8,7 @@ const __filename = fileURLToPath(import.meta.url);
8
8
  const __dirname = path.dirname(__filename);
9
9
 
10
10
  export default async function initCommand(options) {
11
- console.log(chalk.bold.magenta('\nšŸš€ MAD Pro CLI - Skill Wizard v1.2.0'));
11
+ console.log(chalk.bold.magenta('\nšŸš€ MAD Pro CLI - Skill Wizard v1.3.0'));
12
12
  console.log(chalk.gray('Setting up your Android project with architecture excellence...\n'));
13
13
 
14
14
  // 1. Identify Source & Target
@@ -47,7 +47,8 @@ export default async function initCommand(options) {
47
47
  { name: 'AI & Emerging (Gemini, LLM UI, ARCore)', value: 'ai', checked: false },
48
48
  { name: 'Industry Verticals (Banking, E-commerce, etc.)', value: 'industry', checked: false },
49
49
  { name: 'Monetization & Play (Billing, Subs)', value: 'monetization', checked: false },
50
- { name: 'Engineering Excellence (Modularization, CI/CD)', value: 'engineering', checked: false }
50
+ { name: 'Engineering Excellence (Modularization, CI/CD)', value: 'engineering', checked: false },
51
+ { name: 'Design Tokens (Colors, Typography, Spacing)', value: 'tokens', checked: true }
51
52
  ]
52
53
  }
53
54
  ]);
@@ -72,10 +73,14 @@ export default async function initCommand(options) {
72
73
  // 4. Create Target Directories
73
74
  const targetRefDir = path.join(rootDir, 'references');
74
75
  const targetIndustryDir = path.join(targetRefDir, 'industry');
76
+ const targetTokenDir = path.join(targetRefDir, 'design-tokens');
75
77
  await fs.ensureDir(targetRefDir);
76
78
  if (selectedIndustries.length > 0 || answers.categories.includes('monetization')) {
77
79
  await fs.ensureDir(targetIndustryDir);
78
80
  }
81
+ if (answers.categories.includes('tokens')) {
82
+ await fs.ensureDir(targetTokenDir);
83
+ }
79
84
 
80
85
  // 5. Build Mapping & Copy Files
81
86
  const skillMapping = {
@@ -92,7 +97,8 @@ export default async function initCommand(options) {
92
97
  'engineering': [
93
98
  'modularization.md', 'architecture_di.md', 'concurrency.md', 'performance.md', 'testing.md',
94
99
  'design_systems.md', 'observability.md', 'ci_cd.md'
95
- ]
100
+ ],
101
+ 'tokens': ['design-tokens/color-tokens.md', 'design-tokens/typography-tokens.md', 'design-tokens/spacing-tokens.md']
96
102
  };
97
103
 
98
104
  let filesToCopy = [];
@@ -115,7 +121,7 @@ export default async function initCommand(options) {
115
121
 
116
122
  for (const file of filesToCopy) {
117
123
  const src = path.join(sourceDir, file);
118
- const destName = file.includes('industry/') ? file.replace('industry/', 'industry/') : file;
124
+ const destName = file.includes('industry/') ? file.replace('industry/', 'industry/') : (file.includes('design-tokens/') ? file.replace('design-tokens/', 'design-tokens/') : file);
119
125
  const dest = path.join(targetRefDir, destName);
120
126
 
121
127
  if (fs.existsSync(src)) {
@@ -1,12 +1,15 @@
1
1
  import fs from 'fs-extra';
2
2
  import path from 'path';
3
3
  import chalk from 'chalk';
4
+ import inquirer from 'inquirer';
5
+ import { loadConfig } from '../utils/config.js';
4
6
 
5
- export default async function promptCommand() {
7
+ export default async function promptCommand(cmdObj) {
6
8
  const rootDir = process.cwd();
7
9
  const targetRefDir = path.join(rootDir, 'references');
10
+ const config = loadConfig(rootDir) || { architecture: 'mvvm', di: 'hilt' };
8
11
 
9
- console.log(chalk.bold.magenta('\nšŸ¤– MAD Pro - Agent Instruction Generator'));
12
+ console.log(chalk.bold.magenta('\nšŸ¤– MAD Pro - Contextual Agent Instruction Generator'));
10
13
 
11
14
  if (!fs.existsSync(targetRefDir)) {
12
15
  console.log(chalk.yellow('\nNo skills found. Run `mad-pro init` first to generate a tailored prompt.'));
@@ -16,43 +19,110 @@ export default async function promptCommand() {
16
19
  // Identify installed skills
17
20
  const getFiles = (dir) => {
18
21
  let results = [];
22
+ if (!fs.existsSync(dir)) return results;
19
23
  const list = fs.readdirSync(dir);
20
24
  list.forEach(file => {
21
25
  const fullPath = path.join(dir, file);
22
26
  if (fs.statSync(fullPath).isDirectory()) {
23
- results = results.concat(getFiles(fullPath));
27
+ if (file !== 'design-tokens') {
28
+ results = results.concat(getFiles(fullPath));
29
+ }
24
30
  } else if (file.endsWith('.md')) {
25
- results.push(path.basename(file, '.md').replace(/_/g, ' ').toUpperCase());
31
+ if (!fullPath.includes('design-tokens')) {
32
+ results.push(path.basename(file, '.md').replace(/_/g, ' ').toUpperCase());
33
+ }
26
34
  }
27
35
  });
28
36
  return results;
29
37
  };
30
38
 
31
39
  const skills = getFiles(targetRefDir);
40
+ const industryDir = path.join(targetRefDir, 'industry');
41
+ const industrySkills = getFiles(industryDir);
32
42
 
33
- const prompt = `
34
- # AI AGENT INSTRUCTIONS (MAD PRO)
35
- You are an expert Android Developer using the MAD Pro v1.2.0 framework.
36
- Your goal is to build highly scalable, testable, and maintainable Android apps.
37
-
38
- ## PROJECT CONTEXT
39
- This project has the following architectural patterns installed in /references:
40
- ${skills.map(s => `- ${s}`).join('\n')}
41
-
42
- ## CORE RULES
43
- 1. ALWAYS check the matching file in /references before implementing a new feature.
44
- 2. Follow Clean Architecture: UI -> Domain -> Data layers.
45
- 3. Use Hilt for Dependency Injection.
46
- 4. Use Jetpack Compose with Unidirectional Data Flow (UDF).
47
- 5. If a requested industry pattern (e.g., Banking, E-commerce) exists in /references/industry, adhere strictly to its security and structure guidelines.
48
-
49
- ## OUTPUT STYLE
50
- - Provide production-ready Kotlin code.
51
- - Include KDoc for public functions.
52
- - Ensure all UI components follow the defined Material 3 theme.
53
- `;
54
-
55
- console.log(chalk.white('\n--- COPY THE TEXT BELOW TO YOUR SYSTEM PROMPT / .cursorrules ---\n'));
56
- console.log(chalk.cyan(prompt));
43
+ const archRules = {
44
+ 'mvvm': 'Follow MVVM architecture: UI -> ViewModel -> (UseCase) -> Repository -> Data Source.',
45
+ 'mvi': 'Follow MVI architecture: UI -> Intent -> ViewModel -> StateFlow -> UI.',
46
+ 'mvp': 'Follow MVP architecture: View <-> Presenter -> Repository.',
47
+ 'clean+mvi': 'Follow Clean + MVI: UI -> Intent -> ViewModel -> UseCase -> Repository.',
48
+ 'viper': 'Follow VIPER architecture: View, Interactor, Presenter, Entity, Router.',
49
+ 'custom': 'Follow the custom architecture patterns established in the project.'
50
+ };
51
+
52
+ const diRules = {
53
+ 'hilt': 'Use Hilt for Dependency Injection (@HiltViewModel, @Inject).',
54
+ 'koin': 'Use Koin for Dependency Injection (constructor injection, get()).',
55
+ 'none': 'Use manual Dependency Injection.'
56
+ };
57
+
58
+ const hasTokens = fs.existsSync(path.join(targetRefDir, 'design-tokens'));
59
+
60
+ const formatRes = await inquirer.prompt([{
61
+ type: 'list',
62
+ name: 'format',
63
+ message: 'Select target format:',
64
+ choices: [
65
+ { name: '.cursorrules (Cursor)', value: 'cursor' },
66
+ { name: '.windsurfrules (Windsurf)', value: 'windsurf' },
67
+ { name: 'System Prompt (Clipboard / Generic)', value: 'generic' }
68
+ ]
69
+ }]);
70
+
71
+ const promptBuilder = [];
72
+
73
+ if (formatRes.format !== 'generic') {
74
+ promptBuilder.push(`You are an expert Android Developer using the MAD Pro v1.3.0 framework.`);
75
+ promptBuilder.push(`Your goal is to build highly scalable, testable, and maintainable Android apps.\n`);
76
+ } else {
77
+ promptBuilder.push(`# AI AGENT INSTRUCTIONS (MAD PRO)`);
78
+ promptBuilder.push(`System Role: Expert Android Developer\n`);
79
+ }
80
+
81
+ promptBuilder.push(`## PROJECT CONTEXT`);
82
+ promptBuilder.push(`This project uses the following architecture: **${config.architecture.toUpperCase()}** with **${config.di.toUpperCase()}**.`);
83
+ if (skills.length > 0) {
84
+ promptBuilder.push(`Installed architectural patterns in /references:`);
85
+ skills.slice(0, 15).forEach(s => promptBuilder.push(`- ${s}`));
86
+ if (skills.length > 15) promptBuilder.push(`- ...and ${skills.length - 15} more.`);
87
+ }
88
+ promptBuilder.push('');
89
+
90
+ promptBuilder.push(`## CORE RULES`);
91
+ promptBuilder.push(`1. ALWAYS check the matching file in /references before implementing a new feature.`);
92
+ promptBuilder.push(`2. ${archRules[config.architecture] || archRules['mvvm']}`);
93
+ promptBuilder.push(`3. ${diRules[config.di] || diRules['hilt']}`);
94
+ promptBuilder.push(`4. Use Jetpack Compose with Unidirectional Data Flow (UDF).`);
95
+
96
+ if (industrySkills.length > 0) {
97
+ promptBuilder.push(`5. Adhere strictly to the industry best practices found in /references/industry/ for: ${industrySkills.join(', ')}.`);
98
+ }
99
+
100
+ promptBuilder.push(`\n## OUTPUT STYLE`);
101
+ promptBuilder.push(`- Provide production-ready Kotlin code.`);
102
+ if (hasTokens) {
103
+ promptBuilder.push(`- STYLING: You MUST use the design tokens defined in /references/design-tokens/ (colors, typography, spacing). Do not hardcode dimensions or generic colors.`);
104
+ }
105
+ promptBuilder.push(`- Ensure all UI components follow the defined Material 3 theme. Avoid outdated XML layouts unless required.`);
106
+
107
+ const finalPrompt = promptBuilder.join('\n');
108
+
109
+ console.log(chalk.white(`\n--- COPY THIS TO YOUR ${formatRes.format === 'generic' ? 'SYSTEM PROMPT' : formatRes.format === 'cursor' ? '.cursorrules' : '.windsurfrules'} ---\n`));
110
+ console.log(chalk.cyan(finalPrompt));
57
111
  console.log(chalk.white('------------------------------------------------------------------\n'));
112
+
113
+ // Optionally auto-write to file if it's cursor or windsurf
114
+ if (formatRes.format === 'cursor' || formatRes.format === 'windsurf') {
115
+ const fileName = formatRes.format === 'cursor' ? '.cursorrules' : '.windsurfrules';
116
+ const fileRes = await inquirer.prompt([{
117
+ type: 'confirm',
118
+ name: 'write',
119
+ message: `Do you want to write this to ${fileName} in the current directory?`,
120
+ default: true
121
+ }]);
122
+
123
+ if (fileRes.write) {
124
+ fs.writeFileSync(path.join(rootDir, fileName), finalPrompt);
125
+ console.log(chalk.green(`āœ“ Saved to ${fileName}`));
126
+ }
127
+ }
58
128
  }
@@ -0,0 +1,32 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+
4
+ const CONFIG_FILE = '.mad-pro.json';
5
+
6
+ export function getConfigPath(rootDir = process.cwd()) {
7
+ return path.join(rootDir, CONFIG_FILE);
8
+ }
9
+
10
+ export function loadConfig(rootDir = process.cwd()) {
11
+ const configPath = getConfigPath(rootDir);
12
+ if (fs.existsSync(configPath)) {
13
+ try {
14
+ return fs.readJsonSync(configPath);
15
+ } catch (e) {
16
+ return null;
17
+ }
18
+ }
19
+ return null;
20
+ }
21
+
22
+ export function saveConfig(config, rootDir = process.cwd()) {
23
+ const configPath = getConfigPath(rootDir);
24
+ fs.writeJsonSync(configPath, config, { spaces: 2 });
25
+ }
26
+
27
+ export function updateConfig(updates, rootDir = process.cwd()) {
28
+ const currentConfig = loadConfig(rootDir) || {};
29
+ const newConfig = { ...currentConfig, ...updates };
30
+ saveConfig(newConfig, rootDir);
31
+ return newConfig;
32
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mad-pro-cli",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "type": "module",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -27,5 +27,8 @@
27
27
  "commander": "^14.0.3",
28
28
  "fs-extra": "^11.3.4",
29
29
  "inquirer": "^12.4.2"
30
+ },
31
+ "publishConfig": {
32
+ "access": "public"
30
33
  }
31
34
  }