@jetstart/cli 1.6.0 → 2.0.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/README.md +133 -41
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +11 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/android-emulator.d.ts.map +1 -0
- package/dist/commands/android-emulator.js.map +1 -0
- package/dist/commands/build.d.ts +13 -1
- package/dist/commands/build.d.ts.map +1 -0
- package/dist/commands/build.js +279 -29
- package/dist/commands/build.js.map +1 -0
- package/dist/commands/clean.d.ts +23 -0
- package/dist/commands/clean.d.ts.map +1 -0
- package/dist/commands/clean.js +191 -0
- package/dist/commands/clean.js.map +1 -0
- package/dist/commands/create.d.ts.map +1 -0
- package/dist/commands/create.js +41 -0
- package/dist/commands/create.js.map +1 -0
- package/dist/commands/dev.d.ts.map +1 -0
- package/dist/commands/dev.js +51 -9
- package/dist/commands/dev.js.map +1 -0
- package/dist/commands/index.d.ts.map +1 -0
- package/dist/commands/index.js.map +1 -0
- package/dist/commands/install-audit.d.ts.map +1 -0
- package/dist/commands/install-audit.js.map +1 -0
- package/dist/commands/logs.d.ts.map +1 -0
- package/dist/commands/logs.js.map +1 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js.map +1 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/android-sdk.d.ts.map +1 -0
- package/dist/utils/android-sdk.js +2 -2
- package/dist/utils/android-sdk.js.map +1 -0
- package/dist/utils/downloader.d.ts.map +1 -0
- package/dist/utils/downloader.js.map +1 -0
- package/dist/utils/emulator-deployer.d.ts.map +1 -0
- package/dist/utils/emulator-deployer.js.map +1 -0
- package/dist/utils/emulator.d.ts.map +1 -0
- package/dist/utils/emulator.js +5 -4
- package/dist/utils/emulator.js.map +1 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/java.d.ts.map +1 -0
- package/dist/utils/java.js +5 -5
- package/dist/utils/java.js.map +1 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/open.d.ts.map +1 -0
- package/dist/utils/open.js.map +1 -0
- package/dist/utils/prompt.d.ts.map +1 -0
- package/dist/utils/prompt.js.map +1 -0
- package/dist/utils/spinner.d.ts.map +1 -0
- package/dist/utils/spinner.js.map +1 -0
- package/dist/utils/system-tools.d.ts.map +1 -0
- package/dist/utils/system-tools.js.map +1 -0
- package/dist/utils/template.d.ts +13 -1
- package/dist/utils/template.d.ts.map +1 -0
- package/dist/utils/template.js +133 -1003
- package/dist/utils/template.js.map +1 -0
- package/maven-repo/com/jetstart/hot-reload/com.jetstart.hot-reload.gradle.plugin/1.0.0/com.jetstart.hot-reload.gradle.plugin-1.0.0.pom +15 -0
- package/maven-repo/com/jetstart/hot-reload/com.jetstart.hot-reload.gradle.plugin/1.0.0/com.jetstart.hot-reload.gradle.plugin-1.0.0.pom.md5 +1 -0
- package/maven-repo/com/jetstart/hot-reload/com.jetstart.hot-reload.gradle.plugin/1.0.0/com.jetstart.hot-reload.gradle.plugin-1.0.0.pom.sha1 +1 -0
- package/maven-repo/com/jetstart/hot-reload/com.jetstart.hot-reload.gradle.plugin/1.0.0/com.jetstart.hot-reload.gradle.plugin-1.0.0.pom.sha256 +1 -0
- package/maven-repo/com/jetstart/hot-reload/com.jetstart.hot-reload.gradle.plugin/1.0.0/com.jetstart.hot-reload.gradle.plugin-1.0.0.pom.sha512 +1 -0
- package/maven-repo/com/jetstart/hot-reload/com.jetstart.hot-reload.gradle.plugin/maven-metadata.xml +13 -0
- package/maven-repo/com/jetstart/hot-reload/com.jetstart.hot-reload.gradle.plugin/maven-metadata.xml.md5 +1 -0
- package/maven-repo/com/jetstart/hot-reload/com.jetstart.hot-reload.gradle.plugin/maven-metadata.xml.sha1 +1 -0
- package/maven-repo/com/jetstart/hot-reload/com.jetstart.hot-reload.gradle.plugin/maven-metadata.xml.sha256 +1 -0
- package/maven-repo/com/jetstart/hot-reload/com.jetstart.hot-reload.gradle.plugin/maven-metadata.xml.sha512 +1 -0
- package/maven-repo/com/jetstart/hot-reload-runtime/1.0.0/hot-reload-runtime-1.0.0-sources.jar +0 -0
- package/maven-repo/com/jetstart/hot-reload-runtime/1.0.0/hot-reload-runtime-1.0.0-sources.jar.md5 +1 -0
- package/maven-repo/com/jetstart/hot-reload-runtime/1.0.0/hot-reload-runtime-1.0.0-sources.jar.sha1 +1 -0
- package/maven-repo/com/jetstart/hot-reload-runtime/1.0.0/hot-reload-runtime-1.0.0-sources.jar.sha256 +1 -0
- package/maven-repo/com/jetstart/hot-reload-runtime/1.0.0/hot-reload-runtime-1.0.0-sources.jar.sha512 +1 -0
- package/maven-repo/com/jetstart/hot-reload-runtime/1.0.0/hot-reload-runtime-1.0.0.aar +0 -0
- package/maven-repo/com/jetstart/hot-reload-runtime/1.0.0/hot-reload-runtime-1.0.0.aar.md5 +1 -0
- package/maven-repo/com/jetstart/hot-reload-runtime/1.0.0/hot-reload-runtime-1.0.0.aar.sha1 +1 -0
- package/maven-repo/com/jetstart/hot-reload-runtime/1.0.0/hot-reload-runtime-1.0.0.aar.sha256 +1 -0
- package/maven-repo/com/jetstart/hot-reload-runtime/1.0.0/hot-reload-runtime-1.0.0.aar.sha512 +1 -0
- package/maven-repo/com/jetstart/hot-reload-runtime/1.0.0/hot-reload-runtime-1.0.0.module +124 -0
- package/maven-repo/com/jetstart/hot-reload-runtime/1.0.0/hot-reload-runtime-1.0.0.module.md5 +1 -0
- package/maven-repo/com/jetstart/hot-reload-runtime/1.0.0/hot-reload-runtime-1.0.0.module.sha1 +1 -0
- package/maven-repo/com/jetstart/hot-reload-runtime/1.0.0/hot-reload-runtime-1.0.0.module.sha256 +1 -0
- package/maven-repo/com/jetstart/hot-reload-runtime/1.0.0/hot-reload-runtime-1.0.0.module.sha512 +1 -0
- package/maven-repo/com/jetstart/hot-reload-runtime/1.0.0/hot-reload-runtime-1.0.0.pom +46 -0
- package/maven-repo/com/jetstart/hot-reload-runtime/1.0.0/hot-reload-runtime-1.0.0.pom.md5 +1 -0
- package/maven-repo/com/jetstart/hot-reload-runtime/1.0.0/hot-reload-runtime-1.0.0.pom.sha1 +1 -0
- package/maven-repo/com/jetstart/hot-reload-runtime/1.0.0/hot-reload-runtime-1.0.0.pom.sha256 +1 -0
- package/maven-repo/com/jetstart/hot-reload-runtime/1.0.0/hot-reload-runtime-1.0.0.pom.sha512 +1 -0
- package/maven-repo/com/jetstart/hot-reload-runtime/maven-metadata.xml +13 -0
- package/maven-repo/com/jetstart/hot-reload-runtime/maven-metadata.xml.md5 +1 -0
- package/maven-repo/com/jetstart/hot-reload-runtime/maven-metadata.xml.sha1 +1 -0
- package/maven-repo/com/jetstart/hot-reload-runtime/maven-metadata.xml.sha256 +1 -0
- package/maven-repo/com/jetstart/hot-reload-runtime/maven-metadata.xml.sha512 +1 -0
- package/maven-repo/com/jetstart/jetstart-gradle-plugin/1.0.0/jetstart-gradle-plugin-1.0.0.jar +0 -0
- package/maven-repo/com/jetstart/jetstart-gradle-plugin/1.0.0/jetstart-gradle-plugin-1.0.0.jar.md5 +1 -0
- package/maven-repo/com/jetstart/jetstart-gradle-plugin/1.0.0/jetstart-gradle-plugin-1.0.0.jar.sha1 +1 -0
- package/maven-repo/com/jetstart/jetstart-gradle-plugin/1.0.0/jetstart-gradle-plugin-1.0.0.jar.sha256 +1 -0
- package/maven-repo/com/jetstart/jetstart-gradle-plugin/1.0.0/jetstart-gradle-plugin-1.0.0.jar.sha512 +1 -0
- package/maven-repo/com/jetstart/jetstart-gradle-plugin/1.0.0/jetstart-gradle-plugin-1.0.0.module +103 -0
- package/maven-repo/com/jetstart/jetstart-gradle-plugin/1.0.0/jetstart-gradle-plugin-1.0.0.module.md5 +1 -0
- package/maven-repo/com/jetstart/jetstart-gradle-plugin/1.0.0/jetstart-gradle-plugin-1.0.0.module.sha1 +1 -0
- package/maven-repo/com/jetstart/jetstart-gradle-plugin/1.0.0/jetstart-gradle-plugin-1.0.0.module.sha256 +1 -0
- package/maven-repo/com/jetstart/jetstart-gradle-plugin/1.0.0/jetstart-gradle-plugin-1.0.0.module.sha512 +1 -0
- package/maven-repo/com/jetstart/jetstart-gradle-plugin/1.0.0/jetstart-gradle-plugin-1.0.0.pom +38 -0
- package/maven-repo/com/jetstart/jetstart-gradle-plugin/1.0.0/jetstart-gradle-plugin-1.0.0.pom.md5 +1 -0
- package/maven-repo/com/jetstart/jetstart-gradle-plugin/1.0.0/jetstart-gradle-plugin-1.0.0.pom.sha1 +1 -0
- package/maven-repo/com/jetstart/jetstart-gradle-plugin/1.0.0/jetstart-gradle-plugin-1.0.0.pom.sha256 +1 -0
- package/maven-repo/com/jetstart/jetstart-gradle-plugin/1.0.0/jetstart-gradle-plugin-1.0.0.pom.sha512 +1 -0
- package/maven-repo/com/jetstart/jetstart-gradle-plugin/maven-metadata.xml +13 -0
- package/maven-repo/com/jetstart/jetstart-gradle-plugin/maven-metadata.xml.md5 +1 -0
- package/maven-repo/com/jetstart/jetstart-gradle-plugin/maven-metadata.xml.sha1 +1 -0
- package/maven-repo/com/jetstart/jetstart-gradle-plugin/maven-metadata.xml.sha256 +1 -0
- package/maven-repo/com/jetstart/jetstart-gradle-plugin/maven-metadata.xml.sha512 +1 -0
- package/package.json +13 -5
- package/scripts/build-java.js +30 -0
- package/.eslintrc.json +0 -6
- package/src/cli.ts +0 -99
- package/src/commands/android-emulator.ts +0 -304
- package/src/commands/build.ts +0 -60
- package/src/commands/create.ts +0 -232
- package/src/commands/dev.ts +0 -198
- package/src/commands/index.ts +0 -10
- package/src/commands/install-audit.ts +0 -227
- package/src/commands/logs.ts +0 -117
- package/src/index.ts +0 -17
- package/src/types/index.ts +0 -53
- package/src/utils/android-sdk.ts +0 -512
- package/src/utils/downloader.ts +0 -201
- package/src/utils/emulator-deployer.ts +0 -210
- package/src/utils/emulator.ts +0 -463
- package/src/utils/index.ts +0 -8
- package/src/utils/java.ts +0 -369
- package/src/utils/logger.ts +0 -42
- package/src/utils/open.ts +0 -36
- package/src/utils/prompt.ts +0 -56
- package/src/utils/spinner.ts +0 -25
- package/src/utils/system-tools.ts +0 -648
- package/src/utils/template.ts +0 -1217
- package/tests/create.test.ts +0 -33
- package/tests/utils.test.ts +0 -17
- package/tsconfig.json +0 -25
package/src/utils/template.ts
DELETED
|
@@ -1,1217 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Template Generator
|
|
3
|
-
* Creates project structure from templates
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import path from 'path';
|
|
7
|
-
import fs from 'fs-extra';
|
|
8
|
-
import { spawn } from 'child_process';
|
|
9
|
-
import { TemplateOptions } from '../types';
|
|
10
|
-
import { MIN_ANDROID_API_LEVEL, TARGET_ANDROID_API_LEVEL } from '@jetstart/shared';
|
|
11
|
-
|
|
12
|
-
export async function generateProjectTemplate(
|
|
13
|
-
projectPath: string,
|
|
14
|
-
options: TemplateOptions
|
|
15
|
-
): Promise<void> {
|
|
16
|
-
const { projectName, packageName } = options;
|
|
17
|
-
|
|
18
|
-
// Create directory structure
|
|
19
|
-
await createDirectoryStructure(projectPath);
|
|
20
|
-
|
|
21
|
-
// Generate files
|
|
22
|
-
await generateRootBuildGradle(projectPath);
|
|
23
|
-
await generateBuildGradle(projectPath, options);
|
|
24
|
-
await generateSettingsGradle(projectPath, projectName);
|
|
25
|
-
await generateGradleProperties(projectPath);
|
|
26
|
-
await generateGradleWrapper(projectPath);
|
|
27
|
-
await generateMainActivity(projectPath, packageName);
|
|
28
|
-
await generateJetStart(projectPath, packageName);
|
|
29
|
-
await generateAndroidManifest(projectPath, options);
|
|
30
|
-
await generateResourceFiles(projectPath, projectName);
|
|
31
|
-
await generateLocalProperties(projectPath);
|
|
32
|
-
await generateJetStartConfig(projectPath, options);
|
|
33
|
-
await generateGitignore(projectPath);
|
|
34
|
-
await generateReadme(projectPath, projectName);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
async function generateJetStart(
|
|
38
|
-
projectPath: string,
|
|
39
|
-
packageName: string
|
|
40
|
-
): Promise<void> {
|
|
41
|
-
const packagePath = packageName.replace(/\./g, '/');
|
|
42
|
-
const jetStartPath = path.join(
|
|
43
|
-
projectPath,
|
|
44
|
-
'app/src/main/java',
|
|
45
|
-
packagePath,
|
|
46
|
-
'JetStart.kt'
|
|
47
|
-
);
|
|
48
|
-
|
|
49
|
-
const content = `package ${packageName}
|
|
50
|
-
|
|
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
|
|
60
|
-
import androidx.compose.foundation.layout.*
|
|
61
|
-
import androidx.compose.material3.*
|
|
62
|
-
import androidx.compose.runtime.*
|
|
63
|
-
import androidx.compose.ui.Alignment
|
|
64
|
-
import androidx.compose.ui.Modifier
|
|
65
|
-
import androidx.compose.ui.graphics.Color
|
|
66
|
-
import androidx.compose.ui.text.font.FontWeight
|
|
67
|
-
import androidx.compose.ui.unit.dp
|
|
68
|
-
|
|
69
|
-
// Third-party
|
|
70
|
-
import kotlinx.coroutines.flow.MutableStateFlow
|
|
71
|
-
import kotlinx.coroutines.flow.StateFlow
|
|
72
|
-
import okhttp3.*
|
|
73
|
-
|
|
74
|
-
// Standard library & JSON
|
|
75
|
-
import org.json.JSONArray
|
|
76
|
-
import org.json.JSONObject
|
|
77
|
-
import java.io.File
|
|
78
|
-
import java.io.IOException
|
|
79
|
-
|
|
80
|
-
// ============================================================================
|
|
81
|
-
// DSL Type Definitions
|
|
82
|
-
// ============================================================================
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* DSL Type Definitions
|
|
86
|
-
* Represents UI elements in JSON format that can be interpreted at runtime
|
|
87
|
-
*/
|
|
88
|
-
|
|
89
|
-
data class UIDefinition(
|
|
90
|
-
val version: String = "1.0",
|
|
91
|
-
val screen: DSLElement
|
|
92
|
-
)
|
|
93
|
-
|
|
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
|
-
)
|
|
112
|
-
|
|
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
|
-
)
|
|
125
|
-
|
|
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")
|
|
133
|
-
|
|
134
|
-
return UIDefinition(
|
|
135
|
-
version = version,
|
|
136
|
-
screen = parseDSLElement(screenObj)
|
|
137
|
-
)
|
|
138
|
-
}
|
|
139
|
-
|
|
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
|
|
150
|
-
|
|
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
|
|
166
|
-
|
|
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
|
-
)
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// ============================================================================
|
|
188
|
-
// DSL Interpreter
|
|
189
|
-
// ============================================================================
|
|
190
|
-
|
|
191
|
-
/**
|
|
192
|
-
* DSL Interpreter
|
|
193
|
-
* Converts JSON DSL to Compose UI at runtime
|
|
194
|
-
*/
|
|
195
|
-
object DSLInterpreter {
|
|
196
|
-
private const val TAG = "DSLInterpreter"
|
|
197
|
-
|
|
198
|
-
private val _currentDSL = MutableStateFlow<UIDefinition?>(null)
|
|
199
|
-
val currentDSL: StateFlow<UIDefinition?> = _currentDSL
|
|
200
|
-
|
|
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
|
-
}
|
|
213
|
-
|
|
214
|
-
/**
|
|
215
|
-
* Render DSL as Compose UI
|
|
216
|
-
*/
|
|
217
|
-
@Composable
|
|
218
|
-
fun RenderDSL(definition: UIDefinition) {
|
|
219
|
-
RenderElement(definition.screen)
|
|
220
|
-
}
|
|
221
|
-
|
|
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
|
-
}
|
|
240
|
-
|
|
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
|
-
}
|
|
253
|
-
|
|
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
|
-
}
|
|
266
|
-
|
|
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
|
-
}
|
|
278
|
-
|
|
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
|
-
}
|
|
288
|
-
|
|
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
|
-
}
|
|
299
|
-
|
|
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
|
-
}
|
|
308
|
-
|
|
309
|
-
/**
|
|
310
|
-
* Parse DSL modifier to Compose Modifier
|
|
311
|
-
*/
|
|
312
|
-
private fun parseModifier(dslModifier: DSLModifier?): Modifier {
|
|
313
|
-
var modifier: Modifier = Modifier
|
|
314
|
-
|
|
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()
|
|
319
|
-
|
|
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) }
|
|
323
|
-
|
|
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) }
|
|
327
|
-
|
|
328
|
-
// Note: weight() is only available in RowScope/ColumnScope
|
|
329
|
-
// We'll handle it separately when needed
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
return modifier
|
|
333
|
-
}
|
|
334
|
-
|
|
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
|
-
}
|
|
346
|
-
|
|
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
|
-
}
|
|
355
|
-
|
|
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
|
-
}
|
|
370
|
-
|
|
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
|
-
}
|
|
381
|
-
}
|
|
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
|
-
}
|
|
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
|
-
}
|
|
415
|
-
}
|
|
416
|
-
|
|
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
|
-
}
|
|
435
|
-
}
|
|
436
|
-
|
|
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")
|
|
443
|
-
|
|
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
|
-
}
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
// ============================================================================
|
|
476
|
-
// Hot Reload Manager
|
|
477
|
-
// ============================================================================
|
|
478
|
-
|
|
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()
|
|
490
|
-
|
|
491
|
-
/**
|
|
492
|
-
* Get the current WebSocket connection (for sending messages)
|
|
493
|
-
*/
|
|
494
|
-
fun getWebSocket(): WebSocket? = webSocket
|
|
495
|
-
|
|
496
|
-
fun connect(activity: Activity, serverUrl: String, sessionId: String) {
|
|
497
|
-
this.activity = activity
|
|
498
|
-
|
|
499
|
-
val wsUrl = serverUrl.replace("http://", "ws://").replace("https://", "wss://")
|
|
500
|
-
Log.d(TAG, "Connecting to dev server: \$wsUrl")
|
|
501
|
-
|
|
502
|
-
val client = OkHttpClient()
|
|
503
|
-
val request = Request.Builder()
|
|
504
|
-
.url(wsUrl)
|
|
505
|
-
.build()
|
|
506
|
-
|
|
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
|
|
512
|
-
|
|
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
|
-
}
|
|
521
|
-
|
|
522
|
-
override fun onMessage(webSocket: WebSocket, text: String) {
|
|
523
|
-
Log.d(TAG, "Received: \$text")
|
|
524
|
-
|
|
525
|
-
try {
|
|
526
|
-
val json = JSONObject(text)
|
|
527
|
-
val type = json.getString("type")
|
|
528
|
-
|
|
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
|
-
}
|
|
537
|
-
|
|
538
|
-
val timestamp = json.optLong("timestamp", 0)
|
|
539
|
-
val dslContent = json.optString("dslContent", "")
|
|
540
|
-
|
|
541
|
-
Log.d(TAG, "UI update received at \$timestamp, connection at \$connectionTime")
|
|
542
|
-
|
|
543
|
-
// Only update if changes happened AFTER we connected
|
|
544
|
-
if (timestamp > connectionTime && dslContent.isNotEmpty()) {
|
|
545
|
-
Log.d(TAG, "New UI update detected, recomposing (\${dslContent.length} bytes)")
|
|
546
|
-
|
|
547
|
-
// Update DSL on main thread
|
|
548
|
-
activity?.runOnUiThread {
|
|
549
|
-
DSLInterpreter.updateDSL(dslContent)
|
|
550
|
-
}
|
|
551
|
-
} else {
|
|
552
|
-
Log.d(TAG, "Ignoring old UI update")
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
"core:reload" -> {
|
|
557
|
-
Log.d(TAG, "Reload triggered!")
|
|
558
|
-
// Restart activity on main thread
|
|
559
|
-
activity?.runOnUiThread {
|
|
560
|
-
activity?.recreate()
|
|
561
|
-
}
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
"core:build-complete" -> {
|
|
565
|
-
// Full APK rebuild (SLOW - fallback for non-UI changes)
|
|
566
|
-
if (ignoreFirstBuild) {
|
|
567
|
-
Log.d(TAG, "Ignoring first build-complete (old build)")
|
|
568
|
-
ignoreFirstBuild = false
|
|
569
|
-
return@onMessage
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
val timestamp = json.optLong("timestamp", 0)
|
|
573
|
-
val downloadUrl = json.optString("downloadUrl", "")
|
|
574
|
-
|
|
575
|
-
Log.d(TAG, "Build complete at \$timestamp, connection at \$connectionTime")
|
|
576
|
-
Log.d(TAG, "Download URL: \$downloadUrl")
|
|
577
|
-
|
|
578
|
-
// Only reload if build happened AFTER we connected
|
|
579
|
-
if (timestamp > connectionTime && downloadUrl.isNotEmpty()) {
|
|
580
|
-
Log.d(TAG, "New build detected, downloading and installing APK")
|
|
581
|
-
downloadAndInstallApk(downloadUrl)
|
|
582
|
-
} else {
|
|
583
|
-
Log.d(TAG, "Ignoring old build (timestamp before connection)")
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
|
-
}
|
|
587
|
-
} catch (e: Exception) {
|
|
588
|
-
Log.e(TAG, "Failed to parse message: \${e.message}")
|
|
589
|
-
}
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
|
|
593
|
-
Log.e(TAG, "WebSocket error: \${t.message}")
|
|
594
|
-
// Auto-reconnect after 5 seconds
|
|
595
|
-
android.os.Handler(android.os.Looper.getMainLooper()).postDelayed({
|
|
596
|
-
connect(activity!!, serverUrl, sessionId)
|
|
597
|
-
}, 5000)
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
|
|
601
|
-
Log.d(TAG, "WebSocket closed: \$reason")
|
|
602
|
-
}
|
|
603
|
-
})
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
fun disconnect() {
|
|
607
|
-
webSocket?.close(1000, "App closing")
|
|
608
|
-
webSocket = null
|
|
609
|
-
activity = null
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
private fun downloadAndInstallApk(downloadUrl: String) {
|
|
613
|
-
Log.d(TAG, "Starting APK download from: \$downloadUrl")
|
|
614
|
-
|
|
615
|
-
val request = Request.Builder()
|
|
616
|
-
.url(downloadUrl)
|
|
617
|
-
.build()
|
|
618
|
-
|
|
619
|
-
httpClient.newCall(request).enqueue(object : Callback {
|
|
620
|
-
override fun onFailure(call: Call, e: IOException) {
|
|
621
|
-
Log.e(TAG, "APK download failed: \${e.message}")
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
override fun onResponse(call: Call, response: Response) {
|
|
625
|
-
if (!response.isSuccessful) {
|
|
626
|
-
Log.e(TAG, "APK download failed with code: \${response.code}")
|
|
627
|
-
return
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
try {
|
|
631
|
-
val apkData = response.body?.bytes()
|
|
632
|
-
if (apkData == null) {
|
|
633
|
-
Log.e(TAG, "APK data is null")
|
|
634
|
-
return
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
Log.d(TAG, "APK downloaded successfully (\${apkData.size} bytes)")
|
|
638
|
-
|
|
639
|
-
// Save APK to cache directory
|
|
640
|
-
val cacheDir = activity?.cacheDir
|
|
641
|
-
if (cacheDir == null) {
|
|
642
|
-
Log.e(TAG, "Cache directory is null")
|
|
643
|
-
return
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
val apkFile = File(cacheDir, "update.apk")
|
|
647
|
-
apkFile.outputStream().use { output ->
|
|
648
|
-
output.write(apkData)
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
Log.d(TAG, "APK saved to: \${apkFile.absolutePath}")
|
|
652
|
-
|
|
653
|
-
// Install APK on main thread
|
|
654
|
-
activity?.runOnUiThread {
|
|
655
|
-
installApk(apkFile)
|
|
656
|
-
}
|
|
657
|
-
} catch (e: Exception) {
|
|
658
|
-
Log.e(TAG, "Failed to save/install APK: \${e.message}")
|
|
659
|
-
}
|
|
660
|
-
}
|
|
661
|
-
})
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
private fun installApk(apkFile: File) {
|
|
665
|
-
try {
|
|
666
|
-
val context = activity ?: return
|
|
667
|
-
|
|
668
|
-
val uri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
|
669
|
-
FileProvider.getUriForFile(
|
|
670
|
-
context,
|
|
671
|
-
"\${context.packageName}.fileprovider",
|
|
672
|
-
apkFile
|
|
673
|
-
)
|
|
674
|
-
} else {
|
|
675
|
-
Uri.fromFile(apkFile)
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
val intent = Intent(Intent.ACTION_VIEW).apply {
|
|
679
|
-
setDataAndType(uri, "application/vnd.android.package-archive")
|
|
680
|
-
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
|
681
|
-
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
context.startActivity(intent)
|
|
685
|
-
Log.d(TAG, "Installation intent started")
|
|
686
|
-
} catch (e: Exception) {
|
|
687
|
-
Log.e(TAG, "Failed to install APK: \${e.message}")
|
|
688
|
-
}
|
|
689
|
-
}
|
|
690
|
-
}
|
|
691
|
-
`;
|
|
692
|
-
|
|
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);
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
async function generateMainActivity(
|
|
794
|
-
projectPath: string,
|
|
795
|
-
packageName: string
|
|
796
|
-
): Promise<void> {
|
|
797
|
-
const packagePath = packageName.replace(/\./g, '/');
|
|
798
|
-
const activityPath = path.join(
|
|
799
|
-
projectPath,
|
|
800
|
-
'app/src/main/java',
|
|
801
|
-
packagePath,
|
|
802
|
-
'MainActivity.kt'
|
|
803
|
-
);
|
|
804
|
-
|
|
805
|
-
const content = `package ${packageName}
|
|
806
|
-
|
|
807
|
-
import android.os.Bundle
|
|
808
|
-
import androidx.activity.ComponentActivity
|
|
809
|
-
import androidx.activity.compose.setContent
|
|
810
|
-
import androidx.compose.foundation.layout.*
|
|
811
|
-
import androidx.compose.material3.*
|
|
812
|
-
import androidx.compose.runtime.*
|
|
813
|
-
import androidx.compose.ui.Alignment
|
|
814
|
-
import androidx.compose.ui.Modifier
|
|
815
|
-
import androidx.compose.ui.unit.dp
|
|
816
|
-
|
|
817
|
-
class MainActivity : ComponentActivity() {
|
|
818
|
-
override fun onCreate(savedInstanceState: Bundle?) {
|
|
819
|
-
super.onCreate(savedInstanceState)
|
|
820
|
-
|
|
821
|
-
// Initialize hot reload - reads from BuildConfig injected by jetstart dev
|
|
822
|
-
try {
|
|
823
|
-
val serverUrl = BuildConfig.JETSTART_SERVER_URL
|
|
824
|
-
val sessionId = BuildConfig.JETSTART_SESSION_ID
|
|
825
|
-
HotReload.connect(this, serverUrl, sessionId)
|
|
826
|
-
} catch (e: Exception) {
|
|
827
|
-
// BuildConfig not available yet, hot reload will be disabled
|
|
828
|
-
android.util.Log.w("MainActivity", "Hot reload not configured: \${e.message}")
|
|
829
|
-
}
|
|
830
|
-
|
|
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()
|
|
839
|
-
|
|
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
|
-
}
|
|
848
|
-
}
|
|
849
|
-
}
|
|
850
|
-
}
|
|
851
|
-
|
|
852
|
-
override fun onDestroy() {
|
|
853
|
-
super.onDestroy()
|
|
854
|
-
HotReload.disconnect()
|
|
855
|
-
}
|
|
856
|
-
}
|
|
857
|
-
|
|
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
|
-
)
|
|
875
|
-
|
|
876
|
-
Spacer(modifier = Modifier.height(16.dp))
|
|
877
|
-
|
|
878
|
-
Text(
|
|
879
|
-
text = "Edit this code and save to see hot reload!",
|
|
880
|
-
style = MaterialTheme.typography.bodyMedium
|
|
881
|
-
)
|
|
882
|
-
|
|
883
|
-
Spacer(modifier = Modifier.height(24.dp))
|
|
884
|
-
|
|
885
|
-
Button(
|
|
886
|
-
onClick = { /* Handle click */ },
|
|
887
|
-
modifier = Modifier.fillMaxWidth()
|
|
888
|
-
) {
|
|
889
|
-
Text("Click Me!")
|
|
890
|
-
}
|
|
891
|
-
}
|
|
892
|
-
}`;
|
|
893
|
-
|
|
894
|
-
await fs.ensureDir(path.dirname(activityPath));
|
|
895
|
-
await fs.writeFile(activityPath, content);
|
|
896
|
-
}
|
|
897
|
-
|
|
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">
|
|
905
|
-
|
|
906
|
-
<uses-permission android:name="android.permission.INTERNET" />
|
|
907
|
-
|
|
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>
|
|
922
|
-
|
|
923
|
-
</manifest>`;
|
|
924
|
-
|
|
925
|
-
await fs.writeFile(
|
|
926
|
-
path.join(projectPath, 'app/src/main/AndroidManifest.xml'),
|
|
927
|
-
content
|
|
928
|
-
);
|
|
929
|
-
}
|
|
930
|
-
|
|
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
|
-
};
|
|
946
|
-
|
|
947
|
-
await fs.writeJSON(
|
|
948
|
-
path.join(projectPath, 'jetstart.config.json'),
|
|
949
|
-
config,
|
|
950
|
-
{ spaces: 2 }
|
|
951
|
-
);
|
|
952
|
-
}
|
|
953
|
-
|
|
954
|
-
async function generateGitignore(projectPath: string): Promise<void> {
|
|
955
|
-
const content = `# Build
|
|
956
|
-
/build
|
|
957
|
-
/app/build
|
|
958
|
-
.gradle
|
|
959
|
-
*.hprof
|
|
960
|
-
|
|
961
|
-
# IDE
|
|
962
|
-
.idea
|
|
963
|
-
*.iml
|
|
964
|
-
.vscode
|
|
965
|
-
.DS_Store
|
|
966
|
-
|
|
967
|
-
# Claude Code
|
|
968
|
-
.claude
|
|
969
|
-
.claude-worktrees
|
|
970
|
-
|
|
971
|
-
# JetStart
|
|
972
|
-
.jetstart
|
|
973
|
-
|
|
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
|
|
987
|
-
|
|
988
|
-
# Log files
|
|
989
|
-
*.log
|
|
990
|
-
|
|
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()
|
|
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
|
-
}
|
|
1054
|
-
|
|
1055
|
-
allprojects {
|
|
1056
|
-
repositories {
|
|
1057
|
-
google()
|
|
1058
|
-
mavenCentral()
|
|
1059
|
-
}
|
|
1060
|
-
}
|
|
1061
|
-
|
|
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
|
-
});
|
|
1103
|
-
}
|
|
1104
|
-
|
|
1105
|
-
async function generateResourceFiles(
|
|
1106
|
-
projectPath: string,
|
|
1107
|
-
projectName: string
|
|
1108
|
-
): Promise<void> {
|
|
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
|
|
1118
|
-
);
|
|
1119
|
-
|
|
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>`;
|
|
1131
|
-
|
|
1132
|
-
await fs.writeFile(
|
|
1133
|
-
path.join(projectPath, 'app/src/main/res/values/colors.xml'),
|
|
1134
|
-
colorsXml
|
|
1135
|
-
);
|
|
1136
|
-
|
|
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>`;
|
|
1142
|
-
|
|
1143
|
-
await fs.writeFile(
|
|
1144
|
-
path.join(projectPath, 'app/src/main/res/values/themes.xml'),
|
|
1145
|
-
themesXml
|
|
1146
|
-
);
|
|
1147
|
-
|
|
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>`;
|
|
1157
|
-
|
|
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
|
-
}
|
|
1164
|
-
|
|
1165
|
-
async function generateLocalProperties(projectPath: string): Promise<void> {
|
|
1166
|
-
// Auto-detect Android SDK location
|
|
1167
|
-
let androidSdkPath: string | undefined;
|
|
1168
|
-
|
|
1169
|
-
// Check environment variables first
|
|
1170
|
-
androidSdkPath = process.env.ANDROID_HOME || process.env.ANDROID_SDK_ROOT;
|
|
1171
|
-
|
|
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
|
-
];
|
|
1180
|
-
|
|
1181
|
-
for (const p of commonPaths) {
|
|
1182
|
-
if (fs.existsSync(p)) {
|
|
1183
|
-
androidSdkPath = p;
|
|
1184
|
-
break;
|
|
1185
|
-
}
|
|
1186
|
-
}
|
|
1187
|
-
}
|
|
1188
|
-
|
|
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, '\\\\')}
|
|
1213
|
-
`;
|
|
1214
|
-
|
|
1215
|
-
await fs.writeFile(path.join(projectPath, 'local.properties'), content);
|
|
1216
|
-
console.log(`[JetStart] Created local.properties with SDK: ${androidSdkPath}`);
|
|
1217
|
-
}
|