@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.
- package/README.md +122 -0
- package/dist/android-build.d.ts +14 -0
- package/dist/android-build.d.ts.map +1 -0
- package/dist/android-build.js +201 -0
- package/dist/artifact-name.d.ts +3 -0
- package/dist/artifact-name.d.ts.map +1 -0
- package/dist/artifact-name.js +4 -0
- package/dist/autolink.d.ts +14 -0
- package/dist/autolink.d.ts.map +1 -0
- package/dist/autolink.js +144 -0
- package/dist/config.d.ts +51 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +62 -0
- package/dist/configure.d.ts +11 -0
- package/dist/configure.d.ts.map +1 -0
- package/dist/configure.js +172 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1082 -0
- package/dist/ios-build.d.ts +13 -0
- package/dist/ios-build.d.ts.map +1 -0
- package/dist/ios-build.js +174 -0
- package/dist/ota-assets.d.ts +3 -0
- package/dist/ota-assets.d.ts.map +1 -0
- package/dist/ota-assets.js +48 -0
- package/dist/ota-doctor.d.ts +10 -0
- package/dist/ota-doctor.d.ts.map +1 -0
- package/dist/ota-doctor.js +53 -0
- package/dist/paths.d.ts +2 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/paths.js +12 -0
- package/dist/process-runner.d.ts +18 -0
- package/dist/process-runner.d.ts.map +1 -0
- package/dist/process-runner.js +97 -0
- package/dist/prompt.d.ts +3 -0
- package/dist/prompt.d.ts.map +1 -0
- package/dist/prompt.js +58 -0
- package/dist/r2.d.ts +32 -0
- package/dist/r2.d.ts.map +1 -0
- package/dist/r2.js +135 -0
- package/dist/remote.d.ts +33 -0
- package/dist/remote.d.ts.map +1 -0
- package/dist/remote.js +116 -0
- package/dist/runtime-fingerprint.d.ts +10 -0
- package/dist/runtime-fingerprint.d.ts.map +1 -0
- package/dist/runtime-fingerprint.js +238 -0
- package/dist/secure-store.d.ts +32 -0
- package/dist/secure-store.d.ts.map +1 -0
- package/dist/secure-store.js +263 -0
- package/dist/ui/colors.d.ts +24 -0
- package/dist/ui/colors.d.ts.map +1 -0
- package/dist/ui/colors.js +64 -0
- package/dist/ui/components.d.ts +36 -0
- package/dist/ui/components.d.ts.map +1 -0
- package/dist/ui/components.js +268 -0
- package/dist/ui/index.d.ts +24 -0
- package/dist/ui/index.d.ts.map +1 -0
- package/dist/ui/index.js +68 -0
- package/dist/ui/logo.d.ts +3 -0
- package/dist/ui/logo.d.ts.map +1 -0
- package/dist/ui/logo.js +32 -0
- package/dist/ui/state.d.ts +5 -0
- package/dist/ui/state.d.ts.map +1 -0
- package/dist/ui/state.js +7 -0
- package/dist/ui/terminal.d.ts +12 -0
- package/dist/ui/terminal.d.ts.map +1 -0
- package/dist/ui/terminal.js +23 -0
- 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 @@
|
|
|
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,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"}
|
package/dist/autolink.js
ADDED
|
@@ -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
|
+
}
|
package/dist/config.d.ts
ADDED
|
@@ -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"}
|