@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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Jordan Miller
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,307 @@
1
+ # Tamer4Lynx
2
+
3
+ A CLI tool for creating, linking, and bundling Lynx native extensions. Aligned with the [Lynx Autolink RFC](https://github.com/lynx-family/lynx/discussions/2653).
4
+
5
+ Inspired by [Expo](https://expo.dev) and [Expo Go](https://expo.dev/go).
6
+
7
+ **Standalone app generation** follows patterns from [Lynx Explorer](https://github.com/lynx-family/lynx/tree/develop/explorer), patched for Maven-based standalone builds (no monorepo required). See [lynx#695](https://github.com/lynx-family/lynx/issues/695) for context.
8
+
9
+ ## Installation
10
+
11
+ All Tamer packages are published under the `@tamer4lynx` scope on npm. Install the CLI globally:
12
+
13
+ ```bash
14
+ npm i -g @tamer4lynx/cli
15
+ ```
16
+
17
+ With pnpm or Bun:
18
+
19
+ ```bash
20
+ pnpm add -g @tamer4lynx/cli
21
+ bun add -g @tamer4lynx/cli
22
+ ```
23
+
24
+ Or from GitHub (run `npm uninstall -g @tamer4lynx/cli` first if switching):
25
+
26
+ ```bash
27
+ npm i -g tamer4lynx/tamer4lynx
28
+ ```
29
+
30
+ Then run from your project directory (where `tamer.config.json` lives):
31
+
32
+ ```bash
33
+ t4l init
34
+ t4l start
35
+ t4l android build --install
36
+ ```
37
+
38
+ Tamer4Lynx uses configuration files to manage your project.
39
+
40
+ ### 1. Host Application (`tamer.config.json`)
41
+
42
+ Create this file in the root of your repository to define your main application's properties.
43
+
44
+ #### Quick Init (Recommended)
45
+
46
+ You can interactively generate your config file using:
47
+
48
+ ```bash
49
+ t4l init
50
+ ```
51
+ Or simply run:
52
+ ```bash
53
+ t4l
54
+ ```
55
+ If no arguments are provided, the CLI will launch the interactive setup.
56
+
57
+ The script will prompt for:
58
+ - Android app name
59
+ - Android package name
60
+ - Android SDK path (supports `~`, `$ENV_VAR`)
61
+ - Whether to use the same name and bundle ID for iOS as Android
62
+ - iOS app name and bundle ID (if not using Android values)
63
+ - Lynx project path (relative to project root, optional)
64
+
65
+ Example output:
66
+ ```json
67
+ {
68
+ "android": {
69
+ "appName": "MyApp",
70
+ "packageName": "com.example.myapp",
71
+ "sdk": "~/Library/Android/sdk"
72
+ },
73
+ "ios": {
74
+ "appName": "MyApp",
75
+ "bundleId": "com.example.MyApp"
76
+ },
77
+ "lynxProject": "packages/example",
78
+ "paths": {
79
+ "androidDir": "android",
80
+ "iosDir": "ios",
81
+ "lynxBundleRoot": "dist",
82
+ "lynxBundleFile": "main.lynx.bundle"
83
+ }
84
+ }
85
+ ```
86
+
87
+ **Dynamic Lynx discovery:** If `lynxProject` is omitted, Tamer4Lynx auto-discovers the Lynx project by scanning workspaces for `lynx.config.ts` or `@lynx-js/rspeedy`. Bundle output path is read from `lynx.config.ts` `output.distPath.root` when present.
88
+
89
+ ---
90
+
91
+ ### Asset Bundling
92
+
93
+ **Note:** To ensure assets like images are bundled into the app, import them with the `?inline` suffix. For example:
94
+ ```js
95
+ import logo from './assets/lynx-logo.png?inline';
96
+ ```
97
+ This will include the asset directly in your app bundle.
98
+
99
+ ---
100
+
101
+ ### Show Help & Version
102
+
103
+ ```bash
104
+ t4l --help
105
+ t4l --version
106
+ ```
107
+
108
+ ### Documentation
109
+
110
+ The docs site lives at `packages/docs`. From the repo root:
111
+
112
+ ```bash
113
+ cd packages/docs && bun run dev
114
+ ```
115
+
116
+ Build: `bun run build`. Output: `doc_build/`.
117
+
118
+ ---
119
+
120
+ ### **Extension Commands (RFC-compliant)**
121
+
122
+ #### Create a Lynx Extension
123
+
124
+ ```bash
125
+ t4l create
126
+ ```
127
+
128
+ Scaffolds a new extension project with `lynx.ext.json`, Android/iOS native code, and optional Element/Service support.
129
+
130
+ #### Code Generation
131
+
132
+ ```bash
133
+ t4l codegen
134
+ ```
135
+
136
+ Run from an extension package root to generate code from `@lynxmodule` declarations in `.d.ts` files.
137
+
138
+ ---
139
+
140
+ ### **Android Commands**
141
+
142
+ | Command | Flags | Description |
143
+ |---------|-------|-------------|
144
+ | `t4l android create` | `-t, --target <host\|dev-app>` | Create Android project. Default: `host`. |
145
+ | `t4l android link` | — | Link native modules to Gradle. Build runs this automatically. |
146
+ | `t4l android bundle` | `-t, --target`, `-d, --debug`, `-r, --release` | Build Lynx bundle and copy to assets. Runs autolink first. |
147
+ | `t4l android build` | `-i, --install`, `-t, --target`, `-e, --embeddable`, `-d, --debug`, `-r, --release` | Build APK. `--install` deploys to device. `--embeddable` outputs AAR to `embeddable/` for existing apps. |
148
+ | `t4l android sync` | — | Sync dev client files from tamer.config.json. |
149
+ | `t4l android inject` | `-f, --force` | Inject tamer-host templates. `--force` overwrites existing files. |
150
+
151
+ ### **iOS Commands**
152
+
153
+ | Command | Flags | Description |
154
+ |---------|-------|-------------|
155
+ | `t4l ios create` | — | Create iOS project. |
156
+ | `t4l ios link` | — | Link native modules to Podfile. Build runs this automatically. |
157
+ | `t4l ios bundle` | `-t, --target`, `-d, --debug`, `-r, --release` | Build Lynx bundle and copy to iOS project. |
158
+ | `t4l ios build` | `-t, --target`, `-e, --embeddable`, `-i, --install`, `-d, --debug`, `-r, --release` | Build iOS app. `--install` deploys to simulator. |
159
+ | `t4l ios inject` | `-f, --force` | Inject tamer-host templates. `--force` overwrites existing files. |
160
+
161
+ ### **Unified Build** (`t4l build`)
162
+
163
+ | Flag | Short | Default | Description |
164
+ |------|-------|---------|-------------|
165
+ | `--platform` | `-p` | `all` | `android`, `ios`, or `all` |
166
+ | `--target` | `-t` | `dev-app` | `host` (production) or `dev-app` (QR scan, HMR) |
167
+ | `--embeddable` | `-e` | — | Output to `embeddable/`: **AAR** (Android) + **CocoaPod** (iOS). Use with `--release`. |
168
+ | `--debug` | `-d` | default | Debug build |
169
+ | `--release` | `-r` | — | Release build (optimized) |
170
+ | `--install` | `-i` | — | Install to device/simulator after build |
171
+
172
+ ### **Other Commands**
173
+
174
+ | Command | Flags | Description |
175
+ |---------|-------|-------------|
176
+ | `t4l add [packages...]` | — | Add @tamer4lynx packages. Future: version tracking (Expo-style). |
177
+ | `t4l add-core` | — | Add core packages (app-shell, screen, router, insets, transports, text-input, system-ui, icons). |
178
+ | `t4l start` | `-v, --verbose` | Dev server with HMR. `--verbose` shows native + JS logs. |
179
+ | `t4l link` | `-i, --ios`, `-a, --android`, `-s, --silent` | Link modules. `--ios`/`--android` limit to one platform. `--silent` for CI/postinstall. |
180
+ | `t4l autolink-toggle` | — | Toggle `autolink` in tamer.config.json (postinstall linking). |
181
+
182
+ See [Commands Reference](packages/docs/docs/commands.md) for full flag details.
183
+
184
+ ---
185
+
186
+ ## Automatic Linking on Install
187
+
188
+ For the best experience, add a `postinstall` script to your project's `package.json`:
189
+
190
+ ```json
191
+ "scripts": {
192
+ "postinstall": "t4l link --silent"
193
+ }
194
+ ```
195
+
196
+ ---
197
+
198
+ ## Configuration
199
+
200
+ - **Project:** `tamer.config.json` — host app (Android/iOS identity, paths, dev server). Create with `t4l init`.
201
+ - **Component:** `tamer.config.ts` — Rsbuild plugins (routing, etc.). Used by tamer-plugin.
202
+ - **Extension:** `lynx.ext.json` — native module registration per platform. Lives in each extension package.
203
+
204
+ See [Configuration Reference](packages/docs/docs/docs/configuration.md) for field-by-field docs. lynx.ext.json follows the [Lynx Autolink RFC](https://github.com/lynx-family/lynx/discussions/2653); [contribute to the RFC](https://github.com/lynx-family/lynx/discussions/2653) if you want to help improve it.
205
+
206
+ ## Extension Configuration
207
+
208
+ Extensions are discovered via **lynx.ext.json** (RFC standard) or **tamer.json** (legacy).
209
+
210
+ ### lynx.ext.json (recommended)
211
+
212
+ ```json
213
+ {
214
+ "platforms": {
215
+ "android": {
216
+ "packageName": "com.example.mymodule",
217
+ "moduleClassName": "com.example.mymodule.MyModule",
218
+ "sourceDir": "android"
219
+ },
220
+ "ios": {
221
+ "podspecPath": "ios/mymodule",
222
+ "moduleClassName": "MyModule"
223
+ },
224
+ "web": {}
225
+ }
226
+ }
227
+ ```
228
+
229
+ ### tamer.json (legacy, still supported)
230
+
231
+ ```json
232
+ {
233
+ "android": {
234
+ "moduleClassName": "com.my-awesome-module.MyAwesomeModule",
235
+ "sourceDir": "android"
236
+ },
237
+ "ios": {
238
+ "podspecPath": "ios/mymodule",
239
+ "moduleClassName": "MyModule"
240
+ }
241
+ }
242
+ ```
243
+
244
+ ---
245
+
246
+ ## Roadmap
247
+
248
+ * [x] Fix iOS linking
249
+ * [x] lynx.ext.json support (RFC #2653)
250
+ * [x] create-lynx-extension command
251
+ * [x] codegen command
252
+ * [ ] Full codegen (Android Spec, iOS Spec, web)
253
+
254
+ ---
255
+
256
+ ## Native Module References
257
+
258
+ Install from npm and run `t4l link` after adding to your app:
259
+
260
+ | Package | Install | Description |
261
+ |---------|---------|-------------|
262
+ | [@tamer4lynx/jiggle](https://www.npmjs.com/package/@tamer4lynx/jiggle) | `npm i @tamer4lynx/jiggle` | Vibration/haptic |
263
+ | [@tamer4lynx/lynxwebsockets](https://www.npmjs.com/package/@tamer4lynx/lynxwebsockets) | `npm i @tamer4lynx/lynxwebsockets` | WebSocket native bridge |
264
+ | [@tamer4lynx/tamer-host](https://www.npmjs.com/package/@tamer4lynx/tamer-host) | `npm i @tamer4lynx/tamer-host` | Production Lynx host templates |
265
+ | [tamer-dev-app](https://github.com/tamer4lynx/tamer-dev-app) | workspace | Dev app (QR scan, HMR) |
266
+ | [@tamer4lynx/tamer-dev-client](https://www.npmjs.com/package/@tamer4lynx/tamer-dev-client) | `npm i @tamer4lynx/tamer-dev-client` | Dev launcher UI, discovery |
267
+ | [@tamer4lynx/tamer-plugin](https://www.npmjs.com/package/@tamer4lynx/tamer-plugin) | `npm i @tamer4lynx/tamer-plugin` | Rsbuild plugin middleman |
268
+ | [@tamer4lynx/tamer-router](https://www.npmjs.com/package/@tamer4lynx/tamer-router) | `npm i @tamer4lynx/tamer-router` | File-based routing, Stack/Tabs |
269
+ | [@tamer4lynx/tamer-icons](https://www.npmjs.com/package/@tamer4lynx/tamer-icons) | `npm i @tamer4lynx/tamer-icons` | Icon fonts (Material, Font Awesome) |
270
+ | [@tamer4lynx/tamer-insets](https://www.npmjs.com/package/@tamer4lynx/tamer-insets) | `npm i @tamer4lynx/tamer-insets` | System insets, keyboard state |
271
+ | [@tamer4lynx/tamer-system-ui](https://www.npmjs.com/package/@tamer4lynx/tamer-system-ui) | `npm i @tamer4lynx/tamer-system-ui` | Status bar, navigation bar |
272
+ | [@tamer4lynx/tamer-app-shell](https://www.npmjs.com/package/@tamer4lynx/tamer-app-shell) | `npm i @tamer4lynx/tamer-app-shell` | AppBar, TabBar, Content layout |
273
+ | [@tamer4lynx/tamer-text-input](https://www.npmjs.com/package/@tamer4lynx/tamer-text-input) | `npm i @tamer4lynx/tamer-text-input` | React TextInput |
274
+ | [@tamer4lynx/tamer-auth](https://www.npmjs.com/package/@tamer4lynx/tamer-auth) | `npm i @tamer4lynx/tamer-auth` | OAuth 2.0 / OIDC |
275
+ | [@tamer4lynx/tamer-biometric](https://www.npmjs.com/package/@tamer4lynx/tamer-biometric) | `npm i @tamer4lynx/tamer-biometric` | Fingerprint, Face ID |
276
+ | [@tamer4lynx/tamer-display-browser](https://www.npmjs.com/package/@tamer4lynx/tamer-display-browser) | `npm i @tamer4lynx/tamer-display-browser` | Open URLs in system browser |
277
+ | [@tamer4lynx/tamer-linking](https://www.npmjs.com/package/@tamer4lynx/tamer-linking) | `npm i @tamer4lynx/tamer-linking` | Deep linking |
278
+ | [@tamer4lynx/tamer-screen](https://www.npmjs.com/package/@tamer4lynx/tamer-screen) | `npm i @tamer4lynx/tamer-screen` | SafeArea, Screen, AvoidKeyboard |
279
+ | [@tamer4lynx/tamer-secure-store](https://www.npmjs.com/package/@tamer4lynx/tamer-secure-store) | `npm i @tamer4lynx/tamer-secure-store` | Secure key-value storage |
280
+ | [@tamer4lynx/tamer-transports](https://www.npmjs.com/package/@tamer4lynx/tamer-transports) | `npm i @tamer4lynx/tamer-transports` | Fetch, WebSocket, EventSource polyfills |
281
+
282
+ The iOS autolinking feature runs `pod install` automatically.
283
+
284
+ ---
285
+
286
+ ## Examples
287
+
288
+ - [Example LynxJS project with Jiggle and Lynx-Websockets](https://github.com/tamer4lynx/tamer4lynx/tree/main/packages/example)
289
+ ## Contributing
290
+
291
+ Contributions are welcome! To develop on Tamer4Lynx:
292
+
293
+ ```bash
294
+ git clone https://github.com/tamer4lynx/tamer4lynx.git
295
+ cd tamer4lynx
296
+ npm install
297
+ ```
298
+
299
+ Please feel free to submit issues or pull requests.
300
+
301
+ ---
302
+
303
+ ## Support
304
+
305
+ If you find this tool helpful, consider supporting its development.
306
+
307
+ <a href="https://ko-fi.com/nanofuxion"> <img src="https://ko-fi.com/img/githubbutton_sm.svg" alt="Support me on Ko-fi"> </a>
@@ -0,0 +1,272 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { loadExtensionConfig, hasExtensionConfig } from '../common/config';
4
+ import { resolveHostPaths } from '../common/hostConfig';
5
+ import { getLynxExplorerInputSource, getAppBarElementSource, } from './coreElements';
6
+ const autolink = () => {
7
+ let resolved;
8
+ try {
9
+ resolved = resolveHostPaths();
10
+ if (!resolved.config.android?.packageName) {
11
+ throw new Error('"android.packageName" must be defined in tamer.config.json');
12
+ }
13
+ }
14
+ catch (error) {
15
+ console.error(`āŒ Error loading configuration: ${error.message}`);
16
+ process.exit(1);
17
+ }
18
+ const { androidDir: appAndroidPath, config } = resolved;
19
+ const packageName = config.android.packageName;
20
+ const projectRoot = resolved.projectRoot;
21
+ let nodeModulesPath = path.join(projectRoot, 'node_modules');
22
+ const workspaceRoot = path.join(projectRoot, '..', '..');
23
+ const rootNodeModules = path.join(workspaceRoot, 'node_modules');
24
+ if (fs.existsSync(path.join(workspaceRoot, 'package.json')) && fs.existsSync(rootNodeModules) && path.basename(path.dirname(projectRoot)) === 'packages') {
25
+ nodeModulesPath = rootNodeModules;
26
+ }
27
+ else if (!fs.existsSync(nodeModulesPath)) {
28
+ const altRoot = path.join(projectRoot, '..', '..');
29
+ const altNodeModules = path.join(altRoot, 'node_modules');
30
+ if (fs.existsSync(path.join(altRoot, 'package.json')) && fs.existsSync(altNodeModules)) {
31
+ nodeModulesPath = altNodeModules;
32
+ }
33
+ }
34
+ // --- Core Logic ---
35
+ /**
36
+ * Replaces the content of a file between specified start and end markers.
37
+ * @param filePath The path to the file to update.
38
+ * @param newContent The new content to insert between the markers.
39
+ * @param startMarker The starting delimiter.
40
+ * @param endMarker The ending delimiter.
41
+ */
42
+ function updateGeneratedSection(filePath, newContent, startMarker, endMarker) {
43
+ if (!fs.existsSync(filePath)) {
44
+ console.warn(`āš ļø File not found, skipping update: ${filePath}`);
45
+ return;
46
+ }
47
+ let fileContent = fs.readFileSync(filePath, 'utf8');
48
+ // Escape special characters in markers for RegExp
49
+ const escapedStartMarker = startMarker.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
50
+ const escapedEndMarker = endMarker.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
51
+ const regex = new RegExp(`${escapedStartMarker}[\\s\\S]*?${escapedEndMarker}`, 'g');
52
+ const replacementBlock = `${startMarker}\n${newContent}\n${endMarker}`;
53
+ if (regex.test(fileContent)) {
54
+ fileContent = fileContent.replace(regex, replacementBlock);
55
+ }
56
+ else {
57
+ console.warn(`āš ļø Could not find autolink markers in ${path.basename(filePath)}. Appending to the end of the file.`);
58
+ fileContent += `\n${replacementBlock}\n`;
59
+ }
60
+ fs.writeFileSync(filePath, fileContent);
61
+ console.log(`āœ… Updated autolinked section in ${path.basename(filePath)}`);
62
+ }
63
+ function findExtensionPackages() {
64
+ const packages = [];
65
+ if (!fs.existsSync(nodeModulesPath)) {
66
+ console.warn('āš ļø node_modules directory not found. Skipping autolinking.');
67
+ return [];
68
+ }
69
+ const packageDirs = fs.readdirSync(nodeModulesPath);
70
+ for (const dirName of packageDirs) {
71
+ const fullPath = path.join(nodeModulesPath, dirName);
72
+ const checkPackage = (name, packagePath) => {
73
+ if (!hasExtensionConfig(packagePath))
74
+ return;
75
+ const config = loadExtensionConfig(packagePath);
76
+ if (config?.android) {
77
+ packages.push({ name, config, packagePath });
78
+ }
79
+ };
80
+ if (dirName.startsWith('@')) {
81
+ try {
82
+ const scopedDirs = fs.readdirSync(fullPath);
83
+ for (const scopedDirName of scopedDirs) {
84
+ const scopedPackagePath = path.join(fullPath, scopedDirName);
85
+ checkPackage(`${dirName}/${scopedDirName}`, scopedPackagePath);
86
+ }
87
+ }
88
+ catch (e) {
89
+ console.warn(`āš ļø Could not read scoped package directory ${fullPath}: ${e.message}`);
90
+ }
91
+ }
92
+ else {
93
+ checkPackage(dirName, fullPath);
94
+ }
95
+ }
96
+ return packages;
97
+ }
98
+ /**
99
+ * Generates the Gradle settings script content and updates settings.gradle.kts.
100
+ * @param packages The list of discovered native packages.
101
+ */
102
+ function updateSettingsGradle(packages) {
103
+ const settingsFilePath = path.join(appAndroidPath, 'settings.gradle.kts');
104
+ let scriptContent = `// This section is automatically generated by Tamer4Lynx.\n// Manual edits will be overwritten.`;
105
+ const androidPackages = packages.filter(p => p.config.android);
106
+ if (androidPackages.length > 0) {
107
+ androidPackages.forEach(pkg => {
108
+ // Sanitize package name for Gradle: @org/name -> org_name
109
+ const gradleProjectName = pkg.name.replace(/^@/, '').replace(/\//g, '_');
110
+ const sourceDir = pkg.config.android?.sourceDir || 'android';
111
+ // Use forward slashes for Gradle paths, even on Windows
112
+ const projectPath = path.join(pkg.packagePath, sourceDir).replace(/\\/g, '/');
113
+ const relativePath = path.relative(appAndroidPath, projectPath).replace(/\\/g, '/');
114
+ scriptContent += `\ninclude(":${gradleProjectName}")`;
115
+ scriptContent += `\nproject(":${gradleProjectName}").projectDir = file("${relativePath}")`;
116
+ });
117
+ }
118
+ else {
119
+ scriptContent += `\nprintln("No native modules found by Tamer4Lynx autolinker.")`;
120
+ }
121
+ updateGeneratedSection(settingsFilePath, scriptContent.trim(), '// GENERATED AUTOLINK START', '// GENERATED AUTOLINK END');
122
+ }
123
+ /**
124
+ * Generates the app-level dependencies and updates app/build.gradle.kts.
125
+ * @param packages The list of discovered native packages.
126
+ */
127
+ function updateAppBuildGradle(packages) {
128
+ const appBuildGradlePath = path.join(appAndroidPath, 'app', 'build.gradle.kts');
129
+ const androidPackages = packages.filter(p => p.config.android);
130
+ const implementationLines = androidPackages
131
+ .map(p => {
132
+ // Sanitize package name for Gradle: @org/name -> org_name
133
+ const gradleProjectName = p.name.replace(/^@/, '').replace(/\//g, '_');
134
+ return ` implementation(project(":${gradleProjectName}"))`;
135
+ })
136
+ .join('\n');
137
+ const scriptContent = `// This section is automatically generated by Tamer4Lynx.\n // Manual edits will be overwritten.\n${implementationLines || ' // No native dependencies found to link.'}`;
138
+ updateGeneratedSection(appBuildGradlePath, scriptContent, '// GENERATED AUTOLINK DEPENDENCIES START', '// GENERATED AUTOLINK DEPENDENCIES END');
139
+ }
140
+ /**
141
+ * Generates a self-contained GeneratedLynxExtensions.kt file with all necessary imports and registrations.
142
+ * @param packages The list of discovered native packages.
143
+ * @param projectPackage The package name of the main Android app, from tamer.config.json.
144
+ */
145
+ function generateKotlinExtensionsFile(packages, projectPackage) {
146
+ const packagePath = projectPackage.replace(/\./g, '/');
147
+ const generatedDir = path.join(appAndroidPath, 'app', 'src', 'main', 'kotlin', packagePath, 'generated');
148
+ const kotlinExtensionsPath = path.join(generatedDir, 'GeneratedLynxExtensions.kt');
149
+ const modulePackages = packages.filter(p => p.config.android?.moduleClassName);
150
+ const elementPackages = packages.filter(p => p.config.android?.elements && Object.keys(p.config.android.elements).length > 0)
151
+ .map(p => ({
152
+ ...p,
153
+ config: {
154
+ ...p.config,
155
+ android: {
156
+ ...p.config.android,
157
+ elements: Object.fromEntries(Object.entries(p.config.android.elements).filter(([tag]) => tag !== 'explorer-input' && tag !== 'input')),
158
+ },
159
+ },
160
+ }))
161
+ .filter(p => Object.keys(p.config.android?.elements ?? {}).length > 0);
162
+ const coreElementImport = `import ${projectPackage}.core.LynxExplorerInput
163
+ import ${projectPackage}.core.AppBarElement`;
164
+ const coreElementRegistration = ` LynxEnv.inst().addBehavior(object : com.lynx.tasm.behavior.Behavior("input") {
165
+ override fun createUI(context: com.lynx.tasm.behavior.LynxContext): com.lynx.tasm.behavior.ui.LynxUI<*> {
166
+ return LynxExplorerInput(context)
167
+ }
168
+ })
169
+ LynxEnv.inst().addBehavior(object : com.lynx.tasm.behavior.Behavior("explorer-input") {
170
+ override fun createUI(context: com.lynx.tasm.behavior.LynxContext): com.lynx.tasm.behavior.ui.LynxUI<*> {
171
+ return LynxExplorerInput(context)
172
+ }
173
+ })
174
+ LynxEnv.inst().addBehavior(object : com.lynx.tasm.behavior.Behavior("app-bar") {
175
+ override fun createUI(context: com.lynx.tasm.behavior.LynxContext): com.lynx.tasm.behavior.ui.LynxUI<*> {
176
+ return AppBarElement(context)
177
+ }
178
+ })`;
179
+ const moduleImports = modulePackages
180
+ .map(p => `import ${p.config.android.moduleClassName}`)
181
+ .join('\n');
182
+ const elementImports = elementPackages.flatMap(p => Object.values(p.config.android.elements).map(cls => `import ${cls}`)).filter((v, i, a) => a.indexOf(v) === i).join('\n');
183
+ const moduleRegistrations = modulePackages
184
+ .map(p => {
185
+ const fullClassName = p.config.android.moduleClassName;
186
+ const simpleClassName = fullClassName.split('.').pop();
187
+ return ` LynxEnv.inst().registerModule("${simpleClassName}", ${simpleClassName}::class.java)`;
188
+ })
189
+ .join('\n');
190
+ const behaviorRegistrations = elementPackages.flatMap(p => Object.entries(p.config.android.elements).map(([tag, fullClassName]) => {
191
+ const simpleClassName = fullClassName.split('.').pop();
192
+ return ` LynxEnv.inst().addBehavior(object : com.lynx.tasm.behavior.Behavior("${tag}") {
193
+ override fun createUI(context: com.lynx.tasm.behavior.LynxContext): com.lynx.tasm.behavior.ui.LynxUI<*> {
194
+ return ${simpleClassName}(context)
195
+ }
196
+ })`;
197
+ })).join('\n');
198
+ const allRegistrations = [moduleRegistrations, behaviorRegistrations, coreElementRegistration].filter(Boolean).join('\n');
199
+ const kotlinContent = `package ${projectPackage}.generated
200
+
201
+ import android.content.Context
202
+ import com.lynx.tasm.LynxEnv
203
+ ${coreElementImport}
204
+ ${moduleImports}
205
+ ${elementImports}
206
+
207
+ /**
208
+ * This file is generated by the Tamer4Lynx autolinker.
209
+ * Do not edit this file manually.
210
+ */
211
+ object GeneratedLynxExtensions {
212
+ fun register(context: Context) {
213
+ ${allRegistrations}
214
+ }
215
+ }
216
+ `;
217
+ // Ensure the `generated` directory exists before writing the file
218
+ fs.mkdirSync(generatedDir, { recursive: true });
219
+ fs.writeFileSync(kotlinExtensionsPath, kotlinContent.trimStart());
220
+ console.log(`āœ… Generated Kotlin extensions at ${kotlinExtensionsPath}`);
221
+ }
222
+ // --- Main Execution ---
223
+ function run() {
224
+ console.log('šŸ”Ž Finding Lynx extension packages (lynx.ext.json / tamer.json)...');
225
+ const packages = findExtensionPackages();
226
+ if (packages.length > 0) {
227
+ console.log(`Found ${packages.length} package(s): ${packages.map(p => p.name).join(', ')}`);
228
+ }
229
+ else {
230
+ console.log('ā„¹ļø No Tamer4Lynx native packages found.');
231
+ }
232
+ updateSettingsGradle(packages);
233
+ updateAppBuildGradle(packages);
234
+ const coreDir = path.join(appAndroidPath, 'app', 'src', 'main', 'kotlin', packageName.replace(/\./g, '/'), 'core');
235
+ fs.mkdirSync(coreDir, { recursive: true });
236
+ fs.writeFileSync(path.join(coreDir, 'LynxExplorerInput.kt'), getLynxExplorerInputSource(packageName));
237
+ fs.writeFileSync(path.join(coreDir, 'AppBarElement.kt'), getAppBarElementSource(packageName));
238
+ console.log('āœ… Synced core elements (input, explorer-input, app-bar)');
239
+ generateKotlinExtensionsFile(packages, packageName);
240
+ syncManifestPermissions(packages);
241
+ console.log('✨ Autolinking complete.');
242
+ }
243
+ function syncManifestPermissions(packages) {
244
+ const manifestPath = path.join(appAndroidPath, 'app', 'src', 'main', 'AndroidManifest.xml');
245
+ if (!fs.existsSync(manifestPath))
246
+ return;
247
+ const allPermissions = new Set();
248
+ for (const pkg of packages) {
249
+ const perms = pkg.config.android?.permissions;
250
+ if (Array.isArray(perms)) {
251
+ for (const p of perms) {
252
+ const name = p.startsWith('android.permission.') ? p : `android.permission.${p}`;
253
+ allPermissions.add(name);
254
+ }
255
+ }
256
+ }
257
+ if (allPermissions.size === 0)
258
+ return;
259
+ let manifest = fs.readFileSync(manifestPath, 'utf8');
260
+ const existingMatch = [...manifest.matchAll(/<uses-permission android:name="(android\.permission\.\w+)"\s*\/>/g)];
261
+ const existing = new Set(existingMatch.map((m) => m[1]));
262
+ const toAdd = [...allPermissions].filter((p) => !existing.has(p));
263
+ if (toAdd.length === 0)
264
+ return;
265
+ const newLines = toAdd.map((p) => ` <uses-permission android:name="${p}" />`).join('\n');
266
+ manifest = manifest.replace(/(\s*)(<application)/, `${newLines}\n$1$2`);
267
+ fs.writeFileSync(manifestPath, manifest);
268
+ console.log(`āœ… Synced manifest permissions: ${toAdd.map((p) => p.split('.').pop()).join(', ')}`);
269
+ }
270
+ run();
271
+ };
272
+ export default autolink;
@@ -0,0 +1,36 @@
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_bundle from './bundle';
6
+ function findRepoRoot(start) {
7
+ let dir = path.resolve(start);
8
+ const root = path.parse(dir).root;
9
+ while (dir !== root) {
10
+ const pkgPath = path.join(dir, 'package.json');
11
+ if (fs.existsSync(pkgPath)) {
12
+ try {
13
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
14
+ if (pkg.workspaces)
15
+ return dir;
16
+ }
17
+ catch { }
18
+ }
19
+ dir = path.dirname(dir);
20
+ }
21
+ return start;
22
+ }
23
+ async function buildApk(opts = {}) {
24
+ const target = opts.target ?? 'host';
25
+ const resolved = target === 'dev-app'
26
+ ? resolveDevAppPaths(findRepoRoot(process.cwd()))
27
+ : resolveHostPaths();
28
+ await android_bundle({ target });
29
+ const androidDir = resolved.androidDir;
30
+ const gradlew = path.join(androidDir, process.platform === 'win32' ? 'gradlew.bat' : 'gradlew');
31
+ const task = opts.install ? 'installDebug' : 'assembleDebug';
32
+ console.log(`\nšŸ”Ø Building ${opts.install ? 'and installing' : ''} APK...`);
33
+ execSync(`"${gradlew}" ${task}`, { stdio: 'inherit', cwd: androidDir });
34
+ console.log(`āœ… APK ${opts.install ? 'installed' : 'built'} successfully.`);
35
+ }
36
+ export default buildApk;