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