@lynxship/cli 0.1.8 → 0.1.10

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 (51) hide show
  1. package/README.md +175 -19
  2. package/dist/android-toolchain.d.ts +23 -0
  3. package/dist/android-toolchain.d.ts.map +1 -0
  4. package/dist/android-toolchain.js +319 -0
  5. package/dist/artifact-build.d.ts +17 -0
  6. package/dist/artifact-build.d.ts.map +1 -0
  7. package/dist/artifact-build.js +56 -0
  8. package/dist/artifact-name.d.ts +1 -1
  9. package/dist/artifact-name.d.ts.map +1 -1
  10. package/dist/autolink.d.ts +3 -2
  11. package/dist/autolink.d.ts.map +1 -1
  12. package/dist/autolink.js +130 -6
  13. package/dist/bundle-build.d.ts +2 -0
  14. package/dist/bundle-build.d.ts.map +1 -1
  15. package/dist/bundle-build.js +11 -3
  16. package/dist/config.d.ts +19 -0
  17. package/dist/config.d.ts.map +1 -1
  18. package/dist/config.js +1 -1
  19. package/dist/desktop-build.d.ts +22 -0
  20. package/dist/desktop-build.d.ts.map +1 -0
  21. package/dist/desktop-build.js +161 -0
  22. package/dist/desktop-signing.d.ts +14 -0
  23. package/dist/desktop-signing.d.ts.map +1 -0
  24. package/dist/desktop-signing.js +200 -0
  25. package/dist/guidance.d.ts.map +1 -1
  26. package/dist/guidance.js +113 -4
  27. package/dist/harmony-build.d.ts +22 -0
  28. package/dist/harmony-build.d.ts.map +1 -0
  29. package/dist/harmony-build.js +194 -0
  30. package/dist/index.js +333 -73
  31. package/dist/ios-toolchain.d.ts +19 -0
  32. package/dist/ios-toolchain.d.ts.map +1 -0
  33. package/dist/ios-toolchain.js +303 -0
  34. package/dist/lynx-devtool.d.ts +18 -0
  35. package/dist/lynx-devtool.d.ts.map +1 -0
  36. package/dist/lynx-devtool.js +125 -0
  37. package/dist/process-runner.d.ts +11 -0
  38. package/dist/process-runner.d.ts.map +1 -1
  39. package/dist/process-runner.js +29 -0
  40. package/dist/prompt.d.ts +1 -0
  41. package/dist/prompt.d.ts.map +1 -1
  42. package/dist/prompt.js +6 -0
  43. package/dist/remote.d.ts +3 -3
  44. package/dist/remote.d.ts.map +1 -1
  45. package/dist/target-toolchain.d.ts +17 -0
  46. package/dist/target-toolchain.d.ts.map +1 -0
  47. package/dist/target-toolchain.js +122 -0
  48. package/dist/web-build.d.ts +16 -0
  49. package/dist/web-build.d.ts.map +1 -0
  50. package/dist/web-build.js +116 -0
  51. package/package.json +4 -4
package/README.md CHANGED
@@ -2,14 +2,14 @@
2
2
 
3
3
  Build, sign, store, submit and update LynxJS applications from the terminal.
4
4
 
5
- LynxShip connects a real Rspeedy bundle to the native Android or iOS build
6
- toolchain, verifies the resulting signature, uploads immutable artifacts to
7
- Cloudflare R2 and exposes an expiring download URL with a compact terminal QR
8
- code.
5
+ LynxShip connects a real Rspeedy bundle to the selected Lynx target, runs the
6
+ official target toolchain, verifies the artifact where the platform exposes an
7
+ official verifier, uploads immutable artifacts to Cloudflare R2 and exposes an
8
+ expiring download URL with a compact terminal QR code.
9
9
 
10
- > LynxShip is currently beta software. Android local builds are exercised
11
- > end-to-end. iOS builds require macOS and Xcode. Store submission still uses
12
- > the developer's own Google Play or App Store Connect credentials.
10
+ > LynxShip never fabricates a target artifact. Android and iOS require their
11
+ > native hosts; HarmonyOS requires the official Hvigor/DevEco host; Web and
12
+ > Desktop require the project's official Rspeedy/Lynxtron configuration.
13
13
 
14
14
  ## Install
15
15
 
@@ -33,8 +33,29 @@ The package exposes the `lynxship` executable.
33
33
  - Android builds on Windows, macOS or Linux: JDK 17, Android SDK command-line
34
34
  tools, `adb`, build tools and an executable project Gradle wrapper.
35
35
  - iOS builds: macOS, Xcode and Xcode command-line tools.
36
+ - HarmonyOS builds: an official Lynx Harmony host, DevEco/OpenHarmony SDK,
37
+ `ohpm`, the project `hvigorw` wrapper, Java and the official HAP signing tool.
38
+ - Web builds: the project's Web environment (`environments.web`) or an
39
+ explicit `build:web` script that produces one `*.web.bundle` in `dist/`,
40
+ `build/web/` or `build/`.
41
+ - Desktop builds: the official Lynxtron host/builder, or an Electron Builder
42
+ host with `pack`, `build:desktop` or `build:app`, on a supported target OS.
36
43
  - Cloudflare R2 credentials for artifact storage.
37
44
 
45
+ After a native host exists, inspect and repair the Android toolchain with:
46
+
47
+ ```bash
48
+ lynxship doctor --project-dir ./my-lynx-app --platform android
49
+ lynxship doctor --project-dir ./my-lynx-app --platform android --fix
50
+ ```
51
+
52
+ The Android doctor reads the project's wrapper and Android Gradle Plugin before
53
+ checking Java, the SDK path, `compileSdk`, Build Tools, `adb`, `apksigner` and
54
+ licenses. The wrapper is authoritative; a machine-wide Gradle installation is
55
+ not required. `--fix` may install missing SDK packages with `sdkmanager` only
56
+ after confirmation. It never silently installs Android Studio or a JDK, edits
57
+ Gradle files, changes the wrapper or touches signing credentials.
58
+
38
59
  ## First configuration
39
60
 
40
61
  Run these once on each development or CI machine:
@@ -119,8 +140,11 @@ init Initialize or link a project
119
140
  doctor Check the local toolchain and project
120
141
  dev Run Rspeedy dev with Lynx Explorer QR/HMR
121
142
  preview Preview the production bundle locally
143
+ devtool doctor Check Lynx DevTool and development runtime
144
+ trace doctor Check Lynx Trace prerequisites
145
+ recorder doctor Check Lynx Recorder prerequisites
122
146
  build create Build, sign and upload an artifact
123
- build all Build Android and iOS on a macOS host
147
+ build all Build Android, iOS, HarmonyOS, Web and Desktop
124
148
  build list List build jobs
125
149
  build status <id> Inspect one build job
126
150
  build cancel <id> Cancel a build job
@@ -129,8 +153,8 @@ submit Submit the latest successful artifact
129
153
  update Publish a signed OTA update
130
154
  update rollback Roll back an OTA channel to a previous release
131
155
  rollback Compatibility alias for update rollback
132
- run Install an artifact on a target
133
- logs Stream native logs
156
+ run Install an Android, iOS or HarmonyOS artifact
157
+ logs Stream Android, iOS or HarmonyOS native logs
134
158
  autolink check Check Lynx native-library wiring
135
159
  autolink codegen Run native-module codegen
136
160
  ota doctor Check native OTA host integration
@@ -161,8 +185,11 @@ Next steps
161
185
  ```
162
186
 
163
187
  `lynxship dev` is for Lynx Explorer and live QR/HMR development. The Android
164
- host command creates the native Gradle project needed for a real APK/AAB.
165
- `--local` remains a contract-test mode and never creates a fake artifact.
188
+ host command creates the native Gradle project needed for a real APK/AAB. An
189
+ interactive real build creates a missing host after asking for the application
190
+ ID; CI must pass `--application-id`. Existing native directories are never
191
+ overwritten. `--local` remains a contract-test mode and never creates a fake
192
+ artifact.
166
193
 
167
194
  To build both native platforms in one local workflow, use the multi-platform
168
195
  selector:
@@ -180,10 +207,110 @@ signing setup. Windows and Linux can still run
180
207
  `lynxship build --platform android`; use a macOS CI worker for the iOS half.
181
208
  `--local` can exercise both contract paths without creating APK or IPA files.
182
209
 
183
- The same guidance covers missing R2 or signing setup, Android SDK tools,
184
- Autolink/codegen, iOS Xcode/CocoaPods, device tools, store credentials and
185
- OTA compatibility. In automation, use `--json`; failures include a
186
- machine-readable `nextSteps` array and optional `note` field.
210
+ The same guidance covers missing R2 or signing setup, native SDK tools,
211
+ Autolink/codegen, target packaging, device tools, store credentials and OTA
212
+ compatibility. In automation, use `--json`; failures include a machine-readable
213
+ `nextSteps` array and optional `note` field.
214
+
215
+ ## Web, HarmonyOS and Desktop targets
216
+
217
+ These adapters use existing official host contracts; they do not generate a
218
+ fictional native runtime.
219
+
220
+ ### Web
221
+
222
+ Configure Rspeedy with the official Web environment and build the Web bundle:
223
+
224
+ ```json
225
+ {
226
+ "build": {
227
+ "production": {
228
+ "web": {
229
+ "environment": "web",
230
+ "artifact": "dist/main.web.bundle"
231
+ }
232
+ }
233
+ }
234
+ }
235
+ ```
236
+
237
+ ```bash
238
+ lynxship doctor --platform web
239
+ lynxship build --platform web --profile production
240
+ ```
241
+
242
+ The adapter invokes the profile's `web.script`, then an existing project
243
+ `build:web` script, and only otherwise uses `rspeedy build --environment web`.
244
+ It then requires exactly one Web bundle output unless the profile declares an
245
+ explicit artifact path.
246
+
247
+ ### HarmonyOS
248
+
249
+ Use the official Lynx Harmony integration under `harmony/`. The project must
250
+ provide `hvigorw`, `hvigorfile.ts`, `build-profile.json5` and
251
+ `oh-package.json5`. LynxShip runs `ohpm install`, the pinned wrapper in release
252
+ mode, then verifies the signed HAP with the official `hap-sign-tool.jar`.
253
+
254
+ Optional profile overrides are explicit and remain project-owned:
255
+
256
+ ```json
257
+ {
258
+ "build": {
259
+ "production": {
260
+ "harmony": {
261
+ "mode": "module",
262
+ "product": "default",
263
+ "module": "entry@default",
264
+ "buildMode": "release",
265
+ "task": "assembleHap"
266
+ }
267
+ }
268
+ }
269
+ }
270
+ ```
271
+
272
+ ```bash
273
+ lynxship doctor --platform harmony
274
+ lynxship build --platform harmony --profile production --no-upload
275
+ lynxship run --platform harmony --artifact .lynxship/artifacts/<uuid>.hap
276
+ lynxship logs --platform harmony --device <device-id>
277
+ ```
278
+
279
+ Set `LYNXSHIP_HAP_SIGN_TOOL` or `build.<profile>.harmony.signTool`; HAP signing
280
+ keys and profiles remain owned by the Harmony project and are never generated
281
+ or printed by LynxShip.
282
+
283
+ ### Desktop
284
+
285
+ Use the official Lynxtron host and builder, or an Electron Builder host. The
286
+ adapter invokes the configured `desktop.script`, then `pack`, `build:desktop`,
287
+ or `build:app`, and otherwise runs `lynxtron-builder --publish never`. It then
288
+ requires one distributable such as `.dmg`, `.exe`, `.appimage` or `.zip`.
289
+
290
+ ```json
291
+ {
292
+ "build": {
293
+ "production": {
294
+ "desktop": {
295
+ "script": "pack"
296
+ }
297
+ }
298
+ }
299
+ }
300
+ ```
301
+
302
+ ```bash
303
+ lynxship doctor --platform desktop
304
+ lynxship build --platform desktop --profile production --no-upload
305
+ ```
306
+
307
+ Desktop signing is checked before and after packaging. Windows builds require
308
+ a valid Authenticode signature; macOS builds require a valid Apple code
309
+ signature when the artifact exposes an app bundle. Configure the Lynxtron/
310
+ `electron-builder` signing inputs (`WIN_CSC_LINK`/`CSC_LINK`, protected
311
+ passwords, or the platform identity). Production and uploaded artifacts stop
312
+ when verification fails. Use `--allow-unsigned --no-upload` only for local
313
+ packaging tests.
187
314
 
188
315
  ## OTA rollback
189
316
 
@@ -228,9 +355,10 @@ but no `android/` directory.
228
355
 
229
356
  Production APK/AAB builds require a native Android host. The host is the
230
357
  Android application that initializes Lynx, creates `LynxView`, loads the bundle
231
- and contains the Gradle wrapper. `lynxship build` detects that requirement and
232
- fails clearly when `android/gradlew` is absent; `--local` only tests LynxShip's
233
- contract state machine and never fabricates an APK.
358
+ and contains the Gradle wrapper. `lynxship build` creates the host when
359
+ `android/` is absent, or fails with a repair instruction when an existing host
360
+ is incomplete; `--local` only tests LynxShip's contract state machine and never
361
+ fabricates an APK.
234
362
 
235
363
  To create a minimal host for a pure project:
236
364
 
@@ -258,6 +386,34 @@ script. On macOS, `lynxship build --platform ios` installs CocoaPods before
258
386
  archiving. Xcode, CocoaPods and real Apple signing credentials are still
259
387
  required for a signed IPA; the CLI never fabricates them.
260
388
 
389
+ Run the complete first-use diagnosis before building:
390
+
391
+ ```bash
392
+ lynxship doctor --project-dir ./my-lynx-app --platform ios
393
+ ```
394
+
395
+ The iOS doctor checks the active macOS developer directory, Xcode and
396
+ `xcodebuild`, `xcrun`, `codesign`, IPA verification tools, the host and scheme,
397
+ Xcode build settings, Apple team, valid Apple Distribution/Development
398
+ identity, CocoaPods when a `Podfile` exists, export method and provisioning
399
+ profile validity. It does not print certificate or provisioning-profile
400
+ contents. Xcode-managed signing is reported as a warning when profiles are not
401
+ cached locally; manual signing without a valid matching profile fails before
402
+ the archive starts.
403
+
404
+ ### Lynx DevTool, Trace and Recorder
405
+
406
+ ```bash
407
+ lynxship devtool doctor --platform android
408
+ lynxship trace doctor --platform android
409
+ lynxship recorder doctor --platform android
410
+ ```
411
+
412
+ These checks validate the project development script, native DevTool/Trace
413
+ dependencies and USB transport. Trace and Recorder require Lynx's matching
414
+ `-dev` runtime; the official Lynx DevTool Desktop application performs the
415
+ actual recording and analysis.
416
+
261
417
  ## Package layout
262
418
 
263
419
  The CLI is backed by the public `@lynxship/*` runtime packages in this
@@ -0,0 +1,23 @@
1
+ export type ToolchainCheckStatus = "pass" | "warn" | "fail";
2
+ export interface ToolchainCheck {
3
+ name: string;
4
+ status: ToolchainCheckStatus;
5
+ ok: boolean;
6
+ value: string;
7
+ fix?: string;
8
+ }
9
+ export interface AndroidToolchainReport {
10
+ ok: boolean;
11
+ checks: ToolchainCheck[];
12
+ sdkPath?: string;
13
+ compileSdk?: number;
14
+ buildToolsVersion?: string;
15
+ agpVersion?: string;
16
+ gradleVersion?: string;
17
+ javaVersion?: number;
18
+ sdkPackages: string[];
19
+ }
20
+ export declare function inspectAndroidToolchain(root: string): Promise<AndroidToolchainReport>;
21
+ export declare function fixAndroidToolchain(root: string, report: AndroidToolchainReport, confirm: (message: string) => Promise<boolean>, onOutput?: (line: string) => void): Promise<void>;
22
+ export declare function formatAndroidToolchainFailure(report: AndroidToolchainReport): string;
23
+ //# sourceMappingURL=android-toolchain.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"android-toolchain.d.ts","sourceRoot":"","sources":["../src/android-toolchain.ts"],"names":[],"mappings":"AAUA,MAAM,MAAM,oBAAoB,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;AAE5D,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,oBAAoB,CAAC;IAC7B,EAAE,EAAE,OAAO,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,sBAAsB;IACrC,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,EAAE,cAAc,EAAE,CAAC;IACzB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,EAAE,CAAC;CACvB;AAgQD,wBAAsB,uBAAuB,CAC3C,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,sBAAsB,CAAC,CA2LjC;AAED,wBAAsB,mBAAmB,CACvC,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,sBAAsB,EAC9B,OAAO,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,EAC9C,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,GAChC,OAAO,CAAC,IAAI,CAAC,CAef;AAED,wBAAgB,6BAA6B,CAC3C,MAAM,EAAE,sBAAsB,GAC7B,MAAM,CAQR"}
@@ -0,0 +1,319 @@
1
+ import { homedir, platform } from "node:os";
2
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
3
+ import { join, normalize } from "node:path";
4
+ import { captureProcess, commandExists, executableExists, runProcess, } from "./process-runner.js";
5
+ const agpMinimumGradle = [
6
+ ["9.2", "9.4.1"],
7
+ ["9.1", "9.3.1"],
8
+ ["9.0", "9.1.0"],
9
+ ["8.13", "8.13"],
10
+ ["8.12", "8.13"],
11
+ ["8.11", "8.13"],
12
+ ["8.10", "8.11.1"],
13
+ ["8.9", "8.11.1"],
14
+ ["8.8", "8.10.2"],
15
+ ["8.7", "8.9"],
16
+ ["8.6", "8.7"],
17
+ ["8.5", "8.7"],
18
+ ["8.4", "8.6"],
19
+ ["8.3", "8.4"],
20
+ ["8.2", "8.2"],
21
+ ["8.1", "8.0"],
22
+ ["8.0", "8.0"],
23
+ ["7.4", "7.5"],
24
+ ["7.3", "7.4"],
25
+ ["7.2", "7.3.3"],
26
+ ["7.1", "7.2"],
27
+ ["7.0", "7.0"],
28
+ ];
29
+ function readProjectFiles(root) {
30
+ const files = [
31
+ "android/build.gradle",
32
+ "android/build.gradle.kts",
33
+ "android/settings.gradle",
34
+ "android/settings.gradle.kts",
35
+ "android/app/build.gradle",
36
+ "android/app/build.gradle.kts",
37
+ "android/gradle/libs.versions.toml",
38
+ ];
39
+ return files
40
+ .filter((file) => existsSync(join(root, file)))
41
+ .map((file) => readFileSync(join(root, file), "utf8"))
42
+ .join("\n");
43
+ }
44
+ function readWrapperVersion(root) {
45
+ const file = join(root, "android", "gradle", "wrapper", "gradle-wrapper.properties");
46
+ if (!existsSync(file))
47
+ return undefined;
48
+ const content = readFileSync(file, "utf8");
49
+ return content.match(/gradle-([0-9]+(?:\.[0-9]+){1,2})-(?:bin|all)\.zip/)?.[1];
50
+ }
51
+ function readAndroidInputs(root) {
52
+ const content = readProjectFiles(root);
53
+ const agpVersion = content.match(/com\.android\.(?:application|library)["']?\)?\s+version\s+["']([0-9]+(?:\.[0-9]+){1,2})["']/)?.[1] ??
54
+ content.match(/com\.android\.tools\.build:gradle:([0-9]+(?:\.[0-9]+){1,2})/)?.[1];
55
+ const compileSdk = content.match(/compileSdk(?:Version)?\s*(?:=|\s)\s*["']?(\d+)/)?.[1];
56
+ const buildToolsVersion = content.match(/buildToolsVersion\s*(?:=|\s)\s*["']([^"']+)["']/)?.[1];
57
+ return {
58
+ agpVersion,
59
+ compileSdk: compileSdk ? Number(compileSdk) : undefined,
60
+ buildToolsVersion,
61
+ gradleVersion: readWrapperVersion(root),
62
+ };
63
+ }
64
+ function versionParts(version) {
65
+ return version.split(".").map((part) => Number(part) || 0);
66
+ }
67
+ function compareVersions(left, right) {
68
+ const a = versionParts(left);
69
+ const b = versionParts(right);
70
+ for (let index = 0; index < Math.max(a.length, b.length); index += 1) {
71
+ const difference = (a[index] ?? 0) - (b[index] ?? 0);
72
+ if (difference)
73
+ return difference;
74
+ }
75
+ return 0;
76
+ }
77
+ function requiredGradleForAgp(agp) {
78
+ if (!agp)
79
+ return undefined;
80
+ return agpMinimumGradle.find(([minimum]) => compareVersions(agp, minimum) >= 0)?.[1];
81
+ }
82
+ function requiredJavaForAgp(agp) {
83
+ if (!agp)
84
+ return 17;
85
+ if (compareVersions(agp, "8.0") >= 0)
86
+ return 17;
87
+ if (compareVersions(agp, "7.0") >= 0)
88
+ return 11;
89
+ return 8;
90
+ }
91
+ function sdkCandidates() {
92
+ const values = [process.env.ANDROID_SDK_ROOT, process.env.ANDROID_HOME];
93
+ if (platform() === "win32") {
94
+ values.push(process.env.LOCALAPPDATA
95
+ ? join(process.env.LOCALAPPDATA, "Android", "Sdk")
96
+ : undefined);
97
+ }
98
+ else if (platform() === "darwin") {
99
+ values.push(join(homedir(), "Library", "Android", "sdk"));
100
+ }
101
+ else {
102
+ values.push(join(homedir(), "Android", "Sdk"));
103
+ }
104
+ return [
105
+ ...new Set(values.filter((value) => Boolean(value)).map(normalize)),
106
+ ];
107
+ }
108
+ function sdkExecutable(sdkPath, name) {
109
+ const extension = platform() === "win32" ? ".exe" : "";
110
+ const candidates = [
111
+ join(sdkPath, "platform-tools", `${name}${extension}`),
112
+ ...readDirectories(join(sdkPath, "build-tools")).map((version) => join(sdkPath, "build-tools", version, `${name}${extension}`)),
113
+ ];
114
+ return candidates.find(executableExists);
115
+ }
116
+ function readDirectories(directory) {
117
+ try {
118
+ return readdirSync(directory);
119
+ }
120
+ catch {
121
+ return [];
122
+ }
123
+ }
124
+ function sdkManagerPath(sdkPath) {
125
+ const extension = platform() === "win32" ? ".bat" : "";
126
+ const candidates = [
127
+ join(sdkPath, "cmdline-tools", "latest", "bin", `sdkmanager${extension}`),
128
+ ...readDirectories(join(sdkPath, "cmdline-tools"))
129
+ .filter((name) => name !== "latest")
130
+ .map((name) => join(sdkPath, "cmdline-tools", name, "bin", `sdkmanager${extension}`)),
131
+ join(sdkPath, "tools", "bin", `sdkmanager${extension}`),
132
+ ];
133
+ return (candidates.find(executableExists) ??
134
+ (commandExists("sdkmanager") ? "sdkmanager" : undefined));
135
+ }
136
+ async function javaVersion(javaPath, cwd) {
137
+ try {
138
+ const result = await captureProcess(javaPath, ["-version"], { cwd });
139
+ const output = `${result.stdout}\n${result.stderr}`;
140
+ const match = output.match(/version\s+["'](\d+)(?:\.(\d+))?/i);
141
+ if (!match)
142
+ return undefined;
143
+ return Number(match[1]) === 1 ? Number(match[2]) : Number(match[1]);
144
+ }
145
+ catch {
146
+ return undefined;
147
+ }
148
+ }
149
+ function javaCandidates() {
150
+ const executable = platform() === "win32" ? "java.exe" : "java";
151
+ const values = [
152
+ process.env.JAVA_HOME
153
+ ? join(process.env.JAVA_HOME, "bin", executable)
154
+ : undefined,
155
+ commandExists("java") ? "java" : undefined,
156
+ platform() === "win32"
157
+ ? join(process.env.LOCALAPPDATA ?? "", "Programs", "Android", "Android Studio", "jbr", "bin", executable)
158
+ : undefined,
159
+ platform() === "win32"
160
+ ? join(process.env.PROGRAMFILES ?? "", "Android", "Android Studio", "jbr", "bin", executable)
161
+ : undefined,
162
+ platform() === "win32"
163
+ ? join(process.env.PROGRAMFILES ?? "", "Java", "jdk-17", "bin", executable)
164
+ : undefined,
165
+ platform() === "win32"
166
+ ? join(process.env.PROGRAMFILES ?? "", "Eclipse Adoptium", "jdk-17", "bin", executable)
167
+ : undefined,
168
+ platform() === "darwin"
169
+ ? "/Applications/Android Studio.app/Contents/jbr/Contents/Home/bin/java"
170
+ : undefined,
171
+ platform() === "darwin"
172
+ ? "/Applications/Android Studio.app/Contents/jre/Contents/Home/bin/java"
173
+ : undefined,
174
+ platform() === "linux" ? "/opt/android-studio/jbr/bin/java" : undefined,
175
+ platform() === "linux"
176
+ ? join(homedir(), "android-studio", "jbr", "bin", "java")
177
+ : undefined,
178
+ ];
179
+ return values.filter((value) => Boolean(value));
180
+ }
181
+ function check(name, status, value, fix) {
182
+ return { name, status, ok: status !== "fail", value, fix };
183
+ }
184
+ export async function inspectAndroidToolchain(root) {
185
+ const inputs = readAndroidInputs(root);
186
+ const checks = [];
187
+ const wrapper = join(root, "android", platform() === "win32" ? "gradlew.bat" : "gradlew");
188
+ const wrapperExists = existsSync(wrapper) &&
189
+ (platform() === "win32" || executableExists(wrapper));
190
+ checks.push(check("gradle-wrapper", wrapperExists ? "pass" : "fail", wrapperExists
191
+ ? inputs.gradleVersion
192
+ ? `Gradle ${inputs.gradleVersion}`
193
+ : "found · version unknown"
194
+ : "missing", wrapperExists
195
+ ? undefined
196
+ : "lynxship android host init --application-id com.example.myapp"));
197
+ const minimumGradle = requiredGradleForAgp(inputs.agpVersion);
198
+ const gradleCompatible = Boolean(inputs.gradleVersion &&
199
+ minimumGradle &&
200
+ compareVersions(inputs.gradleVersion, minimumGradle) >= 0);
201
+ const gradleStatus = !inputs.gradleVersion || !inputs.agpVersion
202
+ ? "warn"
203
+ : gradleCompatible
204
+ ? "pass"
205
+ : "fail";
206
+ checks.push(check("agp-gradle-compatibility", gradleStatus, inputs.agpVersion && inputs.gradleVersion
207
+ ? `AGP ${inputs.agpVersion} · Gradle ${inputs.gradleVersion}${minimumGradle ? ` · minimum Gradle ${minimumGradle}` : ""}`
208
+ : "AGP or wrapper version not detected", gradleStatus === "fail" && minimumGradle
209
+ ? `Update the project wrapper to Gradle ${minimumGradle}; do not use a global Gradle installation`
210
+ : undefined));
211
+ const requiredJava = requiredJavaForAgp(inputs.agpVersion);
212
+ let detectedJava;
213
+ let javaPath;
214
+ for (const candidate of javaCandidates()) {
215
+ const version = await javaVersion(candidate, root);
216
+ if (version !== undefined) {
217
+ detectedJava = version;
218
+ javaPath = candidate;
219
+ break;
220
+ }
221
+ }
222
+ const javaStatus = detectedJava === undefined
223
+ ? "fail"
224
+ : detectedJava >= requiredJava
225
+ ? "pass"
226
+ : "fail";
227
+ checks.push(check("java", javaStatus, detectedJava === undefined
228
+ ? "missing"
229
+ : `JDK ${detectedJava}${javaPath === "java" ? " · PATH" : ""}`, javaStatus === "fail"
230
+ ? `Install/configure JDK ${requiredJava} and set JAVA_HOME, then run lynxship doctor --platform android`
231
+ : undefined));
232
+ const sdkPath = sdkCandidates().find((candidate) => existsSync(candidate));
233
+ const compileSdk = inputs.compileSdk;
234
+ const buildTools = inputs.buildToolsVersion;
235
+ const platformPackage = compileSdk
236
+ ? `platforms;android-${compileSdk}`
237
+ : undefined;
238
+ const buildToolsPackage = buildTools
239
+ ? `build-tools;${buildTools}`
240
+ : undefined;
241
+ const missingPackages = [
242
+ platformPackage &&
243
+ sdkPath &&
244
+ !existsSync(join(sdkPath, "platforms", `android-${compileSdk}`))
245
+ ? platformPackage
246
+ : undefined,
247
+ buildTools &&
248
+ sdkPath &&
249
+ !existsSync(join(sdkPath, "build-tools", buildTools))
250
+ ? buildToolsPackage
251
+ : undefined,
252
+ ].filter((value) => Boolean(value));
253
+ const sdkStatus = !sdkPath
254
+ ? "fail"
255
+ : missingPackages.length
256
+ ? "fail"
257
+ : "pass";
258
+ checks.push(check("android-sdk", sdkStatus, !sdkPath
259
+ ? "missing · set ANDROID_HOME or ANDROID_SDK_ROOT"
260
+ : missingPackages.length
261
+ ? `missing: ${missingPackages.join(", ")}`
262
+ : sdkPath, sdkStatus === "fail"
263
+ ? "Install Android Studio or the Android command-line tools, then run lynxship doctor --platform android --fix"
264
+ : undefined));
265
+ const manager = sdkPath ? sdkManagerPath(sdkPath) : undefined;
266
+ checks.push(check("sdkmanager", manager ? "pass" : sdkPath ? "warn" : "fail", manager
267
+ ? "available"
268
+ : sdkPath
269
+ ? "missing · use Android Studio SDK Manager"
270
+ : "unavailable until Android SDK is installed", manager && missingPackages.length
271
+ ? `sdkmanager ${missingPackages.map((value) => `"${value}"`).join(" ")}`
272
+ : undefined));
273
+ const adb = sdkPath ? sdkExecutable(sdkPath, "adb") : undefined;
274
+ checks.push(check("adb", adb || commandExists("adb") ? "pass" : "warn", adb || commandExists("adb")
275
+ ? "Android Platform-Tools available"
276
+ : "missing · required for lynxship run/logs", !adb && !commandExists("adb")
277
+ ? "Install Android SDK Platform-Tools, then run adb devices"
278
+ : undefined));
279
+ const apksigner = sdkPath ? sdkExecutable(sdkPath, "apksigner") : undefined;
280
+ checks.push(check("apksigner", apksigner || commandExists("apksigner") ? "pass" : "warn", apksigner || commandExists("apksigner")
281
+ ? "Android Build Tools available"
282
+ : "missing · required to verify APK signatures", !apksigner && !commandExists("apksigner")
283
+ ? "Install Android SDK Build Tools, then run lynxship doctor --platform android"
284
+ : undefined));
285
+ checks.push(check("android-sdk-licenses", sdkPath && existsSync(join(sdkPath, "licenses")) ? "pass" : "warn", sdkPath && existsSync(join(sdkPath, "licenses"))
286
+ ? "license directory found"
287
+ : "not verified", sdkPath ? "sdkmanager --licenses" : undefined));
288
+ const failures = checks.filter((item) => item.status === "fail");
289
+ return {
290
+ ok: failures.length === 0,
291
+ checks,
292
+ sdkPath,
293
+ compileSdk,
294
+ buildToolsVersion: buildTools,
295
+ agpVersion: inputs.agpVersion,
296
+ gradleVersion: inputs.gradleVersion,
297
+ javaVersion: detectedJava,
298
+ sdkPackages: missingPackages,
299
+ };
300
+ }
301
+ export async function fixAndroidToolchain(root, report, confirm, onOutput) {
302
+ const managerCheck = report.checks.find((item) => item.name === "sdkmanager");
303
+ const manager = report.sdkPath ? sdkManagerPath(report.sdkPath) : undefined;
304
+ if (!manager || !report.sdkPackages.length)
305
+ return;
306
+ const packages = report.sdkPackages;
307
+ if (!(await confirm(`Install missing Android SDK packages (${packages.join(", ")}) now?`)))
308
+ return;
309
+ await runProcess(manager, packages, { cwd: root, onOutput });
310
+ if (managerCheck && !(await confirm("Accept Android SDK licenses now?")))
311
+ return;
312
+ await runProcess(manager, ["--licenses"], { cwd: root, onOutput });
313
+ }
314
+ export function formatAndroidToolchainFailure(report) {
315
+ return report.checks
316
+ .filter((item) => item.status === "fail")
317
+ .map((item) => `${item.name}: ${item.value}${item.fix ? ` · fix: ${item.fix}` : ""}`)
318
+ .join("; ");
319
+ }
@@ -0,0 +1,17 @@
1
+ import { type BuildJob, type Platform } from "@lynxship/contracts";
2
+ import { type NativeArtifactExtension } from "./artifact-name.js";
3
+ export interface ArtifactBuildOptions {
4
+ root: string;
5
+ job: BuildJob;
6
+ platform: Platform;
7
+ artifactPath: string;
8
+ extension: NativeArtifactExtension;
9
+ contentType: string;
10
+ uploadArtifacts: boolean;
11
+ verificationMessage: string;
12
+ quiet?: boolean;
13
+ onEvent?: (message: string) => void;
14
+ onProgress?: (value?: number, label?: string) => void;
15
+ }
16
+ export declare function publishBuiltArtifact(options: ArtifactBuildOptions): Promise<BuildJob>;
17
+ //# sourceMappingURL=artifact-build.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"artifact-build.d.ts","sourceRoot":"","sources":["../src/artifact-build.ts"],"names":[],"mappings":"AAGA,OAAO,EAGL,KAAK,QAAQ,EACb,KAAK,QAAQ,EACd,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAEL,KAAK,uBAAuB,EAC7B,MAAM,oBAAoB,CAAC;AAG5B,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,QAAQ,CAAC;IACd,QAAQ,EAAE,QAAQ,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,uBAAuB,CAAC;IACnC,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,OAAO,CAAC;IACzB,mBAAmB,EAAE,MAAM,CAAC;IAC5B,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,UAAU,CAAC,EAAE,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;CACvD;AAED,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,oBAAoB,GAC5B,OAAO,CAAC,QAAQ,CAAC,CA0EnB"}
@@ -0,0 +1,56 @@
1
+ import { copyFile, mkdir, readFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { transitionBuild } from "@lynxship/build-orchestrator";
4
+ import { assert, sha256, } from "@lynxship/contracts";
5
+ import { nativeArtifactName, } from "./artifact-name.js";
6
+ import { loadR2, uploadR2Artifact } from "./r2.js";
7
+ export async function publishBuiltArtifact(options) {
8
+ const { root, job, platform, artifactPath, extension, contentType, uploadArtifacts, onEvent, onProgress, } = options;
9
+ const artifactName = nativeArtifactName(extension);
10
+ const artifactDirectory = join(root, ".lynxship", "artifacts");
11
+ const artifactPathCopy = join(artifactDirectory, artifactName);
12
+ await mkdir(artifactDirectory, { recursive: true });
13
+ await copyFile(artifactPath, artifactPathCopy);
14
+ const content = await readFile(artifactPathCopy);
15
+ const hash = sha256(content);
16
+ job.attempts += 1;
17
+ transitionBuild(job, "signing", options.verificationMessage);
18
+ transitionBuild(job, "uploading_artifacts", `${platform} artifact collected`);
19
+ const step = (message, value) => {
20
+ onEvent?.(message);
21
+ onProgress?.(value, message);
22
+ };
23
+ if (!uploadArtifacts) {
24
+ step("Artifact collected locally; R2 upload skipped", 100);
25
+ job.artifact = {
26
+ name: artifactName,
27
+ hash,
28
+ path: artifactPathCopy,
29
+ size: content.length,
30
+ contentType,
31
+ };
32
+ }
33
+ else {
34
+ await loadR2(root);
35
+ step("Uploading artifact to Cloudflare R2…", 80);
36
+ const uploaded = await uploadR2Artifact(root, job.projectId, job.id, artifactPathCopy, contentType, undefined, {
37
+ onProgress: (uploadedBytes, totalBytes) => {
38
+ const transfer = totalBytes === 0 ? 1 : uploadedBytes / totalBytes;
39
+ onProgress?.(80 + transfer * 19, `Uploading artifact to Cloudflare R2… ${Math.round(transfer * 10000) / 100}%`);
40
+ },
41
+ });
42
+ assert(uploaded.hash === hash, "BUILD_ARTIFACT_HASH", "R2 artifact hash mismatch");
43
+ job.artifact = {
44
+ name: artifactName,
45
+ hash,
46
+ path: artifactPathCopy,
47
+ key: uploaded.key,
48
+ size: uploaded.size,
49
+ contentType: uploaded.contentType,
50
+ url: uploaded.url,
51
+ expiresAt: uploaded.expiresAt,
52
+ };
53
+ }
54
+ step(`Artifact ready: ${artifactName}`, 100);
55
+ return transitionBuild(job, "success", `real ${platform} artifact created`);
56
+ }