@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.
Files changed (142) hide show
  1. package/README.md +133 -41
  2. package/dist/cli.d.ts.map +1 -0
  3. package/dist/cli.js +11 -0
  4. package/dist/cli.js.map +1 -0
  5. package/dist/commands/android-emulator.d.ts.map +1 -0
  6. package/dist/commands/android-emulator.js.map +1 -0
  7. package/dist/commands/build.d.ts +13 -1
  8. package/dist/commands/build.d.ts.map +1 -0
  9. package/dist/commands/build.js +279 -29
  10. package/dist/commands/build.js.map +1 -0
  11. package/dist/commands/clean.d.ts +23 -0
  12. package/dist/commands/clean.d.ts.map +1 -0
  13. package/dist/commands/clean.js +191 -0
  14. package/dist/commands/clean.js.map +1 -0
  15. package/dist/commands/create.d.ts.map +1 -0
  16. package/dist/commands/create.js +41 -0
  17. package/dist/commands/create.js.map +1 -0
  18. package/dist/commands/dev.d.ts.map +1 -0
  19. package/dist/commands/dev.js +51 -9
  20. package/dist/commands/dev.js.map +1 -0
  21. package/dist/commands/index.d.ts.map +1 -0
  22. package/dist/commands/index.js.map +1 -0
  23. package/dist/commands/install-audit.d.ts.map +1 -0
  24. package/dist/commands/install-audit.js.map +1 -0
  25. package/dist/commands/logs.d.ts.map +1 -0
  26. package/dist/commands/logs.js.map +1 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js.map +1 -0
  29. package/dist/types/index.d.ts.map +1 -0
  30. package/dist/types/index.js.map +1 -0
  31. package/dist/utils/android-sdk.d.ts.map +1 -0
  32. package/dist/utils/android-sdk.js +2 -2
  33. package/dist/utils/android-sdk.js.map +1 -0
  34. package/dist/utils/downloader.d.ts.map +1 -0
  35. package/dist/utils/downloader.js.map +1 -0
  36. package/dist/utils/emulator-deployer.d.ts.map +1 -0
  37. package/dist/utils/emulator-deployer.js.map +1 -0
  38. package/dist/utils/emulator.d.ts.map +1 -0
  39. package/dist/utils/emulator.js +5 -4
  40. package/dist/utils/emulator.js.map +1 -0
  41. package/dist/utils/index.d.ts.map +1 -0
  42. package/dist/utils/index.js.map +1 -0
  43. package/dist/utils/java.d.ts.map +1 -0
  44. package/dist/utils/java.js +5 -5
  45. package/dist/utils/java.js.map +1 -0
  46. package/dist/utils/logger.d.ts.map +1 -0
  47. package/dist/utils/logger.js.map +1 -0
  48. package/dist/utils/open.d.ts.map +1 -0
  49. package/dist/utils/open.js.map +1 -0
  50. package/dist/utils/prompt.d.ts.map +1 -0
  51. package/dist/utils/prompt.js.map +1 -0
  52. package/dist/utils/spinner.d.ts.map +1 -0
  53. package/dist/utils/spinner.js.map +1 -0
  54. package/dist/utils/system-tools.d.ts.map +1 -0
  55. package/dist/utils/system-tools.js.map +1 -0
  56. package/dist/utils/template.d.ts +13 -1
  57. package/dist/utils/template.d.ts.map +1 -0
  58. package/dist/utils/template.js +133 -1003
  59. package/dist/utils/template.js.map +1 -0
  60. 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
  61. 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
  62. 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
  63. 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
  64. 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
  65. package/maven-repo/com/jetstart/hot-reload/com.jetstart.hot-reload.gradle.plugin/maven-metadata.xml +13 -0
  66. package/maven-repo/com/jetstart/hot-reload/com.jetstart.hot-reload.gradle.plugin/maven-metadata.xml.md5 +1 -0
  67. package/maven-repo/com/jetstart/hot-reload/com.jetstart.hot-reload.gradle.plugin/maven-metadata.xml.sha1 +1 -0
  68. package/maven-repo/com/jetstart/hot-reload/com.jetstart.hot-reload.gradle.plugin/maven-metadata.xml.sha256 +1 -0
  69. package/maven-repo/com/jetstart/hot-reload/com.jetstart.hot-reload.gradle.plugin/maven-metadata.xml.sha512 +1 -0
  70. package/maven-repo/com/jetstart/hot-reload-runtime/1.0.0/hot-reload-runtime-1.0.0-sources.jar +0 -0
  71. package/maven-repo/com/jetstart/hot-reload-runtime/1.0.0/hot-reload-runtime-1.0.0-sources.jar.md5 +1 -0
  72. package/maven-repo/com/jetstart/hot-reload-runtime/1.0.0/hot-reload-runtime-1.0.0-sources.jar.sha1 +1 -0
  73. package/maven-repo/com/jetstart/hot-reload-runtime/1.0.0/hot-reload-runtime-1.0.0-sources.jar.sha256 +1 -0
  74. package/maven-repo/com/jetstart/hot-reload-runtime/1.0.0/hot-reload-runtime-1.0.0-sources.jar.sha512 +1 -0
  75. package/maven-repo/com/jetstart/hot-reload-runtime/1.0.0/hot-reload-runtime-1.0.0.aar +0 -0
  76. package/maven-repo/com/jetstart/hot-reload-runtime/1.0.0/hot-reload-runtime-1.0.0.aar.md5 +1 -0
  77. package/maven-repo/com/jetstart/hot-reload-runtime/1.0.0/hot-reload-runtime-1.0.0.aar.sha1 +1 -0
  78. package/maven-repo/com/jetstart/hot-reload-runtime/1.0.0/hot-reload-runtime-1.0.0.aar.sha256 +1 -0
  79. package/maven-repo/com/jetstart/hot-reload-runtime/1.0.0/hot-reload-runtime-1.0.0.aar.sha512 +1 -0
  80. package/maven-repo/com/jetstart/hot-reload-runtime/1.0.0/hot-reload-runtime-1.0.0.module +124 -0
  81. package/maven-repo/com/jetstart/hot-reload-runtime/1.0.0/hot-reload-runtime-1.0.0.module.md5 +1 -0
  82. package/maven-repo/com/jetstart/hot-reload-runtime/1.0.0/hot-reload-runtime-1.0.0.module.sha1 +1 -0
  83. package/maven-repo/com/jetstart/hot-reload-runtime/1.0.0/hot-reload-runtime-1.0.0.module.sha256 +1 -0
  84. package/maven-repo/com/jetstart/hot-reload-runtime/1.0.0/hot-reload-runtime-1.0.0.module.sha512 +1 -0
  85. package/maven-repo/com/jetstart/hot-reload-runtime/1.0.0/hot-reload-runtime-1.0.0.pom +46 -0
  86. package/maven-repo/com/jetstart/hot-reload-runtime/1.0.0/hot-reload-runtime-1.0.0.pom.md5 +1 -0
  87. package/maven-repo/com/jetstart/hot-reload-runtime/1.0.0/hot-reload-runtime-1.0.0.pom.sha1 +1 -0
  88. package/maven-repo/com/jetstart/hot-reload-runtime/1.0.0/hot-reload-runtime-1.0.0.pom.sha256 +1 -0
  89. package/maven-repo/com/jetstart/hot-reload-runtime/1.0.0/hot-reload-runtime-1.0.0.pom.sha512 +1 -0
  90. package/maven-repo/com/jetstart/hot-reload-runtime/maven-metadata.xml +13 -0
  91. package/maven-repo/com/jetstart/hot-reload-runtime/maven-metadata.xml.md5 +1 -0
  92. package/maven-repo/com/jetstart/hot-reload-runtime/maven-metadata.xml.sha1 +1 -0
  93. package/maven-repo/com/jetstart/hot-reload-runtime/maven-metadata.xml.sha256 +1 -0
  94. package/maven-repo/com/jetstart/hot-reload-runtime/maven-metadata.xml.sha512 +1 -0
  95. package/maven-repo/com/jetstart/jetstart-gradle-plugin/1.0.0/jetstart-gradle-plugin-1.0.0.jar +0 -0
  96. package/maven-repo/com/jetstart/jetstart-gradle-plugin/1.0.0/jetstart-gradle-plugin-1.0.0.jar.md5 +1 -0
  97. package/maven-repo/com/jetstart/jetstart-gradle-plugin/1.0.0/jetstart-gradle-plugin-1.0.0.jar.sha1 +1 -0
  98. package/maven-repo/com/jetstart/jetstart-gradle-plugin/1.0.0/jetstart-gradle-plugin-1.0.0.jar.sha256 +1 -0
  99. package/maven-repo/com/jetstart/jetstart-gradle-plugin/1.0.0/jetstart-gradle-plugin-1.0.0.jar.sha512 +1 -0
  100. package/maven-repo/com/jetstart/jetstart-gradle-plugin/1.0.0/jetstart-gradle-plugin-1.0.0.module +103 -0
  101. package/maven-repo/com/jetstart/jetstart-gradle-plugin/1.0.0/jetstart-gradle-plugin-1.0.0.module.md5 +1 -0
  102. package/maven-repo/com/jetstart/jetstart-gradle-plugin/1.0.0/jetstart-gradle-plugin-1.0.0.module.sha1 +1 -0
  103. package/maven-repo/com/jetstart/jetstart-gradle-plugin/1.0.0/jetstart-gradle-plugin-1.0.0.module.sha256 +1 -0
  104. package/maven-repo/com/jetstart/jetstart-gradle-plugin/1.0.0/jetstart-gradle-plugin-1.0.0.module.sha512 +1 -0
  105. package/maven-repo/com/jetstart/jetstart-gradle-plugin/1.0.0/jetstart-gradle-plugin-1.0.0.pom +38 -0
  106. package/maven-repo/com/jetstart/jetstart-gradle-plugin/1.0.0/jetstart-gradle-plugin-1.0.0.pom.md5 +1 -0
  107. package/maven-repo/com/jetstart/jetstart-gradle-plugin/1.0.0/jetstart-gradle-plugin-1.0.0.pom.sha1 +1 -0
  108. package/maven-repo/com/jetstart/jetstart-gradle-plugin/1.0.0/jetstart-gradle-plugin-1.0.0.pom.sha256 +1 -0
  109. package/maven-repo/com/jetstart/jetstart-gradle-plugin/1.0.0/jetstart-gradle-plugin-1.0.0.pom.sha512 +1 -0
  110. package/maven-repo/com/jetstart/jetstart-gradle-plugin/maven-metadata.xml +13 -0
  111. package/maven-repo/com/jetstart/jetstart-gradle-plugin/maven-metadata.xml.md5 +1 -0
  112. package/maven-repo/com/jetstart/jetstart-gradle-plugin/maven-metadata.xml.sha1 +1 -0
  113. package/maven-repo/com/jetstart/jetstart-gradle-plugin/maven-metadata.xml.sha256 +1 -0
  114. package/maven-repo/com/jetstart/jetstart-gradle-plugin/maven-metadata.xml.sha512 +1 -0
  115. package/package.json +13 -5
  116. package/scripts/build-java.js +30 -0
  117. package/.eslintrc.json +0 -6
  118. package/src/cli.ts +0 -99
  119. package/src/commands/android-emulator.ts +0 -304
  120. package/src/commands/build.ts +0 -60
  121. package/src/commands/create.ts +0 -232
  122. package/src/commands/dev.ts +0 -198
  123. package/src/commands/index.ts +0 -10
  124. package/src/commands/install-audit.ts +0 -227
  125. package/src/commands/logs.ts +0 -117
  126. package/src/index.ts +0 -17
  127. package/src/types/index.ts +0 -53
  128. package/src/utils/android-sdk.ts +0 -512
  129. package/src/utils/downloader.ts +0 -201
  130. package/src/utils/emulator-deployer.ts +0 -210
  131. package/src/utils/emulator.ts +0 -463
  132. package/src/utils/index.ts +0 -8
  133. package/src/utils/java.ts +0 -369
  134. package/src/utils/logger.ts +0 -42
  135. package/src/utils/open.ts +0 -36
  136. package/src/utils/prompt.ts +0 -56
  137. package/src/utils/spinner.ts +0 -25
  138. package/src/utils/system-tools.ts +0 -648
  139. package/src/utils/template.ts +0 -1217
  140. package/tests/create.test.ts +0 -33
  141. package/tests/utils.test.ts +0 -17
  142. package/tsconfig.json +0 -25
@@ -1,7 +1,11 @@
1
1
  "use strict";
2
2
  /**
3
3
  * Template Generator
4
- * Creates project structure from templates
4
+ * Creates project structure from file-based templates
5
+ *
6
+ * Instead of inline string templates, this uses the `packages/template/base/`
7
+ * folder. Template files contain {{PLACEHOLDER}} variables that are substituted
8
+ * at scaffold time.
5
9
  */
6
10
  var __importDefault = (this && this.__importDefault) || function (mod) {
7
11
  return (mod && mod.__esModule) ? mod : { "default": mod };
@@ -12,875 +16,127 @@ const path_1 = __importDefault(require("path"));
12
16
  const fs_extra_1 = __importDefault(require("fs-extra"));
13
17
  const child_process_1 = require("child_process");
14
18
  const shared_1 = require("@jetstart/shared");
15
- async function generateProjectTemplate(projectPath, options) {
16
- const { projectName, packageName } = options;
17
- // Create directory structure
18
- await createDirectoryStructure(projectPath);
19
- // Generate files
20
- await generateRootBuildGradle(projectPath);
21
- await generateBuildGradle(projectPath, options);
22
- await generateSettingsGradle(projectPath, projectName);
23
- await generateGradleProperties(projectPath);
24
- await generateGradleWrapper(projectPath);
25
- await generateMainActivity(projectPath, packageName);
26
- await generateJetStart(projectPath, packageName);
27
- await generateAndroidManifest(projectPath, options);
28
- await generateResourceFiles(projectPath, projectName);
29
- await generateLocalProperties(projectPath);
30
- await generateJetStartConfig(projectPath, options);
31
- await generateGitignore(projectPath);
32
- await generateReadme(projectPath, projectName);
33
- }
34
- async function generateJetStart(projectPath, packageName) {
35
- const packagePath = packageName.replace(/\./g, '/');
36
- const jetStartPath = path_1.default.join(projectPath, 'app/src/main/java', packagePath, 'JetStart.kt');
37
- const content = `package ${packageName}
38
-
39
- // Android
40
- import android.app.Activity
41
- import android.content.Intent
42
- import android.net.Uri
43
- import android.os.Build
44
- import android.util.Log
45
- import androidx.core.content.FileProvider
46
-
47
- // Compose
48
- import androidx.compose.foundation.layout.*
49
- import androidx.compose.material3.*
50
- import androidx.compose.runtime.*
51
- import androidx.compose.ui.Alignment
52
- import androidx.compose.ui.Modifier
53
- import androidx.compose.ui.graphics.Color
54
- import androidx.compose.ui.text.font.FontWeight
55
- import androidx.compose.ui.unit.dp
56
-
57
- // Third-party
58
- import kotlinx.coroutines.flow.MutableStateFlow
59
- import kotlinx.coroutines.flow.StateFlow
60
- import okhttp3.*
61
-
62
- // Standard library & JSON
63
- import org.json.JSONArray
64
- import org.json.JSONObject
65
- import java.io.File
66
- import java.io.IOException
67
-
68
19
  // ============================================================================
69
- // DSL Type Definitions
20
+ // Placeholder Variables
70
21
  // ============================================================================
71
-
72
22
  /**
73
- * DSL Type Definitions
74
- * Represents UI elements in JSON format that can be interpreted at runtime
23
+ * Build the variable map from TemplateOptions
75
24
  */
76
-
77
- data class UIDefinition(
78
- val version: String = "1.0",
79
- val screen: DSLElement
80
- )
81
-
82
- data class DSLElement(
83
- val type: String,
84
- val text: String? = null,
85
- val style: String? = null,
86
- val color: String? = null,
87
- val modifier: DSLModifier? = null,
88
- val horizontalAlignment: String? = null,
89
- val verticalArrangement: String? = null,
90
- val contentAlignment: String? = null,
91
- val height: Int? = null,
92
- val width: Int? = null,
93
- val onClick: String? = null,
94
- val enabled: Boolean? = true,
95
- val imageVector: String? = null,
96
- val tint: String? = null,
97
- val contentDescription: String? = null,
98
- val children: List<DSLElement>? = null
99
- )
100
-
101
- data class DSLModifier(
102
- val fillMaxSize: Boolean? = null,
103
- val fillMaxWidth: Boolean? = null,
104
- val fillMaxHeight: Boolean? = null,
105
- val padding: Int? = null,
106
- val paddingHorizontal: Int? = null,
107
- val paddingVertical: Int? = null,
108
- val size: Int? = null,
109
- val height: Int? = null,
110
- val width: Int? = null,
111
- val weight: Float? = null
112
- )
113
-
25
+ function buildVariableMap(options) {
26
+ const themeName = options.projectName.replace(/[^a-zA-Z0-9]/g, '');
27
+ return {
28
+ '{{PROJECT_NAME}}': options.projectName,
29
+ '{{PACKAGE_NAME}}': options.packageName,
30
+ '{{THEME_NAME}}': themeName,
31
+ '{{MIN_SDK}}': String(shared_1.MIN_ANDROID_API_LEVEL),
32
+ '{{TARGET_SDK}}': String(shared_1.TARGET_ANDROID_API_LEVEL),
33
+ };
34
+ }
35
+ // ============================================================================
36
+ // Template Processing
37
+ // ============================================================================
114
38
  /**
115
- * Parse JSON string to UIDefinition
39
+ * Resolve the template directory.
40
+ * When compiled, template.ts lives at packages/cli/dist/utils/template.js,
41
+ * so we go up to packages/ then into template/base/.
42
+ * When running via ts-node, template.ts lives at packages/cli/src/utils/template.ts,
43
+ * same relative traversal works.
116
44
  */
117
- fun parseUIDefinition(json: String): UIDefinition {
118
- val obj = JSONObject(json)
119
- val version = obj.optString("version", "1.0")
120
- val screenObj = obj.getJSONObject("screen")
121
-
122
- return UIDefinition(
123
- version = version,
124
- screen = parseDSLElement(screenObj)
125
- )
45
+ function getTemplateDir() {
46
+ // __dirname at compile time: packages/cli/dist/utils/
47
+ // __dirname at dev time: packages/cli/src/utils/
48
+ // Either way, go up 3 levels to packages/, then into template/base/
49
+ return path_1.default.resolve(__dirname, '..', '..', '..', 'template', 'base');
126
50
  }
127
-
128
51
  /**
129
- * Parse JSONObject to DSLElement
52
+ * Known binary/non-text extensions that should be copied without substitution.
130
53
  */
131
- fun parseDSLElement(obj: JSONObject): DSLElement {
132
- val children = if (obj.has("children")) {
133
- val childrenArray = obj.getJSONArray("children")
134
- List(childrenArray.length()) { i ->
135
- parseDSLElement(childrenArray.getJSONObject(i))
136
- }
137
- } else null
138
-
139
- val modifier = if (obj.has("modifier")) {
140
- val modObj = obj.getJSONObject("modifier")
141
- DSLModifier(
142
- fillMaxSize = modObj.optBoolean("fillMaxSize"),
143
- fillMaxWidth = modObj.optBoolean("fillMaxWidth"),
144
- fillMaxHeight = modObj.optBoolean("fillMaxHeight"),
145
- padding = if (modObj.has("padding")) modObj.getInt("padding") else null,
146
- paddingHorizontal = if (modObj.has("paddingHorizontal")) modObj.getInt("paddingHorizontal") else null,
147
- paddingVertical = if (modObj.has("paddingVertical")) modObj.getInt("paddingVertical") else null,
148
- size = if (modObj.has("size")) modObj.getInt("size") else null,
149
- height = if (modObj.has("height")) modObj.getInt("height") else null,
150
- width = if (modObj.has("width")) modObj.getInt("width") else null,
151
- weight = if (modObj.has("weight")) modObj.getDouble("weight").toFloat() else null
152
- )
153
- } else null
154
-
155
- return DSLElement(
156
- type = obj.getString("type"),
157
- text = if (obj.has("text")) obj.getString("text") else null,
158
- style = if (obj.has("style")) obj.getString("style") else null,
159
- color = if (obj.has("color")) obj.getString("color") else null,
160
- modifier = modifier,
161
- horizontalAlignment = if (obj.has("horizontalAlignment")) obj.getString("horizontalAlignment") else null,
162
- verticalArrangement = if (obj.has("verticalArrangement")) obj.getString("verticalArrangement") else null,
163
- contentAlignment = if (obj.has("contentAlignment")) obj.getString("contentAlignment") else null,
164
- height = if (obj.has("height")) obj.getInt("height") else null,
165
- width = if (obj.has("width")) obj.getInt("width") else null,
166
- onClick = if (obj.has("onClick")) obj.getString("onClick") else null,
167
- enabled = obj.optBoolean("enabled", true),
168
- imageVector = if (obj.has("imageVector")) obj.getString("imageVector") else null,
169
- tint = if (obj.has("tint")) obj.getString("tint") else null,
170
- contentDescription = if (obj.has("contentDescription")) obj.getString("contentDescription") else null,
171
- children = children
172
- )
54
+ const BINARY_EXTENSIONS = new Set([
55
+ '.jar',
56
+ '.png',
57
+ '.jpg',
58
+ '.jpeg',
59
+ '.gif',
60
+ '.webp',
61
+ '.ico',
62
+ '.pdf',
63
+ '.zip',
64
+ '.gz',
65
+ '.tar',
66
+ '.class',
67
+ ]);
68
+ /**
69
+ * Check if a file is a text file that should have placeholders replaced.
70
+ */
71
+ function isTextFile(filePath) {
72
+ const ext = path_1.default.extname(filePath).toLowerCase();
73
+ return !BINARY_EXTENSIONS.has(ext);
173
74
  }
174
-
175
- // ============================================================================
176
- // DSL Interpreter
177
- // ============================================================================
178
-
179
75
  /**
180
- * DSL Interpreter
181
- * Converts JSON DSL to Compose UI at runtime
76
+ * Replace all {{PLACEHOLDER}} variables in a text string.
182
77
  */
183
- object DSLInterpreter {
184
- private const val TAG = "DSLInterpreter"
185
-
186
- private val _currentDSL = MutableStateFlow<UIDefinition?>(null)
187
- val currentDSL: StateFlow<UIDefinition?> = _currentDSL
188
-
189
- /**
190
- * Update the current DSL definition
191
- */
192
- fun updateDSL(jsonString: String) {
193
- try {
194
- val definition = parseUIDefinition(jsonString)
195
- _currentDSL.value = definition
196
- Log.d(TAG, "DSL updated successfully")
197
- } catch (e: Exception) {
198
- Log.e(TAG, "Failed to parse DSL: \${e.message}", e)
199
- }
200
- }
201
-
202
- /**
203
- * Render DSL as Compose UI
204
- */
205
- @Composable
206
- fun RenderDSL(definition: UIDefinition) {
207
- RenderElement(definition.screen)
208
- }
209
-
210
- /**
211
- * Render individual DSL element
212
- */
213
- @Composable
214
- fun RenderElement(element: DSLElement) {
215
- when (element.type) {
216
- "Column" -> RenderColumn(element)
217
- "Row" -> RenderRow(element)
218
- "Box" -> RenderBox(element)
219
- "Text" -> RenderText(element)
220
- "Button" -> RenderButton(element)
221
- "Spacer" -> RenderSpacer(element)
222
- else -> {
223
- Log.w(TAG, "Unknown element type: \${element.type}")
224
- Text("Unsupported: \${element.type}", color = Color.Red)
225
- }
226
- }
227
- }
228
-
229
- @Composable
230
- private fun RenderColumn(element: DSLElement) {
231
- Column(
232
- modifier = parseModifier(element.modifier),
233
- horizontalAlignment = parseHorizontalAlignment(element.horizontalAlignment),
234
- verticalArrangement = parseVerticalArrangement(element.verticalArrangement)
235
- ) {
236
- element.children?.forEach { child ->
237
- RenderElement(child)
238
- }
239
- }
240
- }
241
-
242
- @Composable
243
- private fun RenderRow(element: DSLElement) {
244
- Row(
245
- modifier = parseModifier(element.modifier),
246
- verticalAlignment = parseVerticalAlignment(element.horizontalAlignment),
247
- horizontalArrangement = parseHorizontalArrangement(element.verticalArrangement)
248
- ) {
249
- element.children?.forEach { child ->
250
- RenderElement(child)
251
- }
252
- }
253
- }
254
-
255
- @Composable
256
- private fun RenderBox(element: DSLElement) {
257
- Box(
258
- modifier = parseModifier(element.modifier),
259
- contentAlignment = parseContentAlignment(element.contentAlignment)
260
- ) {
261
- element.children?.forEach { child ->
262
- RenderElement(child)
263
- }
264
- }
265
- }
266
-
267
- @Composable
268
- private fun RenderText(element: DSLElement) {
269
- Text(
270
- text = element.text ?: "",
271
- style = parseTextStyle(element.style),
272
- color = parseColor(element.color) ?: Color.Unspecified,
273
- modifier = parseModifier(element.modifier)
274
- )
78
+ function replaceVariables(content, variables) {
79
+ let result = content;
80
+ for (const [placeholder, value] of Object.entries(variables)) {
81
+ // Use split+join for global replace (no regex needed)
82
+ result = result.split(placeholder).join(value);
275
83
  }
276
-
277
- @Composable
278
- private fun RenderButton(element: DSLElement) {
279
- Button(
280
- onClick = { handleClick(element.onClick, element.text) },
281
- modifier = parseModifier(element.modifier),
282
- enabled = element.enabled ?: true
283
- ) {
284
- Text(element.text ?: "Button")
285
- }
286
- }
287
-
288
- @Composable
289
- private fun RenderSpacer(element: DSLElement) {
290
- Spacer(
291
- modifier = Modifier
292
- .height(element.height?.dp ?: 0.dp)
293
- .width(element.width?.dp ?: 0.dp)
294
- )
295
- }
296
-
297
- /**
298
- * Parse DSL modifier to Compose Modifier
299
- */
300
- private fun parseModifier(dslModifier: DSLModifier?): Modifier {
301
- var modifier: Modifier = Modifier
302
-
303
- dslModifier?.let { m ->
304
- if (m.fillMaxSize == true) modifier = modifier.fillMaxSize()
305
- if (m.fillMaxWidth == true) modifier = modifier.fillMaxWidth()
306
- if (m.fillMaxHeight == true) modifier = modifier.fillMaxHeight()
307
-
308
- m.padding?.let { modifier = modifier.padding(it.dp) }
309
- m.paddingHorizontal?.let { modifier = modifier.padding(horizontal = it.dp) }
310
- m.paddingVertical?.let { modifier = modifier.padding(vertical = it.dp) }
311
-
312
- m.size?.let { modifier = modifier.size(it.dp) }
313
- m.height?.let { modifier = modifier.height(it.dp) }
314
- m.width?.let { modifier = modifier.width(it.dp) }
315
-
316
- // Note: weight() is only available in RowScope/ColumnScope
317
- // We'll handle it separately when needed
318
- }
319
-
320
- return modifier
321
- }
322
-
323
- /**
324
- * Parse alignment strings
325
- */
326
- private fun parseHorizontalAlignment(alignment: String?): Alignment.Horizontal {
327
- return when (alignment?.lowercase()) {
328
- "start" -> Alignment.Start
329
- "centerhorizontally", "center" -> Alignment.CenterHorizontally
330
- "end" -> Alignment.End
331
- else -> Alignment.Start
332
- }
333
- }
334
-
335
- private fun parseVerticalAlignment(alignment: String?): Alignment.Vertical {
336
- return when (alignment?.lowercase()) {
337
- "top" -> Alignment.Top
338
- "centervertically", "center" -> Alignment.CenterVertically
339
- "bottom" -> Alignment.Bottom
340
- else -> Alignment.Top
341
- }
342
- }
343
-
344
- private fun parseContentAlignment(alignment: String?): Alignment {
345
- return when (alignment?.lowercase()) {
346
- "center" -> Alignment.Center
347
- "topcenter" -> Alignment.TopCenter
348
- "topstart" -> Alignment.TopStart
349
- "topend" -> Alignment.TopEnd
350
- "bottomcenter" -> Alignment.BottomCenter
351
- "bottomstart" -> Alignment.BottomStart
352
- "bottomend" -> Alignment.BottomEnd
353
- "centerstart" -> Alignment.CenterStart
354
- "centerend" -> Alignment.CenterEnd
355
- else -> Alignment.TopStart
356
- }
357
- }
358
-
359
- private fun parseVerticalArrangement(arrangement: String?): Arrangement.Vertical {
360
- return when (arrangement?.lowercase()) {
361
- "top" -> Arrangement.Top
362
- "center" -> Arrangement.Center
363
- "bottom" -> Arrangement.Bottom
364
- "spacebetween" -> Arrangement.SpaceBetween
365
- "spacearound" -> Arrangement.SpaceAround
366
- "spaceevenly" -> Arrangement.SpaceEvenly
367
- else -> Arrangement.Top
368
- }
369
- }
370
-
371
- private fun parseHorizontalArrangement(arrangement: String?): Arrangement.Horizontal {
372
- return when (arrangement?.lowercase()) {
373
- "start" -> Arrangement.Start
374
- "center" -> Arrangement.Center
375
- "end" -> Arrangement.End
376
- "spacebetween" -> Arrangement.SpaceBetween
377
- "spacearound" -> Arrangement.SpaceAround
378
- "spaceevenly" -> Arrangement.SpaceEvenly
379
- else -> Arrangement.Start
380
- }
381
- }
382
-
383
- /**
384
- * Parse text style
385
- */
386
- @Composable
387
- private fun parseTextStyle(style: String?): androidx.compose.ui.text.TextStyle {
388
- return when (style?.lowercase()) {
389
- "headlinelarge" -> MaterialTheme.typography.headlineLarge
390
- "headlinemedium" -> MaterialTheme.typography.headlineMedium
391
- "headlinesmall" -> MaterialTheme.typography.headlineSmall
392
- "titlelarge" -> MaterialTheme.typography.titleLarge
393
- "titlemedium" -> MaterialTheme.typography.titleMedium
394
- "titlesmall" -> MaterialTheme.typography.titleSmall
395
- "bodylarge" -> MaterialTheme.typography.bodyLarge
396
- "bodymedium" -> MaterialTheme.typography.bodyMedium
397
- "bodysmall" -> MaterialTheme.typography.bodySmall
398
- "labellarge" -> MaterialTheme.typography.labelLarge
399
- "labelmedium" -> MaterialTheme.typography.labelMedium
400
- "labelsmall" -> MaterialTheme.typography.labelSmall
401
- else -> MaterialTheme.typography.bodyMedium
402
- }
403
- }
404
-
405
- /**
406
- * Parse color from string
407
- */
408
- private fun parseColor(colorString: String?): Color? {
409
- if (colorString == null) return null
410
-
411
- return try {
412
- when {
413
- colorString.startsWith("#") -> {
414
- // Hex color
415
- Color(android.graphics.Color.parseColor(colorString))
416
- }
417
- else -> null
418
- }
419
- } catch (e: Exception) {
420
- Log.w(TAG, "Failed to parse color: \$colorString")
421
- null
84
+ return result;
85
+ }
86
+ /**
87
+ * Recursively walk a directory tree and return all file paths (relative).
88
+ */
89
+ async function walkDir(dir, base) {
90
+ const root = base ?? dir;
91
+ const entries = await fs_extra_1.default.readdir(dir, { withFileTypes: true });
92
+ const files = [];
93
+ for (const entry of entries) {
94
+ const fullPath = path_1.default.join(dir, entry.name);
95
+ if (entry.isDirectory()) {
96
+ files.push(...(await walkDir(fullPath, root)));
422
97
  }
423
- }
424
-
425
- /**
426
- * Handle click events
427
- */
428
- private fun handleClick(action: String?, text: String?) {
429
- if (action != null) {
430
- Log.d(TAG, "Button clicked: \$action")
431
-
432
- // Send click event to dev server
433
- sendClickEvent(action, "Button", text)
434
- }
435
- }
436
-
437
- /**
438
- * Send click event to dev server via WebSocket
439
- */
440
- private fun sendClickEvent(action: String, elementType: String, elementText: String?) {
441
- try {
442
- val ws = HotReload.getWebSocket()
443
- if (ws != null) {
444
- val message = JSONObject().apply {
445
- put("type", "client:click")
446
- put("timestamp", System.currentTimeMillis())
447
- put("action", action)
448
- put("elementType", elementType)
449
- elementText?.let { put("elementText", it) }
450
- }
451
-
452
- ws.send(message.toString())
453
- Log.d(TAG, "Sent click event to server: \$action")
454
- } else {
455
- Log.w(TAG, "WebSocket not available, cannot send click event")
456
- }
457
- } catch (e: Exception) {
458
- Log.e(TAG, "Failed to send click event: \${e.message}")
98
+ else {
99
+ files.push(path_1.default.relative(root, fullPath));
459
100
  }
460
101
  }
102
+ return files;
461
103
  }
462
-
463
104
  // ============================================================================
464
- // Hot Reload Manager
105
+ // Core Template Functions
465
106
  // ============================================================================
466
-
467
107
  /**
468
- * Hot Reload Manager
469
- * Connects to JetStart dev server and automatically reloads the app when code changes
108
+ * Copy the template folder to the project path, replacing placeholders.
470
109
  */
471
- object HotReload {
472
- private const val TAG = "HotReload"
473
- private var webSocket: WebSocket? = null
474
- private var activity: Activity? = null
475
- private var connectionTime: Long = 0
476
- private var ignoreFirstBuild = true
477
- private val httpClient = OkHttpClient()
478
-
479
- /**
480
- * Get the current WebSocket connection (for sending messages)
481
- */
482
- fun getWebSocket(): WebSocket? = webSocket
483
-
484
- fun connect(activity: Activity, serverUrl: String, sessionId: String) {
485
- this.activity = activity
486
-
487
- val wsUrl = serverUrl.replace("http://", "ws://").replace("https://", "wss://")
488
- Log.d(TAG, "Connecting to dev server: \$wsUrl")
489
-
490
- val client = OkHttpClient()
491
- val request = Request.Builder()
492
- .url(wsUrl)
493
- .build()
494
-
495
- webSocket = client.newWebSocket(request, object : WebSocketListener() {
496
- override fun onOpen(webSocket: WebSocket, response: Response) {
497
- Log.d(TAG, "WebSocket connected")
498
- connectionTime = System.currentTimeMillis()
499
- ignoreFirstBuild = true // Ignore the first build-complete after connecting
500
-
501
- // Send connect message
502
- val connectMsg = JSONObject().apply {
503
- put("type", "client:connect")
504
- put("sessionId", sessionId)
505
- put("clientType", "test-app")
506
- }
507
- webSocket.send(connectMsg.toString())
508
- }
509
-
510
- override fun onMessage(webSocket: WebSocket, text: String) {
511
- Log.d(TAG, "Received: \$text")
512
-
513
- try {
514
- val json = JSONObject(text)
515
- val type = json.getString("type")
516
-
517
- when (type) {
518
- "core:ui-update" -> {
519
- // DSL-based hot reload (FAST)
520
- if (ignoreFirstBuild) {
521
- Log.d(TAG, "Ignoring first UI update (old build)")
522
- ignoreFirstBuild = false
523
- return@onMessage
524
- }
525
-
526
- val timestamp = json.optLong("timestamp", 0)
527
- val dslContent = json.optString("dslContent", "")
528
-
529
- Log.d(TAG, "UI update received at \$timestamp, connection at \$connectionTime")
530
-
531
- // Only update if changes happened AFTER we connected
532
- if (timestamp > connectionTime && dslContent.isNotEmpty()) {
533
- Log.d(TAG, "New UI update detected, recomposing (\${dslContent.length} bytes)")
534
-
535
- // Update DSL on main thread
536
- activity?.runOnUiThread {
537
- DSLInterpreter.updateDSL(dslContent)
538
- }
539
- } else {
540
- Log.d(TAG, "Ignoring old UI update")
541
- }
542
- }
543
-
544
- "core:reload" -> {
545
- Log.d(TAG, "Reload triggered!")
546
- // Restart activity on main thread
547
- activity?.runOnUiThread {
548
- activity?.recreate()
549
- }
550
- }
551
-
552
- "core:build-complete" -> {
553
- // Full APK rebuild (SLOW - fallback for non-UI changes)
554
- if (ignoreFirstBuild) {
555
- Log.d(TAG, "Ignoring first build-complete (old build)")
556
- ignoreFirstBuild = false
557
- return@onMessage
558
- }
559
-
560
- val timestamp = json.optLong("timestamp", 0)
561
- val downloadUrl = json.optString("downloadUrl", "")
562
-
563
- Log.d(TAG, "Build complete at \$timestamp, connection at \$connectionTime")
564
- Log.d(TAG, "Download URL: \$downloadUrl")
565
-
566
- // Only reload if build happened AFTER we connected
567
- if (timestamp > connectionTime && downloadUrl.isNotEmpty()) {
568
- Log.d(TAG, "New build detected, downloading and installing APK")
569
- downloadAndInstallApk(downloadUrl)
570
- } else {
571
- Log.d(TAG, "Ignoring old build (timestamp before connection)")
572
- }
573
- }
574
- }
575
- } catch (e: Exception) {
576
- Log.e(TAG, "Failed to parse message: \${e.message}")
577
- }
578
- }
579
-
580
- override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
581
- Log.e(TAG, "WebSocket error: \${t.message}")
582
- // Auto-reconnect after 5 seconds
583
- android.os.Handler(android.os.Looper.getMainLooper()).postDelayed({
584
- connect(activity!!, serverUrl, sessionId)
585
- }, 5000)
586
- }
587
-
588
- override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
589
- Log.d(TAG, "WebSocket closed: \$reason")
590
- }
591
- })
592
- }
593
-
594
- fun disconnect() {
595
- webSocket?.close(1000, "App closing")
596
- webSocket = null
597
- activity = null
598
- }
599
-
600
- private fun downloadAndInstallApk(downloadUrl: String) {
601
- Log.d(TAG, "Starting APK download from: \$downloadUrl")
602
-
603
- val request = Request.Builder()
604
- .url(downloadUrl)
605
- .build()
606
-
607
- httpClient.newCall(request).enqueue(object : Callback {
608
- override fun onFailure(call: Call, e: IOException) {
609
- Log.e(TAG, "APK download failed: \${e.message}")
610
- }
611
-
612
- override fun onResponse(call: Call, response: Response) {
613
- if (!response.isSuccessful) {
614
- Log.e(TAG, "APK download failed with code: \${response.code}")
615
- return
616
- }
617
-
618
- try {
619
- val apkData = response.body?.bytes()
620
- if (apkData == null) {
621
- Log.e(TAG, "APK data is null")
622
- return
623
- }
624
-
625
- Log.d(TAG, "APK downloaded successfully (\${apkData.size} bytes)")
626
-
627
- // Save APK to cache directory
628
- val cacheDir = activity?.cacheDir
629
- if (cacheDir == null) {
630
- Log.e(TAG, "Cache directory is null")
631
- return
632
- }
633
-
634
- val apkFile = File(cacheDir, "update.apk")
635
- apkFile.outputStream().use { output ->
636
- output.write(apkData)
637
- }
638
-
639
- Log.d(TAG, "APK saved to: \${apkFile.absolutePath}")
640
-
641
- // Install APK on main thread
642
- activity?.runOnUiThread {
643
- installApk(apkFile)
644
- }
645
- } catch (e: Exception) {
646
- Log.e(TAG, "Failed to save/install APK: \${e.message}")
647
- }
648
- }
649
- })
650
- }
651
-
652
- private fun installApk(apkFile: File) {
653
- try {
654
- val context = activity ?: return
655
-
656
- val uri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
657
- FileProvider.getUriForFile(
658
- context,
659
- "\${context.packageName}.fileprovider",
660
- apkFile
661
- )
662
- } else {
663
- Uri.fromFile(apkFile)
664
- }
665
-
666
- val intent = Intent(Intent.ACTION_VIEW).apply {
667
- setDataAndType(uri, "application/vnd.android.package-archive")
668
- addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
669
- addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
670
- }
671
-
672
- context.startActivity(intent)
673
- Log.d(TAG, "Installation intent started")
674
- } catch (e: Exception) {
675
- Log.e(TAG, "Failed to install APK: \${e.message}")
676
- }
677
- }
678
- }
679
- `;
680
- await fs_extra_1.default.ensureDir(path_1.default.dirname(jetStartPath));
681
- await fs_extra_1.default.writeFile(jetStartPath, content);
682
- }
683
- async function createDirectoryStructure(projectPath) {
684
- const dirs = [
685
- 'app/src/main/java',
686
- 'app/src/main/res/layout',
687
- 'app/src/main/res/values',
688
- 'app/src/main/res/drawable',
689
- 'gradle/wrapper',
690
- ];
691
- for (const dir of dirs) {
692
- await fs_extra_1.default.ensureDir(path_1.default.join(projectPath, dir));
693
- }
694
- }
695
- async function generateBuildGradle(projectPath, options) {
696
- const content = `plugins {
697
- id 'com.android.application'
698
- id 'org.jetbrains.kotlin.android'
699
- }
700
-
701
- android {
702
- namespace '${options.packageName}'
703
- compileSdk ${shared_1.TARGET_ANDROID_API_LEVEL}
704
-
705
- defaultConfig {
706
- applicationId "${options.packageName}"
707
- minSdk ${shared_1.MIN_ANDROID_API_LEVEL}
708
- targetSdk ${shared_1.TARGET_ANDROID_API_LEVEL}
709
- versionCode 1
710
- versionName "1.0.0"
711
- }
712
-
713
- buildTypes {
714
- release {
715
- minifyEnabled false
110
+ async function copyTemplateWithVariables(templateDir, projectPath, variables, packageName) {
111
+ const files = await walkDir(templateDir);
112
+ for (const relPath of files) {
113
+ const srcPath = path_1.default.join(templateDir, relPath);
114
+ // Replace __PACKAGE_PATH__ in the directory structure
115
+ const packageDir = packageName.replace(/\./g, '/');
116
+ const destRelPath = relPath.replace('__PACKAGE_PATH__', packageDir);
117
+ const destPath = path_1.default.join(projectPath, destRelPath);
118
+ // Ensure the destination directory exists
119
+ await fs_extra_1.default.ensureDir(path_1.default.dirname(destPath));
120
+ if (isTextFile(srcPath)) {
121
+ // Read, substitute, and write
122
+ const raw = await fs_extra_1.default.readFile(srcPath, 'utf-8');
123
+ // Strip UTF-8 BOM if present - Gradle/Groovy parsers reject it
124
+ const content = raw.charCodeAt(0) === 0xFEFF ? raw.slice(1) : raw;
125
+ const processed = replaceVariables(content, variables);
126
+ await fs_extra_1.default.writeFile(destPath, processed);
127
+ }
128
+ else {
129
+ // Binary file — copy as-is
130
+ await fs_extra_1.default.copy(srcPath, destPath);
716
131
  }
717
132
  }
718
-
719
- compileOptions {
720
- sourceCompatibility JavaVersion.VERSION_17
721
- targetCompatibility JavaVersion.VERSION_17
722
- }
723
-
724
- kotlinOptions {
725
- jvmTarget = '17'
726
- }
727
-
728
- buildFeatures {
729
- compose true
730
- buildConfig true // Required for JetStart hot reload
731
- }
732
-
733
- composeOptions {
734
- kotlinCompilerExtensionVersion = '1.5.6'
735
- }
736
133
  }
737
-
738
- dependencies {
739
- implementation 'androidx.core:core-ktx:1.12.0'
740
- implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.2'
741
- implementation 'androidx.activity:activity-compose:1.8.1'
742
- implementation platform('androidx.compose:compose-bom:2023.10.01')
743
- implementation 'androidx.compose.ui:ui'
744
- implementation 'androidx.compose.ui:ui-graphics'
745
- implementation 'androidx.compose.ui:ui-tooling-preview'
746
- implementation 'androidx.compose.material3:material3'
747
-
748
- // JetStart Hot Reload dependencies
749
- implementation 'com.squareup.okhttp3:okhttp:4.12.0'
750
- implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3'
751
- }`;
752
- await fs_extra_1.default.writeFile(path_1.default.join(projectPath, 'app/build.gradle'), content);
753
- }
754
- async function generateSettingsGradle(projectPath, projectName) {
755
- const content = `rootProject.name = "${projectName}"
756
- include ':app'`;
757
- await fs_extra_1.default.writeFile(path_1.default.join(projectPath, 'settings.gradle'), content);
758
- }
759
- async function generateGradleProperties(projectPath) {
760
- const content = `org.gradle.jvmargs=-Xmx2048m
761
- android.useAndroidX=true
762
- kotlin.code.style=official`;
763
- await fs_extra_1.default.writeFile(path_1.default.join(projectPath, 'gradle.properties'), content);
764
- }
765
- async function generateMainActivity(projectPath, packageName) {
766
- const packagePath = packageName.replace(/\./g, '/');
767
- const activityPath = path_1.default.join(projectPath, 'app/src/main/java', packagePath, 'MainActivity.kt');
768
- const content = `package ${packageName}
769
-
770
- import android.os.Bundle
771
- import androidx.activity.ComponentActivity
772
- import androidx.activity.compose.setContent
773
- import androidx.compose.foundation.layout.*
774
- import androidx.compose.material3.*
775
- import androidx.compose.runtime.*
776
- import androidx.compose.ui.Alignment
777
- import androidx.compose.ui.Modifier
778
- import androidx.compose.ui.unit.dp
779
-
780
- class MainActivity : ComponentActivity() {
781
- override fun onCreate(savedInstanceState: Bundle?) {
782
- super.onCreate(savedInstanceState)
783
-
784
- // Initialize hot reload - reads from BuildConfig injected by jetstart dev
785
- try {
786
- val serverUrl = BuildConfig.JETSTART_SERVER_URL
787
- val sessionId = BuildConfig.JETSTART_SESSION_ID
788
- HotReload.connect(this, serverUrl, sessionId)
789
- } catch (e: Exception) {
790
- // BuildConfig not available yet, hot reload will be disabled
791
- android.util.Log.w("MainActivity", "Hot reload not configured: \${e.message}")
792
- }
793
-
794
- setContent {
795
- MaterialTheme {
796
- Surface(
797
- modifier = Modifier.fillMaxSize(),
798
- color = MaterialTheme.colorScheme.background
799
- ) {
800
- // Check if we should render from DSL (hot reload mode)
801
- val dsl by DSLInterpreter.currentDSL.collectAsState()
802
-
803
- if (dsl != null) {
804
- // Hot reload mode: render from DSL sent by server
805
- DSLInterpreter.RenderDSL(dsl!!)
806
- } else {
807
- // Normal mode: render actual Compose code
808
- AppContent()
809
- }
810
- }
811
- }
812
- }
813
- }
814
-
815
- override fun onDestroy() {
816
- super.onDestroy()
817
- HotReload.disconnect()
818
- }
819
- }
820
-
134
+ // ============================================================================
135
+ // Dynamic File Generators (keep as code — not template files)
136
+ // ============================================================================
821
137
  /**
822
- * Main App Content - REAL Kotlin Compose Code!
823
- * This gets parsed to DSL and sent via hot reload
138
+ * Generate jetstart.config.json (dynamic JSON structure)
824
139
  */
825
- @Composable
826
- fun AppContent() {
827
- Column(
828
- modifier = Modifier
829
- .fillMaxSize()
830
- .padding(16.dp),
831
- horizontalAlignment = Alignment.CenterHorizontally,
832
- verticalArrangement = Arrangement.Center
833
- ) {
834
- Text(
835
- text = "Welcome to JetStart! 🚀",
836
- style = MaterialTheme.typography.headlineMedium
837
- )
838
-
839
- Spacer(modifier = Modifier.height(16.dp))
840
-
841
- Text(
842
- text = "Edit this code and save to see hot reload!",
843
- style = MaterialTheme.typography.bodyMedium
844
- )
845
-
846
- Spacer(modifier = Modifier.height(24.dp))
847
-
848
- Button(
849
- onClick = { /* Handle click */ },
850
- modifier = Modifier.fillMaxWidth()
851
- ) {
852
- Text("Click Me!")
853
- }
854
- }
855
- }`;
856
- await fs_extra_1.default.ensureDir(path_1.default.dirname(activityPath));
857
- await fs_extra_1.default.writeFile(activityPath, content);
858
- }
859
- async function generateAndroidManifest(projectPath, options) {
860
- const themeName = options.projectName.replace(/[^a-zA-Z0-9]/g, '');
861
- const content = `<?xml version="1.0" encoding="utf-8"?>
862
- <manifest xmlns:android="http://schemas.android.com/apk/res/android">
863
-
864
- <uses-permission android:name="android.permission.INTERNET" />
865
-
866
- <application
867
- android:allowBackup="true"
868
- android:label="@string/app_name"
869
- android:theme="@style/Theme.${themeName}"
870
- android:networkSecurityConfig="@xml/network_security_config">
871
- <activity
872
- android:name=".MainActivity"
873
- android:exported="true">
874
- <intent-filter>
875
- <action android:name="android.intent.action.MAIN" />
876
- <category android:name="android.intent.category.LAUNCHER" />
877
- </intent-filter>
878
- </activity>
879
- </application>
880
-
881
- </manifest>`;
882
- await fs_extra_1.default.writeFile(path_1.default.join(projectPath, 'app/src/main/AndroidManifest.xml'), content);
883
- }
884
140
  async function generateJetStartConfig(projectPath, options) {
885
141
  const config = {
886
142
  projectName: options.projectName,
@@ -895,136 +151,20 @@ async function generateJetStartConfig(projectPath, options) {
895
151
  };
896
152
  await fs_extra_1.default.writeJSON(path_1.default.join(projectPath, 'jetstart.config.json'), config, { spaces: 2 });
897
153
  }
898
- async function generateGitignore(projectPath) {
899
- const content = `# Build
900
- /build
901
- /app/build
902
- .gradle
903
- *.hprof
904
-
905
- # IDE
906
- .idea
907
- *.iml
908
- .vscode
909
- .DS_Store
910
-
911
- # Claude Code
912
- .claude
913
- .claude-worktrees
914
-
915
- # JetStart
916
- .jetstart
917
-
918
- # Android
919
- local.properties
920
- *.apk
921
- *.aab
922
- *.ap_
923
- *.dex
924
- *.class
925
- bin/
926
- gen/
927
- out/
928
- captures/
929
- .externalNativeBuild
930
- .cxx
931
-
932
- # Log files
933
- *.log
934
-
935
- # Keystore files
936
- *.jks
937
- *.keystore`;
938
- await fs_extra_1.default.writeFile(path_1.default.join(projectPath, '.gitignore'), content);
939
- }
940
- async function generateReadme(projectPath, projectName) {
941
- const content = `# ${projectName}
942
-
943
- A JetStart project with Kotlin and Jetpack Compose.
944
-
945
- ## Getting Started
946
-
947
- \`\`\`bash
948
- # Start development server
949
- jetstart dev
950
-
951
- # Build production APK
952
- jetstart build
953
-
954
- # View logs
955
- jetstart logs
956
- \`\`\`
957
-
958
- ## Project Structure
959
-
960
- \`\`\`
961
- ${projectName}/
962
- ├── app/
963
- │ └── src/
964
- │ └── main/
965
- │ ├── java/ # Kotlin source files
966
- │ └── res/ # Resources
967
- ├── jetstart.config.json # JetStart configuration
968
- └── build.gradle # Gradle build file
969
- \`\`\`
970
-
971
- ## Learn More
972
-
973
- - [JetStart Documentation](https://github.com/phantom/jetstart)
974
- - [Jetpack Compose](https://developer.android.com/jetpack/compose)
975
- `;
976
- await fs_extra_1.default.writeFile(path_1.default.join(projectPath, 'README.md'), content);
977
- }
978
- async function generateRootBuildGradle(projectPath) {
979
- const content = `// Top-level build file
980
- buildscript {
981
- ext {
982
- kotlin_version = '1.9.21'
983
- compose_version = '1.5.4'
984
- }
985
- repositories {
986
- google()
987
- mavenCentral()
988
- }
989
- dependencies {
990
- classpath 'com.android.tools.build:gradle:8.2.0'
991
- classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
992
- }
993
- }
994
-
995
- allprojects {
996
- repositories {
997
- google()
998
- mavenCentral()
999
- }
1000
- }
1001
-
1002
- task clean(type: Delete) {
1003
- delete rootProject.buildDir
1004
- }`;
1005
- await fs_extra_1.default.writeFile(path_1.default.join(projectPath, 'build.gradle'), content);
1006
- }
154
+ /**
155
+ * Generate Gradle wrapper by calling system gradle.
156
+ */
1007
157
  async function generateGradleWrapper(projectPath) {
1008
- // Use system Gradle to initialize proper wrapper
1009
- // This generates:
1010
- // - gradle/wrapper/gradle-wrapper.jar
1011
- // - gradle/wrapper/gradle-wrapper.properties
1012
- // - gradlew (Unix shell script)
1013
- // - gradlew.bat (Windows batch script)
1014
158
  return new Promise((resolve) => {
1015
- // Try to use system gradle to generate wrapper
1016
159
  const gradleCmd = process.platform === 'win32' ? 'gradle.bat' : 'gradle';
1017
160
  const gradleProcess = (0, child_process_1.spawn)(gradleCmd, ['wrapper', '--gradle-version', '8.2'], {
1018
161
  cwd: projectPath,
1019
162
  shell: true,
1020
163
  });
1021
- gradleProcess.on('close', (code) => {
1022
- // Continue regardless of success/failure
1023
- // If gradle wrapper command fails, the build will fall back to system gradle
164
+ gradleProcess.on('close', () => {
1024
165
  resolve();
1025
166
  });
1026
167
  gradleProcess.on('error', () => {
1027
- // Continue even if gradle command not found
1028
168
  resolve();
1029
169
  });
1030
170
  // Timeout after 30 seconds
@@ -1034,45 +174,10 @@ async function generateGradleWrapper(projectPath) {
1034
174
  }, 30000);
1035
175
  });
1036
176
  }
1037
- async function generateResourceFiles(projectPath, projectName) {
1038
- // Generate strings.xml
1039
- const stringsXml = `<?xml version="1.0" encoding="utf-8"?>
1040
- <resources>
1041
- <string name="app_name">${projectName}</string>
1042
- </resources>`;
1043
- await fs_extra_1.default.writeFile(path_1.default.join(projectPath, 'app/src/main/res/values/strings.xml'), stringsXml);
1044
- // Generate colors.xml
1045
- const colorsXml = `<?xml version="1.0" encoding="utf-8"?>
1046
- <resources>
1047
- <color name="purple_200">#FFBB86FC</color>
1048
- <color name="purple_500">#FF6200EE</color>
1049
- <color name="purple_700">#FF3700B3</color>
1050
- <color name="teal_200">#FF03DAC5</color>
1051
- <color name="teal_700">#FF018786</color>
1052
- <color name="black">#FF000000</color>
1053
- <color name="white">#FFFFFFFF</color>
1054
- </resources>`;
1055
- await fs_extra_1.default.writeFile(path_1.default.join(projectPath, 'app/src/main/res/values/colors.xml'), colorsXml);
1056
- // Generate themes.xml
1057
- const themesXml = `<?xml version="1.0" encoding="utf-8"?>
1058
- <resources>
1059
- <style name="Theme.${projectName.replace(/[^a-zA-Z0-9]/g, '')}" parent="android:Theme.Material.Light.NoActionBar" />
1060
- </resources>`;
1061
- await fs_extra_1.default.writeFile(path_1.default.join(projectPath, 'app/src/main/res/values/themes.xml'), themesXml);
1062
- // Generate network_security_config.xml for development (allows cleartext traffic)
1063
- const networkSecurityConfig = `<?xml version="1.0" encoding="utf-8"?>
1064
- <network-security-config>
1065
- <base-config cleartextTrafficPermitted="true">
1066
- <trust-anchors>
1067
- <certificates src="system" />
1068
- </trust-anchors>
1069
- </base-config>
1070
- </network-security-config>`;
1071
- await fs_extra_1.default.ensureDir(path_1.default.join(projectPath, 'app/src/main/res/xml'));
1072
- await fs_extra_1.default.writeFile(path_1.default.join(projectPath, 'app/src/main/res/xml/network_security_config.xml'), networkSecurityConfig);
1073
- }
177
+ /**
178
+ * Generate local.properties by auto-detecting Android SDK location.
179
+ */
1074
180
  async function generateLocalProperties(projectPath) {
1075
- // Auto-detect Android SDK location
1076
181
  let androidSdkPath;
1077
182
  // Check environment variables first
1078
183
  androidSdkPath = process.env.ANDROID_HOME || process.env.ANDROID_SDK_ROOT;
@@ -1109,11 +214,36 @@ async function generateLocalProperties(projectPath) {
1109
214
  console.warn('[Warning] Android SDK not found. You may need to set ANDROID_HOME or create local.properties manually.');
1110
215
  return;
1111
216
  }
1112
- // Create local.properties with SDK path
1113
217
  const content = `# Auto-generated by JetStart
1114
218
  sdk.dir=${androidSdkPath.replace(/\\/g, '\\\\')}
1115
219
  `;
1116
220
  await fs_extra_1.default.writeFile(path_1.default.join(projectPath, 'local.properties'), content);
1117
221
  console.log(`[JetStart] Created local.properties with SDK: ${androidSdkPath}`);
1118
222
  }
223
+ // ============================================================================
224
+ // Main Entry Point
225
+ // ============================================================================
226
+ /**
227
+ * Generate a new JetStart project from the file-based template.
228
+ *
229
+ * 1. Copy packages/template/base/ → projectPath
230
+ * 2. Rename __PACKAGE_PATH__ → actual package dir (e.g. com/jetstart/myapp)
231
+ * 3. Replace {{VAR}} placeholders in all text files
232
+ * 4. Generate dynamic files (jetstart.config.json, gradle wrapper, local.properties)
233
+ */
234
+ async function generateProjectTemplate(projectPath, options) {
235
+ const templateDir = getTemplateDir();
236
+ // Validate template directory exists
237
+ if (!(await fs_extra_1.default.pathExists(templateDir))) {
238
+ throw new Error(`Template directory not found: ${templateDir}. ` + `Make sure packages/template/base/ exists.`);
239
+ }
240
+ // Build variable map
241
+ const variables = buildVariableMap(options);
242
+ // 1. Copy template files with variable substitution + package path rename
243
+ await copyTemplateWithVariables(templateDir, projectPath, variables, options.packageName);
244
+ // 2. Generate dynamic files
245
+ await generateGradleWrapper(projectPath);
246
+ await generateLocalProperties(projectPath);
247
+ await generateJetStartConfig(projectPath, options);
248
+ }
1119
249
  //# sourceMappingURL=template.js.map