@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.
- package/dist/cli.js +18 -1
- package/dist/commands/android-emulator.d.ts +8 -0
- package/dist/commands/android-emulator.js +280 -0
- package/dist/commands/create.d.ts +1 -6
- package/dist/commands/create.js +119 -0
- package/dist/commands/dev.d.ts +1 -7
- package/dist/commands/dev.js +69 -10
- package/dist/commands/index.d.ts +2 -0
- package/dist/commands/index.js +5 -1
- package/dist/commands/install-audit.d.ts +9 -0
- package/dist/commands/install-audit.js +185 -0
- package/dist/types/index.d.ts +22 -0
- package/dist/types/index.js +8 -0
- package/dist/utils/android-sdk.d.ts +81 -0
- package/dist/utils/android-sdk.js +432 -0
- package/dist/utils/downloader.d.ts +35 -0
- package/dist/utils/downloader.js +214 -0
- package/dist/utils/emulator-deployer.d.ts +29 -0
- package/dist/utils/emulator-deployer.js +224 -0
- package/dist/utils/emulator.d.ts +101 -0
- package/dist/utils/emulator.js +410 -0
- package/dist/utils/java.d.ts +25 -0
- package/dist/utils/java.js +363 -0
- package/dist/utils/system-tools.d.ts +93 -0
- package/dist/utils/system-tools.js +599 -0
- package/dist/utils/template.js +777 -748
- package/package.json +7 -3
- package/src/cli.ts +20 -2
- package/src/commands/android-emulator.ts +304 -0
- package/src/commands/create.ts +128 -5
- package/src/commands/dev.ts +71 -18
- package/src/commands/index.ts +3 -1
- package/src/commands/install-audit.ts +227 -0
- package/src/types/index.ts +30 -0
- package/src/utils/android-sdk.ts +478 -0
- package/src/utils/downloader.ts +201 -0
- package/src/utils/emulator-deployer.ts +210 -0
- package/src/utils/emulator.ts +463 -0
- package/src/utils/java.ts +369 -0
- package/src/utils/system-tools.ts +648 -0
- package/src/utils/template.ts +875 -867
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/commands/build.d.ts.map +0 -1
- package/dist/commands/build.js.map +0 -1
- package/dist/commands/create.d.ts.map +0 -1
- package/dist/commands/create.js.map +0 -1
- package/dist/commands/dev.d.ts.map +0 -1
- package/dist/commands/dev.js.map +0 -1
- package/dist/commands/index.d.ts.map +0 -1
- package/dist/commands/index.js.map +0 -1
- package/dist/commands/logs.d.ts.map +0 -1
- package/dist/commands/logs.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/types/index.d.ts.map +0 -1
- package/dist/types/index.js.map +0 -1
- package/dist/utils/index.d.ts.map +0 -1
- package/dist/utils/index.js.map +0 -1
- package/dist/utils/logger.d.ts.map +0 -1
- package/dist/utils/logger.js.map +0 -1
- package/dist/utils/prompt.d.ts.map +0 -1
- package/dist/utils/prompt.js.map +0 -1
- package/dist/utils/spinner.d.ts.map +0 -1
- package/dist/utils/spinner.js.map +0 -1
- package/dist/utils/template.d.ts.map +0 -1
- package/dist/utils/template.js.map +0 -1
package/src/utils/template.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
42
|
+
const jetStartPath = path.join(
|
|
141
43
|
projectPath,
|
|
142
44
|
'app/src/main/java',
|
|
143
45
|
packagePath,
|
|
144
|
-
'
|
|
46
|
+
'JetStart.kt'
|
|
145
47
|
);
|
|
146
48
|
|
|
147
49
|
const content = `package ${packageName}
|
|
148
50
|
|
|
149
|
-
|
|
150
|
-
import
|
|
151
|
-
import
|
|
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
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
}
|
|
198
|
-
}
|
|
80
|
+
// ============================================================================
|
|
81
|
+
// DSL Type Definitions
|
|
82
|
+
// ============================================================================
|
|
199
83
|
|
|
200
84
|
/**
|
|
201
|
-
*
|
|
202
|
-
*
|
|
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
|
-
|
|
89
|
+
data class UIDefinition(
|
|
90
|
+
val version: String = "1.0",
|
|
91
|
+
val screen: DSLElement
|
|
92
|
+
)
|
|
219
93
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
-
|
|
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
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
-
|
|
237
|
-
|
|
134
|
+
return UIDefinition(
|
|
135
|
+
version = version,
|
|
136
|
+
screen = parseDSLElement(screenObj)
|
|
137
|
+
)
|
|
238
138
|
}
|
|
239
139
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
):
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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
|
-
|
|
297
|
-
|
|
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
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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
|
-
|
|
310
|
-
|
|
311
|
-
|
|
214
|
+
/**
|
|
215
|
+
* Render DSL as Compose UI
|
|
216
|
+
*/
|
|
217
|
+
@Composable
|
|
218
|
+
fun RenderDSL(definition: UIDefinition) {
|
|
219
|
+
RenderElement(definition.screen)
|
|
220
|
+
}
|
|
312
221
|
|
|
313
|
-
|
|
314
|
-
|
|
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
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
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
|
-
|
|
331
|
-
|
|
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
|
-
|
|
334
|
-
|
|
335
|
-
|
|
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
|
-
|
|
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
|
-
|
|
341
|
-
|
|
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
|
-
|
|
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
|
-
|
|
309
|
+
/**
|
|
310
|
+
* Parse DSL modifier to Compose Modifier
|
|
311
|
+
*/
|
|
312
|
+
private fun parseModifier(dslModifier: DSLModifier?): Modifier {
|
|
313
|
+
var modifier: Modifier = Modifier
|
|
346
314
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
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
|
-
|
|
352
|
-
|
|
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
|
-
|
|
355
|
-
|
|
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
|
-
|
|
328
|
+
// Note: weight() is only available in RowScope/ColumnScope
|
|
329
|
+
// We'll handle it separately when needed
|
|
330
|
+
}
|
|
359
331
|
|
|
360
|
-
|
|
361
|
-
|
|
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
|
-
|
|
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
|
-
|
|
374
|
-
|
|
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
|
-
|
|
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
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
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
|
-
|
|
388
|
-
|
|
389
|
-
|
|
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
|
-
|
|
392
|
-
|
|
393
|
-
|
|
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
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
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
|
-
|
|
405
|
-
|
|
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
|
-
|
|
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
|
-
|
|
412
|
-
|
|
413
|
-
|
|
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
|
-
|
|
420
|
-
|
|
421
|
-
|
|
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
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
491
|
+
/**
|
|
492
|
+
* Get the current WebSocket connection (for sending messages)
|
|
493
|
+
*/
|
|
494
|
+
fun getWebSocket(): WebSocket? = webSocket
|
|
427
495
|
|
|
428
|
-
|
|
429
|
-
|
|
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
|
-
|
|
435
|
-
|
|
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
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
}, 30000);
|
|
444
|
-
});
|
|
445
|
-
}
|
|
502
|
+
val client = OkHttpClient()
|
|
503
|
+
val request = Request.Builder()
|
|
504
|
+
.url(wsUrl)
|
|
505
|
+
.build()
|
|
446
506
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
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
|
-
|
|
458
|
-
|
|
459
|
-
|
|
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
|
-
|
|
463
|
-
|
|
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
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
);
|
|
525
|
+
try {
|
|
526
|
+
val json = JSONObject(text)
|
|
527
|
+
val type = json.getString("type")
|
|
478
528
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
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
|
-
|
|
486
|
-
|
|
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(
|
|
795
|
-
await fs.writeFile(
|
|
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
|
|
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
|
|
798
|
+
const activityPath = path.join(
|
|
804
799
|
projectPath,
|
|
805
800
|
'app/src/main/java',
|
|
806
801
|
packagePath,
|
|
807
|
-
'
|
|
802
|
+
'MainActivity.kt'
|
|
808
803
|
);
|
|
809
804
|
|
|
810
805
|
const content = `package ${packageName}
|
|
811
806
|
|
|
812
|
-
import android.
|
|
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
|
-
|
|
826
|
-
|
|
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
|
|
840
|
-
|
|
841
|
-
|
|
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
|
-
|
|
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
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
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
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
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
|
-
|
|
875
|
-
|
|
876
|
-
|
|
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
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
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
|
-
|
|
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 =
|
|
916
|
-
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
|
-
|
|
923
|
-
|
|
883
|
+
Spacer(modifier = Modifier.height(24.dp))
|
|
884
|
+
|
|
924
885
|
Button(
|
|
925
|
-
onClick = {
|
|
926
|
-
modifier =
|
|
927
|
-
enabled = element.enabled ?: true
|
|
886
|
+
onClick = { /* Handle click */ },
|
|
887
|
+
modifier = Modifier.fillMaxWidth()
|
|
928
888
|
) {
|
|
929
|
-
Text(
|
|
889
|
+
Text("Click Me!")
|
|
930
890
|
}
|
|
931
891
|
}
|
|
892
|
+
}`;
|
|
932
893
|
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
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
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
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
|
-
|
|
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
|
-
|
|
954
|
-
|
|
955
|
-
|
|
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
|
-
|
|
958
|
-
m.height?.let { modifier = modifier.height(it.dp) }
|
|
959
|
-
m.width?.let { modifier = modifier.width(it.dp) }
|
|
923
|
+
</manifest>`;
|
|
960
924
|
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
925
|
+
await fs.writeFile(
|
|
926
|
+
path.join(projectPath, 'app/src/main/AndroidManifest.xml'),
|
|
927
|
+
content
|
|
928
|
+
);
|
|
929
|
+
}
|
|
964
930
|
|
|
965
|
-
|
|
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
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
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
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
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
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
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
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
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
|
-
|
|
1017
|
-
|
|
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
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
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
|
-
|
|
1052
|
-
*/
|
|
1053
|
-
private fun parseColor(colorString: String?): Color? {
|
|
1054
|
-
if (colorString == null) return null
|
|
988
|
+
# Log files
|
|
989
|
+
*.log
|
|
1055
990
|
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
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
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
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
|
-
|
|
1083
|
-
|
|
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
|
|
1105
|
+
async function generateResourceFiles(
|
|
1087
1106
|
projectPath: string,
|
|
1088
|
-
|
|
1107
|
+
projectName: string
|
|
1089
1108
|
): Promise<void> {
|
|
1090
|
-
|
|
1091
|
-
const
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1101
|
-
|
|
1132
|
+
await fs.writeFile(
|
|
1133
|
+
path.join(projectPath, 'app/src/main/res/values/colors.xml'),
|
|
1134
|
+
colorsXml
|
|
1135
|
+
);
|
|
1102
1136
|
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
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
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
)
|
|
1143
|
+
await fs.writeFile(
|
|
1144
|
+
path.join(projectPath, 'app/src/main/res/values/themes.xml'),
|
|
1145
|
+
themesXml
|
|
1146
|
+
);
|
|
1112
1147
|
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
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
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1154
|
-
|
|
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
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
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
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
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
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
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.
|
|
1208
|
-
|
|
1209
|
-
}
|
|
1215
|
+
await fs.writeFile(path.join(projectPath, 'local.properties'), content);
|
|
1216
|
+
console.log(`[JetStart] Created local.properties with SDK: ${androidSdkPath}`);
|
|
1217
|
+
}
|