mad-pro-cli 1.1.0 ā 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +32 -3
- package/lib/commands/add.js +68 -0
- package/lib/commands/create.js +644 -0
- package/lib/commands/doctor.js +69 -0
- package/lib/commands/init.js +139 -52
- package/lib/commands/list.js +42 -0
- package/lib/commands/prompt.js +58 -0
- package/package.json +3 -2
package/index.js
CHANGED
|
@@ -2,17 +2,46 @@
|
|
|
2
2
|
|
|
3
3
|
import { program } from 'commander';
|
|
4
4
|
import initCommand from './lib/commands/init.js';
|
|
5
|
+
import addCommand from './lib/commands/add.js';
|
|
6
|
+
import listCommand from './lib/commands/list.js';
|
|
7
|
+
import createCommand from './lib/commands/create.js';
|
|
8
|
+
import doctorCommand from './lib/commands/doctor.js';
|
|
9
|
+
import promptCommand from './lib/commands/prompt.js';
|
|
5
10
|
|
|
6
11
|
program
|
|
7
12
|
.name('mad-pro')
|
|
8
13
|
.description('MAD Pro CLI - Boost your project with AI Expert Skills')
|
|
9
|
-
.version('1.
|
|
14
|
+
.version('1.2.0');
|
|
10
15
|
|
|
11
16
|
program
|
|
12
17
|
.command('init')
|
|
13
|
-
.description('Initialize AI skills for your project')
|
|
14
|
-
.option('--ai <name>', 'Specify the AI bridge to use (e.g., antigravity)')
|
|
18
|
+
.description('Initialize AI skills for your project (Interactive Wizard)')
|
|
15
19
|
.option('--ide <type>', 'Specify the IDE (vscode, cursor, windsurf, android-studio, intellij, sublime, vim, neovim, zed, code-insiders)')
|
|
16
20
|
.action(initCommand);
|
|
17
21
|
|
|
22
|
+
program
|
|
23
|
+
.command('create [name]')
|
|
24
|
+
.description('Scaffold a fresh MAD-compliant Android project')
|
|
25
|
+
.action(createCommand);
|
|
26
|
+
|
|
27
|
+
program
|
|
28
|
+
.command('add')
|
|
29
|
+
.description('Add specific skills to your current project')
|
|
30
|
+
.action(addCommand);
|
|
31
|
+
|
|
32
|
+
program
|
|
33
|
+
.command('list')
|
|
34
|
+
.description('List skills installed in the current project')
|
|
35
|
+
.action(listCommand);
|
|
36
|
+
|
|
37
|
+
program
|
|
38
|
+
.command('doctor')
|
|
39
|
+
.description('Check architectural health of your project')
|
|
40
|
+
.action(doctorCommand);
|
|
41
|
+
|
|
42
|
+
program
|
|
43
|
+
.command('prompt')
|
|
44
|
+
.description('Generate optimized system instructions for your AI Agent')
|
|
45
|
+
.action(promptCommand);
|
|
46
|
+
|
|
18
47
|
program.parse();
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import inquirer from 'inquirer';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = path.dirname(__filename);
|
|
9
|
+
|
|
10
|
+
export default async function addCommand() {
|
|
11
|
+
console.log(chalk.bold.magenta('\nš„ MAD Pro CLI - Add Skills'));
|
|
12
|
+
console.log(chalk.gray('Search and install specific skills to your project...\n'));
|
|
13
|
+
|
|
14
|
+
const rootDir = process.cwd();
|
|
15
|
+
const sourceDir = path.join(__dirname, '../../../references');
|
|
16
|
+
|
|
17
|
+
if (!fs.existsSync(sourceDir)) {
|
|
18
|
+
console.error(chalk.red('Error: Skill library not found. Please reinstall the CLI.'));
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// 1. Get all available skills
|
|
23
|
+
const coreFiles = fs.readdirSync(sourceDir).filter(f => f.endsWith('.md'));
|
|
24
|
+
const industryPath = path.join(sourceDir, 'industry');
|
|
25
|
+
const industryFiles = fs.readdirSync(industryPath).filter(f => f.endsWith('.md')).map(f => `industry/${f}`);
|
|
26
|
+
|
|
27
|
+
const allSkills = [...coreFiles, ...industryFiles].map(f => ({
|
|
28
|
+
name: f.replace('.md', '').replace(/_/g, ' ').toUpperCase(),
|
|
29
|
+
value: f
|
|
30
|
+
})).sort((a, b) => a.name.localeCompare(b.name));
|
|
31
|
+
|
|
32
|
+
// 2. Interactive Selection (Searchable)
|
|
33
|
+
const answers = await inquirer.prompt([
|
|
34
|
+
{
|
|
35
|
+
type: 'checkbox',
|
|
36
|
+
name: 'skills',
|
|
37
|
+
message: 'Search and select skills to add:',
|
|
38
|
+
choices: allSkills,
|
|
39
|
+
pageSize: 15
|
|
40
|
+
}
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
if (answers.skills.length === 0) {
|
|
44
|
+
console.log(chalk.yellow('\nNo skills selected. Exit.'));
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 3. Perform Copying
|
|
49
|
+
const targetRefDir = path.join(rootDir, 'references');
|
|
50
|
+
const targetIndustryDir = path.join(targetRefDir, 'industry');
|
|
51
|
+
await fs.ensureDir(targetRefDir);
|
|
52
|
+
await fs.ensureDir(targetIndustryDir);
|
|
53
|
+
|
|
54
|
+
console.log(chalk.cyan('\nš¦ Adding selected skills...'));
|
|
55
|
+
|
|
56
|
+
for (const skill of answers.skills) {
|
|
57
|
+
const src = path.join(sourceDir, skill);
|
|
58
|
+
const dest = path.join(targetRefDir, skill);
|
|
59
|
+
|
|
60
|
+
if (fs.existsSync(src)) {
|
|
61
|
+
await fs.copy(src, dest);
|
|
62
|
+
console.log(`${chalk.green('ā')} Added ${chalk.gray(skill)}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
console.log(chalk.bold.green(`\nš Successfully added ${answers.skills.length} skills!`));
|
|
67
|
+
console.log(chalk.gray('Your AI Agent can now use these additional patterns.\n'));
|
|
68
|
+
}
|
|
@@ -0,0 +1,644 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import inquirer from 'inquirer';
|
|
5
|
+
import { execSync } from 'child_process';
|
|
6
|
+
import initCommand from './init.js';
|
|
7
|
+
|
|
8
|
+
export default async function createCommand(projectName) {
|
|
9
|
+
console.log(chalk.bold.magenta('\nšļø MAD Pro - New Project Scaffold'));
|
|
10
|
+
console.log(chalk.gray('Generate a production-ready Android/KMP project.\n'));
|
|
11
|
+
|
|
12
|
+
if (!projectName) {
|
|
13
|
+
const res = await inquirer.prompt([{
|
|
14
|
+
type: 'input',
|
|
15
|
+
name: 'name',
|
|
16
|
+
message: 'Project name:',
|
|
17
|
+
default: 'MyMadApp',
|
|
18
|
+
validate: v => /^[A-Za-z][A-Za-z0-9]*$/.test(v) || 'Must start with a letter, alphanumeric only.'
|
|
19
|
+
}]);
|
|
20
|
+
projectName = res.name;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const answers = await inquirer.prompt([
|
|
24
|
+
{
|
|
25
|
+
type: 'list',
|
|
26
|
+
name: 'templateType',
|
|
27
|
+
message: 'Select Template:',
|
|
28
|
+
choices: [
|
|
29
|
+
{ name: 'KrossWave KMP (Recommended - Multi-module, KMP, Koin, Voyager)', value: 'kmp' },
|
|
30
|
+
{ name: 'Basic Android (Single module, Hilt, Navigation Compose)', value: 'basic' }
|
|
31
|
+
]
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
type: 'input',
|
|
35
|
+
name: 'package',
|
|
36
|
+
message: 'Package name:',
|
|
37
|
+
default: `com.example.${projectName.toLowerCase()}`,
|
|
38
|
+
validate: v => /^[a-z][a-z0-9]*(\.[a-z][a-z0-9]*)+$/.test(v) || 'Invalid package (e.g. com.example.app)'
|
|
39
|
+
}
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
let basicAnswers = {};
|
|
43
|
+
if (answers.templateType === 'basic') {
|
|
44
|
+
basicAnswers = await inquirer.prompt([
|
|
45
|
+
{
|
|
46
|
+
type: 'list',
|
|
47
|
+
name: 'minSdk',
|
|
48
|
+
message: 'Minimum SDK:',
|
|
49
|
+
choices: ['24', '26', '28', '30'],
|
|
50
|
+
default: '26'
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
type: 'list',
|
|
54
|
+
name: 'targetSdk',
|
|
55
|
+
message: 'Target SDK:',
|
|
56
|
+
choices: ['34', '35'],
|
|
57
|
+
default: '35'
|
|
58
|
+
}
|
|
59
|
+
]);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const targetDir = path.join(process.cwd(), projectName);
|
|
63
|
+
|
|
64
|
+
if (fs.existsSync(targetDir)) {
|
|
65
|
+
console.error(chalk.red(`\nā Directory "${projectName}" already exists.`));
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const pkg = answers.package;
|
|
70
|
+
const pkgPath = pkg.replace(/\./g, '/');
|
|
71
|
+
|
|
72
|
+
if (answers.templateType === 'kmp') {
|
|
73
|
+
console.log(chalk.cyan(`\nšØ Cloning KrossWave KMP template...\n`));
|
|
74
|
+
try {
|
|
75
|
+
execSync(`git clone --depth 1 https://github.com/derohimat/android-kmp-starter.git "${projectName}"`, { stdio: 'inherit' });
|
|
76
|
+
await fs.remove(path.join(targetDir, '.git'));
|
|
77
|
+
console.log(` ${chalk.green('ā')} Template cloned successfully`);
|
|
78
|
+
|
|
79
|
+
console.log(chalk.cyan(`\nšØ Customizing package name to ${pkg}...\n`));
|
|
80
|
+
const oldPkg = 'com.derohimat.krosswave';
|
|
81
|
+
const oldAppName = 'KrossWave'; // typical app name in that repo
|
|
82
|
+
|
|
83
|
+
async function processFolder(dir) {
|
|
84
|
+
const files = await fs.readdir(dir);
|
|
85
|
+
for (const file of files) {
|
|
86
|
+
const fullPath = path.join(dir, file);
|
|
87
|
+
const stat = await fs.stat(fullPath);
|
|
88
|
+
|
|
89
|
+
if (stat.isDirectory()) {
|
|
90
|
+
await processFolder(fullPath);
|
|
91
|
+
} else {
|
|
92
|
+
const ext = path.extname(fullPath);
|
|
93
|
+
const validExts = ['.kt', '.xml', '.gradle', '.kts', '.pro', '.properties', '.toml', '.md'];
|
|
94
|
+
if (validExts.includes(ext) || file === 'gradlew' || file === 'gradlew.bat') {
|
|
95
|
+
try {
|
|
96
|
+
let content = await fs.readFile(fullPath, 'utf8');
|
|
97
|
+
let modified = false;
|
|
98
|
+
|
|
99
|
+
if (content.includes(oldPkg)) {
|
|
100
|
+
content = content.replace(new RegExp(oldPkg, 'g'), pkg);
|
|
101
|
+
modified = true;
|
|
102
|
+
}
|
|
103
|
+
if (content.includes(oldAppName)) {
|
|
104
|
+
content = content.replace(new RegExp(oldAppName, 'g'), projectName);
|
|
105
|
+
modified = true;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (modified) {
|
|
109
|
+
await fs.writeFile(fullPath, content, 'utf8');
|
|
110
|
+
}
|
|
111
|
+
} catch (e) {
|
|
112
|
+
// ignore read errors on binary files
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
await processFolder(targetDir);
|
|
120
|
+
|
|
121
|
+
async function restructureDirs(currentDir) {
|
|
122
|
+
const items = await fs.readdir(currentDir);
|
|
123
|
+
for (const item of items) {
|
|
124
|
+
const fullPath = path.join(currentDir, item);
|
|
125
|
+
if ((await fs.stat(fullPath)).isDirectory()) {
|
|
126
|
+
if (fullPath.endsWith(path.join('com', 'derohimat', 'krosswave'))) {
|
|
127
|
+
const newPath = fullPath.replace(path.join('com', 'derohimat', 'krosswave'), path.join(...pkg.split('.')));
|
|
128
|
+
await fs.move(fullPath, newPath, { overwrite: true });
|
|
129
|
+
|
|
130
|
+
const derohimatDir = path.dirname(fullPath);
|
|
131
|
+
const comDir = path.dirname(derohimatDir);
|
|
132
|
+
try {
|
|
133
|
+
await fs.rmdir(derohimatDir);
|
|
134
|
+
await fs.rmdir(comDir);
|
|
135
|
+
} catch(e) { }
|
|
136
|
+
} else {
|
|
137
|
+
await restructureDirs(fullPath);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
await restructureDirs(targetDir);
|
|
143
|
+
|
|
144
|
+
console.log(` ${chalk.green('ā')} Package renamed to ${pkg}`);
|
|
145
|
+
console.log(` ${chalk.green('ā')} App name updated to ${projectName}\n`);
|
|
146
|
+
|
|
147
|
+
} catch (e) {
|
|
148
|
+
console.error(chalk.red(`\nā Failed to clone or process template: ${e.message}`));
|
|
149
|
+
process.exit(1);
|
|
150
|
+
}
|
|
151
|
+
} else {
|
|
152
|
+
// Generate Basic Android (existing logic)
|
|
153
|
+
console.log(chalk.cyan(`\nšØ Generating Basic Android project structure...\n`));
|
|
154
|
+
|
|
155
|
+
const compileSdk = basicAnswers.targetSdk;
|
|
156
|
+
const minSdk = basicAnswers.minSdk;
|
|
157
|
+
const targetSdk = basicAnswers.targetSdk;
|
|
158
|
+
|
|
159
|
+
const dirs = [
|
|
160
|
+
`app/src/main/java/${pkgPath}`,
|
|
161
|
+
`app/src/main/java/${pkgPath}/ui/theme`,
|
|
162
|
+
`app/src/main/java/${pkgPath}/ui/navigation`,
|
|
163
|
+
`app/src/main/java/${pkgPath}/ui/screens/home`,
|
|
164
|
+
`app/src/main/java/${pkgPath}/domain/model`,
|
|
165
|
+
`app/src/main/java/${pkgPath}/domain/usecase`,
|
|
166
|
+
`app/src/main/java/${pkgPath}/domain/repository`,
|
|
167
|
+
`app/src/main/java/${pkgPath}/data/repository`,
|
|
168
|
+
`app/src/main/java/${pkgPath}/data/remote`,
|
|
169
|
+
`app/src/main/java/${pkgPath}/data/local`,
|
|
170
|
+
`app/src/main/java/${pkgPath}/di`,
|
|
171
|
+
`app/src/main/res/drawable`,
|
|
172
|
+
`app/src/main/res/values`,
|
|
173
|
+
`app/src/main/res/mipmap-hdpi`,
|
|
174
|
+
`app/src/test/java/${pkgPath}`,
|
|
175
|
+
`app/src/androidTest/java/${pkgPath}`,
|
|
176
|
+
`gradle/wrapper`,
|
|
177
|
+
];
|
|
178
|
+
|
|
179
|
+
for (const d of dirs) {
|
|
180
|
+
await fs.ensureDir(path.join(targetDir, d));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const files = {};
|
|
184
|
+
files['build.gradle.kts'] = `// Top-level build file
|
|
185
|
+
plugins {
|
|
186
|
+
alias(libs.plugins.android.application) apply false
|
|
187
|
+
alias(libs.plugins.kotlin.android) apply false
|
|
188
|
+
alias(libs.plugins.kotlin.compose) apply false
|
|
189
|
+
alias(libs.plugins.hilt.android) apply false
|
|
190
|
+
alias(libs.plugins.ksp) apply false
|
|
191
|
+
}
|
|
192
|
+
`;
|
|
193
|
+
|
|
194
|
+
files['settings.gradle.kts'] = `pluginManagement {
|
|
195
|
+
repositories {
|
|
196
|
+
google {
|
|
197
|
+
content {
|
|
198
|
+
includeGroupByRegex("com\\\\.android.*")
|
|
199
|
+
includeGroupByRegex("com\\\\.google.*")
|
|
200
|
+
includeGroupByRegex("androidx.*")
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
mavenCentral()
|
|
204
|
+
gradlePluginPortal()
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
dependencyResolutionManagement {
|
|
208
|
+
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
|
209
|
+
repositories {
|
|
210
|
+
google()
|
|
211
|
+
mavenCentral()
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
rootProject.name = "${projectName}"
|
|
216
|
+
include(":app")
|
|
217
|
+
`;
|
|
218
|
+
|
|
219
|
+
files['gradle/libs.versions.toml'] = `[versions]
|
|
220
|
+
agp = "8.7.3"
|
|
221
|
+
kotlin = "2.1.0"
|
|
222
|
+
ksp = "2.1.0-1.0.29"
|
|
223
|
+
coreKtx = "1.15.0"
|
|
224
|
+
lifecycle = "2.8.7"
|
|
225
|
+
activityCompose = "1.9.3"
|
|
226
|
+
composeBom = "2024.12.01"
|
|
227
|
+
hilt = "2.53.1"
|
|
228
|
+
hiltNavCompose = "1.2.0"
|
|
229
|
+
navigation = "2.8.5"
|
|
230
|
+
coroutines = "1.9.0"
|
|
231
|
+
junit = "4.13.2"
|
|
232
|
+
junitExt = "1.2.1"
|
|
233
|
+
espresso = "3.6.1"
|
|
234
|
+
|
|
235
|
+
[libraries]
|
|
236
|
+
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
|
237
|
+
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycle" }
|
|
238
|
+
androidx-lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycle" }
|
|
239
|
+
androidx-lifecycle-runtime-compose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "lifecycle" }
|
|
240
|
+
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
|
|
241
|
+
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
|
|
242
|
+
androidx-ui = { group = "androidx.compose.ui", name = "ui" }
|
|
243
|
+
androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
|
|
244
|
+
androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
|
|
245
|
+
androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
|
|
246
|
+
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
|
|
247
|
+
androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigation" }
|
|
248
|
+
hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" }
|
|
249
|
+
hilt-compiler = { group = "com.google.dagger", name = "hilt-compiler", version.ref = "hilt" }
|
|
250
|
+
hilt-navigation-compose = { group = "androidx.hilt", name = "hilt-navigation-compose", version.ref = "hiltNavCompose" }
|
|
251
|
+
kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "coroutines" }
|
|
252
|
+
junit = { group = "junit", name = "junit", version.ref = "junit" }
|
|
253
|
+
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitExt" }
|
|
254
|
+
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espresso" }
|
|
255
|
+
|
|
256
|
+
[plugins]
|
|
257
|
+
android-application = { id = "com.android.application", version.ref = "agp" }
|
|
258
|
+
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
|
259
|
+
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
|
260
|
+
hilt-android = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }
|
|
261
|
+
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
|
|
262
|
+
`;
|
|
263
|
+
|
|
264
|
+
files['app/build.gradle.kts'] = `plugins {
|
|
265
|
+
alias(libs.plugins.android.application)
|
|
266
|
+
alias(libs.plugins.kotlin.android)
|
|
267
|
+
alias(libs.plugins.kotlin.compose)
|
|
268
|
+
alias(libs.plugins.hilt.android)
|
|
269
|
+
alias(libs.plugins.ksp)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
android {
|
|
273
|
+
namespace = "${pkg}"
|
|
274
|
+
compileSdk = ${compileSdk}
|
|
275
|
+
|
|
276
|
+
defaultConfig {
|
|
277
|
+
applicationId = "${pkg}"
|
|
278
|
+
minSdk = ${minSdk}
|
|
279
|
+
targetSdk = ${targetSdk}
|
|
280
|
+
versionCode = 1
|
|
281
|
+
versionName = "1.0"
|
|
282
|
+
|
|
283
|
+
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
buildTypes {
|
|
287
|
+
release {
|
|
288
|
+
isMinifyEnabled = true
|
|
289
|
+
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
compileOptions {
|
|
294
|
+
sourceCompatibility = JavaVersion.VERSION_17
|
|
295
|
+
targetCompatibility = JavaVersion.VERSION_17
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
kotlinOptions {
|
|
299
|
+
jvmTarget = "17"
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
buildFeatures {
|
|
303
|
+
compose = true
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
dependencies {
|
|
308
|
+
// Core
|
|
309
|
+
implementation(libs.androidx.core.ktx)
|
|
310
|
+
implementation(libs.androidx.lifecycle.runtime.ktx)
|
|
311
|
+
implementation(libs.androidx.lifecycle.viewmodel.compose)
|
|
312
|
+
implementation(libs.androidx.lifecycle.runtime.compose)
|
|
313
|
+
implementation(libs.androidx.activity.compose)
|
|
314
|
+
|
|
315
|
+
// Compose
|
|
316
|
+
implementation(platform(libs.androidx.compose.bom))
|
|
317
|
+
implementation(libs.androidx.ui)
|
|
318
|
+
implementation(libs.androidx.ui.graphics)
|
|
319
|
+
implementation(libs.androidx.ui.tooling.preview)
|
|
320
|
+
implementation(libs.androidx.material3)
|
|
321
|
+
|
|
322
|
+
// Navigation
|
|
323
|
+
implementation(libs.androidx.navigation.compose)
|
|
324
|
+
|
|
325
|
+
// Hilt
|
|
326
|
+
implementation(libs.hilt.android)
|
|
327
|
+
ksp(libs.hilt.compiler)
|
|
328
|
+
implementation(libs.hilt.navigation.compose)
|
|
329
|
+
|
|
330
|
+
// Coroutines
|
|
331
|
+
implementation(libs.kotlinx.coroutines.android)
|
|
332
|
+
|
|
333
|
+
// Testing
|
|
334
|
+
testImplementation(libs.junit)
|
|
335
|
+
androidTestImplementation(libs.androidx.junit)
|
|
336
|
+
androidTestImplementation(libs.androidx.espresso.core)
|
|
337
|
+
debugImplementation(libs.androidx.ui.tooling)
|
|
338
|
+
}
|
|
339
|
+
`;
|
|
340
|
+
|
|
341
|
+
files['gradle.properties'] = `# Project-wide Gradle settings.
|
|
342
|
+
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
|
343
|
+
org.gradle.parallel=true
|
|
344
|
+
org.gradle.caching=true
|
|
345
|
+
android.useAndroidX=true
|
|
346
|
+
kotlin.code.style=official
|
|
347
|
+
android.nonTransitiveRClass=true
|
|
348
|
+
`;
|
|
349
|
+
|
|
350
|
+
files['gradle/wrapper/gradle-wrapper.properties'] = `distributionBase=GRADLE_USER_HOME
|
|
351
|
+
distributionPath=wrapper/dists
|
|
352
|
+
distributionUrl=https\\://services.gradle.org/distributions/gradle-8.9-bin.zip
|
|
353
|
+
networkTimeout=10000
|
|
354
|
+
validateDistributionUrl=true
|
|
355
|
+
zipStoreBase=GRADLE_USER_HOME
|
|
356
|
+
zipStorePath=wrapper/dists
|
|
357
|
+
`;
|
|
358
|
+
|
|
359
|
+
files['app/src/main/AndroidManifest.xml'] = `<?xml version="1.0" encoding="utf-8"?>
|
|
360
|
+
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
361
|
+
|
|
362
|
+
<application
|
|
363
|
+
android:name=".${projectName}Application"
|
|
364
|
+
android:allowBackup=true
|
|
365
|
+
android:icon="@mipmap/ic_launcher"
|
|
366
|
+
android:label="${projectName}"
|
|
367
|
+
android:supportsRtl="true"
|
|
368
|
+
android:theme="@android:style/Theme.Material.Light.NoActionBar">
|
|
369
|
+
<activity
|
|
370
|
+
android:name=".MainActivity"
|
|
371
|
+
android:exported="true">
|
|
372
|
+
<intent-filter>
|
|
373
|
+
<action android:name="android.intent.action.MAIN" />
|
|
374
|
+
<category android:name="android.intent.category.LAUNCHER" />
|
|
375
|
+
</intent-filter>
|
|
376
|
+
</activity>
|
|
377
|
+
</application>
|
|
378
|
+
|
|
379
|
+
</manifest>
|
|
380
|
+
`;
|
|
381
|
+
|
|
382
|
+
files[`app/src/main/java/${pkgPath}/${projectName}Application.kt`] = `package ${pkg}
|
|
383
|
+
|
|
384
|
+
import android.app.Application
|
|
385
|
+
import dagger.hilt.android.HiltAndroidApp
|
|
386
|
+
|
|
387
|
+
@HiltAndroidApp
|
|
388
|
+
class ${projectName}Application : Application()
|
|
389
|
+
`;
|
|
390
|
+
|
|
391
|
+
files[`app/src/main/java/${pkgPath}/MainActivity.kt`] = `package ${pkg}
|
|
392
|
+
|
|
393
|
+
import android.os.Bundle
|
|
394
|
+
import androidx.activity.ComponentActivity
|
|
395
|
+
import androidx.activity.compose.setContent
|
|
396
|
+
import androidx.activity.enableEdgeToEdge
|
|
397
|
+
import ${pkg}.ui.navigation.AppNavHost
|
|
398
|
+
import ${pkg}.ui.theme.${projectName}Theme
|
|
399
|
+
import dagger.hilt.android.AndroidEntryPoint
|
|
400
|
+
|
|
401
|
+
@AndroidEntryPoint
|
|
402
|
+
class MainActivity : ComponentActivity() {
|
|
403
|
+
override fun onCreate(savedInstanceState: Bundle?) {
|
|
404
|
+
super.onCreate(savedInstanceState)
|
|
405
|
+
enableEdgeToEdge()
|
|
406
|
+
setContent {
|
|
407
|
+
${projectName}Theme {
|
|
408
|
+
AppNavHost()
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
`;
|
|
414
|
+
|
|
415
|
+
files[`app/src/main/java/${pkgPath}/ui/theme/Color.kt`] = `package ${pkg}.ui.theme
|
|
416
|
+
|
|
417
|
+
import androidx.compose.ui.graphics.Color
|
|
418
|
+
|
|
419
|
+
val Purple80 = Color(0xFFD0BCFF)
|
|
420
|
+
val PurpleGrey80 = Color(0xFFCCC2DC)
|
|
421
|
+
val Pink80 = Color(0xFFEFB8C8)
|
|
422
|
+
|
|
423
|
+
val Purple40 = Color(0xFF6650a4)
|
|
424
|
+
val PurpleGrey40 = Color(0xFF625b71)
|
|
425
|
+
val Pink40 = Color(0xFF7D5260)
|
|
426
|
+
`;
|
|
427
|
+
|
|
428
|
+
files[`app/src/main/java/${pkgPath}/ui/theme/Type.kt`] = `package ${pkg}.ui.theme
|
|
429
|
+
|
|
430
|
+
import androidx.compose.material3.Typography
|
|
431
|
+
import androidx.compose.ui.text.TextStyle
|
|
432
|
+
import androidx.compose.ui.text.font.FontWeight
|
|
433
|
+
import androidx.compose.ui.unit.sp
|
|
434
|
+
|
|
435
|
+
val Typography = Typography(
|
|
436
|
+
displayLarge = TextStyle(
|
|
437
|
+
fontWeight = FontWeight.Bold,
|
|
438
|
+
fontSize = 57.sp,
|
|
439
|
+
lineHeight = 64.sp,
|
|
440
|
+
letterSpacing = (-0.25).sp
|
|
441
|
+
),
|
|
442
|
+
headlineMedium = TextStyle(
|
|
443
|
+
fontWeight = FontWeight.SemiBold,
|
|
444
|
+
fontSize = 28.sp,
|
|
445
|
+
lineHeight = 36.sp
|
|
446
|
+
),
|
|
447
|
+
bodyLarge = TextStyle(
|
|
448
|
+
fontWeight = FontWeight.Normal,
|
|
449
|
+
fontSize = 16.sp,
|
|
450
|
+
lineHeight = 24.sp,
|
|
451
|
+
letterSpacing = 0.5.sp
|
|
452
|
+
),
|
|
453
|
+
labelSmall = TextStyle(
|
|
454
|
+
fontWeight = FontWeight.Medium,
|
|
455
|
+
fontSize = 11.sp,
|
|
456
|
+
lineHeight = 16.sp,
|
|
457
|
+
letterSpacing = 0.5.sp
|
|
458
|
+
)
|
|
459
|
+
)
|
|
460
|
+
`;
|
|
461
|
+
|
|
462
|
+
files[`app/src/main/java/${pkgPath}/ui/theme/Theme.kt`] = `package ${pkg}.ui.theme
|
|
463
|
+
|
|
464
|
+
import android.os.Build
|
|
465
|
+
import androidx.compose.foundation.isSystemInDarkTheme
|
|
466
|
+
import androidx.compose.material3.MaterialTheme
|
|
467
|
+
import androidx.compose.material3.darkColorScheme
|
|
468
|
+
import androidx.compose.material3.dynamicDarkColorScheme
|
|
469
|
+
import androidx.compose.material3.dynamicLightColorScheme
|
|
470
|
+
import androidx.compose.material3.lightColorScheme
|
|
471
|
+
import androidx.compose.runtime.Composable
|
|
472
|
+
import androidx.compose.ui.platform.LocalContext
|
|
473
|
+
|
|
474
|
+
private val DarkColorScheme = darkColorScheme(
|
|
475
|
+
primary = Purple80,
|
|
476
|
+
secondary = PurpleGrey80,
|
|
477
|
+
tertiary = Pink80
|
|
478
|
+
)
|
|
479
|
+
|
|
480
|
+
private val LightColorScheme = lightColorScheme(
|
|
481
|
+
primary = Purple40,
|
|
482
|
+
secondary = PurpleGrey40,
|
|
483
|
+
tertiary = Pink40
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
@Composable
|
|
487
|
+
fun ${projectName}Theme(
|
|
488
|
+
darkTheme: Boolean = isSystemInDarkTheme(),
|
|
489
|
+
dynamicColor: Boolean = true,
|
|
490
|
+
content: @Composable () -> Unit
|
|
491
|
+
) {
|
|
492
|
+
val colorScheme = when {
|
|
493
|
+
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
|
494
|
+
val context = LocalContext.current
|
|
495
|
+
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
|
|
496
|
+
}
|
|
497
|
+
darkTheme -> DarkColorScheme
|
|
498
|
+
else -> LightColorScheme
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
MaterialTheme(
|
|
502
|
+
colorScheme = colorScheme,
|
|
503
|
+
typography = Typography,
|
|
504
|
+
content = content
|
|
505
|
+
)
|
|
506
|
+
}
|
|
507
|
+
`;
|
|
508
|
+
|
|
509
|
+
files[`app/src/main/java/${pkgPath}/ui/navigation/AppNavHost.kt`] = `package ${pkg}.ui.navigation
|
|
510
|
+
|
|
511
|
+
import androidx.compose.runtime.Composable
|
|
512
|
+
import androidx.navigation.compose.NavHost
|
|
513
|
+
import androidx.navigation.compose.composable
|
|
514
|
+
import androidx.navigation.compose.rememberNavController
|
|
515
|
+
import ${pkg}.ui.screens.home.HomeScreen
|
|
516
|
+
|
|
517
|
+
@Composable
|
|
518
|
+
fun AppNavHost() {
|
|
519
|
+
val navController = rememberNavController()
|
|
520
|
+
NavHost(navController = navController, startDestination = "home") {
|
|
521
|
+
composable("home") {
|
|
522
|
+
HomeScreen()
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
`;
|
|
527
|
+
|
|
528
|
+
files[`app/src/main/java/${pkgPath}/ui/screens/home/HomeScreen.kt`] = `package ${pkg}.ui.screens.home
|
|
529
|
+
|
|
530
|
+
import androidx.compose.foundation.layout.Box
|
|
531
|
+
import androidx.compose.foundation.layout.fillMaxSize
|
|
532
|
+
import androidx.compose.foundation.layout.padding
|
|
533
|
+
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
534
|
+
import androidx.compose.material3.MaterialTheme
|
|
535
|
+
import androidx.compose.material3.Scaffold
|
|
536
|
+
import androidx.compose.material3.Text
|
|
537
|
+
import androidx.compose.material3.TopAppBar
|
|
538
|
+
import androidx.compose.runtime.Composable
|
|
539
|
+
import androidx.compose.ui.Alignment
|
|
540
|
+
import androidx.compose.ui.Modifier
|
|
541
|
+
import androidx.hilt.navigation.compose.hiltViewModel
|
|
542
|
+
|
|
543
|
+
@OptIn(ExperimentalMaterial3Api::class)
|
|
544
|
+
@Composable
|
|
545
|
+
fun HomeScreen(viewModel: HomeViewModel = hiltViewModel()) {
|
|
546
|
+
Scaffold(
|
|
547
|
+
topBar = {
|
|
548
|
+
TopAppBar(title = { Text("${projectName}") })
|
|
549
|
+
}
|
|
550
|
+
) { padding ->
|
|
551
|
+
Box(
|
|
552
|
+
modifier = Modifier
|
|
553
|
+
.fillMaxSize()
|
|
554
|
+
.padding(padding),
|
|
555
|
+
contentAlignment = Alignment.Center
|
|
556
|
+
) {
|
|
557
|
+
Text(
|
|
558
|
+
text = "Welcome to ${projectName}!",
|
|
559
|
+
style = MaterialTheme.typography.headlineMedium
|
|
560
|
+
)
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
`;
|
|
565
|
+
|
|
566
|
+
files[`app/src/main/java/${pkgPath}/ui/screens/home/HomeViewModel.kt`] = `package ${pkg}.ui.screens.home
|
|
567
|
+
|
|
568
|
+
import androidx.lifecycle.ViewModel
|
|
569
|
+
import dagger.hilt.android.lifecycle.HiltViewModel
|
|
570
|
+
import javax.inject.Inject
|
|
571
|
+
|
|
572
|
+
@HiltViewModel
|
|
573
|
+
class HomeViewModel @Inject constructor() : ViewModel()
|
|
574
|
+
`;
|
|
575
|
+
|
|
576
|
+
files[`app/src/main/java/${pkgPath}/di/AppModule.kt`] = `package ${pkg}.di
|
|
577
|
+
|
|
578
|
+
import dagger.Module
|
|
579
|
+
import dagger.hilt.InstallIn
|
|
580
|
+
import dagger.hilt.components.SingletonComponent
|
|
581
|
+
|
|
582
|
+
@Module
|
|
583
|
+
@InstallIn(SingletonComponent::class)
|
|
584
|
+
object AppModule {
|
|
585
|
+
// Provide your app-wide dependencies here
|
|
586
|
+
}
|
|
587
|
+
`;
|
|
588
|
+
|
|
589
|
+
files['app/src/main/res/values/strings.xml'] = `<resources>
|
|
590
|
+
<string name="app_name">${projectName}</string>
|
|
591
|
+
</resources>
|
|
592
|
+
`;
|
|
593
|
+
|
|
594
|
+
files['app/src/main/res/values/colors.xml'] = `<?xml version="1.0" encoding="utf-8"?>
|
|
595
|
+
<resources>
|
|
596
|
+
<color name="black">#FF000000</color>
|
|
597
|
+
<color name="white">#FFFFFFFF</color>
|
|
598
|
+
</resources>
|
|
599
|
+
`;
|
|
600
|
+
|
|
601
|
+
files['app/proguard-rules.pro'] = `# Add project specific ProGuard rules here.
|
|
602
|
+
# -keep class ${pkg}.data.remote.dto.** { *; }
|
|
603
|
+
`;
|
|
604
|
+
|
|
605
|
+
files['.gitignore'] = `*.iml
|
|
606
|
+
.gradle
|
|
607
|
+
/local.properties
|
|
608
|
+
/.idea
|
|
609
|
+
.DS_Store
|
|
610
|
+
/build
|
|
611
|
+
/captures
|
|
612
|
+
.externalNativeBuild
|
|
613
|
+
.cxx
|
|
614
|
+
local.properties
|
|
615
|
+
`;
|
|
616
|
+
|
|
617
|
+
let fileCount = 0;
|
|
618
|
+
for (const [filePath, content] of Object.entries(files)) {
|
|
619
|
+
const fullPath = path.join(targetDir, filePath);
|
|
620
|
+
await fs.ensureDir(path.dirname(fullPath));
|
|
621
|
+
await fs.writeFile(fullPath, content);
|
|
622
|
+
fileCount++;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
console.log(` ${chalk.green('ā')} Generated ${fileCount} files.`);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// āā Chain to Skill Wizard āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
629
|
+
console.log(chalk.yellow('\nā© Launching Skill Wizard...\n'));
|
|
630
|
+
|
|
631
|
+
const originalCwd = process.cwd();
|
|
632
|
+
process.chdir(targetDir);
|
|
633
|
+
await initCommand({});
|
|
634
|
+
process.chdir(originalCwd);
|
|
635
|
+
|
|
636
|
+
console.log(chalk.bold.green(`\nš Project "${projectName}" is ready!`));
|
|
637
|
+
console.log(chalk.white(`\n cd ${projectName}`));
|
|
638
|
+
|
|
639
|
+
if (answers.templateType === 'kmp') {
|
|
640
|
+
console.log(chalk.white(` ./gradlew composeApp:run\n`));
|
|
641
|
+
} else {
|
|
642
|
+
console.log(chalk.white(` ./gradlew assembleDebug\n`));
|
|
643
|
+
}
|
|
644
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
|
|
5
|
+
export default async function doctorCommand() {
|
|
6
|
+
console.log(chalk.bold.magenta('\n𩺠MAD Pro Doctor - Project Audit'));
|
|
7
|
+
console.log(chalk.gray('Checking your project for architectural health...\n'));
|
|
8
|
+
|
|
9
|
+
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
|
+
}
|
|
41
|
+
|
|
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}`));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (issues === 0) {
|
|
53
|
+
console.log(chalk.bold.green('\nš Your project looks 100% MAD-compliant!'));
|
|
54
|
+
} else {
|
|
55
|
+
console.log(chalk.bold.yellow(`\nā ļø Found ${issues} items to improve.`));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
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;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return false;
|
|
69
|
+
}
|
package/lib/commands/init.js
CHANGED
|
@@ -1,63 +1,150 @@
|
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
11
|
+
console.log(chalk.bold.magenta('\nš MAD Pro CLI - Skill Wizard v1.2.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
|
+
]
|
|
52
|
+
}
|
|
53
|
+
]);
|
|
54
|
+
|
|
55
|
+
// 3. Selective Industry prompt (only if 'industry' is selected)
|
|
56
|
+
let selectedIndustries = [];
|
|
57
|
+
if (answers.categories.includes('industry')) {
|
|
58
|
+
const industryPath = path.join(sourceDir, 'industry');
|
|
59
|
+
const allIndustries = fs.readdirSync(industryPath).filter(f => f.endsWith('.md') && f !== 'google_play_subscriptions.md' && f !== 'in_app_payments.md');
|
|
60
|
+
|
|
61
|
+
const industryChoices = await inquirer.prompt([
|
|
62
|
+
{
|
|
63
|
+
type: 'checkbox',
|
|
64
|
+
name: 'industries',
|
|
65
|
+
message: 'Which Industry Verticals do you need?',
|
|
66
|
+
choices: allIndustries.map(f => ({ name: f.replace('.md', '').replace(/_/g, ' ').toUpperCase(), value: f }))
|
|
67
|
+
}
|
|
68
|
+
]);
|
|
69
|
+
selectedIndustries = industryChoices.industries;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// 4. Create Target Directories
|
|
73
|
+
const targetRefDir = path.join(rootDir, 'references');
|
|
74
|
+
const targetIndustryDir = path.join(targetRefDir, 'industry');
|
|
75
|
+
await fs.ensureDir(targetRefDir);
|
|
76
|
+
if (selectedIndustries.length > 0 || answers.categories.includes('monetization')) {
|
|
77
|
+
await fs.ensureDir(targetIndustryDir);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// 5. Build Mapping & Copy Files
|
|
81
|
+
const skillMapping = {
|
|
82
|
+
'core': [
|
|
83
|
+
'ui_layer_state.md', 'ui_patterns.md', 'ui_layouts.md', 'ui_modifiers.md', 'ui_theming.md', 'ui_navigation.md',
|
|
84
|
+
'domain_layer_use_case.md', 'data_layer_room.md', 'data_layer_networking.md', 'data_layer_serialization.md'
|
|
85
|
+
],
|
|
86
|
+
'platform': [
|
|
87
|
+
'camera_media.md', 'barcode_qr.md', 'image_editing.md', 'voice_speech.md', 'biometric_auth.md', 'security.md',
|
|
88
|
+
'maps_location.md', 'push_notifications.md', 'widget_glance.md', 'app_shortcuts.md'
|
|
89
|
+
],
|
|
90
|
+
'ai': ['gemini_api.md', 'llm_ui_patterns.md', 'ar_core.md', 'on_device_ai.md'],
|
|
91
|
+
'monetization': ['industry/in_app_payments.md', 'industry/google_play_subscriptions.md'],
|
|
92
|
+
'engineering': [
|
|
93
|
+
'modularization.md', 'architecture_di.md', 'concurrency.md', 'performance.md', 'testing.md',
|
|
94
|
+
'design_systems.md', 'observability.md', 'ci_cd.md'
|
|
95
|
+
]
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
let filesToCopy = [];
|
|
99
|
+
answers.categories.forEach(cat => {
|
|
100
|
+
if (skillMapping[cat]) {
|
|
101
|
+
filesToCopy = [...filesToCopy, ...skillMapping[cat]];
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// Add individual industry files
|
|
106
|
+
selectedIndustries.forEach(industryFile => {
|
|
107
|
+
filesToCopy.push(`industry/${industryFile}`);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// Always include the master index but pruned
|
|
111
|
+
filesToCopy.push('../SKILL.md');
|
|
112
|
+
|
|
113
|
+
// Perform Copying
|
|
114
|
+
console.log(chalk.cyan('\nš¦ Installing selected skills...'));
|
|
115
|
+
|
|
116
|
+
for (const file of filesToCopy) {
|
|
117
|
+
const src = path.join(sourceDir, file);
|
|
118
|
+
const destName = file.includes('industry/') ? file.replace('industry/', 'industry/') : file;
|
|
119
|
+
const dest = path.join(targetRefDir, destName);
|
|
120
|
+
|
|
121
|
+
if (fs.existsSync(src)) {
|
|
122
|
+
await fs.copy(src, dest);
|
|
123
|
+
console.log(`${chalk.green('ā')} Added ${chalk.gray(destName)}`);
|
|
58
124
|
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// 6. Project Configuration (IDE Specific)
|
|
128
|
+
await configureIDE(answers.ide, rootDir);
|
|
129
|
+
|
|
130
|
+
console.log(chalk.bold.green('\nš Setup Complete!'));
|
|
131
|
+
console.log(chalk.white(`Configured IDE: ${chalk.bold.yellow(answers.ide.toUpperCase())}`));
|
|
132
|
+
console.log(chalk.white(`Skills Installed: ${chalk.bold.yellow(filesToCopy.length - 1)} items`));
|
|
133
|
+
console.log(chalk.gray('\nYour AI Agent is now ready with expert context. Happy coding! š\n'));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function configureIDE(ide, rootDir) {
|
|
137
|
+
const configFiles = {
|
|
138
|
+
'cursor': { file: '.cursorrules', content: 'Use the patterns in the /references directory for all Android work.' },
|
|
139
|
+
'windsurf': { file: '.windsurf/rules', content: 'Follow MAD Pro architecture in /references folder.' },
|
|
140
|
+
'vscode': { file: '.vscode/settings.json', content: '{\n "android.skillPath": "./references"\n}' }
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const config = configFiles[ide];
|
|
144
|
+
if (config) {
|
|
145
|
+
const targetFile = path.join(rootDir, config.file);
|
|
146
|
+
await fs.ensureDir(path.dirname(targetFile));
|
|
147
|
+
await fs.writeFile(targetFile, config.content);
|
|
148
|
+
console.log(`${chalk.blue('ā¹')} Created IDE config: ${chalk.gray(config.file)}`);
|
|
62
149
|
}
|
|
63
150
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
|
|
5
|
+
export default async function listCommand() {
|
|
6
|
+
const rootDir = process.cwd();
|
|
7
|
+
const targetRefDir = path.join(rootDir, 'references');
|
|
8
|
+
|
|
9
|
+
console.log(chalk.bold.magenta('\nš MAD Pro Installed Skills'));
|
|
10
|
+
|
|
11
|
+
if (!fs.existsSync(targetRefDir)) {
|
|
12
|
+
console.log(chalk.yellow('\nNo skills initialized in this project. Run `mad-pro init` first.'));
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const getFiles = (dir) => {
|
|
17
|
+
let results = [];
|
|
18
|
+
const list = fs.readdirSync(dir);
|
|
19
|
+
list.forEach(file => {
|
|
20
|
+
const fullPath = path.join(dir, file);
|
|
21
|
+
const stat = fs.statSync(fullPath);
|
|
22
|
+
if (stat && stat.isDirectory()) {
|
|
23
|
+
results = results.concat(getFiles(fullPath));
|
|
24
|
+
} else if (file.endsWith('.md')) {
|
|
25
|
+
results.push(path.relative(targetRefDir, fullPath));
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
return results;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const installed = getFiles(targetRefDir);
|
|
32
|
+
|
|
33
|
+
if (installed.length === 0) {
|
|
34
|
+
console.log(chalk.gray('Zero skills found in /references directory.'));
|
|
35
|
+
} else {
|
|
36
|
+
installed.sort().forEach(file => {
|
|
37
|
+
console.log(`${chalk.green('ā')} ${file.replace('.md', '').toUpperCase().replace(/_/g, ' ')}`);
|
|
38
|
+
});
|
|
39
|
+
console.log(chalk.cyan(`\nTotal: ${installed.length} skills active.`));
|
|
40
|
+
}
|
|
41
|
+
console.log("");
|
|
42
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
|
|
5
|
+
export default async function promptCommand() {
|
|
6
|
+
const rootDir = process.cwd();
|
|
7
|
+
const targetRefDir = path.join(rootDir, 'references');
|
|
8
|
+
|
|
9
|
+
console.log(chalk.bold.magenta('\nš¤ MAD Pro - Agent Instruction Generator'));
|
|
10
|
+
|
|
11
|
+
if (!fs.existsSync(targetRefDir)) {
|
|
12
|
+
console.log(chalk.yellow('\nNo skills found. Run `mad-pro init` first to generate a tailored prompt.'));
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Identify installed skills
|
|
17
|
+
const getFiles = (dir) => {
|
|
18
|
+
let results = [];
|
|
19
|
+
const list = fs.readdirSync(dir);
|
|
20
|
+
list.forEach(file => {
|
|
21
|
+
const fullPath = path.join(dir, file);
|
|
22
|
+
if (fs.statSync(fullPath).isDirectory()) {
|
|
23
|
+
results = results.concat(getFiles(fullPath));
|
|
24
|
+
} else if (file.endsWith('.md')) {
|
|
25
|
+
results.push(path.basename(file, '.md').replace(/_/g, ' ').toUpperCase());
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
return results;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const skills = getFiles(targetRefDir);
|
|
32
|
+
|
|
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));
|
|
57
|
+
console.log(chalk.white('------------------------------------------------------------------\n'));
|
|
58
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mad-pro-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"chalk": "^5.6.2",
|
|
27
27
|
"commander": "^14.0.3",
|
|
28
|
-
"fs-extra": "^11.3.4"
|
|
28
|
+
"fs-extra": "^11.3.4",
|
|
29
|
+
"inquirer": "^12.4.2"
|
|
29
30
|
}
|
|
30
31
|
}
|