@tamer4lynx/cli 0.0.1

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.
@@ -0,0 +1,99 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { execSync } from 'child_process';
4
+ import { resolveHostPaths, resolveDevAppPaths } from '../common/hostConfig';
5
+ import android_autolink from './autolink';
6
+ import android_create from './create';
7
+ import android_syncDevClient from './syncDevClient';
8
+ function findRepoRoot(start) {
9
+ let dir = path.resolve(start);
10
+ const root = path.parse(dir).root;
11
+ while (dir !== root) {
12
+ const pkgPath = path.join(dir, 'package.json');
13
+ if (fs.existsSync(pkgPath)) {
14
+ try {
15
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
16
+ if (pkg.workspaces)
17
+ return dir;
18
+ }
19
+ catch { }
20
+ }
21
+ dir = path.dirname(dir);
22
+ }
23
+ return start;
24
+ }
25
+ async function bundleAndDeploy(opts = {}) {
26
+ const target = (opts.target ?? 'host');
27
+ const origCwd = process.cwd();
28
+ let resolved;
29
+ try {
30
+ if (target === 'dev-app') {
31
+ const repoRoot = findRepoRoot(origCwd);
32
+ resolved = resolveDevAppPaths(repoRoot);
33
+ const devAppDir = resolved.projectRoot;
34
+ const androidDir = resolved.androidDir;
35
+ if (!fs.existsSync(androidDir)) {
36
+ console.log('📱 Creating Tamer Dev App Android project...');
37
+ await android_create({ target: 'dev-app' });
38
+ }
39
+ process.chdir(devAppDir);
40
+ }
41
+ else {
42
+ resolved = resolveHostPaths();
43
+ }
44
+ }
45
+ catch (error) {
46
+ console.error(`❌ Error loading configuration: ${error.message}`);
47
+ process.exit(1);
48
+ }
49
+ const { lynxProjectDir, lynxBundlePath, androidAssetsDir, devClientBundlePath, devMode } = resolved;
50
+ const destinationDir = androidAssetsDir;
51
+ android_autolink();
52
+ if (devMode === 'embedded') {
53
+ await android_syncDevClient();
54
+ }
55
+ try {
56
+ console.log('📦 Building Lynx project...');
57
+ execSync('npm run build', { stdio: 'inherit', cwd: lynxProjectDir });
58
+ console.log('✅ Build completed successfully.');
59
+ }
60
+ catch (error) {
61
+ console.error('❌ Build process failed.');
62
+ process.exit(1);
63
+ }
64
+ if (target === 'dev-app') {
65
+ process.chdir(origCwd);
66
+ }
67
+ if (target !== 'dev-app' && devMode === 'embedded' && devClientBundlePath && !fs.existsSync(devClientBundlePath)) {
68
+ const devClientDir = path.dirname(path.dirname(devClientBundlePath));
69
+ try {
70
+ console.log('📦 Building dev launcher (tamer-dev-client)...');
71
+ execSync('npm run build', { stdio: 'inherit', cwd: devClientDir });
72
+ console.log('✅ Dev launcher build completed.');
73
+ }
74
+ catch (error) {
75
+ console.error('❌ Dev launcher build failed.');
76
+ process.exit(1);
77
+ }
78
+ }
79
+ try {
80
+ fs.mkdirSync(destinationDir, { recursive: true });
81
+ if (target !== 'dev-app' && devMode === 'embedded' && devClientBundlePath && fs.existsSync(devClientBundlePath)) {
82
+ fs.copyFileSync(devClientBundlePath, path.join(destinationDir, 'dev-client.lynx.bundle'));
83
+ console.log(`✨ Copied dev-client.lynx.bundle to assets`);
84
+ }
85
+ if (!fs.existsSync(lynxBundlePath)) {
86
+ console.error(`❌ Build output not found at: ${lynxBundlePath}`);
87
+ process.exit(1);
88
+ }
89
+ fs.copyFileSync(lynxBundlePath, path.join(destinationDir, resolved.lynxBundleFile));
90
+ console.log(`✨ Copied ${resolved.lynxBundleFile} to assets`);
91
+ }
92
+ catch (error) {
93
+ console.error(`❌ Failed to copy bundle: ${error.message}`);
94
+ process.exit(1);
95
+ }
96
+ }
97
+ export default bundleAndDeploy;
98
+ // // --- Main Execution ---
99
+ // bundleAndDeploy();
@@ -0,0 +1,129 @@
1
+ export function getLynxExplorerInputSource(packageName) {
2
+ return `package ${packageName}.core
3
+
4
+ import android.content.Context
5
+ import android.graphics.Color
6
+ import android.text.Editable
7
+ import android.text.TextWatcher
8
+ import android.util.TypedValue
9
+ import android.view.Gravity
10
+ import android.view.View
11
+ import android.view.inputmethod.EditorInfo
12
+ import android.view.inputmethod.InputMethodManager
13
+ import androidx.appcompat.widget.AppCompatEditText
14
+ import com.lynx.react.bridge.Callback
15
+ import com.lynx.react.bridge.ReadableMap
16
+ import com.lynx.tasm.behavior.LynxContext
17
+ import com.lynx.tasm.behavior.LynxProp
18
+ import com.lynx.tasm.behavior.LynxUIMethod
19
+ import com.lynx.tasm.behavior.LynxUIMethodConstants
20
+ import com.lynx.tasm.behavior.ui.LynxUI
21
+ import com.lynx.tasm.event.LynxCustomEvent
22
+
23
+ class LynxExplorerInput(context: LynxContext) : LynxUI<AppCompatEditText>(context) {
24
+
25
+ override fun createView(context: Context): AppCompatEditText {
26
+ val view = AppCompatEditText(context)
27
+ view.setLines(1)
28
+ view.isSingleLine = true
29
+ view.gravity = Gravity.CENTER_VERTICAL
30
+ view.background = null
31
+ view.imeOptions = EditorInfo.IME_ACTION_NONE
32
+ view.setHorizontallyScrolling(true)
33
+ view.setPadding(0, 0, 0, 0)
34
+ view.setTextSize(TypedValue.COMPLEX_UNIT_SP, 16f)
35
+ view.minHeight = TypedValue.applyDimension(
36
+ TypedValue.COMPLEX_UNIT_DIP,
37
+ 52f,
38
+ context.resources.displayMetrics
39
+ ).toInt()
40
+ view.setTextColor(Color.WHITE)
41
+ view.setHintTextColor(Color.argb(160, 255, 255, 255))
42
+ view.includeFontPadding = false
43
+ view.isFocusableInTouchMode = true
44
+ view.addTextChangedListener(object : TextWatcher {
45
+ override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
46
+ override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
47
+ override fun afterTextChanged(s: Editable?) {
48
+ emitEvent("input", mapOf("value" to (s?.toString() ?: "")))
49
+ }
50
+ })
51
+ view.setOnFocusChangeListener { _: View?, hasFocus: Boolean ->
52
+ if (!hasFocus) emitEvent("blur", null)
53
+ }
54
+ return view
55
+ }
56
+
57
+ override fun onLayoutUpdated() {
58
+ super.onLayoutUpdated()
59
+ val paddingTop = mPaddingTop + mBorderTopWidth
60
+ val paddingBottom = mPaddingBottom + mBorderBottomWidth
61
+ val paddingLeft = mPaddingLeft + mBorderLeftWidth
62
+ val paddingRight = mPaddingRight + mBorderRightWidth
63
+ mView.setPadding(paddingLeft, paddingTop, paddingRight, paddingBottom)
64
+ }
65
+
66
+ @LynxProp(name = "value")
67
+ fun setValue(value: String) {
68
+ if (value != mView.text.toString()) {
69
+ mView.setText(value)
70
+ }
71
+ }
72
+
73
+ @LynxProp(name = "placeholder")
74
+ fun setPlaceholder(value: String) {
75
+ mView.hint = value
76
+ }
77
+
78
+ @LynxUIMethod
79
+ fun focus(params: ReadableMap?, callback: Callback) {
80
+ if (mView.requestFocus()) {
81
+ if (showSoftInput()) {
82
+ callback.invoke(LynxUIMethodConstants.SUCCESS)
83
+ } else {
84
+ callback.invoke(LynxUIMethodConstants.UNKNOWN, "fail to show keyboard")
85
+ }
86
+ } else {
87
+ callback.invoke(LynxUIMethodConstants.UNKNOWN, "fail to focus")
88
+ }
89
+ }
90
+
91
+ private fun showSoftInput(): Boolean {
92
+ val imm = lynxContext.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
93
+ ?: return false
94
+ return imm.showSoftInput(mView, InputMethodManager.SHOW_IMPLICIT, null)
95
+ }
96
+
97
+ private fun emitEvent(name: String, detail: Map<String, Any>?) {
98
+ val event = LynxCustomEvent(sign, name)
99
+ detail?.forEach { (k, v) -> event.addDetail(k, v) }
100
+ lynxContext.eventEmitter.sendCustomEvent(event)
101
+ }
102
+ }
103
+ `;
104
+ }
105
+ export function getAppBarElementSource(packageName) {
106
+ return `package ${packageName}.core
107
+
108
+ import android.content.Context
109
+ import android.view.View
110
+ import androidx.core.view.ViewCompat
111
+ import androidx.core.view.WindowInsetsCompat
112
+ import com.lynx.tasm.behavior.LynxContext
113
+ import com.lynx.tasm.behavior.ui.LynxUI
114
+
115
+ class AppBarElement(context: LynxContext) : LynxUI<View>(context) {
116
+ override fun createView(context: Context): View {
117
+ return View(context).apply {
118
+ minimumHeight = (56 * context.resources.displayMetrics.density).toInt()
119
+ ViewCompat.setOnApplyWindowInsetsListener(this) { v, insets ->
120
+ val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
121
+ v.setPadding(systemBars.left, systemBars.top, systemBars.right, 0)
122
+ insets
123
+ }
124
+ requestApplyInsets()
125
+ }
126
+ }
127
+ }
128
+ `;
129
+ }
@@ -0,0 +1,423 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import os from "os";
4
+ import { setupGradleWrapper } from "./getGradle";
5
+ import { loadHostConfig, resolveDevMode, resolveHostPaths, resolveIconPaths } from "../common/hostConfig";
6
+ import { fetchAndPatchApplication, fetchAndPatchTemplateProvider, getDevClientManager, getProjectActivity, getStandaloneMainActivity, } from "../explorer/patches";
7
+ import { getDevServerPrefs } from "../explorer/devLauncher";
8
+ import { getLynxExplorerInputSource } from "./coreElements";
9
+ function findRepoRoot(start) {
10
+ let dir = path.resolve(start);
11
+ const root = path.parse(dir).root;
12
+ while (dir !== root) {
13
+ const pkgPath = path.join(dir, "package.json");
14
+ if (fs.existsSync(pkgPath)) {
15
+ try {
16
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
17
+ if (pkg.workspaces)
18
+ return dir;
19
+ }
20
+ catch { }
21
+ }
22
+ dir = path.dirname(dir);
23
+ }
24
+ return start;
25
+ }
26
+ const create = async (opts = {}) => {
27
+ const target = opts.target ?? "host";
28
+ const origCwd = process.cwd();
29
+ if (target === "dev-app") {
30
+ const repoRoot = findRepoRoot(origCwd);
31
+ const devAppDir = path.join(repoRoot, "packages", "tamer-dev-app");
32
+ if (!fs.existsSync(path.join(devAppDir, "tamer.config.json"))) {
33
+ console.error("❌ packages/tamer-dev-app/tamer.config.json not found.");
34
+ process.exit(1);
35
+ }
36
+ process.chdir(devAppDir);
37
+ }
38
+ let appName;
39
+ let packageName;
40
+ let androidSdk;
41
+ let config;
42
+ try {
43
+ config = loadHostConfig();
44
+ packageName = config.android?.packageName;
45
+ appName = config.android?.appName;
46
+ androidSdk = config.android?.sdk;
47
+ if (androidSdk && androidSdk.startsWith("~")) {
48
+ androidSdk = androidSdk.replace(/^~/, os.homedir());
49
+ }
50
+ if (!appName || !packageName) {
51
+ throw new Error('"android.appName" and "android.packageName" must be defined in tamer.config.json');
52
+ }
53
+ }
54
+ catch (error) {
55
+ console.error(`❌ Error loading configuration: ${error.message}`);
56
+ process.exit(1);
57
+ }
58
+ const packagePath = packageName.replace(/\./g, "/");
59
+ const gradleVersion = "8.14.2";
60
+ const androidDir = config.paths?.androidDir ?? "android";
61
+ const rootDir = path.join(process.cwd(), androidDir);
62
+ const appDir = path.join(rootDir, "app");
63
+ const mainDir = path.join(appDir, "src", "main");
64
+ const javaDir = path.join(mainDir, "java", packagePath);
65
+ const kotlinDir = path.join(mainDir, "kotlin", packagePath);
66
+ const kotlinGeneratedDir = path.join(kotlinDir, "generated");
67
+ const assetsDir = path.join(mainDir, "assets");
68
+ const themesDir = path.join(mainDir, "res", "values");
69
+ const gradleDir = path.join(rootDir, "gradle");
70
+ function writeFile(filePath, content, options) {
71
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
72
+ fs.writeFileSync(filePath, content.trimStart(), options?.encoding ?? "utf8");
73
+ }
74
+ // Clean up previous generation if it exists
75
+ if (fs.existsSync(rootDir)) {
76
+ console.log(`🧹 Removing existing directory: ${rootDir}`);
77
+ fs.rmSync(rootDir, { recursive: true, force: true });
78
+ }
79
+ console.log(`🚀 Creating a new Tamer4Lynx project in: ${rootDir}`);
80
+ // --- Start File Generation ---
81
+ // gradle/libs.versions.toml
82
+ writeFile(path.join(gradleDir, "libs.versions.toml"), `
83
+ [versions]
84
+ agp = "8.9.1"
85
+ commonsCompress = "1.26.1"
86
+ commonsLang3 = "3.14.0"
87
+ fresco = "2.3.0"
88
+ kotlin = "2.0.21"
89
+ coreKtx = "1.10.1"
90
+ junit = "4.13.2"
91
+ junitVersion = "1.1.5"
92
+ espressoCore = "3.5.1"
93
+ appcompat = "1.6.1"
94
+ lynx = "3.3.1"
95
+ material = "1.10.0"
96
+ activity = "1.8.0"
97
+ constraintlayout = "2.1.4"
98
+ okhttp = "4.9.0"
99
+ primjs = "2.12.0"
100
+ zxing = "4.3.0"
101
+
102
+ [libraries]
103
+ androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
104
+ animated-base = { module = "com.facebook.fresco:animated-base", version.ref = "fresco" }
105
+ animated-gif = { module = "com.facebook.fresco:animated-gif", version.ref = "fresco" }
106
+ animated-webp = { module = "com.facebook.fresco:animated-webp", version.ref = "fresco" }
107
+ commons-compress = { module = "org.apache.commons:commons-compress", version.ref = "commonsCompress" }
108
+ commons-lang3 = { module = "org.apache.commons:commons-lang3", version.ref = "commonsLang3" }
109
+ fresco = { module = "com.facebook.fresco:fresco", version.ref = "fresco" }
110
+ junit = { group = "junit", name = "junit", version.ref = "junit" }
111
+ androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
112
+ androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
113
+ androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
114
+ lynx = { module = "org.lynxsdk.lynx:lynx", version.ref = "lynx" }
115
+ lynx-jssdk = { module = "org.lynxsdk.lynx:lynx-jssdk", version.ref = "lynx" }
116
+ lynx-processor = { module = "org.lynxsdk.lynx:lynx-processor", version.ref = "lynx" }
117
+ lynx-service-http = { module = "org.lynxsdk.lynx:lynx-service-http", version.ref = "lynx" }
118
+ lynx-service-image = { module = "org.lynxsdk.lynx:lynx-service-image", version.ref = "lynx" }
119
+ lynx-service-log = { module = "org.lynxsdk.lynx:lynx-service-log", version.ref = "lynx" }
120
+ lynx-trace = { module = "org.lynxsdk.lynx:lynx-trace", version.ref = "lynx" }
121
+ material = { group = "com.google.android.material", name = "material", version.ref = "material" }
122
+ androidx-activity = { group = "androidx.activity", name = "activity", version.ref = "activity" }
123
+ androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
124
+ okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
125
+ primjs = { module = "org.lynxsdk.lynx:primjs", version.ref = "primjs" }
126
+ webpsupport = { module = "com.facebook.fresco:webpsupport", version.ref = "fresco" }
127
+ zxing = { module = "com.journeyapps:zxing-android-embedded", version.ref = "zxing" }
128
+
129
+ [plugins]
130
+ android-application = { id = "com.android.application", version.ref = "agp" }
131
+ kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
132
+ kotlin-kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlin" }
133
+ `);
134
+ // settings.gradle.kts (with GENERATED block)
135
+ writeFile(path.join(rootDir, "settings.gradle.kts"), `
136
+ pluginManagement {
137
+ repositories {
138
+ google {
139
+ content {
140
+ includeGroupByRegex("com\\\\.android.*")
141
+ includeGroupByRegex("com\\\\.google.*")
142
+ includeGroupByRegex("androidx.*")
143
+ }
144
+ }
145
+ mavenCentral()
146
+ gradlePluginPortal()
147
+ }
148
+ }
149
+ dependencyResolutionManagement {
150
+ repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
151
+ repositories {
152
+ google()
153
+ mavenCentral()
154
+ }
155
+ }
156
+
157
+ rootProject.name = "${appName}"
158
+ include(":app")
159
+
160
+ // GENERATED AUTOLINK START
161
+ // This section is automatically generated by Tamer4Lynx.
162
+ // Manual edits will be overwritten.
163
+ println("If you have native modules please run tamer android link")
164
+ // GENERATED AUTOLINK END
165
+ `);
166
+ // Root build.gradle
167
+ writeFile(path.join(rootDir, "build.gradle.kts"), `
168
+ // Top-level build file where you can add configuration options common to all sub-projects/modules.
169
+ plugins {
170
+ alias(libs.plugins.android.application) apply false
171
+ alias(libs.plugins.kotlin.android) apply false
172
+ }
173
+ `);
174
+ // gradle.properties
175
+ writeFile(path.join(rootDir, "gradle.properties"), `
176
+ org.gradle.jvmargs=-Xmx2048m
177
+ android.useAndroidX=true
178
+ kotlin.code.style=official
179
+ android.enableJetifier=true
180
+ `);
181
+ // app/build.gradle.kts (UPDATED)
182
+ writeFile(path.join(appDir, "build.gradle.kts"), `
183
+ plugins {
184
+ alias(libs.plugins.android.application)
185
+ alias(libs.plugins.kotlin.android)
186
+ id("org.jetbrains.kotlin.kapt")
187
+ }
188
+
189
+ android {
190
+ namespace = "${packageName}"
191
+ compileSdk = 35
192
+
193
+ defaultConfig {
194
+ applicationId = "${packageName}"
195
+ minSdk = 28
196
+ targetSdk = 35
197
+ versionCode = 1
198
+ versionName = "1.0"
199
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
200
+ ndk {
201
+ abiFilters += listOf("armeabi-v7a", "arm64-v8a")
202
+ }
203
+ }
204
+
205
+ buildTypes {
206
+ release {
207
+ isMinifyEnabled = false
208
+ proguardFiles(
209
+ getDefaultProguardFile("proguard-android-optimize.txt"),
210
+ "proguard-rules.pro"
211
+ )
212
+ }
213
+ }
214
+
215
+ compileOptions {
216
+ sourceCompatibility = JavaVersion.VERSION_17
217
+ targetCompatibility = JavaVersion.VERSION_17
218
+ }
219
+
220
+ kotlinOptions {
221
+ jvmTarget = "17"
222
+ }
223
+
224
+ buildFeatures {
225
+ buildConfig = true
226
+ }
227
+
228
+ sourceSets {
229
+ getByName("main") {
230
+ jniLibs.srcDirs("src/main/jniLibs")
231
+ }
232
+ }
233
+ }
234
+
235
+ dependencies {
236
+ implementation(libs.androidx.core.ktx)
237
+ implementation(libs.androidx.appcompat)
238
+ implementation(libs.material)
239
+ implementation(libs.androidx.activity)
240
+ implementation(libs.androidx.constraintlayout)
241
+ testImplementation(libs.junit)
242
+ androidTestImplementation(libs.androidx.junit)
243
+ androidTestImplementation(libs.androidx.espresso.core)
244
+ implementation(libs.lynx)
245
+ implementation(libs.lynx.jssdk)
246
+ implementation(libs.lynx.trace)
247
+ implementation(libs.primjs)
248
+ implementation(libs.lynx.service.image)
249
+ implementation(libs.fresco)
250
+ implementation(libs.animated.gif)
251
+ implementation(libs.animated.webp)
252
+ implementation(libs.webpsupport)
253
+ implementation(libs.animated.base)
254
+ implementation(libs.lynx.service.log)
255
+ implementation(libs.lynx.service.http)
256
+ implementation(libs.okhttp)
257
+ implementation(libs.zxing)
258
+ kapt(libs.lynx.processor)
259
+ implementation(libs.commons.lang3)
260
+ implementation(libs.commons.compress)
261
+
262
+ // GENERATED AUTOLINK DEPENDENCIES START
263
+ // This section is automatically generated by Tamer4Lynx.
264
+ // Manual edits will be overwritten.
265
+ // GENERATED AUTOLINK DEPENDENCIES END
266
+ }
267
+ `);
268
+ // themes.xml
269
+ writeFile(path.join(themesDir, "themes.xml"), `
270
+ <resources>
271
+ <style name="Theme.MyApp" parent="Theme.AppCompat.Light.NoActionBar">
272
+ <item name="android:statusBarColor">@android:color/transparent</item>
273
+ <item name="android:windowLightStatusBar">false</item>
274
+ <item name="android:navigationBarColor">@android:color/transparent</item>
275
+ </style>
276
+ </resources>
277
+ `);
278
+ const devMode = resolveDevMode(config);
279
+ const hasDevLauncher = devMode === "embedded";
280
+ const manifestActivities = hasDevLauncher
281
+ ? `
282
+ <activity android:name=".MainActivity" android:exported="true">
283
+ <intent-filter>
284
+ <action android:name="android.intent.action.MAIN" />
285
+ <category android:name="android.intent.category.LAUNCHER" />
286
+ </intent-filter>
287
+ </activity>
288
+ <activity android:name=".ProjectActivity" android:exported="false" android:taskAffinity="" android:launchMode="singleTask" android:documentLaunchMode="always" />
289
+ `
290
+ : `
291
+ <activity android:name=".MainActivity" android:exported="true">
292
+ <intent-filter>
293
+ <action android:name="android.intent.action.MAIN" />
294
+ <category android:name="android.intent.category.LAUNCHER" />
295
+ </intent-filter>
296
+ </activity>
297
+ `;
298
+ const manifestPermissions = hasDevLauncher
299
+ ? ` <uses-permission android:name="android.permission.INTERNET" />
300
+ <uses-permission android:name="android.permission.CAMERA" />`
301
+ : ` <uses-permission android:name="android.permission.INTERNET" />`;
302
+ const iconPaths = resolveIconPaths(process.cwd(), config);
303
+ const manifestIconAttrs = iconPaths
304
+ ? " android:icon=\"@mipmap/ic_launcher\"\n android:roundIcon=\"@mipmap/ic_launcher\"\n"
305
+ : "";
306
+ writeFile(path.join(mainDir, "AndroidManifest.xml"), `
307
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
308
+ ${manifestPermissions}
309
+ <application
310
+ android:name=".App"
311
+ android:label="${appName}"
312
+ ${manifestIconAttrs} android:usesCleartextTraffic="true"
313
+ android:theme="@style/Theme.MyApp">${manifestActivities}
314
+ </application>
315
+ </manifest>
316
+ `);
317
+ // Placeholder GeneratedLynxExtensions.kt
318
+ writeFile(path.join(kotlinGeneratedDir, "GeneratedLynxExtensions.kt"), `
319
+ package ${packageName}.generated
320
+
321
+ import android.content.Context
322
+ import com.lynx.tasm.LynxEnv
323
+
324
+ /**
325
+ * This file is generated by the Tamer4Lynx autolinker.
326
+ * Do not edit this file manually.
327
+ */
328
+ object GeneratedLynxExtensions {
329
+ fun register(context: Context) {
330
+ // This will be populated by the autolinker.
331
+ }
332
+ }
333
+ `);
334
+ const devServer = config.devServer
335
+ ? {
336
+ host: config.devServer.host ?? "10.0.2.2",
337
+ port: config.devServer.port ?? config.devServer.httpPort ?? 3000,
338
+ }
339
+ : undefined;
340
+ const resolved = resolveHostPaths(process.cwd());
341
+ const vars = { packageName, appName, devMode, devServer, projectRoot: resolved.lynxProjectDir };
342
+ const [applicationSource, templateProviderSource] = await Promise.all([
343
+ fetchAndPatchApplication(vars),
344
+ fetchAndPatchTemplateProvider(vars),
345
+ ]);
346
+ writeFile(path.join(javaDir, "App.java"), applicationSource);
347
+ writeFile(path.join(javaDir, "TemplateProvider.java"), templateProviderSource);
348
+ writeFile(path.join(kotlinDir, "MainActivity.kt"), getStandaloneMainActivity(vars));
349
+ if (hasDevLauncher) {
350
+ writeFile(path.join(kotlinDir, "ProjectActivity.kt"), getProjectActivity(vars));
351
+ }
352
+ const coreDir = path.join(kotlinDir, "core");
353
+ writeFile(path.join(coreDir, "LynxExplorerInput.kt"), getLynxExplorerInputSource(packageName));
354
+ const devClientManagerSource = getDevClientManager(vars);
355
+ if (devClientManagerSource) {
356
+ writeFile(path.join(kotlinDir, "DevClientManager.kt"), devClientManagerSource);
357
+ writeFile(path.join(kotlinDir, "DevServerPrefs.kt"), getDevServerPrefs(vars));
358
+ }
359
+ if (iconPaths) {
360
+ const resDir = path.join(mainDir, "res");
361
+ if (iconPaths.android) {
362
+ const src = iconPaths.android;
363
+ const entries = fs.readdirSync(src, { withFileTypes: true });
364
+ for (const e of entries) {
365
+ const dest = path.join(resDir, e.name);
366
+ if (e.isDirectory()) {
367
+ fs.cpSync(path.join(src, e.name), dest, { recursive: true });
368
+ }
369
+ else {
370
+ fs.mkdirSync(resDir, { recursive: true });
371
+ fs.copyFileSync(path.join(src, e.name), dest);
372
+ }
373
+ }
374
+ console.log("✅ Copied Android icon from tamer.config.json icon.android");
375
+ }
376
+ else if (iconPaths.source) {
377
+ const mipmapDensities = ["mdpi", "hdpi", "xhdpi", "xxhdpi", "xxxhdpi"];
378
+ for (const d of mipmapDensities) {
379
+ const dir = path.join(resDir, `mipmap-${d}`);
380
+ fs.mkdirSync(dir, { recursive: true });
381
+ fs.copyFileSync(iconPaths.source, path.join(dir, "ic_launcher.png"));
382
+ }
383
+ console.log("✅ Copied app icon from tamer.config.json icon.source");
384
+ }
385
+ }
386
+ // Create an empty assets directory so the project builds correctly
387
+ fs.mkdirSync(assetsDir, { recursive: true });
388
+ fs.writeFileSync(path.join(assetsDir, ".gitkeep"), "");
389
+ console.log(`✅ Android Kotlin project created at ${rootDir}`);
390
+ async function finalizeProjectSetup() {
391
+ if (androidSdk) {
392
+ try {
393
+ const sdkDirContent = `sdk.dir=${androidSdk.replace(/\\/g, "/")}`;
394
+ writeFile(path.join(rootDir, "local.properties"), sdkDirContent);
395
+ console.log("📦 Created local.properties from tamer.config.json.");
396
+ }
397
+ catch (err) {
398
+ console.error(`❌ Failed to create local.properties: ${err.message}`);
399
+ }
400
+ }
401
+ else {
402
+ const localPropsPath = path.join(process.cwd(), "local.properties");
403
+ if (fs.existsSync(localPropsPath)) {
404
+ try {
405
+ fs.copyFileSync(localPropsPath, path.join(rootDir, "local.properties"));
406
+ console.log("📦 Copied existing local.properties to the android project.");
407
+ }
408
+ catch (err) {
409
+ console.error("❌ Failed to copy local.properties:", err);
410
+ }
411
+ }
412
+ else {
413
+ console.warn("⚠️ `android.sdk` not found in tamer.config.json. You may need to create local.properties manually.");
414
+ }
415
+ }
416
+ // The Gradle wrapper setup logic is now handled by the imported function
417
+ await setupGradleWrapper(rootDir, gradleVersion);
418
+ }
419
+ await finalizeProjectSetup();
420
+ if (target === "dev-app")
421
+ process.chdir(origCwd);
422
+ };
423
+ export default create;