@jetstart/cli 1.2.0 → 1.5.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.
Files changed (67) hide show
  1. package/dist/cli.js +18 -1
  2. package/dist/commands/android-emulator.d.ts +8 -0
  3. package/dist/commands/android-emulator.js +280 -0
  4. package/dist/commands/create.d.ts +1 -6
  5. package/dist/commands/create.js +119 -0
  6. package/dist/commands/dev.d.ts +1 -7
  7. package/dist/commands/dev.js +69 -10
  8. package/dist/commands/index.d.ts +2 -0
  9. package/dist/commands/index.js +5 -1
  10. package/dist/commands/install-audit.d.ts +9 -0
  11. package/dist/commands/install-audit.js +185 -0
  12. package/dist/types/index.d.ts +22 -0
  13. package/dist/types/index.js +8 -0
  14. package/dist/utils/android-sdk.d.ts +81 -0
  15. package/dist/utils/android-sdk.js +432 -0
  16. package/dist/utils/downloader.d.ts +35 -0
  17. package/dist/utils/downloader.js +214 -0
  18. package/dist/utils/emulator-deployer.d.ts +29 -0
  19. package/dist/utils/emulator-deployer.js +224 -0
  20. package/dist/utils/emulator.d.ts +101 -0
  21. package/dist/utils/emulator.js +410 -0
  22. package/dist/utils/java.d.ts +25 -0
  23. package/dist/utils/java.js +363 -0
  24. package/dist/utils/system-tools.d.ts +93 -0
  25. package/dist/utils/system-tools.js +599 -0
  26. package/dist/utils/template.js +777 -748
  27. package/package.json +7 -3
  28. package/src/cli.ts +20 -2
  29. package/src/commands/android-emulator.ts +304 -0
  30. package/src/commands/create.ts +128 -5
  31. package/src/commands/dev.ts +71 -18
  32. package/src/commands/index.ts +3 -1
  33. package/src/commands/install-audit.ts +227 -0
  34. package/src/types/index.ts +30 -0
  35. package/src/utils/android-sdk.ts +478 -0
  36. package/src/utils/downloader.ts +201 -0
  37. package/src/utils/emulator-deployer.ts +210 -0
  38. package/src/utils/emulator.ts +463 -0
  39. package/src/utils/java.ts +369 -0
  40. package/src/utils/system-tools.ts +648 -0
  41. package/src/utils/template.ts +875 -867
  42. package/dist/cli.d.ts.map +0 -1
  43. package/dist/cli.js.map +0 -1
  44. package/dist/commands/build.d.ts.map +0 -1
  45. package/dist/commands/build.js.map +0 -1
  46. package/dist/commands/create.d.ts.map +0 -1
  47. package/dist/commands/create.js.map +0 -1
  48. package/dist/commands/dev.d.ts.map +0 -1
  49. package/dist/commands/dev.js.map +0 -1
  50. package/dist/commands/index.d.ts.map +0 -1
  51. package/dist/commands/index.js.map +0 -1
  52. package/dist/commands/logs.d.ts.map +0 -1
  53. package/dist/commands/logs.js.map +0 -1
  54. package/dist/index.d.ts.map +0 -1
  55. package/dist/index.js.map +0 -1
  56. package/dist/types/index.d.ts.map +0 -1
  57. package/dist/types/index.js.map +0 -1
  58. package/dist/utils/index.d.ts.map +0 -1
  59. package/dist/utils/index.js.map +0 -1
  60. package/dist/utils/logger.d.ts.map +0 -1
  61. package/dist/utils/logger.js.map +0 -1
  62. package/dist/utils/prompt.d.ts.map +0 -1
  63. package/dist/utils/prompt.js.map +0 -1
  64. package/dist/utils/spinner.d.ts.map +0 -1
  65. package/dist/utils/spinner.js.map +0 -1
  66. package/dist/utils/template.d.ts.map +0 -1
  67. package/dist/utils/template.js.map +0 -1
@@ -25,9 +25,7 @@ export async function generateProjectTemplate(
25
25
  await generateGradleProperties(projectPath);
26
26
  await generateGradleWrapper(projectPath);
27
27
  await generateMainActivity(projectPath, packageName);
28
- await generateHotReload(projectPath, packageName);
29
- await generateDSLInterpreter(projectPath, packageName);
30
- await generateDSLTypes(projectPath, packageName);
28
+ await generateJetStart(projectPath, packageName);
31
29
  await generateAndroidManifest(projectPath, options);
32
30
  await generateResourceFiles(projectPath, projectName);
33
31
  await generateLocalProperties(projectPath);
@@ -36,608 +34,509 @@ export async function generateProjectTemplate(
36
34
  await generateReadme(projectPath, projectName);
37
35
  }
38
36
 
39
- async function createDirectoryStructure(projectPath: string): Promise<void> {
40
- const dirs = [
41
- 'app/src/main/java',
42
- 'app/src/main/res/layout',
43
- 'app/src/main/res/values',
44
- 'app/src/main/res/drawable',
45
- 'gradle/wrapper',
46
- ];
47
-
48
- for (const dir of dirs) {
49
- await fs.ensureDir(path.join(projectPath, dir));
50
- }
51
- }
52
-
53
- async function generateBuildGradle(
54
- projectPath: string,
55
- options: TemplateOptions
56
- ): Promise<void> {
57
- const content = `plugins {
58
- id 'com.android.application'
59
- id 'org.jetbrains.kotlin.android'
60
- }
61
-
62
- android {
63
- namespace '${options.packageName}'
64
- compileSdk ${TARGET_ANDROID_API_LEVEL}
65
-
66
- defaultConfig {
67
- applicationId "${options.packageName}"
68
- minSdk ${MIN_ANDROID_API_LEVEL}
69
- targetSdk ${TARGET_ANDROID_API_LEVEL}
70
- versionCode 1
71
- versionName "1.0.0"
72
- }
73
-
74
- buildTypes {
75
- release {
76
- minifyEnabled false
77
- }
78
- }
79
-
80
- compileOptions {
81
- sourceCompatibility JavaVersion.VERSION_17
82
- targetCompatibility JavaVersion.VERSION_17
83
- }
84
-
85
- kotlinOptions {
86
- jvmTarget = '17'
87
- }
88
-
89
- buildFeatures {
90
- compose true
91
- buildConfig true // Required for JetStart hot reload
92
- }
93
-
94
- composeOptions {
95
- kotlinCompilerExtensionVersion = '1.5.6'
96
- }
97
- }
98
-
99
- dependencies {
100
- implementation 'androidx.core:core-ktx:1.12.0'
101
- implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.2'
102
- implementation 'androidx.activity:activity-compose:1.8.1'
103
- implementation platform('androidx.compose:compose-bom:2023.10.01')
104
- implementation 'androidx.compose.ui:ui'
105
- implementation 'androidx.compose.ui:ui-graphics'
106
- implementation 'androidx.compose.ui:ui-tooling-preview'
107
- implementation 'androidx.compose.material3:material3'
108
-
109
- // JetStart Hot Reload dependencies
110
- implementation 'com.squareup.okhttp3:okhttp:4.12.0'
111
- implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3'
112
- }`;
113
-
114
- await fs.writeFile(path.join(projectPath, 'app/build.gradle'), content);
115
- }
116
-
117
- async function generateSettingsGradle(
118
- projectPath: string,
119
- projectName: string
120
- ): Promise<void> {
121
- const content = `rootProject.name = "${projectName}"
122
- include ':app'`;
123
-
124
- await fs.writeFile(path.join(projectPath, 'settings.gradle'), content);
125
- }
126
-
127
- async function generateGradleProperties(projectPath: string): Promise<void> {
128
- const content = `org.gradle.jvmargs=-Xmx2048m
129
- android.useAndroidX=true
130
- kotlin.code.style=official`;
131
-
132
- await fs.writeFile(path.join(projectPath, 'gradle.properties'), content);
133
- }
134
-
135
- async function generateMainActivity(
37
+ async function generateJetStart(
136
38
  projectPath: string,
137
39
  packageName: string
138
40
  ): Promise<void> {
139
41
  const packagePath = packageName.replace(/\./g, '/');
140
- const activityPath = path.join(
42
+ const jetStartPath = path.join(
141
43
  projectPath,
142
44
  'app/src/main/java',
143
45
  packagePath,
144
- 'MainActivity.kt'
46
+ 'JetStart.kt'
145
47
  );
146
48
 
147
49
  const content = `package ${packageName}
148
50
 
149
- import android.os.Bundle
150
- import androidx.activity.ComponentActivity
151
- import androidx.activity.compose.setContent
51
+ // Android
52
+ import android.app.Activity
53
+ import android.content.Intent
54
+ import android.net.Uri
55
+ import android.os.Build
56
+ import android.util.Log
57
+ import androidx.core.content.FileProvider
58
+
59
+ // Compose
152
60
  import androidx.compose.foundation.layout.*
153
61
  import androidx.compose.material3.*
154
62
  import androidx.compose.runtime.*
155
63
  import androidx.compose.ui.Alignment
156
64
  import androidx.compose.ui.Modifier
65
+ import androidx.compose.ui.graphics.Color
66
+ import androidx.compose.ui.text.font.FontWeight
157
67
  import androidx.compose.ui.unit.dp
158
68
 
159
- class MainActivity : ComponentActivity() {
160
- override fun onCreate(savedInstanceState: Bundle?) {
161
- super.onCreate(savedInstanceState)
162
-
163
- // Initialize hot reload - reads from BuildConfig injected by jetstart dev
164
- try {
165
- val serverUrl = BuildConfig.JETSTART_SERVER_URL
166
- val sessionId = BuildConfig.JETSTART_SESSION_ID
167
- HotReload.connect(this, serverUrl, sessionId)
168
- } catch (e: Exception) {
169
- // BuildConfig not available yet, hot reload will be disabled
170
- android.util.Log.w("MainActivity", "Hot reload not configured: \${e.message}")
171
- }
172
-
173
- setContent {
174
- MaterialTheme {
175
- Surface(
176
- modifier = Modifier.fillMaxSize(),
177
- color = MaterialTheme.colorScheme.background
178
- ) {
179
- // Check if we should render from DSL (hot reload mode)
180
- val dsl by DSLInterpreter.currentDSL.collectAsState()
69
+ // Third-party
70
+ import kotlinx.coroutines.flow.MutableStateFlow
71
+ import kotlinx.coroutines.flow.StateFlow
72
+ import okhttp3.*
181
73
 
182
- if (dsl != null) {
183
- // Hot reload mode: render from DSL sent by server
184
- DSLInterpreter.RenderDSL(dsl!!)
185
- } else {
186
- // Normal mode: render actual Compose code
187
- AppContent()
188
- }
189
- }
190
- }
191
- }
192
- }
74
+ // Standard library & JSON
75
+ import org.json.JSONArray
76
+ import org.json.JSONObject
77
+ import java.io.File
78
+ import java.io.IOException
193
79
 
194
- override fun onDestroy() {
195
- super.onDestroy()
196
- HotReload.disconnect()
197
- }
198
- }
80
+ // ============================================================================
81
+ // DSL Type Definitions
82
+ // ============================================================================
199
83
 
200
84
  /**
201
- * Main App Content - REAL Kotlin Compose Code!
202
- * This gets parsed to DSL and sent via hot reload
85
+ * DSL Type Definitions
86
+ * Represents UI elements in JSON format that can be interpreted at runtime
203
87
  */
204
- @Composable
205
- fun AppContent() {
206
- Column(
207
- modifier = Modifier
208
- .fillMaxSize()
209
- .padding(16.dp),
210
- horizontalAlignment = Alignment.CenterHorizontally,
211
- verticalArrangement = Arrangement.Center
212
- ) {
213
- Text(
214
- text = "Welcome to JetStart! 🚀",
215
- style = MaterialTheme.typography.headlineMedium
216
- )
217
88
 
218
- Spacer(modifier = Modifier.height(16.dp))
89
+ data class UIDefinition(
90
+ val version: String = "1.0",
91
+ val screen: DSLElement
92
+ )
219
93
 
220
- Text(
221
- text = "Edit this code and save to see hot reload!",
222
- style = MaterialTheme.typography.bodyMedium
223
- )
94
+ data class DSLElement(
95
+ val type: String,
96
+ val text: String? = null,
97
+ val style: String? = null,
98
+ val color: String? = null,
99
+ val modifier: DSLModifier? = null,
100
+ val horizontalAlignment: String? = null,
101
+ val verticalArrangement: String? = null,
102
+ val contentAlignment: String? = null,
103
+ val height: Int? = null,
104
+ val width: Int? = null,
105
+ val onClick: String? = null,
106
+ val enabled: Boolean? = true,
107
+ val imageVector: String? = null,
108
+ val tint: String? = null,
109
+ val contentDescription: String? = null,
110
+ val children: List<DSLElement>? = null
111
+ )
224
112
 
225
- Spacer(modifier = Modifier.height(24.dp))
113
+ data class DSLModifier(
114
+ val fillMaxSize: Boolean? = null,
115
+ val fillMaxWidth: Boolean? = null,
116
+ val fillMaxHeight: Boolean? = null,
117
+ val padding: Int? = null,
118
+ val paddingHorizontal: Int? = null,
119
+ val paddingVertical: Int? = null,
120
+ val size: Int? = null,
121
+ val height: Int? = null,
122
+ val width: Int? = null,
123
+ val weight: Float? = null
124
+ )
226
125
 
227
- Button(
228
- onClick = { /* Handle click */ },
229
- modifier = Modifier.fillMaxWidth()
230
- ) {
231
- Text("Click Me!")
232
- }
233
- }
234
- }`;
126
+ /**
127
+ * Parse JSON string to UIDefinition
128
+ */
129
+ fun parseUIDefinition(json: String): UIDefinition {
130
+ val obj = JSONObject(json)
131
+ val version = obj.optString("version", "1.0")
132
+ val screenObj = obj.getJSONObject("screen")
235
133
 
236
- await fs.ensureDir(path.dirname(activityPath));
237
- await fs.writeFile(activityPath, content);
134
+ return UIDefinition(
135
+ version = version,
136
+ screen = parseDSLElement(screenObj)
137
+ )
238
138
  }
239
139
 
240
- async function generateAndroidManifest(
241
- projectPath: string,
242
- options: TemplateOptions
243
- ): Promise<void> {
244
- const themeName = options.projectName.replace(/[^a-zA-Z0-9]/g, '');
245
- const content = `<?xml version="1.0" encoding="utf-8"?>
246
- <manifest xmlns:android="http://schemas.android.com/apk/res/android">
247
-
248
- <uses-permission android:name="android.permission.INTERNET" />
140
+ /**
141
+ * Parse JSONObject to DSLElement
142
+ */
143
+ fun parseDSLElement(obj: JSONObject): DSLElement {
144
+ val children = if (obj.has("children")) {
145
+ val childrenArray = obj.getJSONArray("children")
146
+ List(childrenArray.length()) { i ->
147
+ parseDSLElement(childrenArray.getJSONObject(i))
148
+ }
149
+ } else null
249
150
 
250
- <application
251
- android:allowBackup="true"
252
- android:label="@string/app_name"
253
- android:theme="@style/Theme.${themeName}"
254
- android:networkSecurityConfig="@xml/network_security_config">
255
- <activity
256
- android:name=".MainActivity"
257
- android:exported="true">
258
- <intent-filter>
259
- <action android:name="android.intent.action.MAIN" />
260
- <category android:name="android.intent.category.LAUNCHER" />
261
- </intent-filter>
262
- </activity>
263
- </application>
264
-
265
- </manifest>`;
151
+ val modifier = if (obj.has("modifier")) {
152
+ val modObj = obj.getJSONObject("modifier")
153
+ DSLModifier(
154
+ fillMaxSize = modObj.optBoolean("fillMaxSize"),
155
+ fillMaxWidth = modObj.optBoolean("fillMaxWidth"),
156
+ fillMaxHeight = modObj.optBoolean("fillMaxHeight"),
157
+ padding = if (modObj.has("padding")) modObj.getInt("padding") else null,
158
+ paddingHorizontal = if (modObj.has("paddingHorizontal")) modObj.getInt("paddingHorizontal") else null,
159
+ paddingVertical = if (modObj.has("paddingVertical")) modObj.getInt("paddingVertical") else null,
160
+ size = if (modObj.has("size")) modObj.getInt("size") else null,
161
+ height = if (modObj.has("height")) modObj.getInt("height") else null,
162
+ width = if (modObj.has("width")) modObj.getInt("width") else null,
163
+ weight = if (modObj.has("weight")) modObj.getDouble("weight").toFloat() else null
164
+ )
165
+ } else null
266
166
 
267
- await fs.writeFile(
268
- path.join(projectPath, 'app/src/main/AndroidManifest.xml'),
269
- content
270
- );
167
+ return DSLElement(
168
+ type = obj.getString("type"),
169
+ text = if (obj.has("text")) obj.getString("text") else null,
170
+ style = if (obj.has("style")) obj.getString("style") else null,
171
+ color = if (obj.has("color")) obj.getString("color") else null,
172
+ modifier = modifier,
173
+ horizontalAlignment = if (obj.has("horizontalAlignment")) obj.getString("horizontalAlignment") else null,
174
+ verticalArrangement = if (obj.has("verticalArrangement")) obj.getString("verticalArrangement") else null,
175
+ contentAlignment = if (obj.has("contentAlignment")) obj.getString("contentAlignment") else null,
176
+ height = if (obj.has("height")) obj.getInt("height") else null,
177
+ width = if (obj.has("width")) obj.getInt("width") else null,
178
+ onClick = if (obj.has("onClick")) obj.getString("onClick") else null,
179
+ enabled = obj.optBoolean("enabled", true),
180
+ imageVector = if (obj.has("imageVector")) obj.getString("imageVector") else null,
181
+ tint = if (obj.has("tint")) obj.getString("tint") else null,
182
+ contentDescription = if (obj.has("contentDescription")) obj.getString("contentDescription") else null,
183
+ children = children
184
+ )
271
185
  }
272
186
 
273
- async function generateJetStartConfig(
274
- projectPath: string,
275
- options: TemplateOptions
276
- ): Promise<void> {
277
- const config = {
278
- projectName: options.projectName,
279
- packageName: options.packageName,
280
- version: '1.0.0',
281
- jetstart: {
282
- version: '0.1.0',
283
- enableHotReload: true,
284
- enableLogs: true,
285
- port: 8765,
286
- },
287
- };
187
+ // ============================================================================
188
+ // DSL Interpreter
189
+ // ============================================================================
288
190
 
289
- await fs.writeJSON(
290
- path.join(projectPath, 'jetstart.config.json'),
291
- config,
292
- { spaces: 2 }
293
- );
294
- }
191
+ /**
192
+ * DSL Interpreter
193
+ * Converts JSON DSL to Compose UI at runtime
194
+ */
195
+ object DSLInterpreter {
196
+ private const val TAG = "DSLInterpreter"
295
197
 
296
- async function generateGitignore(projectPath: string): Promise<void> {
297
- const content = `# Build
298
- /build
299
- /app/build
300
- .gradle
301
- *.hprof
198
+ private val _currentDSL = MutableStateFlow<UIDefinition?>(null)
199
+ val currentDSL: StateFlow<UIDefinition?> = _currentDSL
302
200
 
303
- # IDE
304
- .idea
305
- *.iml
306
- .vscode
307
- .DS_Store
201
+ /**
202
+ * Update the current DSL definition
203
+ */
204
+ fun updateDSL(jsonString: String) {
205
+ try {
206
+ val definition = parseUIDefinition(jsonString)
207
+ _currentDSL.value = definition
208
+ Log.d(TAG, "DSL updated successfully")
209
+ } catch (e: Exception) {
210
+ Log.e(TAG, "Failed to parse DSL: \${e.message}", e)
211
+ }
212
+ }
308
213
 
309
- # Claude Code
310
- .claude
311
- .claude-worktrees
214
+ /**
215
+ * Render DSL as Compose UI
216
+ */
217
+ @Composable
218
+ fun RenderDSL(definition: UIDefinition) {
219
+ RenderElement(definition.screen)
220
+ }
312
221
 
313
- # JetStart
314
- .jetstart
222
+ /**
223
+ * Render individual DSL element
224
+ */
225
+ @Composable
226
+ fun RenderElement(element: DSLElement) {
227
+ when (element.type) {
228
+ "Column" -> RenderColumn(element)
229
+ "Row" -> RenderRow(element)
230
+ "Box" -> RenderBox(element)
231
+ "Text" -> RenderText(element)
232
+ "Button" -> RenderButton(element)
233
+ "Spacer" -> RenderSpacer(element)
234
+ else -> {
235
+ Log.w(TAG, "Unknown element type: \${element.type}")
236
+ Text("Unsupported: \${element.type}", color = Color.Red)
237
+ }
238
+ }
239
+ }
315
240
 
316
- # Android
317
- local.properties
318
- *.apk
319
- *.aab
320
- *.ap_
321
- *.dex
322
- *.class
323
- bin/
324
- gen/
325
- out/
326
- captures/
327
- .externalNativeBuild
328
- .cxx
241
+ @Composable
242
+ private fun RenderColumn(element: DSLElement) {
243
+ Column(
244
+ modifier = parseModifier(element.modifier),
245
+ horizontalAlignment = parseHorizontalAlignment(element.horizontalAlignment),
246
+ verticalArrangement = parseVerticalArrangement(element.verticalArrangement)
247
+ ) {
248
+ element.children?.forEach { child ->
249
+ RenderElement(child)
250
+ }
251
+ }
252
+ }
329
253
 
330
- # Log files
331
- *.log
254
+ @Composable
255
+ private fun RenderRow(element: DSLElement) {
256
+ Row(
257
+ modifier = parseModifier(element.modifier),
258
+ verticalAlignment = parseVerticalAlignment(element.horizontalAlignment),
259
+ horizontalArrangement = parseHorizontalArrangement(element.verticalArrangement)
260
+ ) {
261
+ element.children?.forEach { child ->
262
+ RenderElement(child)
263
+ }
264
+ }
265
+ }
332
266
 
333
- # Keystore files
334
- *.jks
335
- *.keystore`;
267
+ @Composable
268
+ private fun RenderBox(element: DSLElement) {
269
+ Box(
270
+ modifier = parseModifier(element.modifier),
271
+ contentAlignment = parseContentAlignment(element.contentAlignment)
272
+ ) {
273
+ element.children?.forEach { child ->
274
+ RenderElement(child)
275
+ }
276
+ }
277
+ }
336
278
 
337
- await fs.writeFile(path.join(projectPath, '.gitignore'), content);
338
- }
279
+ @Composable
280
+ private fun RenderText(element: DSLElement) {
281
+ Text(
282
+ text = element.text ?: "",
283
+ style = parseTextStyle(element.style),
284
+ color = parseColor(element.color) ?: Color.Unspecified,
285
+ modifier = parseModifier(element.modifier)
286
+ )
287
+ }
339
288
 
340
- async function generateReadme(projectPath: string, projectName: string): Promise<void> {
341
- const content = `# ${projectName}
289
+ @Composable
290
+ private fun RenderButton(element: DSLElement) {
291
+ Button(
292
+ onClick = { handleClick(element.onClick, element.text) },
293
+ modifier = parseModifier(element.modifier),
294
+ enabled = element.enabled ?: true
295
+ ) {
296
+ Text(element.text ?: "Button")
297
+ }
298
+ }
342
299
 
343
- A JetStart project with Kotlin and Jetpack Compose.
300
+ @Composable
301
+ private fun RenderSpacer(element: DSLElement) {
302
+ Spacer(
303
+ modifier = Modifier
304
+ .height(element.height?.dp ?: 0.dp)
305
+ .width(element.width?.dp ?: 0.dp)
306
+ )
307
+ }
344
308
 
345
- ## Getting Started
309
+ /**
310
+ * Parse DSL modifier to Compose Modifier
311
+ */
312
+ private fun parseModifier(dslModifier: DSLModifier?): Modifier {
313
+ var modifier: Modifier = Modifier
346
314
 
347
- \`\`\`bash
348
- # Start development server
349
- jetstart dev
315
+ dslModifier?.let { m ->
316
+ if (m.fillMaxSize == true) modifier = modifier.fillMaxSize()
317
+ if (m.fillMaxWidth == true) modifier = modifier.fillMaxWidth()
318
+ if (m.fillMaxHeight == true) modifier = modifier.fillMaxHeight()
350
319
 
351
- # Build production APK
352
- jetstart build
320
+ m.padding?.let { modifier = modifier.padding(it.dp) }
321
+ m.paddingHorizontal?.let { modifier = modifier.padding(horizontal = it.dp) }
322
+ m.paddingVertical?.let { modifier = modifier.padding(vertical = it.dp) }
353
323
 
354
- # View logs
355
- jetstart logs
356
- \`\`\`
324
+ m.size?.let { modifier = modifier.size(it.dp) }
325
+ m.height?.let { modifier = modifier.height(it.dp) }
326
+ m.width?.let { modifier = modifier.width(it.dp) }
357
327
 
358
- ## Project Structure
328
+ // Note: weight() is only available in RowScope/ColumnScope
329
+ // We'll handle it separately when needed
330
+ }
359
331
 
360
- \`\`\`
361
- ${projectName}/
362
- ├── app/
363
- │ └── src/
364
- │ └── main/
365
- │ ├── java/ # Kotlin source files
366
- │ └── res/ # Resources
367
- ├── jetstart.config.json # JetStart configuration
368
- └── build.gradle # Gradle build file
369
- \`\`\`
332
+ return modifier
333
+ }
370
334
 
371
- ## Learn More
335
+ /**
336
+ * Parse alignment strings
337
+ */
338
+ private fun parseHorizontalAlignment(alignment: String?): Alignment.Horizontal {
339
+ return when (alignment?.lowercase()) {
340
+ "start" -> Alignment.Start
341
+ "centerhorizontally", "center" -> Alignment.CenterHorizontally
342
+ "end" -> Alignment.End
343
+ else -> Alignment.Start
344
+ }
345
+ }
372
346
 
373
- - [JetStart Documentation](https://github.com/phantom/jetstart)
374
- - [Jetpack Compose](https://developer.android.com/jetpack/compose)
375
- `;
347
+ private fun parseVerticalAlignment(alignment: String?): Alignment.Vertical {
348
+ return when (alignment?.lowercase()) {
349
+ "top" -> Alignment.Top
350
+ "centervertically", "center" -> Alignment.CenterVertically
351
+ "bottom" -> Alignment.Bottom
352
+ else -> Alignment.Top
353
+ }
354
+ }
376
355
 
377
- await fs.writeFile(path.join(projectPath, 'README.md'), content);
378
- }
356
+ private fun parseContentAlignment(alignment: String?): Alignment {
357
+ return when (alignment?.lowercase()) {
358
+ "center" -> Alignment.Center
359
+ "topcenter" -> Alignment.TopCenter
360
+ "topstart" -> Alignment.TopStart
361
+ "topend" -> Alignment.TopEnd
362
+ "bottomcenter" -> Alignment.BottomCenter
363
+ "bottomstart" -> Alignment.BottomStart
364
+ "bottomend" -> Alignment.BottomEnd
365
+ "centerstart" -> Alignment.CenterStart
366
+ "centerend" -> Alignment.CenterEnd
367
+ else -> Alignment.TopStart
368
+ }
369
+ }
379
370
 
380
- async function generateRootBuildGradle(projectPath: string): Promise<void> {
381
- const content = `// Top-level build file
382
- buildscript {
383
- ext {
384
- kotlin_version = '1.9.21'
385
- compose_version = '1.5.4'
371
+ private fun parseVerticalArrangement(arrangement: String?): Arrangement.Vertical {
372
+ return when (arrangement?.lowercase()) {
373
+ "top" -> Arrangement.Top
374
+ "center" -> Arrangement.Center
375
+ "bottom" -> Arrangement.Bottom
376
+ "spacebetween" -> Arrangement.SpaceBetween
377
+ "spacearound" -> Arrangement.SpaceAround
378
+ "spaceevenly" -> Arrangement.SpaceEvenly
379
+ else -> Arrangement.Top
380
+ }
386
381
  }
387
- repositories {
388
- google()
389
- mavenCentral()
382
+
383
+ private fun parseHorizontalArrangement(arrangement: String?): Arrangement.Horizontal {
384
+ return when (arrangement?.lowercase()) {
385
+ "start" -> Arrangement.Start
386
+ "center" -> Arrangement.Center
387
+ "end" -> Arrangement.End
388
+ "spacebetween" -> Arrangement.SpaceBetween
389
+ "spacearound" -> Arrangement.SpaceAround
390
+ "spaceevenly" -> Arrangement.SpaceEvenly
391
+ else -> Arrangement.Start
392
+ }
390
393
  }
391
- dependencies {
392
- classpath 'com.android.tools.build:gradle:8.2.0'
393
- classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
394
+
395
+ /**
396
+ * Parse text style
397
+ */
398
+ @Composable
399
+ private fun parseTextStyle(style: String?): androidx.compose.ui.text.TextStyle {
400
+ return when (style?.lowercase()) {
401
+ "headlinelarge" -> MaterialTheme.typography.headlineLarge
402
+ "headlinemedium" -> MaterialTheme.typography.headlineMedium
403
+ "headlinesmall" -> MaterialTheme.typography.headlineSmall
404
+ "titlelarge" -> MaterialTheme.typography.titleLarge
405
+ "titlemedium" -> MaterialTheme.typography.titleMedium
406
+ "titlesmall" -> MaterialTheme.typography.titleSmall
407
+ "bodylarge" -> MaterialTheme.typography.bodyLarge
408
+ "bodymedium" -> MaterialTheme.typography.bodyMedium
409
+ "bodysmall" -> MaterialTheme.typography.bodySmall
410
+ "labellarge" -> MaterialTheme.typography.labelLarge
411
+ "labelmedium" -> MaterialTheme.typography.labelMedium
412
+ "labelsmall" -> MaterialTheme.typography.labelSmall
413
+ else -> MaterialTheme.typography.bodyMedium
414
+ }
394
415
  }
395
- }
396
416
 
397
- allprojects {
398
- repositories {
399
- google()
400
- mavenCentral()
417
+ /**
418
+ * Parse color from string
419
+ */
420
+ private fun parseColor(colorString: String?): Color? {
421
+ if (colorString == null) return null
422
+
423
+ return try {
424
+ when {
425
+ colorString.startsWith("#") -> {
426
+ // Hex color
427
+ Color(android.graphics.Color.parseColor(colorString))
428
+ }
429
+ else -> null
430
+ }
431
+ } catch (e: Exception) {
432
+ Log.w(TAG, "Failed to parse color: \$colorString")
433
+ null
434
+ }
401
435
  }
402
- }
403
436
 
404
- task clean(type: Delete) {
405
- delete rootProject.buildDir
406
- }`;
437
+ /**
438
+ * Handle click events
439
+ */
440
+ private fun handleClick(action: String?, text: String?) {
441
+ if (action != null) {
442
+ Log.d(TAG, "Button clicked: \$action")
407
443
 
408
- await fs.writeFile(path.join(projectPath, 'build.gradle'), content);
444
+ // Send click event to dev server
445
+ sendClickEvent(action, "Button", text)
446
+ }
447
+ }
448
+
449
+ /**
450
+ * Send click event to dev server via WebSocket
451
+ */
452
+ private fun sendClickEvent(action: String, elementType: String, elementText: String?) {
453
+ try {
454
+ val ws = HotReload.getWebSocket()
455
+ if (ws != null) {
456
+ val message = JSONObject().apply {
457
+ put("type", "client:click")
458
+ put("timestamp", System.currentTimeMillis())
459
+ put("action", action)
460
+ put("elementType", elementType)
461
+ elementText?.let { put("elementText", it) }
462
+ }
463
+
464
+ ws.send(message.toString())
465
+ Log.d(TAG, "Sent click event to server: \$action")
466
+ } else {
467
+ Log.w(TAG, "WebSocket not available, cannot send click event")
468
+ }
469
+ } catch (e: Exception) {
470
+ Log.e(TAG, "Failed to send click event: \${e.message}")
471
+ }
472
+ }
409
473
  }
410
474
 
411
- async function generateGradleWrapper(projectPath: string): Promise<void> {
412
- // Use system Gradle to initialize proper wrapper
413
- // This generates:
414
- // - gradle/wrapper/gradle-wrapper.jar
415
- // - gradle/wrapper/gradle-wrapper.properties
416
- // - gradlew (Unix shell script)
417
- // - gradlew.bat (Windows batch script)
475
+ // ============================================================================
476
+ // Hot Reload Manager
477
+ // ============================================================================
418
478
 
419
- return new Promise<void>((resolve) => {
420
- // Try to use system gradle to generate wrapper
421
- const gradleCmd = process.platform === 'win32' ? 'gradle.bat' : 'gradle';
479
+ /**
480
+ * Hot Reload Manager
481
+ * Connects to JetStart dev server and automatically reloads the app when code changes
482
+ */
483
+ object HotReload {
484
+ private const val TAG = "HotReload"
485
+ private var webSocket: WebSocket? = null
486
+ private var activity: Activity? = null
487
+ private var connectionTime: Long = 0
488
+ private var ignoreFirstBuild = true
489
+ private val httpClient = OkHttpClient()
422
490
 
423
- const gradleProcess = spawn(gradleCmd, ['wrapper', '--gradle-version', '8.2'], {
424
- cwd: projectPath,
425
- shell: true,
426
- });
491
+ /**
492
+ * Get the current WebSocket connection (for sending messages)
493
+ */
494
+ fun getWebSocket(): WebSocket? = webSocket
427
495
 
428
- gradleProcess.on('close', (code) => {
429
- // Continue regardless of success/failure
430
- // If gradle wrapper command fails, the build will fall back to system gradle
431
- resolve();
432
- });
496
+ fun connect(activity: Activity, serverUrl: String, sessionId: String) {
497
+ this.activity = activity
433
498
 
434
- gradleProcess.on('error', () => {
435
- // Continue even if gradle command not found
436
- resolve();
437
- });
499
+ val wsUrl = serverUrl.replace("http://", "ws://").replace("https://", "wss://")
500
+ Log.d(TAG, "Connecting to dev server: \$wsUrl")
438
501
 
439
- // Timeout after 30 seconds
440
- setTimeout(() => {
441
- gradleProcess.kill();
442
- resolve();
443
- }, 30000);
444
- });
445
- }
502
+ val client = OkHttpClient()
503
+ val request = Request.Builder()
504
+ .url(wsUrl)
505
+ .build()
446
506
 
447
- async function generateResourceFiles(
448
- projectPath: string,
449
- projectName: string
450
- ): Promise<void> {
451
- // Generate strings.xml
452
- const stringsXml = `<?xml version="1.0" encoding="utf-8"?>
453
- <resources>
454
- <string name="app_name">${projectName}</string>
455
- </resources>`;
507
+ webSocket = client.newWebSocket(request, object : WebSocketListener() {
508
+ override fun onOpen(webSocket: WebSocket, response: Response) {
509
+ Log.d(TAG, "WebSocket connected")
510
+ connectionTime = System.currentTimeMillis()
511
+ ignoreFirstBuild = true // Ignore the first build-complete after connecting
456
512
 
457
- await fs.writeFile(
458
- path.join(projectPath, 'app/src/main/res/values/strings.xml'),
459
- stringsXml
460
- );
513
+ // Send connect message
514
+ val connectMsg = JSONObject().apply {
515
+ put("type", "client:connect")
516
+ put("sessionId", sessionId)
517
+ put("clientType", "test-app")
518
+ }
519
+ webSocket.send(connectMsg.toString())
520
+ }
461
521
 
462
- // Generate colors.xml
463
- const colorsXml = `<?xml version="1.0" encoding="utf-8"?>
464
- <resources>
465
- <color name="purple_200">#FFBB86FC</color>
466
- <color name="purple_500">#FF6200EE</color>
467
- <color name="purple_700">#FF3700B3</color>
468
- <color name="teal_200">#FF03DAC5</color>
469
- <color name="teal_700">#FF018786</color>
470
- <color name="black">#FF000000</color>
471
- <color name="white">#FFFFFFFF</color>
472
- </resources>`;
522
+ override fun onMessage(webSocket: WebSocket, text: String) {
523
+ Log.d(TAG, "Received: \$text")
473
524
 
474
- await fs.writeFile(
475
- path.join(projectPath, 'app/src/main/res/values/colors.xml'),
476
- colorsXml
477
- );
525
+ try {
526
+ val json = JSONObject(text)
527
+ val type = json.getString("type")
478
528
 
479
- // Generate themes.xml
480
- const themesXml = `<?xml version="1.0" encoding="utf-8"?>
481
- <resources>
482
- <style name="Theme.${projectName.replace(/[^a-zA-Z0-9]/g, '')}" parent="android:Theme.Material.Light.NoActionBar" />
483
- </resources>`;
529
+ when (type) {
530
+ "core:ui-update" -> {
531
+ // DSL-based hot reload (FAST)
532
+ if (ignoreFirstBuild) {
533
+ Log.d(TAG, "Ignoring first UI update (old build)")
534
+ ignoreFirstBuild = false
535
+ return@onMessage
536
+ }
484
537
 
485
- await fs.writeFile(
486
- path.join(projectPath, 'app/src/main/res/values/themes.xml'),
487
- themesXml
488
- );
489
-
490
- // Generate network_security_config.xml for development (allows cleartext traffic)
491
- const networkSecurityConfig = `<?xml version="1.0" encoding="utf-8"?>
492
- <network-security-config>
493
- <base-config cleartextTrafficPermitted="true">
494
- <trust-anchors>
495
- <certificates src="system" />
496
- </trust-anchors>
497
- </base-config>
498
- </network-security-config>`;
499
-
500
- await fs.ensureDir(path.join(projectPath, 'app/src/main/res/xml'));
501
- await fs.writeFile(
502
- path.join(projectPath, 'app/src/main/res/xml/network_security_config.xml'),
503
- networkSecurityConfig
504
- );
505
- }
506
-
507
- async function generateLocalProperties(projectPath: string): Promise<void> {
508
- // Auto-detect Android SDK location
509
- let androidSdkPath: string | undefined;
510
-
511
- // Check environment variables first
512
- androidSdkPath = process.env.ANDROID_HOME || process.env.ANDROID_SDK_ROOT;
513
-
514
- // If not found, check common Windows locations
515
- if (!androidSdkPath && process.platform === 'win32') {
516
- const commonPaths = [
517
- 'C:\\Android',
518
- path.join(require('os').homedir(), 'AppData', 'Local', 'Android', 'Sdk'),
519
- 'C:\\Android\\Sdk',
520
- 'C:\\Program Files (x86)\\Android\\android-sdk',
521
- ];
522
-
523
- for (const p of commonPaths) {
524
- if (fs.existsSync(p)) {
525
- androidSdkPath = p;
526
- break;
527
- }
528
- }
529
- }
530
-
531
- // If not found on macOS/Linux, check common paths
532
- if (!androidSdkPath && process.platform !== 'win32') {
533
- const commonPaths = [
534
- path.join(require('os').homedir(), 'Android', 'Sdk'),
535
- path.join(require('os').homedir(), 'Library', 'Android', 'sdk'),
536
- '/opt/android-sdk',
537
- ];
538
-
539
- for (const p of commonPaths) {
540
- if (fs.existsSync(p)) {
541
- androidSdkPath = p;
542
- break;
543
- }
544
- }
545
- }
546
-
547
- if (!androidSdkPath) {
548
- console.warn('[Warning] Android SDK not found. You may need to set ANDROID_HOME or create local.properties manually.');
549
- return;
550
- }
551
-
552
- // Create local.properties with SDK path
553
- const content = `# Auto-generated by JetStart
554
- sdk.dir=${androidSdkPath.replace(/\\/g, '\\\\')}
555
- `;
556
-
557
- await fs.writeFile(path.join(projectPath, 'local.properties'), content);
558
- console.log(`[JetStart] Created local.properties with SDK: ${androidSdkPath}`);
559
- }
560
- async function generateHotReload(
561
- projectPath: string,
562
- packageName: string
563
- ): Promise<void> {
564
- const packagePath = packageName.replace(/\./g, '/');
565
- const hotReloadPath = path.join(
566
- projectPath,
567
- 'app/src/main/java',
568
- packagePath,
569
- 'HotReload.kt'
570
- );
571
-
572
- const content = `package ${packageName}
573
-
574
- import android.app.Activity
575
- import android.content.Intent
576
- import android.net.Uri
577
- import android.os.Build
578
- import android.util.Log
579
- import androidx.core.content.FileProvider
580
- import okhttp3.*
581
- import org.json.JSONObject
582
- import java.io.File
583
- import java.io.IOException
584
-
585
- /**
586
- * Hot Reload Manager
587
- * Connects to JetStart dev server and automatically reloads the app when code changes
588
- */
589
- object HotReload {
590
- private const val TAG = "HotReload"
591
- private var webSocket: WebSocket? = null
592
- private var activity: Activity? = null
593
- private var connectionTime: Long = 0
594
- private var ignoreFirstBuild = true
595
- private val httpClient = OkHttpClient()
596
-
597
- fun connect(activity: Activity, serverUrl: String, sessionId: String) {
598
- this.activity = activity
599
-
600
- val wsUrl = serverUrl.replace("http://", "ws://").replace("https://", "wss://")
601
- Log.d(TAG, "Connecting to dev server: \$wsUrl")
602
-
603
- val client = OkHttpClient()
604
- val request = Request.Builder()
605
- .url(wsUrl)
606
- .build()
607
-
608
- webSocket = client.newWebSocket(request, object : WebSocketListener() {
609
- override fun onOpen(webSocket: WebSocket, response: Response) {
610
- Log.d(TAG, "WebSocket connected")
611
- connectionTime = System.currentTimeMillis()
612
- ignoreFirstBuild = true // Ignore the first build-complete after connecting
613
-
614
- // Send connect message
615
- val connectMsg = JSONObject().apply {
616
- put("type", "client:connect")
617
- put("sessionId", sessionId)
618
- put("clientType", "test-app")
619
- }
620
- webSocket.send(connectMsg.toString())
621
- }
622
-
623
- override fun onMessage(webSocket: WebSocket, text: String) {
624
- Log.d(TAG, "Received: \$text")
625
-
626
- try {
627
- val json = JSONObject(text)
628
- val type = json.getString("type")
629
-
630
- when (type) {
631
- "core:ui-update" -> {
632
- // DSL-based hot reload (FAST)
633
- if (ignoreFirstBuild) {
634
- Log.d(TAG, "Ignoring first UI update (old build)")
635
- ignoreFirstBuild = false
636
- return@onMessage
637
- }
638
-
639
- val timestamp = json.optLong("timestamp", 0)
640
- val dslContent = json.optString("dslContent", "")
538
+ val timestamp = json.optLong("timestamp", 0)
539
+ val dslContent = json.optString("dslContent", "")
641
540
 
642
541
  Log.d(TAG, "UI update received at \$timestamp, connection at \$connectionTime")
643
542
 
@@ -791,419 +690,528 @@ object HotReload {
791
690
  }
792
691
  `;
793
692
 
794
- await fs.ensureDir(path.dirname(hotReloadPath));
795
- await fs.writeFile(hotReloadPath, content);
693
+ await fs.ensureDir(path.dirname(jetStartPath));
694
+ await fs.writeFile(jetStartPath, content);
695
+ }
696
+
697
+ async function createDirectoryStructure(projectPath: string): Promise<void> {
698
+ const dirs = [
699
+ 'app/src/main/java',
700
+ 'app/src/main/res/layout',
701
+ 'app/src/main/res/values',
702
+ 'app/src/main/res/drawable',
703
+ 'gradle/wrapper',
704
+ ];
705
+
706
+ for (const dir of dirs) {
707
+ await fs.ensureDir(path.join(projectPath, dir));
708
+ }
709
+ }
710
+
711
+ async function generateBuildGradle(
712
+ projectPath: string,
713
+ options: TemplateOptions
714
+ ): Promise<void> {
715
+ const content = `plugins {
716
+ id 'com.android.application'
717
+ id 'org.jetbrains.kotlin.android'
718
+ }
719
+
720
+ android {
721
+ namespace '${options.packageName}'
722
+ compileSdk ${TARGET_ANDROID_API_LEVEL}
723
+
724
+ defaultConfig {
725
+ applicationId "${options.packageName}"
726
+ minSdk ${MIN_ANDROID_API_LEVEL}
727
+ targetSdk ${TARGET_ANDROID_API_LEVEL}
728
+ versionCode 1
729
+ versionName "1.0.0"
730
+ }
731
+
732
+ buildTypes {
733
+ release {
734
+ minifyEnabled false
735
+ }
736
+ }
737
+
738
+ compileOptions {
739
+ sourceCompatibility JavaVersion.VERSION_17
740
+ targetCompatibility JavaVersion.VERSION_17
741
+ }
742
+
743
+ kotlinOptions {
744
+ jvmTarget = '17'
745
+ }
746
+
747
+ buildFeatures {
748
+ compose true
749
+ buildConfig true // Required for JetStart hot reload
750
+ }
751
+
752
+ composeOptions {
753
+ kotlinCompilerExtensionVersion = '1.5.6'
754
+ }
755
+ }
756
+
757
+ dependencies {
758
+ implementation 'androidx.core:core-ktx:1.12.0'
759
+ implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.2'
760
+ implementation 'androidx.activity:activity-compose:1.8.1'
761
+ implementation platform('androidx.compose:compose-bom:2023.10.01')
762
+ implementation 'androidx.compose.ui:ui'
763
+ implementation 'androidx.compose.ui:ui-graphics'
764
+ implementation 'androidx.compose.ui:ui-tooling-preview'
765
+ implementation 'androidx.compose.material3:material3'
766
+
767
+ // JetStart Hot Reload dependencies
768
+ implementation 'com.squareup.okhttp3:okhttp:4.12.0'
769
+ implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3'
770
+ }`;
771
+
772
+ await fs.writeFile(path.join(projectPath, 'app/build.gradle'), content);
773
+ }
774
+
775
+ async function generateSettingsGradle(
776
+ projectPath: string,
777
+ projectName: string
778
+ ): Promise<void> {
779
+ const content = `rootProject.name = "${projectName}"
780
+ include ':app'`;
781
+
782
+ await fs.writeFile(path.join(projectPath, 'settings.gradle'), content);
783
+ }
784
+
785
+ async function generateGradleProperties(projectPath: string): Promise<void> {
786
+ const content = `org.gradle.jvmargs=-Xmx2048m
787
+ android.useAndroidX=true
788
+ kotlin.code.style=official`;
789
+
790
+ await fs.writeFile(path.join(projectPath, 'gradle.properties'), content);
796
791
  }
797
792
 
798
- async function generateDSLInterpreter(
793
+ async function generateMainActivity(
799
794
  projectPath: string,
800
795
  packageName: string
801
796
  ): Promise<void> {
802
797
  const packagePath = packageName.replace(/\./g, '/');
803
- const interpreterPath = path.join(
798
+ const activityPath = path.join(
804
799
  projectPath,
805
800
  'app/src/main/java',
806
801
  packagePath,
807
- 'DSLInterpreter.kt'
802
+ 'MainActivity.kt'
808
803
  );
809
804
 
810
805
  const content = `package ${packageName}
811
806
 
812
- import android.util.Log
807
+ import android.os.Bundle
808
+ import androidx.activity.ComponentActivity
809
+ import androidx.activity.compose.setContent
813
810
  import androidx.compose.foundation.layout.*
814
811
  import androidx.compose.material3.*
815
812
  import androidx.compose.runtime.*
816
813
  import androidx.compose.ui.Alignment
817
814
  import androidx.compose.ui.Modifier
818
- import androidx.compose.ui.graphics.Color
819
- import androidx.compose.ui.text.font.FontWeight
820
815
  import androidx.compose.ui.unit.dp
821
- import kotlinx.coroutines.flow.MutableStateFlow
822
- import kotlinx.coroutines.flow.StateFlow
823
816
 
824
- /**
825
- * DSL Interpreter
826
- * Converts JSON DSL to Compose UI at runtime
827
- */
828
- object DSLInterpreter {
829
- private const val TAG = "DSLInterpreter"
830
-
831
- private val _currentDSL = MutableStateFlow<UIDefinition?>(null)
832
- val currentDSL: StateFlow<UIDefinition?> = _currentDSL
817
+ class MainActivity : ComponentActivity() {
818
+ override fun onCreate(savedInstanceState: Bundle?) {
819
+ super.onCreate(savedInstanceState)
833
820
 
834
- /**
835
- * Update the current DSL definition
836
- */
837
- fun updateDSL(jsonString: String) {
821
+ // Initialize hot reload - reads from BuildConfig injected by jetstart dev
838
822
  try {
839
- val definition = parseUIDefinition(jsonString)
840
- _currentDSL.value = definition
841
- Log.d(TAG, "DSL updated successfully")
823
+ val serverUrl = BuildConfig.JETSTART_SERVER_URL
824
+ val sessionId = BuildConfig.JETSTART_SESSION_ID
825
+ HotReload.connect(this, serverUrl, sessionId)
842
826
  } catch (e: Exception) {
843
- Log.e(TAG, "Failed to parse DSL: \${e.message}", e)
827
+ // BuildConfig not available yet, hot reload will be disabled
828
+ android.util.Log.w("MainActivity", "Hot reload not configured: \${e.message}")
844
829
  }
845
- }
846
830
 
847
- /**
848
- * Render DSL as Compose UI
849
- */
850
- @Composable
851
- fun RenderDSL(definition: UIDefinition) {
852
- RenderElement(definition.screen)
853
- }
831
+ setContent {
832
+ MaterialTheme {
833
+ Surface(
834
+ modifier = Modifier.fillMaxSize(),
835
+ color = MaterialTheme.colorScheme.background
836
+ ) {
837
+ // Check if we should render from DSL (hot reload mode)
838
+ val dsl by DSLInterpreter.currentDSL.collectAsState()
854
839
 
855
- /**
856
- * Render individual DSL element
857
- */
858
- @Composable
859
- fun RenderElement(element: DSLElement) {
860
- when (element.type) {
861
- "Column" -> RenderColumn(element)
862
- "Row" -> RenderRow(element)
863
- "Box" -> RenderBox(element)
864
- "Text" -> RenderText(element)
865
- "Button" -> RenderButton(element)
866
- "Spacer" -> RenderSpacer(element)
867
- else -> {
868
- Log.w(TAG, "Unknown element type: \${element.type}")
869
- Text("Unsupported: \${element.type}", color = Color.Red)
840
+ if (dsl != null) {
841
+ // Hot reload mode: render from DSL sent by server
842
+ DSLInterpreter.RenderDSL(dsl!!)
843
+ } else {
844
+ // Normal mode: render actual Compose code
845
+ AppContent()
846
+ }
847
+ }
870
848
  }
871
849
  }
872
850
  }
873
851
 
874
- @Composable
875
- private fun RenderColumn(element: DSLElement) {
876
- Column(
877
- modifier = parseModifier(element.modifier),
878
- horizontalAlignment = parseHorizontalAlignment(element.horizontalAlignment),
879
- verticalArrangement = parseVerticalArrangement(element.verticalArrangement)
880
- ) {
881
- element.children?.forEach { child ->
882
- RenderElement(child)
883
- }
884
- }
852
+ override fun onDestroy() {
853
+ super.onDestroy()
854
+ HotReload.disconnect()
885
855
  }
856
+ }
886
857
 
887
- @Composable
888
- private fun RenderRow(element: DSLElement) {
889
- Row(
890
- modifier = parseModifier(element.modifier),
891
- verticalAlignment = parseVerticalAlignment(element.horizontalAlignment),
892
- horizontalArrangement = parseHorizontalArrangement(element.verticalArrangement)
893
- ) {
894
- element.children?.forEach { child ->
895
- RenderElement(child)
896
- }
897
- }
898
- }
858
+ /**
859
+ * Main App Content - REAL Kotlin Compose Code!
860
+ * This gets parsed to DSL and sent via hot reload
861
+ */
862
+ @Composable
863
+ fun AppContent() {
864
+ Column(
865
+ modifier = Modifier
866
+ .fillMaxSize()
867
+ .padding(16.dp),
868
+ horizontalAlignment = Alignment.CenterHorizontally,
869
+ verticalArrangement = Arrangement.Center
870
+ ) {
871
+ Text(
872
+ text = "Welcome to JetStart! 🚀",
873
+ style = MaterialTheme.typography.headlineMedium
874
+ )
899
875
 
900
- @Composable
901
- private fun RenderBox(element: DSLElement) {
902
- Box(
903
- modifier = parseModifier(element.modifier),
904
- contentAlignment = parseContentAlignment(element.contentAlignment)
905
- ) {
906
- element.children?.forEach { child ->
907
- RenderElement(child)
908
- }
909
- }
910
- }
876
+ Spacer(modifier = Modifier.height(16.dp))
911
877
 
912
- @Composable
913
- private fun RenderText(element: DSLElement) {
914
878
  Text(
915
- text = element.text ?: "",
916
- style = parseTextStyle(element.style),
917
- color = parseColor(element.color) ?: Color.Unspecified,
918
- modifier = parseModifier(element.modifier)
879
+ text = "Edit this code and save to see hot reload!",
880
+ style = MaterialTheme.typography.bodyMedium
919
881
  )
920
- }
921
882
 
922
- @Composable
923
- private fun RenderButton(element: DSLElement) {
883
+ Spacer(modifier = Modifier.height(24.dp))
884
+
924
885
  Button(
925
- onClick = { handleClick(element.onClick) },
926
- modifier = parseModifier(element.modifier),
927
- enabled = element.enabled ?: true
886
+ onClick = { /* Handle click */ },
887
+ modifier = Modifier.fillMaxWidth()
928
888
  ) {
929
- Text(element.text ?: "Button")
889
+ Text("Click Me!")
930
890
  }
931
891
  }
892
+ }`;
932
893
 
933
- @Composable
934
- private fun RenderSpacer(element: DSLElement) {
935
- Spacer(
936
- modifier = Modifier
937
- .height(element.height?.dp ?: 0.dp)
938
- .width(element.width?.dp ?: 0.dp)
939
- )
940
- }
894
+ await fs.ensureDir(path.dirname(activityPath));
895
+ await fs.writeFile(activityPath, content);
896
+ }
941
897
 
942
- /**
943
- * Parse DSL modifier to Compose Modifier
944
- */
945
- private fun parseModifier(dslModifier: DSLModifier?): Modifier {
946
- var modifier: Modifier = Modifier
898
+ async function generateAndroidManifest(
899
+ projectPath: string,
900
+ options: TemplateOptions
901
+ ): Promise<void> {
902
+ const themeName = options.projectName.replace(/[^a-zA-Z0-9]/g, '');
903
+ const content = `<?xml version="1.0" encoding="utf-8"?>
904
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
947
905
 
948
- dslModifier?.let { m ->
949
- if (m.fillMaxSize == true) modifier = modifier.fillMaxSize()
950
- if (m.fillMaxWidth == true) modifier = modifier.fillMaxWidth()
951
- if (m.fillMaxHeight == true) modifier = modifier.fillMaxHeight()
906
+ <uses-permission android:name="android.permission.INTERNET" />
952
907
 
953
- m.padding?.let { modifier = modifier.padding(it.dp) }
954
- m.paddingHorizontal?.let { modifier = modifier.padding(horizontal = it.dp) }
955
- m.paddingVertical?.let { modifier = modifier.padding(vertical = it.dp) }
908
+ <application
909
+ android:allowBackup="true"
910
+ android:label="@string/app_name"
911
+ android:theme="@style/Theme.${themeName}"
912
+ android:networkSecurityConfig="@xml/network_security_config">
913
+ <activity
914
+ android:name=".MainActivity"
915
+ android:exported="true">
916
+ <intent-filter>
917
+ <action android:name="android.intent.action.MAIN" />
918
+ <category android:name="android.intent.category.LAUNCHER" />
919
+ </intent-filter>
920
+ </activity>
921
+ </application>
956
922
 
957
- m.size?.let { modifier = modifier.size(it.dp) }
958
- m.height?.let { modifier = modifier.height(it.dp) }
959
- m.width?.let { modifier = modifier.width(it.dp) }
923
+ </manifest>`;
960
924
 
961
- // Note: weight() is only available in RowScope/ColumnScope
962
- // We'll handle it separately when needed
963
- }
925
+ await fs.writeFile(
926
+ path.join(projectPath, 'app/src/main/AndroidManifest.xml'),
927
+ content
928
+ );
929
+ }
964
930
 
965
- return modifier
966
- }
931
+ async function generateJetStartConfig(
932
+ projectPath: string,
933
+ options: TemplateOptions
934
+ ): Promise<void> {
935
+ const config = {
936
+ projectName: options.projectName,
937
+ packageName: options.packageName,
938
+ version: '1.0.0',
939
+ jetstart: {
940
+ version: '0.1.0',
941
+ enableHotReload: true,
942
+ enableLogs: true,
943
+ port: 8765,
944
+ },
945
+ };
967
946
 
968
- /**
969
- * Parse alignment strings
970
- */
971
- private fun parseHorizontalAlignment(alignment: String?): Alignment.Horizontal {
972
- return when (alignment?.lowercase()) {
973
- "start" -> Alignment.Start
974
- "centerhorizontally", "center" -> Alignment.CenterHorizontally
975
- "end" -> Alignment.End
976
- else -> Alignment.Start
977
- }
978
- }
947
+ await fs.writeJSON(
948
+ path.join(projectPath, 'jetstart.config.json'),
949
+ config,
950
+ { spaces: 2 }
951
+ );
952
+ }
979
953
 
980
- private fun parseVerticalAlignment(alignment: String?): Alignment.Vertical {
981
- return when (alignment?.lowercase()) {
982
- "top" -> Alignment.Top
983
- "centervertically", "center" -> Alignment.CenterVertically
984
- "bottom" -> Alignment.Bottom
985
- else -> Alignment.Top
986
- }
987
- }
954
+ async function generateGitignore(projectPath: string): Promise<void> {
955
+ const content = `# Build
956
+ /build
957
+ /app/build
958
+ .gradle
959
+ *.hprof
988
960
 
989
- private fun parseContentAlignment(alignment: String?): Alignment {
990
- return when (alignment?.lowercase()) {
991
- "center" -> Alignment.Center
992
- "topcenter" -> Alignment.TopCenter
993
- "topstart" -> Alignment.TopStart
994
- "topend" -> Alignment.TopEnd
995
- "bottomcenter" -> Alignment.BottomCenter
996
- "bottomstart" -> Alignment.BottomStart
997
- "bottomend" -> Alignment.BottomEnd
998
- "centerstart" -> Alignment.CenterStart
999
- "centerend" -> Alignment.CenterEnd
1000
- else -> Alignment.TopStart
1001
- }
1002
- }
961
+ # IDE
962
+ .idea
963
+ *.iml
964
+ .vscode
965
+ .DS_Store
1003
966
 
1004
- private fun parseVerticalArrangement(arrangement: String?): Arrangement.Vertical {
1005
- return when (arrangement?.lowercase()) {
1006
- "top" -> Arrangement.Top
1007
- "center" -> Arrangement.Center
1008
- "bottom" -> Arrangement.Bottom
1009
- "spacebetween" -> Arrangement.SpaceBetween
1010
- "spacearound" -> Arrangement.SpaceAround
1011
- "spaceevenly" -> Arrangement.SpaceEvenly
1012
- else -> Arrangement.Top
1013
- }
1014
- }
967
+ # Claude Code
968
+ .claude
969
+ .claude-worktrees
1015
970
 
1016
- private fun parseHorizontalArrangement(arrangement: String?): Arrangement.Horizontal {
1017
- return when (arrangement?.lowercase()) {
1018
- "start" -> Arrangement.Start
1019
- "center" -> Arrangement.Center
1020
- "end" -> Arrangement.End
1021
- "spacebetween" -> Arrangement.SpaceBetween
1022
- "spacearound" -> Arrangement.SpaceAround
1023
- "spaceevenly" -> Arrangement.SpaceEvenly
1024
- else -> Arrangement.Start
1025
- }
1026
- }
971
+ # JetStart
972
+ .jetstart
1027
973
 
1028
- /**
1029
- * Parse text style
1030
- */
1031
- @Composable
1032
- private fun parseTextStyle(style: String?): androidx.compose.ui.text.TextStyle {
1033
- return when (style?.lowercase()) {
1034
- "headlinelarge" -> MaterialTheme.typography.headlineLarge
1035
- "headlinemedium" -> MaterialTheme.typography.headlineMedium
1036
- "headlinesmall" -> MaterialTheme.typography.headlineSmall
1037
- "titlelarge" -> MaterialTheme.typography.titleLarge
1038
- "titlemedium" -> MaterialTheme.typography.titleMedium
1039
- "titlesmall" -> MaterialTheme.typography.titleSmall
1040
- "bodylarge" -> MaterialTheme.typography.bodyLarge
1041
- "bodymedium" -> MaterialTheme.typography.bodyMedium
1042
- "bodysmall" -> MaterialTheme.typography.bodySmall
1043
- "labellarge" -> MaterialTheme.typography.labelLarge
1044
- "labelmedium" -> MaterialTheme.typography.labelMedium
1045
- "labelsmall" -> MaterialTheme.typography.labelSmall
1046
- else -> MaterialTheme.typography.bodyMedium
1047
- }
1048
- }
974
+ # Android
975
+ local.properties
976
+ *.apk
977
+ *.aab
978
+ *.ap_
979
+ *.dex
980
+ *.class
981
+ bin/
982
+ gen/
983
+ out/
984
+ captures/
985
+ .externalNativeBuild
986
+ .cxx
1049
987
 
1050
- /**
1051
- * Parse color from string
1052
- */
1053
- private fun parseColor(colorString: String?): Color? {
1054
- if (colorString == null) return null
988
+ # Log files
989
+ *.log
1055
990
 
1056
- return try {
1057
- when {
1058
- colorString.startsWith("#") -> {
1059
- // Hex color
1060
- Color(android.graphics.Color.parseColor(colorString))
1061
- }
1062
- else -> null
1063
- }
1064
- } catch (e: Exception) {
1065
- Log.w(TAG, "Failed to parse color: \$colorString")
1066
- null
1067
- }
991
+ # Keystore files
992
+ *.jks
993
+ *.keystore`;
994
+
995
+ await fs.writeFile(path.join(projectPath, '.gitignore'), content);
996
+ }
997
+
998
+ async function generateReadme(projectPath: string, projectName: string): Promise<void> {
999
+ const content = `# ${projectName}
1000
+
1001
+ A JetStart project with Kotlin and Jetpack Compose.
1002
+
1003
+ ## Getting Started
1004
+
1005
+ \`\`\`bash
1006
+ # Start development server
1007
+ jetstart dev
1008
+
1009
+ # Build production APK
1010
+ jetstart build
1011
+
1012
+ # View logs
1013
+ jetstart logs
1014
+ \`\`\`
1015
+
1016
+ ## Project Structure
1017
+
1018
+ \`\`\`
1019
+ ${projectName}/
1020
+ ├── app/
1021
+ │ └── src/
1022
+ │ └── main/
1023
+ │ ├── java/ # Kotlin source files
1024
+ │ └── res/ # Resources
1025
+ ├── jetstart.config.json # JetStart configuration
1026
+ └── build.gradle # Gradle build file
1027
+ \`\`\`
1028
+
1029
+ ## Learn More
1030
+
1031
+ - [JetStart Documentation](https://github.com/phantom/jetstart)
1032
+ - [Jetpack Compose](https://developer.android.com/jetpack/compose)
1033
+ `;
1034
+
1035
+ await fs.writeFile(path.join(projectPath, 'README.md'), content);
1036
+ }
1037
+
1038
+ async function generateRootBuildGradle(projectPath: string): Promise<void> {
1039
+ const content = `// Top-level build file
1040
+ buildscript {
1041
+ ext {
1042
+ kotlin_version = '1.9.21'
1043
+ compose_version = '1.5.4'
1044
+ }
1045
+ repositories {
1046
+ google()
1047
+ mavenCentral()
1068
1048
  }
1049
+ dependencies {
1050
+ classpath 'com.android.tools.build:gradle:8.2.0'
1051
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
1052
+ }
1053
+ }
1069
1054
 
1070
- /**
1071
- * Handle click events
1072
- */
1073
- private fun handleClick(action: String?) {
1074
- if (action != null) {
1075
- Log.d(TAG, "Button clicked: \$action")
1076
- // TODO: Implement action handling
1077
- }
1055
+ allprojects {
1056
+ repositories {
1057
+ google()
1058
+ mavenCentral()
1078
1059
  }
1079
1060
  }
1080
- `;
1081
1061
 
1082
- await fs.ensureDir(path.dirname(interpreterPath));
1083
- await fs.writeFile(interpreterPath, content);
1062
+ task clean(type: Delete) {
1063
+ delete rootProject.buildDir
1064
+ }`;
1065
+
1066
+ await fs.writeFile(path.join(projectPath, 'build.gradle'), content);
1067
+ }
1068
+
1069
+ async function generateGradleWrapper(projectPath: string): Promise<void> {
1070
+ // Use system Gradle to initialize proper wrapper
1071
+ // This generates:
1072
+ // - gradle/wrapper/gradle-wrapper.jar
1073
+ // - gradle/wrapper/gradle-wrapper.properties
1074
+ // - gradlew (Unix shell script)
1075
+ // - gradlew.bat (Windows batch script)
1076
+
1077
+ return new Promise<void>((resolve) => {
1078
+ // Try to use system gradle to generate wrapper
1079
+ const gradleCmd = process.platform === 'win32' ? 'gradle.bat' : 'gradle';
1080
+
1081
+ const gradleProcess = spawn(gradleCmd, ['wrapper', '--gradle-version', '8.2'], {
1082
+ cwd: projectPath,
1083
+ shell: true,
1084
+ });
1085
+
1086
+ gradleProcess.on('close', (code) => {
1087
+ // Continue regardless of success/failure
1088
+ // If gradle wrapper command fails, the build will fall back to system gradle
1089
+ resolve();
1090
+ });
1091
+
1092
+ gradleProcess.on('error', () => {
1093
+ // Continue even if gradle command not found
1094
+ resolve();
1095
+ });
1096
+
1097
+ // Timeout after 30 seconds
1098
+ setTimeout(() => {
1099
+ gradleProcess.kill();
1100
+ resolve();
1101
+ }, 30000);
1102
+ });
1084
1103
  }
1085
1104
 
1086
- async function generateDSLTypes(
1105
+ async function generateResourceFiles(
1087
1106
  projectPath: string,
1088
- packageName: string
1107
+ projectName: string
1089
1108
  ): Promise<void> {
1090
- const packagePath = packageName.replace(/\./g, '/');
1091
- const typesPath = path.join(
1092
- projectPath,
1093
- 'app/src/main/java',
1094
- packagePath,
1095
- 'DSLTypes.kt'
1109
+ // Generate strings.xml
1110
+ const stringsXml = `<?xml version="1.0" encoding="utf-8"?>
1111
+ <resources>
1112
+ <string name="app_name">${projectName}</string>
1113
+ </resources>`;
1114
+
1115
+ await fs.writeFile(
1116
+ path.join(projectPath, 'app/src/main/res/values/strings.xml'),
1117
+ stringsXml
1096
1118
  );
1097
1119
 
1098
- const content = `package ${packageName}
1120
+ // Generate colors.xml
1121
+ const colorsXml = `<?xml version="1.0" encoding="utf-8"?>
1122
+ <resources>
1123
+ <color name="purple_200">#FFBB86FC</color>
1124
+ <color name="purple_500">#FF6200EE</color>
1125
+ <color name="purple_700">#FF3700B3</color>
1126
+ <color name="teal_200">#FF03DAC5</color>
1127
+ <color name="teal_700">#FF018786</color>
1128
+ <color name="black">#FF000000</color>
1129
+ <color name="white">#FFFFFFFF</color>
1130
+ </resources>`;
1099
1131
 
1100
- import org.json.JSONArray
1101
- import org.json.JSONObject
1132
+ await fs.writeFile(
1133
+ path.join(projectPath, 'app/src/main/res/values/colors.xml'),
1134
+ colorsXml
1135
+ );
1102
1136
 
1103
- /**
1104
- * DSL Type Definitions
1105
- * Represents UI elements in JSON format that can be interpreted at runtime
1106
- */
1137
+ // Generate themes.xml
1138
+ const themesXml = `<?xml version="1.0" encoding="utf-8"?>
1139
+ <resources>
1140
+ <style name="Theme.${projectName.replace(/[^a-zA-Z0-9]/g, '')}" parent="android:Theme.Material.Light.NoActionBar" />
1141
+ </resources>`;
1107
1142
 
1108
- data class UIDefinition(
1109
- val version: String = "1.0",
1110
- val screen: DSLElement
1111
- )
1143
+ await fs.writeFile(
1144
+ path.join(projectPath, 'app/src/main/res/values/themes.xml'),
1145
+ themesXml
1146
+ );
1112
1147
 
1113
- data class DSLElement(
1114
- val type: String,
1115
- val text: String? = null,
1116
- val style: String? = null,
1117
- val color: String? = null,
1118
- val modifier: DSLModifier? = null,
1119
- val horizontalAlignment: String? = null,
1120
- val verticalArrangement: String? = null,
1121
- val contentAlignment: String? = null,
1122
- val height: Int? = null,
1123
- val width: Int? = null,
1124
- val onClick: String? = null,
1125
- val enabled: Boolean? = true,
1126
- val imageVector: String? = null,
1127
- val tint: String? = null,
1128
- val contentDescription: String? = null,
1129
- val children: List<DSLElement>? = null
1130
- )
1148
+ // Generate network_security_config.xml for development (allows cleartext traffic)
1149
+ const networkSecurityConfig = `<?xml version="1.0" encoding="utf-8"?>
1150
+ <network-security-config>
1151
+ <base-config cleartextTrafficPermitted="true">
1152
+ <trust-anchors>
1153
+ <certificates src="system" />
1154
+ </trust-anchors>
1155
+ </base-config>
1156
+ </network-security-config>`;
1131
1157
 
1132
- data class DSLModifier(
1133
- val fillMaxSize: Boolean? = null,
1134
- val fillMaxWidth: Boolean? = null,
1135
- val fillMaxHeight: Boolean? = null,
1136
- val padding: Int? = null,
1137
- val paddingHorizontal: Int? = null,
1138
- val paddingVertical: Int? = null,
1139
- val size: Int? = null,
1140
- val height: Int? = null,
1141
- val width: Int? = null,
1142
- val weight: Float? = null
1143
- )
1158
+ await fs.ensureDir(path.join(projectPath, 'app/src/main/res/xml'));
1159
+ await fs.writeFile(
1160
+ path.join(projectPath, 'app/src/main/res/xml/network_security_config.xml'),
1161
+ networkSecurityConfig
1162
+ );
1163
+ }
1144
1164
 
1145
- /**
1146
- * Parse JSON string to UIDefinition
1147
- */
1148
- fun parseUIDefinition(json: String): UIDefinition {
1149
- val obj = JSONObject(json)
1150
- val version = obj.optString("version", "1.0")
1151
- val screenObj = obj.getJSONObject("screen")
1165
+ async function generateLocalProperties(projectPath: string): Promise<void> {
1166
+ // Auto-detect Android SDK location
1167
+ let androidSdkPath: string | undefined;
1152
1168
 
1153
- return UIDefinition(
1154
- version = version,
1155
- screen = parseDSLElement(screenObj)
1156
- )
1157
- }
1169
+ // Check environment variables first
1170
+ androidSdkPath = process.env.ANDROID_HOME || process.env.ANDROID_SDK_ROOT;
1158
1171
 
1159
- /**
1160
- * Parse JSONObject to DSLElement
1161
- */
1162
- fun parseDSLElement(obj: JSONObject): DSLElement {
1163
- val children = if (obj.has("children")) {
1164
- val childrenArray = obj.getJSONArray("children")
1165
- List(childrenArray.length()) { i ->
1166
- parseDSLElement(childrenArray.getJSONObject(i))
1167
- }
1168
- } else null
1172
+ // If not found, check common Windows locations
1173
+ if (!androidSdkPath && process.platform === 'win32') {
1174
+ const commonPaths = [
1175
+ 'C:\\Android',
1176
+ path.join(require('os').homedir(), 'AppData', 'Local', 'Android', 'Sdk'),
1177
+ 'C:\\Android\\Sdk',
1178
+ 'C:\\Program Files (x86)\\Android\\android-sdk',
1179
+ ];
1169
1180
 
1170
- val modifier = if (obj.has("modifier")) {
1171
- val modObj = obj.getJSONObject("modifier")
1172
- DSLModifier(
1173
- fillMaxSize = modObj.optBoolean("fillMaxSize"),
1174
- fillMaxWidth = modObj.optBoolean("fillMaxWidth"),
1175
- fillMaxHeight = modObj.optBoolean("fillMaxHeight"),
1176
- padding = if (modObj.has("padding")) modObj.getInt("padding") else null,
1177
- paddingHorizontal = if (modObj.has("paddingHorizontal")) modObj.getInt("paddingHorizontal") else null,
1178
- paddingVertical = if (modObj.has("paddingVertical")) modObj.getInt("paddingVertical") else null,
1179
- size = if (modObj.has("size")) modObj.getInt("size") else null,
1180
- height = if (modObj.has("height")) modObj.getInt("height") else null,
1181
- width = if (modObj.has("width")) modObj.getInt("width") else null,
1182
- weight = if (modObj.has("weight")) modObj.getDouble("weight").toFloat() else null
1183
- )
1184
- } else null
1181
+ for (const p of commonPaths) {
1182
+ if (fs.existsSync(p)) {
1183
+ androidSdkPath = p;
1184
+ break;
1185
+ }
1186
+ }
1187
+ }
1185
1188
 
1186
- return DSLElement(
1187
- type = obj.getString("type"),
1188
- text = if (obj.has("text")) obj.getString("text") else null,
1189
- style = if (obj.has("style")) obj.getString("style") else null,
1190
- color = if (obj.has("color")) obj.getString("color") else null,
1191
- modifier = modifier,
1192
- horizontalAlignment = if (obj.has("horizontalAlignment")) obj.getString("horizontalAlignment") else null,
1193
- verticalArrangement = if (obj.has("verticalArrangement")) obj.getString("verticalArrangement") else null,
1194
- contentAlignment = if (obj.has("contentAlignment")) obj.getString("contentAlignment") else null,
1195
- height = if (obj.has("height")) obj.getInt("height") else null,
1196
- width = if (obj.has("width")) obj.getInt("width") else null,
1197
- onClick = if (obj.has("onClick")) obj.getString("onClick") else null,
1198
- enabled = obj.optBoolean("enabled", true),
1199
- imageVector = if (obj.has("imageVector")) obj.getString("imageVector") else null,
1200
- tint = if (obj.has("tint")) obj.getString("tint") else null,
1201
- contentDescription = if (obj.has("contentDescription")) obj.getString("contentDescription") else null,
1202
- children = children
1203
- )
1204
- }
1189
+ // If not found on macOS/Linux, check common paths
1190
+ if (!androidSdkPath && process.platform !== 'win32') {
1191
+ const commonPaths = [
1192
+ path.join(require('os').homedir(), 'Android', 'Sdk'),
1193
+ path.join(require('os').homedir(), 'Library', 'Android', 'sdk'),
1194
+ '/opt/android-sdk',
1195
+ ];
1196
+
1197
+ for (const p of commonPaths) {
1198
+ if (fs.existsSync(p)) {
1199
+ androidSdkPath = p;
1200
+ break;
1201
+ }
1202
+ }
1203
+ }
1204
+
1205
+ if (!androidSdkPath) {
1206
+ console.warn('[Warning] Android SDK not found. You may need to set ANDROID_HOME or create local.properties manually.');
1207
+ return;
1208
+ }
1209
+
1210
+ // Create local.properties with SDK path
1211
+ const content = `# Auto-generated by JetStart
1212
+ sdk.dir=${androidSdkPath.replace(/\\/g, '\\\\')}
1205
1213
  `;
1206
1214
 
1207
- await fs.ensureDir(path.dirname(typesPath));
1208
- await fs.writeFile(typesPath, content);
1209
- }
1215
+ await fs.writeFile(path.join(projectPath, 'local.properties'), content);
1216
+ console.log(`[JetStart] Created local.properties with SDK: ${androidSdkPath}`);
1217
+ }