@lynxship/cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/README.md +122 -0
  2. package/dist/android-build.d.ts +14 -0
  3. package/dist/android-build.d.ts.map +1 -0
  4. package/dist/android-build.js +201 -0
  5. package/dist/artifact-name.d.ts +3 -0
  6. package/dist/artifact-name.d.ts.map +1 -0
  7. package/dist/artifact-name.js +4 -0
  8. package/dist/autolink.d.ts +14 -0
  9. package/dist/autolink.d.ts.map +1 -0
  10. package/dist/autolink.js +144 -0
  11. package/dist/config.d.ts +51 -0
  12. package/dist/config.d.ts.map +1 -0
  13. package/dist/config.js +62 -0
  14. package/dist/configure.d.ts +11 -0
  15. package/dist/configure.d.ts.map +1 -0
  16. package/dist/configure.js +172 -0
  17. package/dist/index.d.ts +3 -0
  18. package/dist/index.d.ts.map +1 -0
  19. package/dist/index.js +1082 -0
  20. package/dist/ios-build.d.ts +13 -0
  21. package/dist/ios-build.d.ts.map +1 -0
  22. package/dist/ios-build.js +174 -0
  23. package/dist/ota-assets.d.ts +3 -0
  24. package/dist/ota-assets.d.ts.map +1 -0
  25. package/dist/ota-assets.js +48 -0
  26. package/dist/ota-doctor.d.ts +10 -0
  27. package/dist/ota-doctor.d.ts.map +1 -0
  28. package/dist/ota-doctor.js +53 -0
  29. package/dist/paths.d.ts +2 -0
  30. package/dist/paths.d.ts.map +1 -0
  31. package/dist/paths.js +12 -0
  32. package/dist/process-runner.d.ts +18 -0
  33. package/dist/process-runner.d.ts.map +1 -0
  34. package/dist/process-runner.js +97 -0
  35. package/dist/prompt.d.ts +3 -0
  36. package/dist/prompt.d.ts.map +1 -0
  37. package/dist/prompt.js +58 -0
  38. package/dist/r2.d.ts +32 -0
  39. package/dist/r2.d.ts.map +1 -0
  40. package/dist/r2.js +135 -0
  41. package/dist/remote.d.ts +33 -0
  42. package/dist/remote.d.ts.map +1 -0
  43. package/dist/remote.js +116 -0
  44. package/dist/runtime-fingerprint.d.ts +10 -0
  45. package/dist/runtime-fingerprint.d.ts.map +1 -0
  46. package/dist/runtime-fingerprint.js +238 -0
  47. package/dist/secure-store.d.ts +32 -0
  48. package/dist/secure-store.d.ts.map +1 -0
  49. package/dist/secure-store.js +263 -0
  50. package/dist/ui/colors.d.ts +24 -0
  51. package/dist/ui/colors.d.ts.map +1 -0
  52. package/dist/ui/colors.js +64 -0
  53. package/dist/ui/components.d.ts +36 -0
  54. package/dist/ui/components.d.ts.map +1 -0
  55. package/dist/ui/components.js +268 -0
  56. package/dist/ui/index.d.ts +24 -0
  57. package/dist/ui/index.d.ts.map +1 -0
  58. package/dist/ui/index.js +68 -0
  59. package/dist/ui/logo.d.ts +3 -0
  60. package/dist/ui/logo.d.ts.map +1 -0
  61. package/dist/ui/logo.js +32 -0
  62. package/dist/ui/state.d.ts +5 -0
  63. package/dist/ui/state.d.ts.map +1 -0
  64. package/dist/ui/state.js +7 -0
  65. package/dist/ui/terminal.d.ts +12 -0
  66. package/dist/ui/terminal.d.ts.map +1 -0
  67. package/dist/ui/terminal.js +23 -0
  68. package/package.json +75 -0
package/README.md ADDED
@@ -0,0 +1,122 @@
1
+ # @lynxship/cli
2
+
3
+ Build, sign, store, submit and update LynxJS applications from the terminal.
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.
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.
13
+
14
+ ## Install
15
+
16
+ ```bash
17
+ npm install --global @lynxship/cli
18
+ ```
19
+
20
+ Or run it without a global install:
21
+
22
+ ```bash
23
+ npx @lynxship/cli doctor --project-dir ./my-lynx-app
24
+ ```
25
+
26
+ The package exposes the `lynxship` executable.
27
+
28
+ ## Requirements
29
+
30
+ - Node.js 22 LTS or newer; Node.js 24 LTS is the recommended production
31
+ baseline.
32
+ - A LynxJS project using Rspeedy.
33
+ - Android builds: JDK 17, Android SDK, `adb`, build tools and the project's
34
+ Gradle wrapper.
35
+ - iOS builds: macOS, Xcode and Xcode command-line tools.
36
+ - Cloudflare R2 credentials for artifact storage.
37
+
38
+ ## First configuration
39
+
40
+ Run these once on each development or CI machine:
41
+
42
+ ```bash
43
+ lynxship storage configure
44
+ lynxship android configure
45
+ lynxship store configure --platform android
46
+ ```
47
+
48
+ Secret inputs are hidden. Credentials are stored outside the project using the
49
+ OS-specific secure storage path. They are never written to `lynxship.json`.
50
+
51
+ The project itself is initialized automatically by `build` when needed, or
52
+ explicitly with:
53
+
54
+ ```bash
55
+ lynxship init --project-dir ./my-lynx-app
56
+ ```
57
+
58
+ ## Build a signed Android artifact
59
+
60
+ ```bash
61
+ lynxship build \
62
+ --project-dir ./my-lynx-app \
63
+ --platform android \
64
+ --profile production
65
+ ```
66
+
67
+ The interactive build journal follows the real pipeline:
68
+
69
+ ```text
70
+ Rspeedy bundle
71
+ -> Android asset synchronization
72
+ -> Android SDK and Gradle
73
+ -> release APK or AAB
74
+ -> signature verification
75
+ -> UUID artifact name
76
+ -> Cloudflare R2 upload
77
+ -> expiring download URL and QR code
78
+ ```
79
+
80
+ Progress percentages are shown only when LynxShip has a real measurement. Long
81
+ Rspeedy, Gradle and Xcode operations remain visible in the event journal until
82
+ their completion checkpoint is known; no timer-based percentage is invented.
83
+
84
+ ## Main commands
85
+
86
+ ```text
87
+ init Initialize or link a project
88
+ doctor Check the local toolchain and project
89
+ dev Run the Rspeedy development server
90
+ preview Preview the production bundle locally
91
+ build Build, sign and upload an artifact
92
+ submit Submit the latest successful artifact
93
+ update Publish a signed OTA update
94
+ run Install an artifact on a target
95
+ logs Stream native logs
96
+ autolink check Check Lynx native-library wiring
97
+ autolink codegen Run native-module codegen
98
+ ota doctor Check native OTA host integration
99
+ storage configure Configure Cloudflare R2
100
+ android configure Configure Android signing
101
+ store configure Configure store submission credentials
102
+ ```
103
+
104
+ Use `lynxship --help` or `lynxship <command> --help` for the complete option
105
+ list. `--json`, `--quiet`, `--no-color` and `--non-interactive` are available
106
+ for automation.
107
+
108
+ ## OTA safety
109
+
110
+ OTA updates are for JavaScript and assets compatible with the installed native
111
+ runtime. If native code, permissions, autolinked modules or other runtime
112
+ inputs change, LynxShip blocks the OTA and requires a new signed binary build.
113
+
114
+ ## Package layout
115
+
116
+ The CLI is backed by the public `@lynxship/*` runtime packages in this
117
+ workspace. They are published with the same version and must be available in
118
+ the configured npm scope before installing the CLI package.
119
+
120
+ ## License
121
+
122
+ MIT
@@ -0,0 +1,14 @@
1
+ import { type BuildJob } from "@lynxship/contracts";
2
+ import type { BuildProfile } from "./config.js";
3
+ interface AndroidBuildOptions {
4
+ root: string;
5
+ profile: BuildProfile;
6
+ quiet?: boolean;
7
+ onStep?: (message: string) => void;
8
+ onEvent?: (message: string) => void;
9
+ onProgress?: (value?: number, label?: string) => void;
10
+ }
11
+ export declare function hasAndroidHost(root: string): Promise<boolean>;
12
+ export declare function runRealAndroidBuild(job: BuildJob, options: AndroidBuildOptions): Promise<BuildJob>;
13
+ export {};
14
+ //# sourceMappingURL=android-build.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"android-build.d.ts","sourceRoot":"","sources":["../src/android-build.ts"],"names":[],"mappings":"AAIA,OAAO,EAAkB,KAAK,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAEpE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAUhD,UAAU,mBAAmB;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,YAAY,CAAC;IACtB,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;AAoID,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAU7D;AAED,wBAAsB,mBAAmB,CACvC,GAAG,EAAE,QAAQ,EACb,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,QAAQ,CAAC,CAoInB"}
@@ -0,0 +1,201 @@
1
+ import { access, copyFile, mkdir, readFile } from "node:fs/promises";
2
+ import { execFileSync } from "node:child_process";
3
+ import { existsSync, readdirSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { assert, sha256 } from "@lynxship/contracts";
6
+ import { transitionBuild } from "@lynxship/build-orchestrator";
7
+ import { loadR2, uploadR2Artifact } from "./r2.js";
8
+ import { loadCredentials } from "./secure-store.js";
9
+ import { nativeArtifactName } from "./artifact-name.js";
10
+ import { commandExists, packageManagerScriptCommand, runProcess, } from "./process-runner.js";
11
+ async function projectBuildCommand(root) {
12
+ try {
13
+ const packageJson = JSON.parse(await readFile(join(root, "package.json"), "utf8"));
14
+ if (packageJson.scripts?.["build:mobile"])
15
+ return ["run", "build:mobile"];
16
+ }
17
+ catch {
18
+ // The package manager will report the useful project error below.
19
+ }
20
+ return ["run", "build"];
21
+ }
22
+ async function signingEnvironment(root) {
23
+ const android = (await loadCredentials(root)).android;
24
+ const values = {
25
+ LYNXSHIP_KEYSTORE_PATH: process.env.LYNXSHIP_KEYSTORE_PATH ?? android?.keystorePath,
26
+ LYNXSHIP_KEY_ALIAS: process.env.LYNXSHIP_KEY_ALIAS ?? android?.keyAlias,
27
+ LYNXSHIP_KEYSTORE_PASSWORD: process.env.LYNXSHIP_KEYSTORE_PASSWORD ?? android?.keystorePassword,
28
+ LYNXSHIP_KEY_PASSWORD: process.env.LYNXSHIP_KEY_PASSWORD ?? android?.keyPassword,
29
+ };
30
+ const missing = Object.entries(values)
31
+ .filter(([, value]) => !value)
32
+ .map(([name]) => name);
33
+ assert(missing.length === 0, "BUILD_SIGNING_REQUIRED", `Signed Android builds require configuration. Missing: ${missing.join(", ")}. Run \`lynxship android configure\`.`);
34
+ return { ...process.env, ...values };
35
+ }
36
+ async function verifySignedArtifact(root, artifactPath, options) {
37
+ if (artifactPath.endsWith(".apk")) {
38
+ const apksigner = androidTool("apksigner");
39
+ assert(apksigner, "ANDROID_APKSIGNER_REQUIRED", "apksigner was not found in PATH. Install the Android SDK Build Tools.");
40
+ await runProcess(apksigner, ["verify", "--verbose", artifactPath], {
41
+ cwd: root,
42
+ ...options,
43
+ });
44
+ return;
45
+ }
46
+ const jarsigner = commandExists("jarsigner") ? "jarsigner" : undefined;
47
+ assert(jarsigner, "ANDROID_JARSIGNER_REQUIRED", "jarsigner was not found in PATH. Install JDK 17.");
48
+ await runProcess(jarsigner, ["-verify", "-verbose", "-certs", artifactPath], {
49
+ cwd: root,
50
+ ...options,
51
+ });
52
+ }
53
+ function androidTool(name) {
54
+ if (commandExists(name)) {
55
+ if (process.platform !== "win32")
56
+ return name;
57
+ try {
58
+ return execFileSync("where.exe", [name], { encoding: "utf8" })
59
+ .split(/\r?\n/)
60
+ .map((value) => value.trim())
61
+ .find(Boolean);
62
+ }
63
+ catch {
64
+ return name;
65
+ }
66
+ }
67
+ const sdk = process.env.ANDROID_SDK_ROOT ?? process.env.ANDROID_HOME ?? undefined;
68
+ if (!sdk)
69
+ return undefined;
70
+ const executable = process.platform === "win32" ? `${name}.bat` : name;
71
+ try {
72
+ return readdirSync(join(sdk, "build-tools"))
73
+ .sort()
74
+ .reverse()
75
+ .map((version) => join(sdk, "build-tools", version, executable))
76
+ .find((candidate) => existsSync(candidate));
77
+ }
78
+ catch {
79
+ return undefined;
80
+ }
81
+ }
82
+ function artifactDetails(root, profile) {
83
+ const artifact = profile.android?.artifact ?? "apk";
84
+ assert(artifact === "apk" || artifact === "aab", "BUILD_ARTIFACT_INVALID", "Android artifact must be apk or aab");
85
+ return artifact === "aab"
86
+ ? {
87
+ task: "bundleRelease",
88
+ path: join(root, "android", "app", "build", "outputs", "bundle", "release", "app-release.aab"),
89
+ }
90
+ : {
91
+ task: "assembleRelease",
92
+ path: join(root, "android", "app", "build", "outputs", "apk", "release", "app-release.apk"),
93
+ };
94
+ }
95
+ export function hasAndroidHost(root) {
96
+ return access(join(root, "android", process.platform === "win32" ? "gradlew.bat" : "gradlew"))
97
+ .then(() => true)
98
+ .catch(() => false);
99
+ }
100
+ export async function runRealAndroidBuild(job, options) {
101
+ const android = join(options.root, "android");
102
+ const wrapper = process.platform === "win32" ? "gradlew.bat" : "./gradlew";
103
+ const artifact = artifactDetails(options.root, options.profile);
104
+ await loadR2(options.root);
105
+ const environment = {
106
+ ...(await signingEnvironment(options.root)),
107
+ LYNXSHIP_RUNTIME_VERSION: process.env.LYNXSHIP_RUNTIME_VERSION ?? job.runtimeVersion ?? "",
108
+ };
109
+ const step = (message, progress) => {
110
+ options.onStep?.(message);
111
+ options.onEvent?.(message);
112
+ if (progress !== undefined)
113
+ options.onProgress?.(progress, message);
114
+ };
115
+ const processOptions = {
116
+ quiet: options.quiet ?? false,
117
+ onOutput: options.onEvent,
118
+ };
119
+ try {
120
+ transitionBuild(job, "uploading_source", "local Android source prepared");
121
+ job.logs.push({
122
+ level: "info",
123
+ message: "rspeedy:build",
124
+ at: new Date().toISOString(),
125
+ });
126
+ step("Building Lynx bundle with Rspeedy…");
127
+ const packageManager = packageManagerScriptCommand(options.root, (await projectBuildCommand(options.root))[1] ?? "build");
128
+ await runProcess(packageManager.command, packageManager.args, {
129
+ cwd: options.root,
130
+ env: environment,
131
+ ...processOptions,
132
+ });
133
+ step("Rspeedy bundle ready", 20);
134
+ transitionBuild(job, "queued", "Android build queued locally");
135
+ step("Build queued…");
136
+ transitionBuild(job, "provisioning", "local Android toolchain selected");
137
+ step("Checking Android SDK and Gradle toolchain…");
138
+ transitionBuild(job, "installing_dependencies", "syncing Lynx bundle into Android assets");
139
+ step("Syncing bundle into the Android host…");
140
+ await runProcess(process.execPath, [join("android", "sync-bundle.mjs")], {
141
+ cwd: options.root,
142
+ env: environment,
143
+ ...processOptions,
144
+ });
145
+ step("Android host synchronized", 40);
146
+ transitionBuild(job, "building", `Gradle ${artifact.task}`);
147
+ step(`Running real Gradle task ${artifact.task}…`);
148
+ await runProcess(wrapper, [artifact.task], {
149
+ cwd: android,
150
+ env: environment,
151
+ ...processOptions,
152
+ });
153
+ step(`Gradle ${artifact.task} completed`, 60);
154
+ transitionBuild(job, "signing", "Gradle release signing completed");
155
+ step("Verifying Android release signature…");
156
+ await verifySignedArtifact(options.root, artifact.path, processOptions);
157
+ step("Android release signature verified", 75);
158
+ transitionBuild(job, "uploading_artifacts", "local artifact collected");
159
+ const artifactName = nativeArtifactName(artifact.path.endsWith(".aab") ? "aab" : "apk");
160
+ const artifactDirectory = join(options.root, ".lynxship", "artifacts");
161
+ const artifactPath = join(artifactDirectory, artifactName);
162
+ await mkdir(artifactDirectory, { recursive: true });
163
+ await copyFile(artifact.path, artifactPath);
164
+ const content = await readFile(artifactPath);
165
+ const hash = sha256(content);
166
+ job.attempts += 1;
167
+ step("Uploading signed artifact to Cloudflare R2…", 80);
168
+ const uploaded = await uploadR2Artifact(options.root, job.projectId, job.id, artifactPath, artifactName.endsWith(".aab")
169
+ ? "application/octet-stream"
170
+ : "application/vnd.android.package-archive", undefined, {
171
+ onProgress: (uploadedBytes, totalBytes) => {
172
+ const transfer = totalBytes === 0 ? 1 : uploadedBytes / totalBytes;
173
+ options.onProgress?.(80 + transfer * 19, `Uploading signed artifact to Cloudflare R2… ${Math.round(transfer * 10000) / 100}%`);
174
+ },
175
+ });
176
+ assert(uploaded.hash === hash, "BUILD_ARTIFACT_HASH", "R2 artifact hash mismatch");
177
+ job.artifact = {
178
+ name: artifactName,
179
+ hash,
180
+ path: artifactPath,
181
+ key: uploaded.key,
182
+ size: uploaded.size,
183
+ contentType: uploaded.contentType,
184
+ url: uploaded.url,
185
+ expiresAt: uploaded.expiresAt,
186
+ };
187
+ step(`Artifact ready: ${artifactName}`, 100);
188
+ return transitionBuild(job, "success", "real Android artifact created");
189
+ }
190
+ catch (error) {
191
+ if (!["success", "failed", "canceled", "timed_out"].includes(job.state)) {
192
+ transitionBuild(job, "failed", error instanceof Error ? error.message : "Android build failed");
193
+ }
194
+ job.logs.push({
195
+ level: "error",
196
+ message: error instanceof Error ? error.message : "Android build failed",
197
+ at: new Date().toISOString(),
198
+ });
199
+ throw error;
200
+ }
201
+ }
@@ -0,0 +1,3 @@
1
+ export type NativeArtifactExtension = "apk" | "aab" | "ipa";
2
+ export declare function nativeArtifactName(extension: NativeArtifactExtension): string;
3
+ //# sourceMappingURL=artifact-name.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"artifact-name.d.ts","sourceRoot":"","sources":["../src/artifact-name.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,uBAAuB,GAAG,KAAK,GAAG,KAAK,GAAG,KAAK,CAAC;AAE5D,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,uBAAuB,GAAG,MAAM,CAE7E"}
@@ -0,0 +1,4 @@
1
+ import { randomUUID } from "node:crypto";
2
+ export function nativeArtifactName(extension) {
3
+ return `${randomUUID()}.${extension}`;
4
+ }
@@ -0,0 +1,14 @@
1
+ import { type Platform } from "@lynxship/contracts";
2
+ export interface AutolinkPlatformStatus {
3
+ required: boolean;
4
+ ready: boolean;
5
+ manifests: string[];
6
+ reason: string;
7
+ }
8
+ export interface AutolinkStatus {
9
+ android: AutolinkPlatformStatus;
10
+ ios: AutolinkPlatformStatus;
11
+ }
12
+ export declare function inspectAutolink(root: string): Promise<AutolinkStatus>;
13
+ export declare function requireAutolinkReady(root: string, platform: Platform): Promise<AutolinkPlatformStatus>;
14
+ //# sourceMappingURL=autolink.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"autolink.d.ts","sourceRoot":"","sources":["../src/autolink.ts"],"names":[],"mappings":"AAGA,OAAO,EAAU,KAAK,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAM5D,MAAM,WAAW,sBAAsB;IACrC,QAAQ,EAAE,OAAO,CAAC;IAClB,KAAK,EAAE,OAAO,CAAC;IACf,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,sBAAsB,CAAC;IAChC,GAAG,EAAE,sBAAsB,CAAC;CAC7B;AAqID,wBAAsB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC,CAM3E;AAED,wBAAsB,oBAAoB,CACxC,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,QAAQ,GACjB,OAAO,CAAC,sBAAsB,CAAC,CAWjC"}
@@ -0,0 +1,144 @@
1
+ import { existsSync } from "node:fs";
2
+ import { readdir, readFile, realpath, stat } from "node:fs/promises";
3
+ import { dirname, join } from "node:path";
4
+ import { assert } from "@lynxship/contracts";
5
+ const emptyStatus = () => ({
6
+ required: false,
7
+ ready: true,
8
+ manifests: [],
9
+ reason: "No Lynx native-library manifest found",
10
+ });
11
+ function findNodeModuleRoots(root) {
12
+ const roots = [];
13
+ let current = root;
14
+ while (true) {
15
+ roots.push(join(current, "node_modules"));
16
+ const parent = dirname(current);
17
+ if (parent === current)
18
+ break;
19
+ current = parent;
20
+ }
21
+ return roots;
22
+ }
23
+ async function findManifests(root) {
24
+ const result = new Set();
25
+ const visited = new Set();
26
+ async function visit(current, depth) {
27
+ if (depth > 7)
28
+ return;
29
+ let directory;
30
+ try {
31
+ if (!(await stat(current)).isDirectory())
32
+ return;
33
+ directory = await realpath(current);
34
+ }
35
+ catch {
36
+ return;
37
+ }
38
+ if (visited.has(directory))
39
+ return;
40
+ visited.add(directory);
41
+ let entries;
42
+ try {
43
+ entries = await readdir(current, { withFileTypes: true });
44
+ }
45
+ catch {
46
+ return;
47
+ }
48
+ for (const entry of entries) {
49
+ const file = join(current, entry.name);
50
+ if (entry.isFile() && entry.name === "lynx.lib.json") {
51
+ result.add(file);
52
+ continue;
53
+ }
54
+ if (entry.name === ".cache")
55
+ continue;
56
+ if (entry.isDirectory()) {
57
+ await visit(file, depth + 1);
58
+ continue;
59
+ }
60
+ if (entry.isSymbolicLink()) {
61
+ try {
62
+ if ((await stat(file)).isDirectory())
63
+ await visit(file, depth + 1);
64
+ }
65
+ catch {
66
+ // Broken package links are ignored; the package manager reports them.
67
+ }
68
+ }
69
+ }
70
+ }
71
+ for (const moduleRoot of findNodeModuleRoots(root))
72
+ await visit(moduleRoot, 0);
73
+ return [...result].sort();
74
+ }
75
+ async function hasFile(file) {
76
+ return existsSync(file);
77
+ }
78
+ async function platformStatus(root, platform, manifests) {
79
+ const relevant = [];
80
+ for (const file of manifests) {
81
+ try {
82
+ const manifest = JSON.parse(await readFile(file, "utf8"));
83
+ if (manifest.platforms?.[platform] !== undefined)
84
+ relevant.push(file);
85
+ }
86
+ catch {
87
+ relevant.push(file);
88
+ }
89
+ }
90
+ if (relevant.length === 0)
91
+ return emptyStatus();
92
+ if (platform === "android") {
93
+ const settingsFile = [
94
+ join(root, "android", "settings.gradle"),
95
+ join(root, "android", "settings.gradle.kts"),
96
+ ].find((file) => existsSync(file));
97
+ const appFile = [
98
+ join(root, "android", "app", "build.gradle"),
99
+ join(root, "android", "app", "build.gradle.kts"),
100
+ ].find((file) => existsSync(file));
101
+ const settingsText = settingsFile
102
+ ? await readFile(settingsFile, "utf8")
103
+ : "";
104
+ const appText = appFile ? await readFile(appFile, "utf8") : "";
105
+ const ready = settingsText.includes("org.lynxsdk.library-settings") &&
106
+ appText.includes("org.lynxsdk.library-build");
107
+ return {
108
+ required: true,
109
+ ready,
110
+ manifests: relevant,
111
+ reason: ready
112
+ ? "Lynx Android Autolink plugins are enabled"
113
+ : "Enable org.lynxsdk.library-settings in settings.gradle and org.lynxsdk.library-build in app/build.gradle",
114
+ };
115
+ }
116
+ const podfile = join(root, "ios", "Podfile");
117
+ const podfileText = (await hasFile(podfile))
118
+ ? await readFile(podfile, "utf8")
119
+ : "";
120
+ const ready = podfileText.includes("cocoapods-lynx-library") &&
121
+ podfileText.includes("use_lynx_library!");
122
+ return {
123
+ required: true,
124
+ ready,
125
+ manifests: relevant,
126
+ reason: ready
127
+ ? "Lynx iOS Autolink CocoaPods integration is enabled"
128
+ : "Add cocoapods-lynx-library and use_lynx_library! to ios/Podfile",
129
+ };
130
+ }
131
+ export async function inspectAutolink(root) {
132
+ const manifests = await findManifests(root);
133
+ return {
134
+ android: await platformStatus(root, "android", manifests),
135
+ ios: await platformStatus(root, "ios", manifests),
136
+ };
137
+ }
138
+ export async function requireAutolinkReady(root, platform) {
139
+ const status = (await inspectAutolink(root))[platform];
140
+ assert(!status.required || status.ready, platform === "android"
141
+ ? "LYNX_AUTOLINK_ANDROID_REQUIRED"
142
+ : "LYNX_AUTOLINK_IOS_REQUIRED", status.reason, { platform, manifests: status.manifests });
143
+ return status;
144
+ }
@@ -0,0 +1,51 @@
1
+ import { type Platform } from "@lynxship/contracts";
2
+ export interface BuildProfile {
3
+ distribution?: string;
4
+ channel?: string;
5
+ environment?: string;
6
+ android?: {
7
+ artifact?: string;
8
+ };
9
+ ios?: {
10
+ configuration?: string;
11
+ distribution?: string;
12
+ workspace?: string;
13
+ project?: string;
14
+ scheme?: string;
15
+ exportOptionsPlist?: string;
16
+ bundleScript?: string;
17
+ };
18
+ }
19
+ export interface LynxShipConfig {
20
+ projectId?: string;
21
+ cli?: {
22
+ apiUrl?: string;
23
+ organizationId?: string;
24
+ token?: string;
25
+ };
26
+ runtimeVersion?: {
27
+ policy: "fingerprint" | "manual";
28
+ value?: string;
29
+ };
30
+ build?: Record<string, BuildProfile>;
31
+ update?: {
32
+ protocolVersion?: number;
33
+ channel?: string;
34
+ rollout?: {
35
+ defaultPercentage?: number;
36
+ };
37
+ };
38
+ [key: string]: unknown;
39
+ }
40
+ export declare const DEFAULT_CONFIG: LynxShipConfig;
41
+ export declare function validateConfig(config: unknown, options?: {
42
+ ci?: boolean;
43
+ }): LynxShipConfig;
44
+ export declare function loadConfig(root: string, options?: {
45
+ ci?: boolean;
46
+ }): Promise<LynxShipConfig>;
47
+ export declare function resolveProfile(config: LynxShipConfig, name?: string): BuildProfile & {
48
+ name: string;
49
+ };
50
+ export declare function platformValue(value: string): Platform;
51
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAEA,OAAO,EAAU,KAAK,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAE5D,MAAM,WAAW,YAAY;IAC3B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAChC,GAAG,CAAC,EAAE;QACJ,aAAa,CAAC,EAAE,MAAM,CAAC;QACvB,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,kBAAkB,CAAC,EAAE,MAAM,CAAC;QAC5B,YAAY,CAAC,EAAE,MAAM,CAAC;KACvB,CAAC;CACH;AAED,MAAM,WAAW,cAAc;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE;QACJ,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,cAAc,CAAC,EAAE,MAAM,CAAC;QACxB,KAAK,CAAC,EAAE,MAAM,CAAC;KAChB,CAAC;IACF,cAAc,CAAC,EAAE;QAAE,MAAM,EAAE,aAAa,GAAG,QAAQ,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACtE,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IACrC,MAAM,CAAC,EAAE;QACP,eAAe,CAAC,EAAE,MAAM,CAAC;QACzB,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,OAAO,CAAC,EAAE;YAAE,iBAAiB,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC;KAC1C,CAAC;IACF,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,eAAO,MAAM,cAAc,EAAE,cAU5B,CAAC;AAaF,wBAAgB,cAAc,CAC5B,MAAM,EAAE,OAAO,EACf,OAAO,GAAE;IAAE,EAAE,CAAC,EAAE,OAAO,CAAA;CAAO,GAC7B,cAAc,CAyChB;AAED,wBAAsB,UAAU,CAC9B,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE;IAAE,EAAE,CAAC,EAAE,OAAO,CAAA;CAAO,GAC7B,OAAO,CAAC,cAAc,CAAC,CAazB;AAED,wBAAgB,cAAc,CAC5B,MAAM,EAAE,cAAc,EACtB,IAAI,SAAe,GAClB,YAAY,GAAG;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAIjC;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,QAAQ,CAOrD"}
package/dist/config.js ADDED
@@ -0,0 +1,62 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { assert } from "@lynxship/contracts";
4
+ export const DEFAULT_CONFIG = {
5
+ runtimeVersion: { policy: "fingerprint" },
6
+ build: {
7
+ production: {
8
+ distribution: "store",
9
+ channel: "production",
10
+ environment: "production",
11
+ },
12
+ },
13
+ update: { protocolVersion: 1, channel: "production" },
14
+ };
15
+ const allowedRootKeys = new Set([
16
+ "$schema",
17
+ "projectId",
18
+ "cli",
19
+ "runtimeVersion",
20
+ "build",
21
+ "submit",
22
+ "update",
23
+ "artifacts",
24
+ ]);
25
+ export function validateConfig(config, options = {}) {
26
+ assert(config && typeof config === "object" && !Array.isArray(config), "CONFIG_INVALID", "lynxship.json must contain an object");
27
+ const value = config;
28
+ const unknown = Object.keys(value).filter((key) => !allowedRootKeys.has(key));
29
+ assert(!options.ci || unknown.length === 0, "CONFIG_UNKNOWN_KEY", `Unknown configuration key(s): ${unknown.join(", ")}`, { unknown });
30
+ if (value.projectId !== undefined)
31
+ assert(typeof value.projectId === "string" && value.projectId.length > 0, "CONFIG_PROJECT_ID", "projectId must be a non-empty string");
32
+ assert(value.runtimeVersion?.policy === undefined ||
33
+ ["fingerprint", "manual"].includes(value.runtimeVersion.policy), "CONFIG_RUNTIME_POLICY", "runtimeVersion.policy must be fingerprint or manual");
34
+ if (value.runtimeVersion?.policy === "manual")
35
+ assert(typeof value.runtimeVersion.value === "string" &&
36
+ value.runtimeVersion.value.length > 0, "CONFIG_RUNTIME_VALUE", "runtimeVersion.value is required when runtimeVersion.policy is manual");
37
+ const percentage = value.update?.rollout?.defaultPercentage;
38
+ if (percentage !== undefined)
39
+ assert(Number.isInteger(percentage) && percentage >= 0 && percentage <= 100, "CONFIG_ROLLOUT", "rollout percentage must be an integer between 0 and 100");
40
+ return value;
41
+ }
42
+ export async function loadConfig(root, options = {}) {
43
+ try {
44
+ return validateConfig(JSON.parse(await readFile(join(root, "lynxship.json"), "utf8")), options);
45
+ }
46
+ catch (error) {
47
+ if (error.code === "ENOENT")
48
+ return validateConfig(DEFAULT_CONFIG, options);
49
+ if (error instanceof SyntaxError)
50
+ throw new Error(`Invalid JSON in ${join(root, "lynxship.json")}`);
51
+ throw error;
52
+ }
53
+ }
54
+ export function resolveProfile(config, name = "production") {
55
+ const profile = config.build?.[name];
56
+ assert(profile, "PROFILE_NOT_FOUND", `Build profile '${name}' was not found`);
57
+ return { name, ...profile };
58
+ }
59
+ export function platformValue(value) {
60
+ assert(value === "android" || value === "ios", "PLATFORM_INVALID", "Platform must be android or ios");
61
+ return value;
62
+ }
@@ -0,0 +1,11 @@
1
+ import { type R2Config } from "./r2.js";
2
+ export declare function configureR2(root: string): Promise<R2Config>;
3
+ interface AndroidConfigurationResult {
4
+ keystorePath: string;
5
+ generated: boolean;
6
+ }
7
+ export declare function configureGooglePlay(root: string): Promise<void>;
8
+ export declare function configureAppStoreConnect(root: string): Promise<void>;
9
+ export declare function configureAndroid(root: string): Promise<AndroidConfigurationResult>;
10
+ export {};
11
+ //# sourceMappingURL=configure.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"configure.d.ts","sourceRoot":"","sources":["../src/configure.ts"],"names":[],"mappings":"AAMA,OAAO,EAIL,KAAK,QAAQ,EACd,MAAM,SAAS,CAAC;AAQjB,wBAAsB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC,CA2BjE;AAED,UAAU,0BAA0B;IAClC,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,OAAO,CAAC;CACpB;AAED,wBAAsB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CA6CrE;AAED,wBAAsB,wBAAwB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAuC1E;AA+ED,wBAAsB,gBAAgB,CACpC,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,0BAA0B,CAAC,CAoCrC"}