@lynxship/cli 0.1.4 → 0.1.7

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 (57) hide show
  1. package/README.md +90 -2
  2. package/dist/android-build.d.ts +1 -0
  3. package/dist/android-build.d.ts.map +1 -1
  4. package/dist/android-build.js +12 -19
  5. package/dist/android-host.d.ts +12 -0
  6. package/dist/android-host.d.ts.map +1 -0
  7. package/dist/android-host.js +70 -0
  8. package/dist/bundle-build.d.ts +7 -0
  9. package/dist/bundle-build.d.ts.map +1 -0
  10. package/dist/bundle-build.js +24 -0
  11. package/dist/guidance.d.ts +6 -0
  12. package/dist/guidance.d.ts.map +1 -0
  13. package/dist/guidance.js +354 -0
  14. package/dist/index.js +349 -93
  15. package/dist/ios-build.d.ts +1 -0
  16. package/dist/ios-build.d.ts.map +1 -1
  17. package/dist/ios-build.js +38 -12
  18. package/dist/ios-host.d.ts +17 -0
  19. package/dist/ios-host.d.ts.map +1 -0
  20. package/dist/ios-host.js +93 -0
  21. package/dist/ui/components.d.ts +2 -0
  22. package/dist/ui/components.d.ts.map +1 -1
  23. package/dist/ui/components.js +30 -16
  24. package/dist/ui/index.d.ts +3 -1
  25. package/dist/ui/index.d.ts.map +1 -1
  26. package/dist/ui/index.js +6 -1
  27. package/package.json +5 -4
  28. package/templates/android-host/app/build.gradle +38 -0
  29. package/templates/android-host/app/proguard-rules.pro +2 -0
  30. package/templates/android-host/app/src/main/AndroidManifest.xml +20 -0
  31. package/templates/android-host/app/src/main/java/template/LynxShipApplication.java +13 -0
  32. package/templates/android-host/app/src/main/java/template/MainActivity.java +19 -0
  33. package/templates/android-host/app/src/main/java/template/ProjectTemplateProvider.java +32 -0
  34. package/templates/android-host/app/src/main/res/values/strings.xml +3 -0
  35. package/templates/android-host/app/src/main/res/values/themes.xml +6 -0
  36. package/templates/android-host/build.gradle +3 -0
  37. package/templates/android-host/gradle/wrapper/gradle-wrapper.jar +0 -0
  38. package/templates/android-host/gradle/wrapper/gradle-wrapper.properties +7 -0
  39. package/templates/android-host/gradle.properties +3 -0
  40. package/templates/android-host/gradlew +185 -0
  41. package/templates/android-host/gradlew.bat +89 -0
  42. package/templates/android-host/settings.gradle +20 -0
  43. package/templates/ios-host/ExportOptions.plist +16 -0
  44. package/templates/ios-host/Podfile +42 -0
  45. package/templates/ios-host/__IOS_TARGET_NAME__/AppDelegate.swift +11 -0
  46. package/templates/ios-host/__IOS_TARGET_NAME__/Assets.xcassets/AccentColor.colorset/Contents.json +11 -0
  47. package/templates/ios-host/__IOS_TARGET_NAME__/Assets.xcassets/AppIcon.appiconset/Contents.json +13 -0
  48. package/templates/ios-host/__IOS_TARGET_NAME__/Assets.xcassets/Contents.json +6 -0
  49. package/templates/ios-host/__IOS_TARGET_NAME__/Base.lproj/LaunchScreen.storyboard +25 -0
  50. package/templates/ios-host/__IOS_TARGET_NAME__/Base.lproj/Main.storyboard +24 -0
  51. package/templates/ios-host/__IOS_TARGET_NAME__/DemoLynxProvider.swift +18 -0
  52. package/templates/ios-host/__IOS_TARGET_NAME__/Hello-Lynx-Bridging-Header.h +4 -0
  53. package/templates/ios-host/__IOS_TARGET_NAME__/Info.plist +25 -0
  54. package/templates/ios-host/__IOS_TARGET_NAME__/SceneDelegate.swift +49 -0
  55. package/templates/ios-host/__IOS_TARGET_NAME__/ViewController.swift +22 -0
  56. package/templates/ios-host/__IOS_TARGET_NAME__.xcodeproj/project.pbxproj +452 -0
  57. package/templates/ios-host/sync-bundle.mjs +11 -0
package/README.md CHANGED
@@ -117,9 +117,10 @@ unverified artifact.
117
117
  ```text
118
118
  init Initialize or link a project
119
119
  doctor Check the local toolchain and project
120
- dev Run the Rspeedy development server
120
+ dev Run Rspeedy dev with Lynx Explorer QR/HMR
121
121
  preview Preview the production bundle locally
122
122
  build create Build, sign and upload an artifact
123
+ build all Build Android and iOS on a macOS host
123
124
  build list List build jobs
124
125
  build status <id> Inspect one build job
125
126
  build cancel <id> Cancel a build job
@@ -133,7 +134,8 @@ logs Stream native logs
133
134
  autolink check Check Lynx native-library wiring
134
135
  autolink codegen Run native-module codegen
135
136
  ota doctor Check native OTA host integration
136
- storage configure Configure Cloudflare R2
137
+ storage configure Configure Cloudflare R2
138
+ android host init Create a minimal official Lynx Android host
137
139
  android configure Configure Android signing
138
140
  store configure Configure store submission credentials
139
141
  ```
@@ -142,6 +144,47 @@ Use `lynxship --help` or `lynxship <command> --help` for the complete option
142
144
  list. `--json`, `--quiet`, `--no-color` and `--non-interactive` are available
143
145
  for automation.
144
146
 
147
+ ### Actionable error guidance
148
+
149
+ Recoverable errors print a `Next steps` section with the exact commands to
150
+ run. For example, a pure LynxJS project without a native Android host reports
151
+ the two supported paths instead of stopping at a generic Gradle error:
152
+
153
+ ```text
154
+ x This project has no Android Gradle host...
155
+
156
+ Next steps
157
+ 1. lynxship dev
158
+ 2. lynxship android host init --application-id com.example.myapp
159
+ 3. lynxship doctor --platform android
160
+ 4. lynxship build --platform android --profile production
161
+ ```
162
+
163
+ `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.
166
+
167
+ To build both native platforms in one local workflow, use the multi-platform
168
+ selector:
169
+
170
+ ```bash
171
+ lynxship build --platform all --profile production
172
+ # equivalent convenience form:
173
+ lynxship build all --profile production
174
+ ```
175
+
176
+ This builds Android and iOS concurrently after one shared Lynx bundle step.
177
+ Their jobs, progress events and artifacts remain isolated. A real local `all`
178
+ build requires macOS, an Android host, an iOS Xcode host and the corresponding
179
+ signing setup. Windows and Linux can still run
180
+ `lynxship build --platform android`; use a macOS CI worker for the iOS half.
181
+ `--local` can exercise both contract paths without creating APK or IPA files.
182
+
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.
187
+
145
188
  ## OTA rollback
146
189
 
147
190
  ```bash
@@ -170,6 +213,51 @@ command-line tools with `sdkmanager`, accept the required licenses, set
170
213
  `android/gradlew` is executable. macOS can build both Android and iOS; Windows
171
214
  and Linux can build Android only.
172
215
 
216
+ ## Pure Lynx projects and Lynx Explorer
217
+
218
+ A standard Rspeedy project can be developed without a native Android host:
219
+
220
+ ```bash
221
+ lynxship dev --project-dir ./my-lynx-app
222
+ ```
223
+
224
+ Rspeedy serves the development bundle and prints the QR code. Scan it with the
225
+ official Lynx Explorer app; edits to the Lynx source are then reflected live.
226
+ This is the supported path for projects containing `src/` and `lynx.config.*`
227
+ but no `android/` directory.
228
+
229
+ Production APK/AAB builds require a native Android host. The host is the
230
+ 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.
234
+
235
+ To create a minimal host for a pure project:
236
+
237
+ ```bash
238
+ lynxship android host init --application-id com.example.myapp
239
+ ```
240
+
241
+ This command never overwrites an existing `android/` directory. It creates a
242
+ real Gradle application with the official Lynx Android dependencies,
243
+ `LynxEnv`, `LynxView`, a bundle loader and a Gradle wrapper. Replace the example
244
+ application ID before a store release and add any project-specific native
245
+ modules, permissions, services and OTA integration explicitly.
246
+
247
+ For a pure project that targets iOS, create the native Xcode/CocoaPods host
248
+ with:
249
+
250
+ ```bash
251
+ lynxship ios host init --bundle-identifier com.example.myapp
252
+ ```
253
+
254
+ The command refuses to overwrite an existing `ios/` directory and creates a
255
+ Swift host based on Lynx's official integration shape: `LynxEnv`, `LynxView`,
256
+ `LynxTemplateProvider`, `Podfile`, `ExportOptions.plist` and a bundle sync
257
+ script. On macOS, `lynxship build --platform ios` installs CocoaPods before
258
+ archiving. Xcode, CocoaPods and real Apple signing credentials are still
259
+ required for a signed IPA; the CLI never fabricates them.
260
+
173
261
  ## Package layout
174
262
 
175
263
  The CLI is backed by the public `@lynxship/*` runtime packages in this
@@ -4,6 +4,7 @@ interface AndroidBuildOptions {
4
4
  root: string;
5
5
  profile: BuildProfile;
6
6
  uploadArtifacts?: boolean;
7
+ skipBundleBuild?: boolean;
7
8
  quiet?: boolean;
8
9
  onStep?: (message: string) => void;
9
10
  onEvent?: (message: string) => void;
@@ -1 +1 @@
1
- {"version":3,"file":"android-build.d.ts","sourceRoot":"","sources":["../src/android-build.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAkB,KAAK,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAEpE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAWhD,UAAU,mBAAmB;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,YAAY,CAAC;IACtB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,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;AA0LD,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAa7D;AAED,wBAAgB,0BAA0B,IAAI,OAAO,CAEpD;AAED;;;;;;GAMG;AACH,wBAAsB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAsCpE;AAED,wBAAsB,mBAAmB,CACvC,GAAG,EAAE,QAAQ,EACb,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,QAAQ,CAAC,CAgKnB"}
1
+ {"version":3,"file":"android-build.d.ts","sourceRoot":"","sources":["../src/android-build.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAkB,KAAK,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAEpE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAWhD,UAAU,mBAAmB;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,YAAY,CAAC;IACtB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,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;AA8KD,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAa7D;AAED,wBAAgB,0BAA0B,IAAI,OAAO,CAEpD;AAED;;;;;;GAMG;AACH,wBAAsB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAsCpE;AAED,wBAAsB,mBAAmB,CACvC,GAAG,EAAE,QAAQ,EACb,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,QAAQ,CAAC,CA+JnB"}
@@ -8,18 +8,8 @@ import { transitionBuild } from "@lynxship/build-orchestrator";
8
8
  import { loadR2, uploadR2Artifact } from "./r2.js";
9
9
  import { loadCredentials } from "./secure-store.js";
10
10
  import { nativeArtifactName } from "./artifact-name.js";
11
- import { commandExists, executableExists, packageManagerScriptCommand, runProcess, } from "./process-runner.js";
12
- async function projectBuildCommand(root) {
13
- try {
14
- const packageJson = JSON.parse(await readFile(join(root, "package.json"), "utf8"));
15
- if (packageJson.scripts?.["build:mobile"])
16
- return ["run", "build:mobile"];
17
- }
18
- catch {
19
- // The package manager will report the useful project error below.
20
- }
21
- return ["run", "build"];
22
- }
11
+ import { commandExists, executableExists, runProcess, } from "./process-runner.js";
12
+ import { buildLynxBundle } from "./bundle-build.js";
23
13
  async function signingEnvironment(root) {
24
14
  const android = (await loadCredentials(root)).android;
25
15
  const values = {
@@ -220,13 +210,16 @@ export async function runRealAndroidBuild(job, options) {
220
210
  message: "rspeedy:build",
221
211
  at: new Date().toISOString(),
222
212
  });
223
- step("Building Lynx bundle with Rspeedy…");
224
- const packageManager = packageManagerScriptCommand(options.root, (await projectBuildCommand(options.root))[1] ?? "build");
225
- await runProcess(packageManager.command, packageManager.args, {
226
- cwd: options.root,
227
- env: environment,
228
- ...processOptions,
229
- });
213
+ if (options.skipBundleBuild) {
214
+ step("Using shared Lynx bundle", 20);
215
+ }
216
+ else {
217
+ step("Building Lynx bundle with Rspeedy…");
218
+ await buildLynxBundle(options.root, {
219
+ env: environment,
220
+ ...processOptions,
221
+ });
222
+ }
230
223
  step("Rspeedy bundle ready", 20);
231
224
  transitionBuild(job, "queued", "Android build queued locally");
232
225
  step("Build queued…");
@@ -0,0 +1,12 @@
1
+ export interface AndroidHostOptions {
2
+ applicationId: string;
3
+ appName: string;
4
+ }
5
+ export interface AndroidHostResult {
6
+ directory: string;
7
+ applicationId: string;
8
+ packageName: string;
9
+ }
10
+ export declare function initializeAndroidHost(root: string, options: AndroidHostOptions): Promise<AndroidHostResult>;
11
+ export declare function suggestedAndroidApplicationId(root: string): string;
12
+ //# sourceMappingURL=android-host.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"android-host.d.ts","sourceRoot":"","sources":["../src/android-host.ts"],"names":[],"mappings":"AAiBA,MAAM,WAAW,kBAAkB;IACjC,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC;CACrB;AA2BD,wBAAsB,qBAAqB,CACzC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,kBAAkB,GAC1B,OAAO,CAAC,iBAAiB,CAAC,CA6D5B;AAED,wBAAgB,6BAA6B,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAKlE"}
@@ -0,0 +1,70 @@
1
+ import { access, chmod, cp, mkdir, readFile, rename, writeFile, } from "node:fs/promises";
2
+ import { fileURLToPath } from "node:url";
3
+ import { basename, dirname, join } from "node:path";
4
+ import { assert } from "@lynxship/contracts";
5
+ const templateRoot = fileURLToPath(new URL("../templates/android-host/", import.meta.url));
6
+ function packagePath(packageName) {
7
+ return packageName.split(".").join("/");
8
+ }
9
+ function validateApplicationId(applicationId) {
10
+ assert(/^[a-zA-Z][a-zA-Z0-9_]*(\.[a-zA-Z][a-zA-Z0-9_]*)+$/.test(applicationId), "ANDROID_APPLICATION_ID_INVALID", "Android application ID must contain at least two dot-separated Java package segments, for example com.example.myapp.");
11
+ }
12
+ async function exists(path) {
13
+ try {
14
+ await access(path);
15
+ return true;
16
+ }
17
+ catch {
18
+ return false;
19
+ }
20
+ }
21
+ function safeAppName(value) {
22
+ return value.replace(/[^a-zA-Z0-9 ._-]/g, "").trim() || "Lynx App";
23
+ }
24
+ export async function initializeAndroidHost(root, options) {
25
+ validateApplicationId(options.applicationId);
26
+ const android = join(root, "android");
27
+ assert(!(await exists(android)), "ANDROID_HOST_EXISTS", `The Android host already exists at ${android}. LynxShip will not overwrite it.`);
28
+ const packageName = options.applicationId;
29
+ const packageDirectory = join(android, "app", "src", "main", "java", packagePath(packageName));
30
+ await cp(templateRoot, android, { recursive: true, force: false });
31
+ await mkdir(dirname(packageDirectory), { recursive: true });
32
+ await rename(join(android, "app", "src", "main", "java", "template"), packageDirectory);
33
+ const replacements = {
34
+ __APPLICATION_ID__: options.applicationId,
35
+ __PACKAGE_NAME__: packageName,
36
+ __APP_NAME__: safeAppName(options.appName),
37
+ };
38
+ const textFiles = [
39
+ "build.gradle",
40
+ "settings.gradle",
41
+ "gradle.properties",
42
+ "app/build.gradle",
43
+ "app/src/main/AndroidManifest.xml",
44
+ "app/src/main/res/values/strings.xml",
45
+ "app/src/main/res/values/themes.xml",
46
+ "app/src/main/java/__PACKAGE_PATH__/LynxShipApplication.java",
47
+ "app/src/main/java/__PACKAGE_PATH__/MainActivity.java",
48
+ "app/src/main/java/__PACKAGE_PATH__/ProjectTemplateProvider.java",
49
+ ];
50
+ for (const relativeFile of textFiles) {
51
+ const target = join(android, relativeFile.replace("__PACKAGE_PATH__", packagePath(packageName)));
52
+ let content = await readFile(target, "utf8");
53
+ for (const [placeholder, value] of Object.entries(replacements))
54
+ content = content.replaceAll(placeholder, value);
55
+ await writeFile(target, content, "utf8");
56
+ }
57
+ if (process.platform !== "win32")
58
+ await chmod(join(android, "gradlew"), 0o755);
59
+ return {
60
+ directory: android,
61
+ applicationId: options.applicationId,
62
+ packageName,
63
+ };
64
+ }
65
+ export function suggestedAndroidApplicationId(root) {
66
+ const project = basename(root)
67
+ .toLowerCase()
68
+ .replace(/[^a-z0-9]+/g, "");
69
+ return `com.example.${project || "lynxapp"}`;
70
+ }
@@ -0,0 +1,7 @@
1
+ export interface BundleBuildOptions {
2
+ env?: NodeJS.ProcessEnv;
3
+ quiet?: boolean;
4
+ onOutput?: (line: string) => void;
5
+ }
6
+ export declare function buildLynxBundle(root: string, options?: BundleBuildOptions): Promise<void>;
7
+ //# sourceMappingURL=bundle-build.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bundle-build.d.ts","sourceRoot":"","sources":["../src/bundle-build.ts"],"names":[],"mappings":"AAQA,MAAM,WAAW,kBAAkB;IACjC,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CACnC;AAcD,wBAAsB,eAAe,CACnC,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE,kBAAuB,GAC/B,OAAO,CAAC,IAAI,CAAC,CASf"}
@@ -0,0 +1,24 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { packageManagerScriptCommand, runProcess } from "./process-runner.js";
4
+ async function resolveBundleScript(root) {
5
+ try {
6
+ const manifest = JSON.parse(await readFile(join(root, "package.json"), "utf8"));
7
+ if (manifest.scripts?.["build:mobile"])
8
+ return "build:mobile";
9
+ }
10
+ catch {
11
+ // The package manager will report the useful project error below.
12
+ }
13
+ return "build";
14
+ }
15
+ export async function buildLynxBundle(root, options = {}) {
16
+ const script = await resolveBundleScript(root);
17
+ const packageManager = packageManagerScriptCommand(root, script);
18
+ await runProcess(packageManager.command, packageManager.args, {
19
+ cwd: root,
20
+ env: options.env,
21
+ quiet: options.quiet,
22
+ onOutput: options.onOutput,
23
+ });
24
+ }
@@ -0,0 +1,6 @@
1
+ export interface CliGuidance {
2
+ commands: string[];
3
+ note?: string;
4
+ }
5
+ export declare function guidanceForError(error: unknown): CliGuidance;
6
+ //# sourceMappingURL=guidance.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"guidance.d.ts","sourceRoot":"","sources":["../src/guidance.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAoQD,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,WAAW,CAiG5D"}
@@ -0,0 +1,354 @@
1
+ const guidance = {
2
+ CLI_PROJECT_REQUIRED: {
3
+ commands: ["lynxship init", "lynxship doctor"],
4
+ note: "Run them from the project directory or add --project-dir <path>.",
5
+ },
6
+ CLI_PROJECT_ID_REQUIRED: {
7
+ commands: ["lynxship init", "lynxship doctor"],
8
+ },
9
+ CLI_R2_REQUIRED: {
10
+ commands: ["lynxship storage configure", "lynxship doctor"],
11
+ },
12
+ CLI_R2_ACCOUNT_ID: {
13
+ commands: ["lynxship storage configure"],
14
+ note: "Use the 32-character Cloudflare account ID.",
15
+ },
16
+ CLI_R2_BUCKET: {
17
+ commands: ["lynxship storage configure"],
18
+ note: "Use a lowercase R2 bucket name with no spaces or underscores.",
19
+ },
20
+ CLI_R2_ENDPOINT: {
21
+ commands: ["lynxship storage configure"],
22
+ note: "Use the HTTPS S3 endpoint for the same Cloudflare account.",
23
+ },
24
+ CLI_R2_EXPIRY: {
25
+ commands: ["lynxship storage configure"],
26
+ note: "Choose a download lifetime between 1 second and 7 days.",
27
+ },
28
+ CLI_R2_CREDENTIALS: {
29
+ commands: ["lynxship storage configure", "lynxship doctor"],
30
+ },
31
+ BUILD_SIGNING_REQUIRED: {
32
+ commands: [
33
+ "lynxship android configure",
34
+ "lynxship doctor --platform android",
35
+ ],
36
+ note: "An existing .jks/.keystore is supported; production builds need a real release key.",
37
+ },
38
+ LYNXSHIP_KEYSTORE_PATH: {
39
+ commands: ["lynxship android configure"],
40
+ },
41
+ LYNXSHIP_KEY_ALIAS: {
42
+ commands: ["lynxship android configure"],
43
+ },
44
+ LYNXSHIP_KEYSTORE_PASSWORD: {
45
+ commands: ["lynxship android configure"],
46
+ },
47
+ LYNXSHIP_KEY_PASSWORD: {
48
+ commands: ["lynxship android configure"],
49
+ },
50
+ ANDROID_HOST_REQUIRED: {
51
+ commands: [
52
+ "lynxship dev",
53
+ "lynxship android host init --application-id com.example.myapp",
54
+ "lynxship doctor --platform android",
55
+ "lynxship build --platform android --profile production",
56
+ ],
57
+ note: "Use dev for Lynx Explorer. The host init path is required for a real APK/AAB; --local is only a contract test.",
58
+ },
59
+ ANDROID_HOST_EXISTS: {
60
+ commands: ["lynxship doctor --platform android"],
61
+ note: "Review the existing android/ host instead of overwriting it.",
62
+ },
63
+ ANDROID_APPLICATION_ID_INVALID: {
64
+ commands: ["lynxship android host init --application-id com.example.myapp"],
65
+ note: "Use a reverse-domain Android application ID, for example com.company.app.",
66
+ },
67
+ ANDROID_PLATFORM_UNSUPPORTED: {
68
+ commands: ["lynxship doctor --platform android"],
69
+ note: "Android builds run on Windows, macOS or Linux. Use macOS for iOS builds.",
70
+ },
71
+ BUILD_ALL_MACOS_REQUIRED: {
72
+ commands: [
73
+ "lynxship build --platform android --profile production",
74
+ "lynxship build --platform all --profile production --no-upload",
75
+ ],
76
+ note: "Run the all-platform command on a macOS machine or macOS CI worker; the second command is for CI verification.",
77
+ },
78
+ ANDROID_APKSIGNER_REQUIRED: {
79
+ commands: [
80
+ "lynxship doctor --platform android",
81
+ 'sdkmanager "build-tools;latest"',
82
+ ],
83
+ note: "Ensure Android SDK Build Tools are installed and available through ANDROID_HOME or ANDROID_SDK_ROOT.",
84
+ },
85
+ ANDROID_JARSIGNER_REQUIRED: {
86
+ commands: ["lynxship doctor --platform android", "java -version"],
87
+ note: "Install JDK 17 or newer and make sure jarsigner is on PATH.",
88
+ },
89
+ ANDROID_ADB_REQUIRED: {
90
+ commands: ["lynxship doctor --platform android", "adb devices"],
91
+ note: "Install Android SDK Platform-Tools and connect or start a device.",
92
+ },
93
+ LYNX_BUNDLE_MISSING: {
94
+ commands: ["lynxship dev", "lynxship build --platform android"],
95
+ note: "Build the Lynx bundle with the project's configured package manager.",
96
+ },
97
+ IOS_HOST_REQUIRED: {
98
+ commands: [
99
+ "lynxship dev",
100
+ "lynxship ios host init --bundle-identifier com.example.myapp",
101
+ "lynxship doctor --platform ios",
102
+ "lynxship build --platform ios --profile production",
103
+ ],
104
+ note: "Use dev for Lynx Explorer. A real IPA requires macOS, Xcode and an iOS host.",
105
+ },
106
+ IOS_HOST_EXISTS: {
107
+ commands: ["lynxship doctor --platform ios"],
108
+ note: "Review the existing ios/ host instead of overwriting it.",
109
+ },
110
+ IOS_BUNDLE_IDENTIFIER_INVALID: {
111
+ commands: ["lynxship ios host init --bundle-identifier com.example.myapp"],
112
+ note: "Use a reverse-domain bundle identifier, for example com.company.app.",
113
+ },
114
+ IOS_MACOS_REQUIRED: {
115
+ commands: ["lynxship doctor --platform ios"],
116
+ note: "Run real iOS commands on macOS. Windows and Linux cannot produce an IPA.",
117
+ },
118
+ IOS_XCODE_REQUIRED: {
119
+ commands: ["xcode-select --install", "lynxship doctor --platform ios"],
120
+ note: "Install Xcode from the Mac App Store and select its command-line tools.",
121
+ },
122
+ IOS_XCRUN_REQUIRED: {
123
+ commands: ["xcode-select --install", "lynxship doctor --platform ios"],
124
+ },
125
+ IOS_COCOAPODS_REQUIRED: {
126
+ commands: ["brew install cocoapods", "lynxship build --platform ios"],
127
+ note: "CocoaPods is required when the iOS host contains a Podfile.",
128
+ },
129
+ IOS_PROJECT_REQUIRED: {
130
+ commands: [
131
+ "lynxship ios host init --bundle-identifier com.example.myapp",
132
+ "lynxship doctor --platform ios",
133
+ ],
134
+ },
135
+ IOS_SCHEME_REQUIRED: {
136
+ commands: ["lynxship inspect", "lynxship doctor --platform ios"],
137
+ note: "Set build.<profile>.ios.scheme in lynxship.json.",
138
+ },
139
+ IOS_EXPORT_OPTIONS_REQUIRED: {
140
+ commands: [
141
+ "lynxship ios host init --bundle-identifier com.example.myapp",
142
+ "lynxship doctor --platform ios",
143
+ ],
144
+ note: "Set build.<profile>.ios.exportOptionsPlist to a valid export options file.",
145
+ },
146
+ IOS_DEVICE_REQUIRED: {
147
+ commands: [
148
+ "xcrun devicectl list devices",
149
+ "lynxship run --platform ios --device <device-id>",
150
+ ],
151
+ },
152
+ IOS_DEVICE_LOGS_UNSUPPORTED: {
153
+ commands: [
154
+ "xcrun simctl list devices",
155
+ "lynxship logs --platform ios --device <simulator-id>",
156
+ ],
157
+ },
158
+ DEVICE_ARTIFACT_REQUIRED: {
159
+ commands: [
160
+ "lynxship build --platform android --profile production",
161
+ "lynxship run --platform android --artifact <path-to-apk>",
162
+ ],
163
+ },
164
+ LYNX_CODEGEN_SCRIPT_REQUIRED: {
165
+ commands: [
166
+ "lynxship autolink check --platform android",
167
+ "lynxship autolink codegen --library-dir <native-library>",
168
+ ],
169
+ note: "The native library must expose its official codegen script.",
170
+ },
171
+ LYNX_AUTOLINK_ANDROID_REQUIRED: {
172
+ commands: [
173
+ "lynxship autolink check --platform android",
174
+ "lynxship autolink codegen --library-dir <native-library>",
175
+ ],
176
+ },
177
+ LYNX_AUTOLINK_IOS_REQUIRED: {
178
+ commands: [
179
+ "lynxship autolink check --platform ios",
180
+ "lynxship autolink codegen --library-dir <native-library>",
181
+ ],
182
+ },
183
+ OTA_HOST_INTEGRATION_REQUIRED: {
184
+ commands: [
185
+ "lynxship ota doctor --platform android",
186
+ "lynxship ota doctor --platform ios",
187
+ ],
188
+ note: "Integrate the LynxShip OTA client in the native host before publishing updates.",
189
+ },
190
+ OTA_NATIVE_CHANGE_REQUIRED: {
191
+ commands: [
192
+ "lynxship ota doctor --platform android",
193
+ "lynxship build --platform android --profile production",
194
+ "lynxship update --platform android --bundle dist/main.lynx.bundle",
195
+ ],
196
+ note: "Native changes require a new compatible binary before an OTA release.",
197
+ },
198
+ OTA_BUNDLE_REQUIRED: {
199
+ commands: [
200
+ "lynxship build --platform android --profile production",
201
+ "lynxship update --platform android --bundle dist/main.lynx.bundle",
202
+ ],
203
+ },
204
+ BUILD_REQUIRED: {
205
+ commands: ["lynxship build --platform android --profile production"],
206
+ },
207
+ STORE_ARTIFACT_REQUIRED: {
208
+ commands: [
209
+ "lynxship build --platform android --profile production",
210
+ "lynxship submit --platform android --latest",
211
+ ],
212
+ },
213
+ STORE_SUBMISSION_REQUIRED: {
214
+ commands: [
215
+ "lynxship store configure --platform android",
216
+ "lynxship submit --platform android --latest",
217
+ ],
218
+ },
219
+ RELEASE_NOT_FOUND: {
220
+ commands: [
221
+ "lynxship update --platform android",
222
+ "lynxship update rollback --help",
223
+ ],
224
+ },
225
+ ROLLBACK_RELEASE_REQUIRED: {
226
+ commands: [
227
+ 'lynxship update rollback --platform android --release-id <release-id> --reason "reason"',
228
+ ],
229
+ },
230
+ ROLLBACK_REASON_REQUIRED: {
231
+ commands: [
232
+ 'lynxship update rollback --platform android --release-id <release-id> --reason "reason"',
233
+ ],
234
+ },
235
+ CLI_INTERACTIVE_REQUIRED: {
236
+ commands: ["lynxship <command>"],
237
+ note: "Run without --non-interactive, or provide the documented CI environment variables.",
238
+ },
239
+ PROFILE_NOT_FOUND: {
240
+ commands: ["lynxship inspect", "lynxship build --profile production"],
241
+ note: "Use a profile declared under build in lynxship.json.",
242
+ },
243
+ PLATFORM_INVALID: {
244
+ commands: [
245
+ "lynxship build --platform android",
246
+ "lynxship build --platform ios",
247
+ "lynxship build --platform all",
248
+ ],
249
+ },
250
+ IOS_SIGNATURE_INVALID: {
251
+ commands: [
252
+ "lynxship doctor --platform ios",
253
+ "lynxship build --platform ios --profile production",
254
+ ],
255
+ note: "Check the Apple certificate, provisioning profile, team and export options.",
256
+ },
257
+ };
258
+ export function guidanceForError(error) {
259
+ const code = error.code;
260
+ if (typeof code === "string" && guidance[code])
261
+ return guidance[code];
262
+ if (typeof code === "string") {
263
+ if (code.startsWith("SUBMISSION_") || code.startsWith("AUTH_")) {
264
+ return {
265
+ commands: [
266
+ "lynxship store configure --platform android",
267
+ "lynxship submit --platform android --latest",
268
+ ],
269
+ note: "Check the provider credentials and required account permissions.",
270
+ };
271
+ }
272
+ if (code.startsWith("OTA_")) {
273
+ return {
274
+ commands: [
275
+ "lynxship ota doctor --platform android",
276
+ "lynxship update --platform android --bundle dist/main.lynx.bundle",
277
+ ],
278
+ };
279
+ }
280
+ if (code.startsWith("CONFIG_")) {
281
+ return {
282
+ commands: ["lynxship init", "lynxship doctor"],
283
+ };
284
+ }
285
+ if (code.startsWith("BUILD_")) {
286
+ return {
287
+ commands: [
288
+ "lynxship doctor --platform android",
289
+ "lynxship build --platform android --profile production",
290
+ ],
291
+ note: "Inspect the first failed build event before retrying.",
292
+ };
293
+ }
294
+ if (code.startsWith("STORE_")) {
295
+ return {
296
+ commands: [
297
+ "lynxship store configure --platform android",
298
+ "lynxship submit --platform android --latest",
299
+ ],
300
+ };
301
+ }
302
+ if (code.startsWith("IOS_")) {
303
+ return {
304
+ commands: [
305
+ "lynxship doctor --platform ios",
306
+ "lynxship build --platform ios --profile production",
307
+ ],
308
+ };
309
+ }
310
+ if (code.startsWith("ANDROID_")) {
311
+ return {
312
+ commands: [
313
+ "lynxship doctor --platform android",
314
+ "lynxship build --platform android --profile production",
315
+ ],
316
+ };
317
+ }
318
+ if (code.startsWith("CLI_")) {
319
+ return { commands: ["lynxship --help"] };
320
+ }
321
+ }
322
+ const message = error instanceof Error ? error.message : String(error);
323
+ if (/pnpm|npm|yarn|corepack|not recognized|not found/i.test(message)) {
324
+ return {
325
+ commands: ["corepack enable", "pnpm install", "lynxship doctor"],
326
+ note: "Use the package manager selected by the project's lockfile.",
327
+ };
328
+ }
329
+ if (/ENOENT|no such file or directory/i.test(message)) {
330
+ return {
331
+ commands: ["lynxship init", "lynxship doctor"],
332
+ note: "Check that the project directory and required configuration files exist.",
333
+ };
334
+ }
335
+ if (/gradle|gradlew|android sdk|build tools/i.test(message)) {
336
+ return {
337
+ commands: [
338
+ "lynxship doctor --platform android",
339
+ "lynxship build --platform android --profile production",
340
+ ],
341
+ note: "Check JDK 17, Android SDK, Build Tools and the project Gradle wrapper.",
342
+ };
343
+ }
344
+ if (/xcode|xcrun|cocoapods|pod install/i.test(message)) {
345
+ return {
346
+ commands: [
347
+ "lynxship doctor --platform ios",
348
+ "lynxship build --platform ios",
349
+ ],
350
+ note: "Check macOS, Xcode command-line tools and CocoaPods.",
351
+ };
352
+ }
353
+ return { commands: [] };
354
+ }