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.
@@ -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
+ }