@lynxship/cli 0.1.20 → 0.1.21
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 +66 -5
- package/dist/android/signing.d.ts +16 -0
- package/dist/android/signing.d.ts.map +1 -0
- package/dist/android/signing.js +131 -0
- package/dist/android/types.d.ts +12 -0
- package/dist/android/types.d.ts.map +1 -0
- package/dist/android/types.js +1 -0
- package/dist/android-build.d.ts +4 -13
- package/dist/android-build.d.ts.map +1 -1
- package/dist/android-build.js +14 -133
- package/dist/android-toolchain/inspection.d.ts +3 -0
- package/dist/android-toolchain/inspection.d.ts.map +1 -0
- package/dist/android-toolchain/inspection.js +122 -0
- package/dist/android-toolchain/probes.d.ts +23 -0
- package/dist/android-toolchain/probes.d.ts.map +1 -0
- package/dist/android-toolchain/probes.js +183 -0
- package/dist/android-toolchain/types.d.ts +20 -0
- package/dist/android-toolchain/types.d.ts.map +1 -0
- package/dist/android-toolchain/types.js +1 -0
- package/dist/android-toolchain.d.ts +4 -20
- package/dist/android-toolchain.d.ts.map +1 -1
- package/dist/android-toolchain.js +4 -300
- package/dist/app-config.d.ts +25 -0
- package/dist/app-config.d.ts.map +1 -0
- package/dist/app-config.js +106 -0
- package/dist/bundle-build.d.ts.map +1 -1
- package/dist/bundle-build.js +11 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +66 -0
- package/dist/commands/build-execution.d.ts +41 -0
- package/dist/commands/build-execution.d.ts.map +1 -0
- package/dist/commands/build-execution.js +182 -0
- package/dist/commands/configuration.d.ts +19 -0
- package/dist/commands/configuration.d.ts.map +1 -0
- package/dist/commands/configuration.js +174 -0
- package/dist/commands/development.d.ts +22 -0
- package/dist/commands/development.d.ts.map +1 -0
- package/dist/commands/development.js +132 -0
- package/dist/commands/device.d.ts +3 -0
- package/dist/commands/device.d.ts.map +1 -0
- package/dist/commands/device.js +86 -0
- package/dist/commands/doctor.d.ts +18 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +320 -0
- package/dist/commands/help.d.ts +2 -0
- package/dist/commands/help.d.ts.map +1 -0
- package/dist/commands/help.js +118 -0
- package/dist/commands/i18n.d.ts +14 -0
- package/dist/commands/i18n.d.ts.map +1 -0
- package/dist/commands/i18n.js +88 -0
- package/dist/commands/logs.d.ts +3 -0
- package/dist/commands/logs.d.ts.map +1 -0
- package/dist/commands/logs.js +51 -0
- package/dist/commands/metadata.d.ts +3 -0
- package/dist/commands/metadata.d.ts.map +1 -0
- package/dist/commands/metadata.js +45 -0
- package/dist/commands/ota.d.ts +28 -0
- package/dist/commands/ota.d.ts.map +1 -0
- package/dist/commands/ota.js +203 -0
- package/dist/commands/project.d.ts +14 -0
- package/dist/commands/project.d.ts.map +1 -0
- package/dist/commands/project.js +122 -0
- package/dist/commands/remote-build.d.ts +35 -0
- package/dist/commands/remote-build.d.ts.map +1 -0
- package/dist/commands/remote-build.js +73 -0
- package/dist/commands/submit.d.ts +27 -0
- package/dist/commands/submit.d.ts.map +1 -0
- package/dist/commands/submit.js +92 -0
- package/dist/ecosystem.d.ts +9 -0
- package/dist/ecosystem.d.ts.map +1 -0
- package/dist/ecosystem.js +95 -0
- package/dist/frameworks.d.ts.map +1 -1
- package/dist/frameworks.js +5 -1
- package/dist/guidance/catalog.d.ts +3 -0
- package/dist/guidance/catalog.d.ts.map +1 -0
- package/dist/guidance/catalog.js +504 -0
- package/dist/guidance/types.d.ts +10 -0
- package/dist/guidance/types.d.ts.map +1 -0
- package/dist/guidance/types.js +1 -0
- package/dist/guidance.d.ts +2 -9
- package/dist/guidance.d.ts.map +1 -1
- package/dist/guidance.js +1 -502
- package/dist/i18n/discovery.d.ts +14 -0
- package/dist/i18n/discovery.d.ts.map +1 -0
- package/dist/i18n/discovery.js +106 -0
- package/dist/i18n/plan.d.ts +12 -0
- package/dist/i18n/plan.d.ts.map +1 -0
- package/dist/i18n/plan.js +95 -0
- package/dist/i18n/setup.d.ts +11 -0
- package/dist/i18n/setup.d.ts.map +1 -0
- package/dist/i18n/setup.js +47 -0
- package/dist/i18n/types.d.ts +17 -0
- package/dist/i18n/types.d.ts.map +1 -0
- package/dist/i18n/types.js +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2069
- package/dist/ios/assets.d.ts +19 -0
- package/dist/ios/assets.d.ts.map +1 -0
- package/dist/ios/assets.js +128 -0
- package/dist/ios/production-build.d.ts +6 -0
- package/dist/ios/production-build.d.ts.map +1 -0
- package/dist/ios/production-build.js +200 -0
- package/dist/ios/project.d.ts +4 -0
- package/dist/ios/project.d.ts.map +1 -0
- package/dist/ios/project.js +43 -0
- package/dist/ios/simulator-build.d.ts +8 -0
- package/dist/ios/simulator-build.d.ts.map +1 -0
- package/dist/ios/simulator-build.js +188 -0
- package/dist/ios/simulator.d.ts +5 -0
- package/dist/ios/simulator.d.ts.map +1 -0
- package/dist/ios/simulator.js +60 -0
- package/dist/ios/types.d.ts +14 -0
- package/dist/ios/types.d.ts.map +1 -0
- package/dist/ios/types.js +1 -0
- package/dist/ios-build.d.ts +4 -37
- package/dist/ios-build.d.ts.map +1 -1
- package/dist/ios-build.js +3 -575
- package/dist/ios-host.d.ts.map +1 -1
- package/dist/ios-toolchain/inspection.d.ts +4 -0
- package/dist/ios-toolchain/inspection.d.ts.map +1 -0
- package/dist/ios-toolchain/inspection.js +211 -0
- package/dist/ios-toolchain/probes.d.ts +24 -0
- package/dist/ios-toolchain/probes.d.ts.map +1 -0
- package/dist/ios-toolchain/probes.js +127 -0
- package/dist/ios-toolchain/types.d.ts +17 -0
- package/dist/ios-toolchain/types.d.ts.map +1 -0
- package/dist/ios-toolchain/types.js +1 -0
- package/dist/ios-toolchain.d.ts +3 -18
- package/dist/ios-toolchain.d.ts.map +1 -1
- package/dist/ios-toolchain.js +1 -333
- package/dist/plugins/contracts.d.ts +28 -0
- package/dist/plugins/contracts.d.ts.map +1 -0
- package/dist/plugins/contracts.js +1 -0
- package/dist/plugins/discovery.d.ts +19 -0
- package/dist/plugins/discovery.d.ts.map +1 -0
- package/dist/plugins/discovery.js +250 -0
- package/dist/plugins/native-operations.d.ts +28 -0
- package/dist/plugins/native-operations.d.ts.map +1 -0
- package/dist/plugins/native-operations.js +217 -0
- package/dist/plugins/operations.d.ts +9 -0
- package/dist/plugins/operations.d.ts.map +1 -0
- package/dist/plugins/operations.js +76 -0
- package/dist/plugins/validation.d.ts +9 -0
- package/dist/plugins/validation.d.ts.map +1 -0
- package/dist/plugins/validation.js +52 -0
- package/dist/plugins.d.ts +3 -34
- package/dist/plugins.d.ts.map +1 -1
- package/dist/plugins.js +2 -595
- package/dist/process-runner.d.ts +4 -0
- package/dist/process-runner.d.ts.map +1 -1
- package/dist/process-runner.js +14 -0
- package/dist/remote.d.ts +22 -1
- package/dist/remote.d.ts.map +1 -1
- package/dist/remote.js +141 -2
- package/dist/rspeedy-app-config.d.ts +4 -0
- package/dist/rspeedy-app-config.d.ts.map +1 -0
- package/dist/rspeedy-app-config.js +32 -0
- package/dist/runtime/args.d.ts +3 -0
- package/dist/runtime/args.d.ts.map +1 -0
- package/dist/runtime/args.js +11 -0
- package/dist/runtime/build-commands.d.ts +3 -0
- package/dist/runtime/build-commands.d.ts.map +1 -0
- package/dist/runtime/build-commands.js +282 -0
- package/dist/runtime/context.d.ts +63 -0
- package/dist/runtime/context.d.ts.map +1 -0
- package/dist/runtime/context.js +179 -0
- package/dist/runtime/early-commands.d.ts +4 -0
- package/dist/runtime/early-commands.d.ts.map +1 -0
- package/dist/runtime/early-commands.js +284 -0
- package/dist/runtime/project.d.ts +8 -0
- package/dist/runtime/project.d.ts.map +1 -0
- package/dist/runtime/project.js +38 -0
- package/dist/runtime/state.d.ts +47 -0
- package/dist/runtime/state.d.ts.map +1 -0
- package/dist/runtime/state.js +33 -0
- package/dist/secure-store/backends.d.ts +20 -0
- package/dist/secure-store/backends.d.ts.map +1 -0
- package/dist/secure-store/backends.js +248 -0
- package/dist/secure-store/types.d.ts +27 -0
- package/dist/secure-store/types.d.ts.map +1 -0
- package/dist/secure-store/types.js +1 -0
- package/dist/secure-store.d.ts +2 -26
- package/dist/secure-store.d.ts.map +1 -1
- package/dist/secure-store.js +3 -245
- package/package.json +7 -4
- package/templates/android-host/app/build.gradle +1 -0
- package/templates/android-host/app/src/main/java/template/MainActivity.java +98 -3
- package/templates/ios-host/__IOS_TARGET_NAME__/DemoLynxProvider.swift +26 -16
- package/templates/ios-host/__IOS_TARGET_NAME__/ViewController.swift +107 -19
package/dist/index.js
CHANGED
|
@@ -1,2070 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
import
|
|
6
|
-
import { detectLynxFramework } from "./frameworks.js";
|
|
7
|
-
import { BuildOrchestrator } from "@lynxship/build-orchestrator";
|
|
8
|
-
import { JsonRepository } from "@lynxship/db";
|
|
9
|
-
import { assert, createId, sha256, } from "@lynxship/contracts";
|
|
10
|
-
import { createSigningKey, signManifest, } from "@lynxship/signing";
|
|
11
|
-
import { AppStoreConnectApiProvider, GooglePlayApiProvider, SubmissionService, } from "@lynxship/submit";
|
|
12
|
-
import { DEFAULT_CONFIG, loadConfig, platformValue, resolveProfile, } from "./config.js";
|
|
13
|
-
import { microHsHostTriple } from "@lynxship/microhs";
|
|
14
|
-
import { hasAndroidHost, isSupportedAndroidPlatform, runRealAndroidBuild, } from "./android-build.js";
|
|
15
|
-
import { initializeAndroidHost, suggestedAndroidApplicationId, } from "./android-host.js";
|
|
16
|
-
import { initializeIosHost, suggestedIosBundleIdentifier } from "./ios-host.js";
|
|
17
|
-
import { hasIosHost, launchIosSimulatorApp, runRealIosBuild, } from "./ios-build.js";
|
|
18
|
-
import { configureAndroid, configureAppStoreConnect, configureGooglePlay, configureR2, } from "./configure.js";
|
|
19
|
-
import { fetchOtaPublicKey, publishOtaRelease, rollbackOtaRelease, submitRealArtifact, } from "./remote.js";
|
|
20
|
-
import { uploadR2Artifact } from "./r2.js";
|
|
21
|
-
import { credentialStorageDescription, loadCredentials, } from "./secure-store.js";
|
|
22
|
-
import { createCliUi } from "./ui/index.js";
|
|
23
|
-
import { globalLynxShipDirectory } from "./paths.js";
|
|
24
|
-
import { inspectAutolink, requireAutolinkReady } from "./autolink.js";
|
|
25
|
-
import { assertCompatibleBinaryBuild, inspectRuntimeFingerprint, } from "./runtime-fingerprint.js";
|
|
26
|
-
import { inspectOtaHost } from "./ota-doctor.js";
|
|
27
|
-
import { otaAssetName, otaAssetPaths } from "./ota-assets.js";
|
|
28
|
-
import { confirm, prompt } from "./prompt.js";
|
|
29
|
-
import { commandExists, packageManagerCommand, runProcess, runRspeedy, } from "./process-runner.js";
|
|
30
|
-
import { guidanceForError } from "./guidance.js";
|
|
31
|
-
import { buildLynxBundle } from "./bundle-build.js";
|
|
32
|
-
import { fixAndroidToolchain, formatAndroidToolchainFailure, inspectAndroidToolchain, } from "./android-toolchain.js";
|
|
33
|
-
import { formatIosToolchainFailure, inspectIosToolchain, } from "./ios-toolchain.js";
|
|
34
|
-
import { formatDevToolFailure, inspectLynxDevTool } from "./lynx-devtool.js";
|
|
35
|
-
import { hasHarmonyHost, runRealHarmonyBuild } from "./harmony-build.js";
|
|
36
|
-
import { hasWebConfiguration, runRealWebBuild } from "./web-build.js";
|
|
37
|
-
import { hasDesktopHost, runRealDesktopBuild } from "./desktop-build.js";
|
|
38
|
-
import { extractDevServerUrl, shouldPrintDevServerQr } from "./dev-qr.js";
|
|
39
|
-
import { applyProjectPlugins, inspectProjectPlugins } from "./plugins.js";
|
|
40
|
-
import { inspectDesktopTarget, inspectHarmonyTarget, inspectWebTarget, } from "./target-toolchain.js";
|
|
41
|
-
const rawArgs = process.argv.slice(2);
|
|
42
|
-
const args = [...rawArgs];
|
|
43
|
-
const ui = createCliUi(rawArgs);
|
|
44
|
-
const json = ui.options.json;
|
|
45
|
-
function requestedProjectDirectory() {
|
|
46
|
-
const index = rawArgs.indexOf("--project-dir");
|
|
47
|
-
if (index >= 0)
|
|
48
|
-
return rawArgs[index + 1];
|
|
49
|
-
const inline = rawArgs.find((value) => value.startsWith("--project-dir="));
|
|
50
|
-
return inline?.slice("--project-dir=".length);
|
|
51
|
-
}
|
|
52
|
-
function findProjectRoot(start) {
|
|
53
|
-
const explicit = requestedProjectDirectory() ?? process.env.LYNXSHIP_PROJECT_DIR;
|
|
54
|
-
if (explicit)
|
|
55
|
-
return resolve(explicit);
|
|
56
|
-
let current = resolve(start);
|
|
57
|
-
while (true) {
|
|
58
|
-
if (existsSync(join(current, "lynxship.json")))
|
|
59
|
-
return current;
|
|
60
|
-
const parent = dirname(current);
|
|
61
|
-
if (parent === current)
|
|
62
|
-
return resolve(start);
|
|
63
|
-
current = parent;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
const root = findProjectRoot(process.cwd());
|
|
67
|
-
function flag(name, fallback = null) {
|
|
68
|
-
const index = args.indexOf(name);
|
|
69
|
-
return index >= 0 ? (args[index + 1] ?? "true") : fallback;
|
|
70
|
-
}
|
|
71
|
-
async function assertInteractivePrompt(label, fallback, optionName) {
|
|
72
|
-
assert(ui.interactive, "CLI_INTERACTIVE_REQUIRED", `Pass ${label.toLowerCase()} with ${optionName} in non-interactive mode.`);
|
|
73
|
-
return prompt(label, fallback);
|
|
74
|
-
}
|
|
75
|
-
function printValue(value, view) {
|
|
76
|
-
if (json) {
|
|
77
|
-
console.log(JSON.stringify(typeof value === "string" ? { result: value } : value));
|
|
78
|
-
return;
|
|
79
|
-
}
|
|
80
|
-
if (json || !ui.interactive || !view) {
|
|
81
|
-
console.log(typeof value === "string" ? value : JSON.stringify(value, null, 2));
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
ui.summary(view.title, view.rows);
|
|
85
|
-
ui.done(view.done);
|
|
86
|
-
}
|
|
87
|
-
async function exists(file) {
|
|
88
|
-
try {
|
|
89
|
-
await access(file);
|
|
90
|
-
return true;
|
|
91
|
-
}
|
|
92
|
-
catch {
|
|
93
|
-
return false;
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
async function findLockfile(rootDirectory) {
|
|
97
|
-
let current = resolve(rootDirectory);
|
|
98
|
-
while (true) {
|
|
99
|
-
for (const file of ["pnpm-lock.yaml", "package-lock.json", "yarn.lock"]) {
|
|
100
|
-
if (await exists(join(current, file)))
|
|
101
|
-
return join(current, file);
|
|
102
|
-
}
|
|
103
|
-
const parent = dirname(current);
|
|
104
|
-
if (parent === current)
|
|
105
|
-
return null;
|
|
106
|
-
current = parent;
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
async function requireProjectRoot() {
|
|
110
|
-
assert(await exists(join(root, "lynxship.json")), "CLI_PROJECT_REQUIRED", "Run this command from a LynxShip project directory containing lynxship.json, or run `lynxship init` first.");
|
|
111
|
-
}
|
|
112
|
-
async function readConfigurationStatus() {
|
|
113
|
-
const credentials = await loadCredentials(root);
|
|
114
|
-
const r2Configured = (((await exists(join(root, ".lynxship", "r2.json"))) ||
|
|
115
|
-
(await exists(join(globalLynxShipDirectory(), "r2.json")))) &&
|
|
116
|
-
Boolean(credentials.r2)) ||
|
|
117
|
-
Boolean(process.env.CLOUDFLARE_ACCOUNT_ID &&
|
|
118
|
-
process.env.R2_BUCKET &&
|
|
119
|
-
process.env.R2_ACCESS_KEY_ID &&
|
|
120
|
-
process.env.R2_SECRET_ACCESS_KEY);
|
|
121
|
-
const android = credentials.android;
|
|
122
|
-
const androidConfigured = Boolean(android?.keystorePath && (await exists(android.keystorePath))) ||
|
|
123
|
-
Boolean(process.env.LYNXSHIP_KEYSTORE_PATH &&
|
|
124
|
-
(await exists(process.env.LYNXSHIP_KEYSTORE_PATH)) &&
|
|
125
|
-
process.env.LYNXSHIP_KEY_ALIAS &&
|
|
126
|
-
process.env.LYNXSHIP_KEYSTORE_PASSWORD &&
|
|
127
|
-
process.env.LYNXSHIP_KEY_PASSWORD);
|
|
128
|
-
return { r2: r2Configured, android: androidConfigured };
|
|
129
|
-
}
|
|
130
|
-
async function requireOperationalConfiguration(platform, options = {}) {
|
|
131
|
-
await requireProjectRoot();
|
|
132
|
-
const status = await readConfigurationStatus();
|
|
133
|
-
if (options.requireR2 !== false)
|
|
134
|
-
assert(status.r2, "CLI_R2_REQUIRED", "Cloudflare R2 must be configured first. Run `lynxship storage configure`.");
|
|
135
|
-
if (platform !== "android")
|
|
136
|
-
return;
|
|
137
|
-
assert(status.android, "BUILD_SIGNING_REQUIRED", "Android signing must be configured first. Run `lynxship android configure` or provide an existing keystore.");
|
|
138
|
-
}
|
|
139
|
-
async function requireR2Configuration() {
|
|
140
|
-
await requireProjectRoot();
|
|
141
|
-
const status = await readConfigurationStatus();
|
|
142
|
-
assert(status.r2, "CLI_R2_REQUIRED", "Cloudflare R2 must be configured first. Run `lynxship storage configure`.");
|
|
143
|
-
}
|
|
144
|
-
function configuredProjectId(config) {
|
|
145
|
-
assert(typeof config.projectId === "string" && config.projectId.length > 0, "CLI_PROJECT_ID_REQUIRED", "lynxship.json must contain a generated projectId. Run `lynxship init` for a new project configuration.");
|
|
146
|
-
return config.projectId;
|
|
147
|
-
}
|
|
148
|
-
function mobilePlatformValue(value) {
|
|
149
|
-
const platform = platformValue(value);
|
|
150
|
-
assert(platform === "android" || platform === "ios", "PLATFORM_COMMAND_UNSUPPORTED", "This command supports only android or ios. Web, HarmonyOS and Desktop use their own build adapters.");
|
|
151
|
-
return platform;
|
|
152
|
-
}
|
|
153
|
-
async function renderConfigurationFooter() {
|
|
154
|
-
if (!ui.interactive || ui.options.quiet)
|
|
155
|
-
return;
|
|
156
|
-
if (rawArgs[0] === "inspect")
|
|
157
|
-
return;
|
|
158
|
-
const profileIndex = rawArgs.indexOf("--profile");
|
|
159
|
-
const simulatorProfile = profileIndex >= 0 && rawArgs[profileIndex + 1] === "simulator";
|
|
160
|
-
if (rawArgs.includes("--simulator") || simulatorProfile)
|
|
161
|
-
return;
|
|
162
|
-
const status = await readConfigurationStatus();
|
|
163
|
-
const ready = status.r2 && status.android;
|
|
164
|
-
if (ready)
|
|
165
|
-
return;
|
|
166
|
-
ui.configurationStatus([
|
|
167
|
-
{
|
|
168
|
-
label: "Cloudflare R2",
|
|
169
|
-
value: status.r2 ? "configured" : "required · storage configure",
|
|
170
|
-
valueColor: status.r2 ? "green" : "yellow",
|
|
171
|
-
},
|
|
172
|
-
{
|
|
173
|
-
label: "Android signing",
|
|
174
|
-
value: status.android ? "configured" : "required · android configure",
|
|
175
|
-
valueColor: status.android ? "green" : "yellow",
|
|
176
|
-
},
|
|
177
|
-
{
|
|
178
|
-
label: "Operational CLI",
|
|
179
|
-
value: ready ? "ready" : "blocked until setup is complete",
|
|
180
|
-
valueColor: ready ? "green" : "red",
|
|
181
|
-
},
|
|
182
|
-
]);
|
|
183
|
-
}
|
|
184
|
-
async function loadState() {
|
|
185
|
-
const repository = new JsonRepository(join(root, ".lynxship", "state.json"), {
|
|
186
|
-
builds: [],
|
|
187
|
-
submissions: [],
|
|
188
|
-
releases: [],
|
|
189
|
-
signingKey: null,
|
|
190
|
-
});
|
|
191
|
-
const state = await repository.read();
|
|
192
|
-
state.builds ??= [];
|
|
193
|
-
state.submissions ??= [];
|
|
194
|
-
state.releases ??= [];
|
|
195
|
-
state.signingKey ??= createSigningKey();
|
|
196
|
-
const builds = new BuildOrchestrator();
|
|
197
|
-
for (const job of state.builds)
|
|
198
|
-
builds.jobs.set(job.id, job);
|
|
199
|
-
const submissions = new SubmissionService();
|
|
200
|
-
for (const job of state.submissions)
|
|
201
|
-
submissions.jobs.set(job.id, job);
|
|
202
|
-
return { state, repository, builds, submissions };
|
|
203
|
-
}
|
|
204
|
-
async function saveState(state, repository, builds, submissions) {
|
|
205
|
-
const next = stateSaveQueue.then(async () => {
|
|
206
|
-
state.builds = builds.list();
|
|
207
|
-
state.submissions = submissions.list();
|
|
208
|
-
await repository.write(state);
|
|
209
|
-
});
|
|
210
|
-
stateSaveQueue = next.catch(() => undefined);
|
|
211
|
-
await next;
|
|
212
|
-
}
|
|
213
|
-
let stateSaveQueue = Promise.resolve();
|
|
214
|
-
async function executeBuild(options, showResult = true) {
|
|
215
|
-
const { config, profile, platform, skipUpload, wait, local, state, repository, builds, submissions, progress: sharedProgress, progressPrefix, onProgress, onEvent, skipBundleBuild, simulator = false, simulatorDevice, simulatorAutostart = false, } = options;
|
|
216
|
-
if (wait && !local)
|
|
217
|
-
await ensureNativeHostForBuild(platform, profile, config);
|
|
218
|
-
// Host initialization can write lynxship.json (for example, the generated
|
|
219
|
-
// iOS scheme). Always read it again before resolving the build profile so a
|
|
220
|
-
// first build uses the host that was just created in the same invocation.
|
|
221
|
-
const loadedConfig = wait && !local ? await loadConfig(root) : config;
|
|
222
|
-
const pluginApplication = wait && !local
|
|
223
|
-
? await applyProjectPlugins(root, loadedConfig, { platform, profile })
|
|
224
|
-
: {
|
|
225
|
-
config: loadedConfig,
|
|
226
|
-
report: { configured: 0, plugins: [] },
|
|
227
|
-
applied: [],
|
|
228
|
-
templates: [],
|
|
229
|
-
cloud: [],
|
|
230
|
-
build: [],
|
|
231
|
-
changes: [],
|
|
232
|
-
autolink: [],
|
|
233
|
-
};
|
|
234
|
-
const effectiveConfig = pluginApplication.config;
|
|
235
|
-
if (platform === "android" || platform === "ios")
|
|
236
|
-
await requireAutolinkReady(root, platform);
|
|
237
|
-
const runtime = await inspectRuntimeFingerprint(root, platform, effectiveConfig);
|
|
238
|
-
const resolvedBuildProfile = resolveProfile(effectiveConfig, profile);
|
|
239
|
-
const job = await builds.create({
|
|
240
|
-
projectId: configuredProjectId(effectiveConfig),
|
|
241
|
-
organizationId: "local_org",
|
|
242
|
-
platform,
|
|
243
|
-
profile,
|
|
244
|
-
sourceHash: createHash("sha256").update(root).digest("hex"),
|
|
245
|
-
runtimeVersion: runtime.value,
|
|
246
|
-
runtimeInputs: runtime.inputs,
|
|
247
|
-
});
|
|
248
|
-
ui.info(`Using profile: ${profile} · platform: ${platform}`);
|
|
249
|
-
const ownsProgress = !sharedProgress;
|
|
250
|
-
const progress = sharedProgress ??
|
|
251
|
-
ui.progress(`${platform[0].toUpperCase()}${platform.slice(1)} build`);
|
|
252
|
-
const prefix = progressPrefix ? `${progressPrefix} · ` : "";
|
|
253
|
-
const reportEvent = (message) => {
|
|
254
|
-
if (onEvent)
|
|
255
|
-
onEvent(message);
|
|
256
|
-
else
|
|
257
|
-
progress.event(`${prefix}${message}`);
|
|
258
|
-
};
|
|
259
|
-
const reportProgress = (value, label) => {
|
|
260
|
-
if (onProgress)
|
|
261
|
-
onProgress(value, label);
|
|
262
|
-
else
|
|
263
|
-
progress.update(value, label ? `${prefix}${label}` : undefined);
|
|
264
|
-
};
|
|
265
|
-
try {
|
|
266
|
-
reportProgress(undefined, "Preparing build pipeline…");
|
|
267
|
-
if (wait) {
|
|
268
|
-
if (platform === "android" && !isSupportedAndroidPlatform() && !local)
|
|
269
|
-
assert(false, "ANDROID_PLATFORM_UNSUPPORTED", "Android builds are supported only on Linux, macOS and Windows.");
|
|
270
|
-
const realAndroid = platform === "android" &&
|
|
271
|
-
isSupportedAndroidPlatform() &&
|
|
272
|
-
(await hasAndroidHost(root));
|
|
273
|
-
const realIos = platform === "ios" &&
|
|
274
|
-
hasIosHost(root, resolveProfile(effectiveConfig, profile));
|
|
275
|
-
if (platform === "ios" && !realIos && !local)
|
|
276
|
-
assert(false, "IOS_HOST_REQUIRED", "A macOS Xcode host is required for a real iOS build. Run `lynxship ios host init --bundle-identifier <id>` or pass --bundle-identifier <id> to build.");
|
|
277
|
-
if (platform === "android" && !realAndroid && !local)
|
|
278
|
-
assert(false, "ANDROID_HOST_REQUIRED", "This project has no Android Gradle host. Run `lynxship android host init --application-id <id>` or pass --application-id <id> to build. `lynxship dev` remains available for Lynx Explorer; `--local` does not create an APK.");
|
|
279
|
-
if (realAndroid) {
|
|
280
|
-
const toolchain = await inspectAndroidToolchain(root);
|
|
281
|
-
assert(toolchain.ok, "ANDROID_TOOLCHAIN_REQUIRED", `Android toolchain is not ready: ${formatAndroidToolchainFailure(toolchain)}`);
|
|
282
|
-
await runRealAndroidBuild(job, {
|
|
283
|
-
root,
|
|
284
|
-
profile: resolvedBuildProfile,
|
|
285
|
-
uploadArtifacts: !skipUpload,
|
|
286
|
-
skipBundleBuild,
|
|
287
|
-
quiet: json,
|
|
288
|
-
onEvent: reportEvent,
|
|
289
|
-
onProgress: reportProgress,
|
|
290
|
-
});
|
|
291
|
-
}
|
|
292
|
-
else if (realIos) {
|
|
293
|
-
const toolchain = await inspectIosToolchain(root, resolveProfile(effectiveConfig, profile), simulator ? "simulator" : "device");
|
|
294
|
-
assert(toolchain.ok, "IOS_TOOLCHAIN_REQUIRED", `iOS toolchain is not ready: ${formatIosToolchainFailure(toolchain)}`);
|
|
295
|
-
await runRealIosBuild(job, {
|
|
296
|
-
root,
|
|
297
|
-
profile: resolvedBuildProfile,
|
|
298
|
-
simulator,
|
|
299
|
-
simulatorDevice,
|
|
300
|
-
simulatorAutostart,
|
|
301
|
-
uploadArtifacts: !skipUpload,
|
|
302
|
-
skipBundleBuild,
|
|
303
|
-
quiet: json,
|
|
304
|
-
onEvent: reportEvent,
|
|
305
|
-
onProgress: reportProgress,
|
|
306
|
-
});
|
|
307
|
-
}
|
|
308
|
-
else if (!local && platform === "web") {
|
|
309
|
-
await runRealWebBuild(job, {
|
|
310
|
-
root,
|
|
311
|
-
profile: resolveProfile(effectiveConfig, profile),
|
|
312
|
-
uploadArtifacts: !skipUpload,
|
|
313
|
-
skipBundleBuild,
|
|
314
|
-
quiet: json,
|
|
315
|
-
onEvent: reportEvent,
|
|
316
|
-
onProgress: reportProgress,
|
|
317
|
-
});
|
|
318
|
-
}
|
|
319
|
-
else if (!local && platform === "harmony") {
|
|
320
|
-
await runRealHarmonyBuild(job, {
|
|
321
|
-
root,
|
|
322
|
-
profile: resolveProfile(effectiveConfig, profile),
|
|
323
|
-
uploadArtifacts: !skipUpload,
|
|
324
|
-
skipBundleBuild,
|
|
325
|
-
quiet: json,
|
|
326
|
-
onEvent: reportEvent,
|
|
327
|
-
onProgress: reportProgress,
|
|
328
|
-
});
|
|
329
|
-
}
|
|
330
|
-
else if (!local && platform === "desktop") {
|
|
331
|
-
await runRealDesktopBuild(job, {
|
|
332
|
-
root,
|
|
333
|
-
profile: resolveProfile(effectiveConfig, profile),
|
|
334
|
-
uploadArtifacts: !skipUpload,
|
|
335
|
-
allowUnsigned: options.allowUnsigned,
|
|
336
|
-
skipBundleBuild,
|
|
337
|
-
quiet: json,
|
|
338
|
-
onEvent: reportEvent,
|
|
339
|
-
onProgress: reportProgress,
|
|
340
|
-
});
|
|
341
|
-
}
|
|
342
|
-
else {
|
|
343
|
-
await builds.run(job.id);
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
reportProgress(100);
|
|
347
|
-
await saveState(state, repository, builds, submissions);
|
|
348
|
-
}
|
|
349
|
-
catch (error) {
|
|
350
|
-
await saveState(state, repository, builds, submissions);
|
|
351
|
-
if (error instanceof Error) {
|
|
352
|
-
Object.assign(error, { buildId: job.id });
|
|
353
|
-
}
|
|
354
|
-
throw error;
|
|
355
|
-
}
|
|
356
|
-
finally {
|
|
357
|
-
if (ownsProgress)
|
|
358
|
-
progress.stop();
|
|
359
|
-
}
|
|
360
|
-
const result = builds.get(job.id);
|
|
361
|
-
if (showResult) {
|
|
362
|
-
printValue(result, {
|
|
363
|
-
title: `${platform[0].toUpperCase()}${platform.slice(1)} build result`,
|
|
364
|
-
rows: [
|
|
365
|
-
{ label: "Build ID", value: result.id, valueColor: "purple" },
|
|
366
|
-
{ label: "Platform", value: result.platform, valueColor: "blue" },
|
|
367
|
-
{ label: "Profile", value: result.profile, valueColor: "text" },
|
|
368
|
-
{
|
|
369
|
-
label: "Status",
|
|
370
|
-
value: result.state,
|
|
371
|
-
valueColor: result.state === "success" ? "green" : "yellow",
|
|
372
|
-
},
|
|
373
|
-
],
|
|
374
|
-
done: result.state === "success"
|
|
375
|
-
? "Build complete. Run lynxship submit to publish."
|
|
376
|
-
: "Build queued.",
|
|
377
|
-
});
|
|
378
|
-
if (result.state === "success" && result.artifact?.url)
|
|
379
|
-
ui.downloadArtifact(result.artifact.url, result.artifact.expiresAt);
|
|
380
|
-
}
|
|
381
|
-
return result;
|
|
382
|
-
}
|
|
383
|
-
async function buildSharedLynxBundle(miso) {
|
|
384
|
-
const progress = ui.progress("Shared Lynx bundle");
|
|
385
|
-
try {
|
|
386
|
-
progress.update(undefined, "Building the shared Lynx bundle once for native targets…");
|
|
387
|
-
await buildLynxBundle(root, {
|
|
388
|
-
quiet: json,
|
|
389
|
-
onOutput: (message) => progress.event(message),
|
|
390
|
-
miso,
|
|
391
|
-
});
|
|
392
|
-
progress.update(100, "Shared native Lynx bundle ready");
|
|
393
|
-
}
|
|
394
|
-
finally {
|
|
395
|
-
progress.stop();
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
async function initSelfHost() {
|
|
399
|
-
const directory = join(root, ".lynxship");
|
|
400
|
-
await mkdir(directory, { recursive: true });
|
|
401
|
-
const file = join(directory, ".env");
|
|
402
|
-
if (await exists(file))
|
|
403
|
-
return { status: "unchanged", file };
|
|
404
|
-
const values = {
|
|
405
|
-
POSTGRES_PASSWORD: randomBytes(24).toString("base64url"),
|
|
406
|
-
LYNXSHIP_TOKEN: randomBytes(32).toString("base64url"),
|
|
407
|
-
};
|
|
408
|
-
await writeFile(file, `${Object.entries(values)
|
|
409
|
-
.map(([key, value]) => `${key}=${value}`)
|
|
410
|
-
.join("\n")}\n`, { mode: 0o600 });
|
|
411
|
-
return { status: "created", file };
|
|
412
|
-
}
|
|
413
|
-
function commandTitle(command) {
|
|
414
|
-
return ({
|
|
415
|
-
init: "Initialize project",
|
|
416
|
-
doctor: "Environment doctor",
|
|
417
|
-
dev: "Rspeedy development",
|
|
418
|
-
preview: "Rspeedy preview",
|
|
419
|
-
inspect: "Rspeedy inspection",
|
|
420
|
-
profile: "Rspeedy profiling",
|
|
421
|
-
autolink: "Lynx Autolink",
|
|
422
|
-
run: "Run on device",
|
|
423
|
-
logs: "Native logs",
|
|
424
|
-
ota: "OTA diagnostics",
|
|
425
|
-
build: "Cloud Build",
|
|
426
|
-
submit: "Store Submission",
|
|
427
|
-
update: "OTA Update",
|
|
428
|
-
rollback: "OTA Rollback",
|
|
429
|
-
"self-host": "Self-host setup",
|
|
430
|
-
storage: "Cloudflare R2 setup",
|
|
431
|
-
android: "Android signing setup",
|
|
432
|
-
store: "App store submission setup",
|
|
433
|
-
devtool: "Lynx DevTool diagnostics",
|
|
434
|
-
trace: "Lynx Trace diagnostics",
|
|
435
|
-
recorder: "Lynx Recorder diagnostics",
|
|
436
|
-
plugin: "LynxShip plugins",
|
|
437
|
-
}[command] ?? command);
|
|
438
|
-
}
|
|
439
|
-
function helpText() {
|
|
440
|
-
return `lynxship <command> [options]
|
|
441
|
-
|
|
442
|
-
Commands:
|
|
443
|
-
init Initialize or link a LynxShip project
|
|
444
|
-
doctor Check the local toolchain and project
|
|
445
|
-
dev Run Rspeedy dev with Lynx Explorer QR/HMR
|
|
446
|
-
preview Preview the production Lynx bundle locally
|
|
447
|
-
inspect Inspect Rspeedy/Rspack configuration
|
|
448
|
-
profile Build with Rspack profiling enabled
|
|
449
|
-
devtool doctor Check Lynx DevTool integration and dev runtime
|
|
450
|
-
trace doctor Check Lynx Trace prerequisites
|
|
451
|
-
recorder doctor Check Lynx Recorder prerequisites
|
|
452
|
-
plugin list List project plugins and their capabilities
|
|
453
|
-
plugin doctor Validate plugin packages without modifying native files
|
|
454
|
-
plugin apply Apply project plugin native changes for a platform
|
|
455
|
-
plugin apply --dry-run Preview plugin native changes without writing files
|
|
456
|
-
autolink check Check Lynx native-library Autolink wiring
|
|
457
|
-
autolink codegen Run the project's Native Module codegen script
|
|
458
|
-
ota doctor Check native OTA host integration
|
|
459
|
-
run Install an artifact on an Android/iOS/HarmonyOS target
|
|
460
|
-
logs Stream Android/iOS/HarmonyOS native logs
|
|
461
|
-
build create Create a local/cloud build job
|
|
462
|
-
build list List build jobs
|
|
463
|
-
build status <id> Show one build job
|
|
464
|
-
build cancel <id> Cancel a build job
|
|
465
|
-
build retry <id> Retry a failed build job
|
|
466
|
-
build all Build Android, iOS, HarmonyOS, Web and Desktop
|
|
467
|
-
submit Submit the latest successful build
|
|
468
|
-
update Upload and publish a signed OTA update
|
|
469
|
-
update rollback Roll back an OTA channel to a previous release
|
|
470
|
-
rollback Alias for update rollback
|
|
471
|
-
self-host init Generate local self-host credentials
|
|
472
|
-
storage configure Configure Cloudflare R2 and encrypted R2 credentials
|
|
473
|
-
android host init Create a minimal official Lynx Android host
|
|
474
|
-
ios host init Create a minimal official Lynx iOS/Xcode host
|
|
475
|
-
android configure Configure or generate encrypted Android signing credentials
|
|
476
|
-
store configure Configure Google Play or App Store Connect submission
|
|
477
|
-
|
|
478
|
-
Build options:
|
|
479
|
-
--platform <p> Target android, ios, harmony, web, desktop or all (default: android)
|
|
480
|
-
--profile <name> Build profile (default: production; simulator uses simulator)
|
|
481
|
-
--no-wait Queue the build without executing it locally
|
|
482
|
-
--no-upload Keep the artifact local and skip R2 (CI verification)
|
|
483
|
-
--simulator Build and install an iOS Simulator .app locally
|
|
484
|
-
--device <id> Select the iOS Simulator device for a simulator build
|
|
485
|
-
--autostart Open and launch the iOS Simulator app after install
|
|
486
|
-
--no-autostart Install the Simulator app without launching it
|
|
487
|
-
--allow-unsigned Allow an unsigned Desktop artifact only with --no-upload (local tests)
|
|
488
|
-
--local Use the contract-only build path for tests
|
|
489
|
-
|
|
490
|
-
Submit options:
|
|
491
|
-
--platform <p> Target android or ios (default: android)
|
|
492
|
-
--latest Submit the latest successful build
|
|
493
|
-
--local Use the mock submission provider for tests
|
|
494
|
-
|
|
495
|
-
Update options:
|
|
496
|
-
--platform <p> Target android or ios (default: android)
|
|
497
|
-
--bundle <path[,path]> Lynx bundle/assets (default: discover dist/*.lynx.bundle)
|
|
498
|
-
--message <text> Release message
|
|
499
|
-
--local Create a local contract-only update for tests
|
|
500
|
-
--policy-approval-id Required for an iOS OTA release
|
|
501
|
-
|
|
502
|
-
Rollback options:
|
|
503
|
-
--platform <p> Target android or ios (default: android)
|
|
504
|
-
--release-id <id> Previously published compatible release
|
|
505
|
-
--reason <text> Required audit reason for the rollback
|
|
506
|
-
--local Roll back local contract state for tests
|
|
507
|
-
|
|
508
|
-
Device and diagnostics options:
|
|
509
|
-
doctor --platform <p> Check the local toolchain (default: android)
|
|
510
|
-
doctor --fix Install missing Android SDK packages after confirmation
|
|
511
|
-
autolink check --platform <p>
|
|
512
|
-
Check native-library wiring (default: android)
|
|
513
|
-
ota doctor --platform <p>
|
|
514
|
-
Check OTA host integration (default: android)
|
|
515
|
-
run --artifact <path> Install a specific APK, IPA, app or signed HAP
|
|
516
|
-
run --device <id> Select an Android, iOS or HarmonyOS device
|
|
517
|
-
run --simulator Install an iOS .app with simctl
|
|
518
|
-
run --launch Launch the installed iOS Simulator app after install
|
|
519
|
-
logs --device <id> Select the device or simulator for native logs
|
|
520
|
-
|
|
521
|
-
Global options:
|
|
522
|
-
--json Emit one stable JSON result/error object
|
|
523
|
-
--quiet Print only the final machine-relevant result
|
|
524
|
-
--verbose Include extra operational context
|
|
525
|
-
--no-color Disable ANSI colors
|
|
526
|
-
--non-interactive Never prompt; fail on missing inputs
|
|
527
|
-
--banner Show the Braille LynxShip logo in a TTY
|
|
528
|
-
--project-dir <path> Use a LynxShip project from any working directory
|
|
529
|
-
--project-id <id> Project ID used by init
|
|
530
|
-
--application-id <id> Android package/application ID for host init
|
|
531
|
-
--bundle-identifier <id> iOS bundle identifier for host init
|
|
532
|
-
--icon <path> 1024x1024 PNG app icon for iOS host init
|
|
533
|
-
--library-dir <path> Native library directory for autolink codegen
|
|
534
|
-
--simulator Build/install an iOS .app or install one with simctl
|
|
535
|
-
--help Show this complete command reference
|
|
536
|
-
|
|
537
|
-
Miso compiler profiles:
|
|
538
|
-
ghcjs (default) Use the official Miso GHC/Nix workflow
|
|
539
|
-
microhs Use a pinned MicroHs binary and a real adapter command
|
|
540
|
-
|
|
541
|
-
MicroHs is opt-in. Configure build.<profile>.miso.microhs with either a
|
|
542
|
-
binary path or a pinned manifest, plus an adapter command that writes the
|
|
543
|
-
configured main.lynx.bundle. It is not a drop-in GHCJS replacement.
|
|
544
|
-
|
|
545
|
-
Node support: Node 22/24 LTS or Node 26 Current. Use Node 24 LTS for production.`;
|
|
546
|
-
}
|
|
547
|
-
async function looksLikeLynxProject() {
|
|
548
|
-
const framework = await detectLynxFramework(root);
|
|
549
|
-
if (framework.framework !== "unknown")
|
|
550
|
-
return true;
|
|
551
|
-
const configFiles = [
|
|
552
|
-
"lynx.config.ts",
|
|
553
|
-
"lynx.config.js",
|
|
554
|
-
"lynx.config.mjs",
|
|
555
|
-
"lynx.config.cjs",
|
|
556
|
-
];
|
|
557
|
-
if (await Promise.any(configFiles.map((file) => exists(join(root, file)))))
|
|
558
|
-
return true;
|
|
559
|
-
try {
|
|
560
|
-
const packageJson = JSON.parse(await readFile(join(root, "package.json"), "utf8"));
|
|
561
|
-
const dependencies = {
|
|
562
|
-
...packageJson.dependencies,
|
|
563
|
-
...packageJson.devDependencies,
|
|
564
|
-
};
|
|
565
|
-
return (Object.keys(dependencies).some((name) => name.startsWith("@lynx-js/") || name === "vue-lynx") ||
|
|
566
|
-
Object.values(packageJson.scripts ?? {}).some((script) => script.includes("rspeedy")));
|
|
567
|
-
}
|
|
568
|
-
catch {
|
|
569
|
-
return false;
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
async function initializeProject() {
|
|
573
|
-
const file = join(root, "lynxship.json");
|
|
574
|
-
if (await exists(file))
|
|
575
|
-
return file;
|
|
576
|
-
await mkdir(join(root, ".lynxship"), { recursive: true });
|
|
577
|
-
await writeFile(file, `${JSON.stringify({ ...DEFAULT_CONFIG, projectId: flag("--project-id") ?? randomUUID() }, null, 2)}\n`);
|
|
578
|
-
return file;
|
|
579
|
-
}
|
|
580
|
-
async function initializeBuildProject() {
|
|
581
|
-
if (await exists(join(root, "lynxship.json")))
|
|
582
|
-
return;
|
|
583
|
-
assert(await looksLikeLynxProject(), "CLI_PROJECT_REQUIRED", "No LynxJS project was detected. Run this command from the project directory or provide `--project-dir`.");
|
|
584
|
-
ui.info("No lynxship.json found. Running lynxship init automatically…");
|
|
585
|
-
await initializeProject();
|
|
586
|
-
ui.success("Created lynxship.json");
|
|
587
|
-
}
|
|
588
|
-
async function ensureNativeHostForBuild(platform, profile, config) {
|
|
589
|
-
if (platform === "android") {
|
|
590
|
-
assert(isSupportedAndroidPlatform(), "ANDROID_PLATFORM_UNSUPPORTED", "Android builds are supported on Windows, macOS, and Linux.");
|
|
591
|
-
if (await hasAndroidHost(root))
|
|
592
|
-
return;
|
|
593
|
-
assert(!(await exists(join(root, "android"))), "ANDROID_HOST_EXISTS", "An android/ directory exists but does not contain a usable Gradle host. LynxShip will not overwrite it; repair it or remove it deliberately, then rerun the build.");
|
|
594
|
-
const suggestedId = suggestedAndroidApplicationId(root);
|
|
595
|
-
const applicationId = flag("--application-id") ??
|
|
596
|
-
(ui.interactive
|
|
597
|
-
? await prompt("Android application ID", suggestedId)
|
|
598
|
-
: "");
|
|
599
|
-
assert(applicationId, "ANDROID_HOST_REQUIRED", `No Android Gradle host exists. Run \`lynxship android host init --application-id ${suggestedId}\` or pass --application-id <id> to build in non-interactive mode.`);
|
|
600
|
-
ui.info("No Android host found. Creating the official LynxShip host before the build…");
|
|
601
|
-
const result = await initializeAndroidHost(root, {
|
|
602
|
-
applicationId,
|
|
603
|
-
appName: basename(root),
|
|
604
|
-
});
|
|
605
|
-
ui.success(`Android host created: ${result.directory}`);
|
|
606
|
-
return;
|
|
607
|
-
}
|
|
608
|
-
if (platform === "web") {
|
|
609
|
-
const resolvedProfile = resolveProfile(config, profile);
|
|
610
|
-
assert(hasWebConfiguration(root) || Boolean(resolvedProfile.web?.script), "WEB_CONFIGURATION_REQUIRED", "No Lynx Web configuration was detected. Add environments.web to lynx.config.* or configure build.<profile>.web.script.");
|
|
611
|
-
return;
|
|
612
|
-
}
|
|
613
|
-
if (platform === "harmony") {
|
|
614
|
-
assert(hasHarmonyHost(root), "HARMONY_HOST_REQUIRED", "No complete HarmonyOS host was found. Add an official Lynx Harmony host under harmony/ before building.");
|
|
615
|
-
return;
|
|
616
|
-
}
|
|
617
|
-
if (platform === "desktop") {
|
|
618
|
-
const resolvedProfile = resolveProfile(config, profile);
|
|
619
|
-
assert(await hasDesktopHost(root, resolvedProfile), "DESKTOP_HOST_REQUIRED", "No Lynxtron desktop host was found. Use the official Lynxtron template or configure a pack script before building.");
|
|
620
|
-
return;
|
|
621
|
-
}
|
|
622
|
-
assert(process.platform === "darwin", "IOS_MACOS_REQUIRED", "iOS hosts and IPA builds require macOS with Xcode. Run this build on macOS or a macOS CI worker.");
|
|
623
|
-
const resolvedProfile = resolveProfile(config, profile);
|
|
624
|
-
if (hasIosHost(root, resolvedProfile))
|
|
625
|
-
return;
|
|
626
|
-
assert(!(await exists(join(root, "ios"))), "IOS_HOST_EXISTS", "An ios/ directory exists but does not contain a usable Xcode host. LynxShip will not overwrite it; repair it deliberately, then rerun the build.");
|
|
627
|
-
const suggestedId = suggestedIosBundleIdentifier(root);
|
|
628
|
-
const bundleIdentifier = flag("--bundle-identifier") ??
|
|
629
|
-
(ui.interactive ? await prompt("iOS bundle identifier", suggestedId) : "");
|
|
630
|
-
assert(bundleIdentifier, "IOS_HOST_REQUIRED", `No iOS host exists. Run \`lynxship ios host init --bundle-identifier ${suggestedId}\` or pass --bundle-identifier <id> to build in non-interactive mode.`);
|
|
631
|
-
ui.info("No iOS host found. Creating the official LynxShip host before the build…");
|
|
632
|
-
const result = await initializeIosHost(root, {
|
|
633
|
-
bundleIdentifier,
|
|
634
|
-
appName: basename(root),
|
|
635
|
-
appIcon: flag("--icon") ?? undefined,
|
|
636
|
-
});
|
|
637
|
-
ui.success(`iOS host created: ${result.directory}`);
|
|
638
|
-
}
|
|
639
|
-
function forwardedToolArgs(values) {
|
|
640
|
-
const result = [];
|
|
641
|
-
const valueFlags = new Set([
|
|
642
|
-
"--project-dir",
|
|
643
|
-
"--platform",
|
|
644
|
-
"--profile",
|
|
645
|
-
"--json",
|
|
646
|
-
]);
|
|
647
|
-
for (let index = 0; index < values.length; index += 1) {
|
|
648
|
-
const value = values[index];
|
|
649
|
-
if (!value)
|
|
650
|
-
continue;
|
|
651
|
-
if (valueFlags.has(value)) {
|
|
652
|
-
index += 1;
|
|
653
|
-
continue;
|
|
654
|
-
}
|
|
655
|
-
if ([
|
|
656
|
-
"--quiet",
|
|
657
|
-
"--verbose",
|
|
658
|
-
"--no-color",
|
|
659
|
-
"--non-interactive",
|
|
660
|
-
"--banner",
|
|
661
|
-
"--local",
|
|
662
|
-
].includes(value))
|
|
663
|
-
continue;
|
|
664
|
-
if (value.startsWith("--project-dir="))
|
|
665
|
-
continue;
|
|
666
|
-
result.push(value);
|
|
667
|
-
}
|
|
668
|
-
return result;
|
|
669
|
-
}
|
|
670
|
-
async function runRspeedyCommand(subcommand, environment) {
|
|
671
|
-
await initializeBuildProject();
|
|
672
|
-
const forwarded = forwardedToolArgs(args);
|
|
673
|
-
ui.info(`Running local Rspeedy ${subcommand}…`);
|
|
674
|
-
if (subcommand === "dev") {
|
|
675
|
-
ui.info("Lynx Explorer mode: no Android or iOS native host is required. Scan the QR code printed by Rspeedy; source changes reload automatically.");
|
|
676
|
-
}
|
|
677
|
-
let devUrl;
|
|
678
|
-
let devQrPrinted = false;
|
|
679
|
-
let devQrTimer;
|
|
680
|
-
const printDevQr = async () => {
|
|
681
|
-
if (!devUrl || devQrPrinted || json || ui.options.quiet)
|
|
682
|
-
return;
|
|
683
|
-
devQrPrinted = true;
|
|
684
|
-
if (devQrTimer)
|
|
685
|
-
clearTimeout(devQrTimer);
|
|
686
|
-
await ui.devServerQr(devUrl);
|
|
687
|
-
};
|
|
688
|
-
await runRspeedy(root, subcommand, forwarded, {
|
|
689
|
-
env: environment,
|
|
690
|
-
quiet: json,
|
|
691
|
-
onOutput: (line) => {
|
|
692
|
-
ui.info(`│ ${line}`);
|
|
693
|
-
if (subcommand !== "dev")
|
|
694
|
-
return;
|
|
695
|
-
const url = extractDevServerUrl(line);
|
|
696
|
-
if (url && (!devUrl || url.includes("fullscreen=true"))) {
|
|
697
|
-
devUrl = url;
|
|
698
|
-
if (devQrTimer)
|
|
699
|
-
clearTimeout(devQrTimer);
|
|
700
|
-
devQrTimer = setTimeout(() => {
|
|
701
|
-
void printDevQr();
|
|
702
|
-
}, 500);
|
|
703
|
-
}
|
|
704
|
-
if (devUrl && shouldPrintDevServerQr(line))
|
|
705
|
-
void printDevQr();
|
|
706
|
-
},
|
|
707
|
-
});
|
|
708
|
-
printValue({ status: "success", command: `rspeedy ${subcommand}` }, {
|
|
709
|
-
title: `Rspeedy ${subcommand}`,
|
|
710
|
-
rows: [
|
|
711
|
-
{
|
|
712
|
-
label: "Command",
|
|
713
|
-
value: `rspeedy ${subcommand}`,
|
|
714
|
-
valueColor: "blue",
|
|
715
|
-
},
|
|
716
|
-
],
|
|
717
|
-
done: `Rspeedy ${subcommand} completed successfully.`,
|
|
718
|
-
});
|
|
719
|
-
}
|
|
720
|
-
async function runAutolinkCodegen() {
|
|
721
|
-
const libraryDirectory = resolve(root, flag("--library-dir", "."));
|
|
722
|
-
const packageJson = JSON.parse(await readFile(join(libraryDirectory, "package.json"), "utf8"));
|
|
723
|
-
assert(packageJson.scripts?.codegen, "LYNX_CODEGEN_SCRIPT_REQUIRED", "No codegen script was found. Add the official lynx-autolink-codegen script to the native library package.");
|
|
724
|
-
const manager = packageManagerCommand(libraryDirectory);
|
|
725
|
-
ui.info("Running the project's official Native Module codegen script…");
|
|
726
|
-
await runProcess(manager.command, ["run", "codegen"], {
|
|
727
|
-
cwd: libraryDirectory,
|
|
728
|
-
quiet: json,
|
|
729
|
-
onOutput: (line) => ui.info(`│ ${line}`),
|
|
730
|
-
});
|
|
731
|
-
printValue({ status: "success", directory: libraryDirectory }, {
|
|
732
|
-
title: "Lynx Autolink codegen",
|
|
733
|
-
rows: [{ label: "Library", value: libraryDirectory, valueColor: "blue" }],
|
|
734
|
-
done: "Native Module specifications were generated successfully.",
|
|
735
|
-
});
|
|
736
|
-
}
|
|
737
|
-
async function runDevToolDoctor(kind) {
|
|
738
|
-
await initializeBuildProject();
|
|
739
|
-
const platform = mobilePlatformValue(flag("--platform", "android"));
|
|
740
|
-
const status = await inspectLynxDevTool(root, platform);
|
|
741
|
-
const ready = kind === "devtool"
|
|
742
|
-
? status.ok
|
|
743
|
-
: kind === "trace"
|
|
744
|
-
? status.traceReady
|
|
745
|
-
: status.recorderReady;
|
|
746
|
-
if (!ready)
|
|
747
|
-
process.exitCode = 1;
|
|
748
|
-
printValue({ ...status, requested: kind, ready }, {
|
|
749
|
-
title: `Lynx ${kind === "devtool" ? "DevTool" : kind[0].toUpperCase() + kind.slice(1)} · ${platform}`,
|
|
750
|
-
rows: status.checks.map((item) => ({
|
|
751
|
-
label: item.name,
|
|
752
|
-
value: `${item.status} · ${item.value}${item.fix && item.status !== "pass" ? ` · fix: ${item.fix}` : ""}`,
|
|
753
|
-
valueColor: item.status === "pass"
|
|
754
|
-
? "green"
|
|
755
|
-
: item.status === "warn"
|
|
756
|
-
? "yellow"
|
|
757
|
-
: "red",
|
|
758
|
-
})),
|
|
759
|
-
done: ready
|
|
760
|
-
? `${kind} prerequisites are ready. Open Lynx DevTool Desktop and connect the target device by USB.`
|
|
761
|
-
: `Lynx ${kind} prerequisites are incomplete: ${formatDevToolFailure(status) || "review the failed checks"}`,
|
|
762
|
-
});
|
|
763
|
-
}
|
|
764
|
-
async function runDevice() {
|
|
765
|
-
const platform = platformValue(flag("--platform", "android"));
|
|
766
|
-
const artifact = flag("--artifact");
|
|
767
|
-
const artifactPath = artifact
|
|
768
|
-
? resolve(root, artifact)
|
|
769
|
-
: (await loadState()).builds
|
|
770
|
-
.list()
|
|
771
|
-
.filter((job) => job.platform === platform && job.state === "success")
|
|
772
|
-
.at(-1)?.artifact?.path;
|
|
773
|
-
assert(artifactPath, "DEVICE_ARTIFACT_REQUIRED", "Pass --artifact or create a successful build first");
|
|
774
|
-
if (platform === "android") {
|
|
775
|
-
assert(commandExists("adb"), "ANDROID_ADB_REQUIRED", "adb was not found in PATH");
|
|
776
|
-
const device = flag("--device");
|
|
777
|
-
const argsForAdb = device
|
|
778
|
-
? ["-s", device, "install", "-r", artifactPath]
|
|
779
|
-
: ["install", "-r", artifactPath];
|
|
780
|
-
await runProcess("adb", argsForAdb, {
|
|
781
|
-
cwd: root,
|
|
782
|
-
quiet: json,
|
|
783
|
-
onOutput: (line) => ui.info(`│ ${line}`),
|
|
784
|
-
});
|
|
785
|
-
}
|
|
786
|
-
else if (platform === "ios") {
|
|
787
|
-
assert(process.platform === "darwin", "IOS_MACOS_REQUIRED", "iOS device/simulator commands require macOS");
|
|
788
|
-
assert(commandExists("xcrun"), "IOS_XCRUN_REQUIRED", "xcrun was not found in PATH");
|
|
789
|
-
const device = flag("--device", "booted");
|
|
790
|
-
const simulator = args.includes("--simulator");
|
|
791
|
-
if (simulator || artifactPath.endsWith(".app")) {
|
|
792
|
-
await runProcess("xcrun", ["simctl", "install", device, artifactPath], {
|
|
793
|
-
cwd: root,
|
|
794
|
-
quiet: json,
|
|
795
|
-
onOutput: (line) => ui.info(`│ ${line}`),
|
|
796
|
-
});
|
|
797
|
-
if (args.includes("--launch")) {
|
|
798
|
-
await launchIosSimulatorApp(root, device, artifactPath, {
|
|
799
|
-
quiet: json,
|
|
800
|
-
onEvent: (line) => ui.info(`│ ${line}`),
|
|
801
|
-
});
|
|
802
|
-
}
|
|
803
|
-
}
|
|
804
|
-
else {
|
|
805
|
-
assert(device !== "booted", "IOS_DEVICE_REQUIRED", "A physical iOS install requires --device <device-identifier>; use --simulator for a booted simulator.");
|
|
806
|
-
await runProcess("xcrun", [
|
|
807
|
-
"devicectl",
|
|
808
|
-
"device",
|
|
809
|
-
"install",
|
|
810
|
-
"app",
|
|
811
|
-
"--device",
|
|
812
|
-
device,
|
|
813
|
-
artifactPath,
|
|
814
|
-
], {
|
|
815
|
-
cwd: root,
|
|
816
|
-
quiet: json,
|
|
817
|
-
onOutput: (line) => ui.info(`│ ${line}`),
|
|
818
|
-
});
|
|
819
|
-
}
|
|
820
|
-
}
|
|
821
|
-
else if (platform === "harmony") {
|
|
822
|
-
assert(commandExists("hdc"), "HARMONY_HDC_REQUIRED", "hdc was not found in PATH. Install the OpenHarmony SDK platform tools before installing a HAP.");
|
|
823
|
-
assert(artifactPath.endsWith(".hap"), "DEVICE_ARTIFACT_INVALID", "HarmonyOS run requires a signed .hap artifact.");
|
|
824
|
-
const device = flag("--device");
|
|
825
|
-
await runProcess("hdc", device
|
|
826
|
-
? ["-t", device, "install", "-r", artifactPath]
|
|
827
|
-
: ["install", "-r", artifactPath], {
|
|
828
|
-
cwd: root,
|
|
829
|
-
quiet: json,
|
|
830
|
-
onOutput: (line) => ui.info(`│ ${line}`),
|
|
831
|
-
});
|
|
832
|
-
}
|
|
833
|
-
else {
|
|
834
|
-
assert(false, "TARGET_RUN_UNSUPPORTED", platform === "web"
|
|
835
|
-
? "Web artifacts are previewed with `lynxship preview` or served by the project; they are not installed on a device."
|
|
836
|
-
: "Desktop installers are launched by the operating system after packaging; LynxShip does not claim a cross-platform install command.");
|
|
837
|
-
}
|
|
838
|
-
printValue({ status: "installed", platform, artifact: artifactPath }, {
|
|
839
|
-
title: "Device install",
|
|
840
|
-
rows: [{ label: "Artifact", value: artifactPath, valueColor: "green" }],
|
|
841
|
-
done: "Artifact installed on the selected target.",
|
|
842
|
-
});
|
|
843
|
-
}
|
|
844
|
-
async function streamNativeLogs() {
|
|
845
|
-
const platform = platformValue(flag("--platform", "android"));
|
|
846
|
-
const device = flag("--device", platform === "ios" ? "booted" : undefined);
|
|
847
|
-
if (platform === "android") {
|
|
848
|
-
assert(commandExists("adb"), "ANDROID_ADB_REQUIRED", "adb was not found in PATH");
|
|
849
|
-
await runProcess("adb", device ? ["-s", device, "logcat"] : ["logcat"], {
|
|
850
|
-
cwd: root,
|
|
851
|
-
quiet: json,
|
|
852
|
-
onOutput: (line) => ui.info(`│ ${line}`),
|
|
853
|
-
});
|
|
854
|
-
}
|
|
855
|
-
else if (platform === "ios") {
|
|
856
|
-
assert(process.platform === "darwin", "IOS_MACOS_REQUIRED", "iOS logs require macOS");
|
|
857
|
-
assert(commandExists("xcrun"), "IOS_XCRUN_REQUIRED", "xcrun was not found in PATH");
|
|
858
|
-
assert(device !== "booted", "IOS_DEVICE_LOGS_UNSUPPORTED", "Use --device <simulator-identifier> for iOS simulator logs.");
|
|
859
|
-
await runProcess("xcrun", [
|
|
860
|
-
"simctl",
|
|
861
|
-
"spawn",
|
|
862
|
-
device ?? "booted",
|
|
863
|
-
"log",
|
|
864
|
-
"stream",
|
|
865
|
-
"--style",
|
|
866
|
-
"compact",
|
|
867
|
-
"--level",
|
|
868
|
-
"debug",
|
|
869
|
-
], { cwd: root, quiet: json, onOutput: (line) => ui.info(`│ ${line}`) });
|
|
870
|
-
}
|
|
871
|
-
else if (platform === "harmony") {
|
|
872
|
-
assert(commandExists("hdc"), "HARMONY_HDC_REQUIRED", "hdc was not found in PATH. Install the OpenHarmony SDK platform tools before streaming logs.");
|
|
873
|
-
const argsForHdc = device
|
|
874
|
-
? ["-t", device, "shell", "hilog"]
|
|
875
|
-
: ["shell", "hilog"];
|
|
876
|
-
await runProcess("hdc", argsForHdc, {
|
|
877
|
-
cwd: root,
|
|
878
|
-
quiet: json,
|
|
879
|
-
onOutput: (line) => ui.info(`│ ${line}`),
|
|
880
|
-
});
|
|
881
|
-
}
|
|
882
|
-
else {
|
|
883
|
-
assert(false, "TARGET_LOGS_UNSUPPORTED", platform === "web"
|
|
884
|
-
? "Web logs are emitted by the browser/runtime console; use lynxship dev or the project's Web DevTools."
|
|
885
|
-
: "Desktop logs are emitted by the packaged runtime; use the target OS logging tools.");
|
|
886
|
-
}
|
|
887
|
-
}
|
|
888
|
-
async function main() {
|
|
889
|
-
const command = args.shift() ?? "help";
|
|
890
|
-
const wantsHelp = command === "help" ||
|
|
891
|
-
command === "--help" ||
|
|
892
|
-
command === "-h" ||
|
|
893
|
-
rawArgs.includes("--help") ||
|
|
894
|
-
rawArgs.includes("-h");
|
|
895
|
-
const shouldShowBanner = !json &&
|
|
896
|
-
(wantsHelp || rawArgs.length === 0 || rawArgs.includes("--banner"));
|
|
897
|
-
if (shouldShowBanner)
|
|
898
|
-
ui.banner();
|
|
899
|
-
if (wantsHelp) {
|
|
900
|
-
if (ui.interactive)
|
|
901
|
-
ui.header("Help");
|
|
902
|
-
printValue(helpText());
|
|
903
|
-
return;
|
|
904
|
-
}
|
|
905
|
-
ui.header(commandTitle(command === "update" && args[0] === "rollback" ? "rollback" : command));
|
|
906
|
-
ui.debug(`cwd=${root}`);
|
|
907
|
-
if (command === "init") {
|
|
908
|
-
ui.info("Scanning project structure…");
|
|
909
|
-
const file = join(root, "lynxship.json");
|
|
910
|
-
if (await exists(file)) {
|
|
911
|
-
ui.warn("lynxship.json already exists; leaving the project unchanged");
|
|
912
|
-
printValue({ status: "unchanged", file }, {
|
|
913
|
-
title: "Project",
|
|
914
|
-
rows: [{ label: "Configuration", value: file, valueColor: "muted" }],
|
|
915
|
-
done: "Project already initialized.",
|
|
916
|
-
});
|
|
917
|
-
return;
|
|
918
|
-
}
|
|
919
|
-
await initializeProject();
|
|
920
|
-
const initializedConfig = await loadConfig(root);
|
|
921
|
-
ui.success("Created lynxship.json");
|
|
922
|
-
printValue({ status: "created", file }, {
|
|
923
|
-
title: "Initialized",
|
|
924
|
-
rows: [
|
|
925
|
-
{
|
|
926
|
-
label: "Project ID",
|
|
927
|
-
value: initializedConfig.projectId ?? "unassigned",
|
|
928
|
-
valueColor: "purple",
|
|
929
|
-
},
|
|
930
|
-
{ label: "Configuration", value: file, valueColor: "muted" },
|
|
931
|
-
],
|
|
932
|
-
done: "Project initialized. Run lynxship build to get started.",
|
|
933
|
-
});
|
|
934
|
-
return;
|
|
935
|
-
}
|
|
936
|
-
if (command === "plugin") {
|
|
937
|
-
const subcommand = args.shift() ?? "list";
|
|
938
|
-
assert(["list", "doctor", "apply"].includes(subcommand), "CLI_PLUGIN_COMMAND", "Use \`lynxship plugin list\`, \`lynxship plugin doctor\` or \`lynxship plugin apply\`.");
|
|
939
|
-
await requireProjectRoot();
|
|
940
|
-
const config = await loadConfig(root);
|
|
941
|
-
const report = await inspectProjectPlugins(root, config);
|
|
942
|
-
if (subcommand === "apply") {
|
|
943
|
-
const platform = platformValue(flag("--platform", "android"));
|
|
944
|
-
const profile = flag("--profile", "production");
|
|
945
|
-
const dryRun = args.includes("--dry-run");
|
|
946
|
-
const result = await applyProjectPlugins(root, config, {
|
|
947
|
-
platform,
|
|
948
|
-
profile,
|
|
949
|
-
mode: dryRun ? "plan" : "apply",
|
|
950
|
-
});
|
|
951
|
-
printValue({
|
|
952
|
-
status: dryRun ? "planned" : "applied",
|
|
953
|
-
platform,
|
|
954
|
-
profile,
|
|
955
|
-
plugins: result.applied,
|
|
956
|
-
nativeChanges: result.changes.filter((change) => change.changed)
|
|
957
|
-
.length,
|
|
958
|
-
changes: result.changes,
|
|
959
|
-
templates: result.templates,
|
|
960
|
-
cloud: result.cloud,
|
|
961
|
-
build: result.build,
|
|
962
|
-
}, {
|
|
963
|
-
title: `LynxShip plugins · ${platform}`,
|
|
964
|
-
rows: [
|
|
965
|
-
{
|
|
966
|
-
label: "Applied",
|
|
967
|
-
value: result.applied.length ? result.applied.join(", ") : "none",
|
|
968
|
-
valueColor: "green",
|
|
969
|
-
},
|
|
970
|
-
{
|
|
971
|
-
label: "Native changes",
|
|
972
|
-
value: String(result.changes.filter((change) => change.changed).length),
|
|
973
|
-
valueColor: "blue",
|
|
974
|
-
},
|
|
975
|
-
],
|
|
976
|
-
done: dryRun
|
|
977
|
-
? "No native files were modified; review the planned changes."
|
|
978
|
-
: "Project plugin changes are applied atomically and idempotently.",
|
|
979
|
-
});
|
|
980
|
-
return;
|
|
981
|
-
}
|
|
982
|
-
const invalid = report.plugins.filter((plugin) => plugin.status !== "ready");
|
|
983
|
-
printValue(report, {
|
|
984
|
-
title: `LynxShip plugins · ${subcommand}`,
|
|
985
|
-
rows: report.plugins.length > 0
|
|
986
|
-
? report.plugins.map((plugin) => ({
|
|
987
|
-
label: plugin.name,
|
|
988
|
-
value: `${plugin.status} · ${plugin.capabilities.join(", ") || "no capabilities"} · ${plugin.reason}`,
|
|
989
|
-
valueColor: plugin.status === "ready"
|
|
990
|
-
? "green"
|
|
991
|
-
: plugin.status === "missing"
|
|
992
|
-
? "red"
|
|
993
|
-
: "yellow",
|
|
994
|
-
}))
|
|
995
|
-
: [
|
|
996
|
-
{
|
|
997
|
-
label: "Plugins",
|
|
998
|
-
value: "none configured",
|
|
999
|
-
valueColor: "muted",
|
|
1000
|
-
},
|
|
1001
|
-
],
|
|
1002
|
-
done: invalid.length === 0
|
|
1003
|
-
? "All project plugin manifests are valid."
|
|
1004
|
-
: "Fix the invalid plugin package before building.",
|
|
1005
|
-
});
|
|
1006
|
-
if (subcommand === "doctor" && invalid.length > 0)
|
|
1007
|
-
process.exitCode = 1;
|
|
1008
|
-
return;
|
|
1009
|
-
}
|
|
1010
|
-
if (command === "doctor") {
|
|
1011
|
-
const config = await loadConfig(root);
|
|
1012
|
-
const configuration = await readConfigurationStatus();
|
|
1013
|
-
const doctorPlatform = platformValue(flag("--platform", "android"));
|
|
1014
|
-
const autolink = doctorPlatform === "android" || doctorPlatform === "ios"
|
|
1015
|
-
? await inspectAutolink(root)
|
|
1016
|
-
: undefined;
|
|
1017
|
-
const autolinkForPlatform = autolink && (doctorPlatform === "android" || doctorPlatform === "ios")
|
|
1018
|
-
? autolink[doctorPlatform]
|
|
1019
|
-
: undefined;
|
|
1020
|
-
const framework = await detectLynxFramework(root);
|
|
1021
|
-
const packageLockfile = await findLockfile(root);
|
|
1022
|
-
const lockfile = packageLockfile ??
|
|
1023
|
-
(framework.framework === "miso" &&
|
|
1024
|
-
(await exists(join(root, "flake.lock")))
|
|
1025
|
-
? join(root, "flake.lock")
|
|
1026
|
-
: null);
|
|
1027
|
-
const credentialStore = credentialStorageDescription();
|
|
1028
|
-
const nativeCredentialStore = !credentialStore.includes("owner-only");
|
|
1029
|
-
const androidHost = doctorPlatform === "android" ? await hasAndroidHost(root) : false;
|
|
1030
|
-
const doctorProfileName = flag("--profile", "production");
|
|
1031
|
-
const doctorProfile = resolveProfile(config, doctorProfileName);
|
|
1032
|
-
const targetToolchain = doctorPlatform === "web"
|
|
1033
|
-
? await inspectWebTarget(root, doctorProfile)
|
|
1034
|
-
: doctorPlatform === "harmony"
|
|
1035
|
-
? await inspectHarmonyTarget(root, doctorProfile)
|
|
1036
|
-
: doctorPlatform === "desktop"
|
|
1037
|
-
? await inspectDesktopTarget(root, doctorProfile)
|
|
1038
|
-
: undefined;
|
|
1039
|
-
assert(!args.includes("--fix") || doctorPlatform === "android", "CLI_DOCTOR_FIX_PLATFORM", "`doctor --fix` currently repairs Android SDK packages; run it with `--platform android`.");
|
|
1040
|
-
assert(!args.includes("--fix") || ui.interactive, "CLI_INTERACTIVE_REQUIRED", "Run `lynxship doctor --platform android --fix` in an interactive terminal.");
|
|
1041
|
-
let androidToolchain = doctorPlatform === "android"
|
|
1042
|
-
? await inspectAndroidToolchain(root)
|
|
1043
|
-
: undefined;
|
|
1044
|
-
const iosToolchain = doctorPlatform === "ios"
|
|
1045
|
-
? await inspectIosToolchain(root, doctorProfile, doctorProfile.ios?.simulator || doctorProfileName === "simulator"
|
|
1046
|
-
? "simulator"
|
|
1047
|
-
: "device")
|
|
1048
|
-
: undefined;
|
|
1049
|
-
if (androidToolchain && args.includes("--fix")) {
|
|
1050
|
-
await fixAndroidToolchain(root, androidToolchain, (message) => confirm(message), (line) => ui.info(line));
|
|
1051
|
-
androidToolchain = await inspectAndroidToolchain(root);
|
|
1052
|
-
}
|
|
1053
|
-
const nodeMajor = Number(process.versions.node.split(".")[0]);
|
|
1054
|
-
const nodeSupported = nodeMajor >= 22;
|
|
1055
|
-
const nodeRecommended = nodeMajor % 2 === 0;
|
|
1056
|
-
const misoCompiler = framework.framework === "miso"
|
|
1057
|
-
? (doctorProfile.miso?.compiler ?? "ghcjs")
|
|
1058
|
-
: undefined;
|
|
1059
|
-
const microhsConfig = doctorProfile.miso?.microhs;
|
|
1060
|
-
const microhsBinary = microhsConfig?.binary ?? process.env.LYNXSHIP_MICROHS_BINARY;
|
|
1061
|
-
const microhsBinaryReady = Boolean(microhsBinary && existsSync(resolve(root, microhsBinary)));
|
|
1062
|
-
const microhsManifestPath = microhsConfig?.manifest ?? process.env.LYNXSHIP_MICROHS_MANIFEST;
|
|
1063
|
-
const microhsManifestReady = Boolean((microhsManifestPath && existsSync(resolve(root, microhsManifestPath))) ||
|
|
1064
|
-
microhsConfig?.manifestUrl ||
|
|
1065
|
-
process.env.LYNXSHIP_MICROHS_MANIFEST_URL);
|
|
1066
|
-
let microhsHost;
|
|
1067
|
-
try {
|
|
1068
|
-
microhsHost = microHsHostTriple();
|
|
1069
|
-
}
|
|
1070
|
-
catch {
|
|
1071
|
-
microhsHost = undefined;
|
|
1072
|
-
}
|
|
1073
|
-
const pluginReport = await inspectProjectPlugins(root, config);
|
|
1074
|
-
const invalidPlugins = pluginReport.plugins.filter((plugin) => plugin.status !== "ready");
|
|
1075
|
-
const checks = [
|
|
1076
|
-
{
|
|
1077
|
-
name: "lynx-framework",
|
|
1078
|
-
ok: framework.framework !== "unknown",
|
|
1079
|
-
status: framework.framework === "unknown"
|
|
1080
|
-
? "warn"
|
|
1081
|
-
: framework.experimental
|
|
1082
|
-
? "warn"
|
|
1083
|
-
: "pass",
|
|
1084
|
-
value: framework.framework === "unknown"
|
|
1085
|
-
? "unknown · verify the official Lynx/Rspeedy integration"
|
|
1086
|
-
: framework.label +
|
|
1087
|
-
" · " +
|
|
1088
|
-
framework.evidence +
|
|
1089
|
-
(framework.experimental ? " · early access" : ""),
|
|
1090
|
-
},
|
|
1091
|
-
...(framework.framework === "miso" && misoCompiler !== "microhs"
|
|
1092
|
-
? [
|
|
1093
|
-
{
|
|
1094
|
-
name: "miso-nix",
|
|
1095
|
-
ok: commandExists("nix"),
|
|
1096
|
-
status: commandExists("nix")
|
|
1097
|
-
? "pass"
|
|
1098
|
-
: "fail",
|
|
1099
|
-
value: commandExists("nix")
|
|
1100
|
-
? "Nix detected"
|
|
1101
|
-
: "missing · fix: install Nix and rerun doctor",
|
|
1102
|
-
},
|
|
1103
|
-
]
|
|
1104
|
-
: framework.framework === "miso"
|
|
1105
|
-
? [
|
|
1106
|
-
{
|
|
1107
|
-
name: "miso-compiler",
|
|
1108
|
-
ok: true,
|
|
1109
|
-
status: "pass",
|
|
1110
|
-
value: "MicroHs selected explicitly",
|
|
1111
|
-
},
|
|
1112
|
-
{
|
|
1113
|
-
name: "miso-microhs-host",
|
|
1114
|
-
ok: Boolean(microhsHost),
|
|
1115
|
-
status: microhsHost ? "pass" : "fail",
|
|
1116
|
-
value: microhsHost ??
|
|
1117
|
-
"unsupported host architecture · fix: use a supported MicroHs host or compiler=ghcjs",
|
|
1118
|
-
},
|
|
1119
|
-
{
|
|
1120
|
-
name: "miso-microhs-toolchain",
|
|
1121
|
-
ok: microhsBinaryReady || microhsManifestReady,
|
|
1122
|
-
status: microhsBinaryReady || microhsManifestReady
|
|
1123
|
-
? "pass"
|
|
1124
|
-
: "fail",
|
|
1125
|
-
value: microhsBinaryReady
|
|
1126
|
-
? "external MicroHs binary found"
|
|
1127
|
-
: microhsManifestReady
|
|
1128
|
-
? "pinned release manifest configured · downloaded on build"
|
|
1129
|
-
: "missing · fix: configure build.<profile>.miso.microhs.binary or manifestUrl",
|
|
1130
|
-
},
|
|
1131
|
-
{
|
|
1132
|
-
name: "miso-microhs-adapter",
|
|
1133
|
-
ok: Boolean(microhsConfig?.adapter?.command),
|
|
1134
|
-
status: microhsConfig?.adapter?.command
|
|
1135
|
-
? "pass"
|
|
1136
|
-
: "fail",
|
|
1137
|
-
value: microhsConfig?.adapter?.command
|
|
1138
|
-
? "adapter command configured"
|
|
1139
|
-
: "missing · fix: configure build.<profile>.miso.microhs.adapter",
|
|
1140
|
-
},
|
|
1141
|
-
]
|
|
1142
|
-
: []),
|
|
1143
|
-
{
|
|
1144
|
-
name: "node",
|
|
1145
|
-
ok: nodeSupported,
|
|
1146
|
-
status: !nodeSupported
|
|
1147
|
-
? "fail"
|
|
1148
|
-
: nodeRecommended
|
|
1149
|
-
? "pass"
|
|
1150
|
-
: "warn",
|
|
1151
|
-
value: !nodeSupported
|
|
1152
|
-
? `${process.version} · fix: use Node 24 LTS`
|
|
1153
|
-
: nodeRecommended
|
|
1154
|
-
? process.version
|
|
1155
|
-
: `${process.version} · recommended: Node 24 LTS`,
|
|
1156
|
-
},
|
|
1157
|
-
{
|
|
1158
|
-
name: "dependency-lockfile",
|
|
1159
|
-
ok: Boolean(lockfile),
|
|
1160
|
-
status: lockfile ? "pass" : "fail",
|
|
1161
|
-
value: lockfile ?? "missing · fix: pnpm install (or npm install)",
|
|
1162
|
-
},
|
|
1163
|
-
{
|
|
1164
|
-
name: "lynxship.json",
|
|
1165
|
-
ok: config.projectId !== undefined,
|
|
1166
|
-
status: config.projectId ? "pass" : "fail",
|
|
1167
|
-
value: config.projectId ? "found" : "missing · fix: lynxship init",
|
|
1168
|
-
},
|
|
1169
|
-
{
|
|
1170
|
-
name: "lynxship-plugins",
|
|
1171
|
-
ok: invalidPlugins.length === 0,
|
|
1172
|
-
status: invalidPlugins.length === 0 ? "pass" : "fail",
|
|
1173
|
-
value: invalidPlugins.length === 0
|
|
1174
|
-
? pluginReport.configured === 0
|
|
1175
|
-
? "none configured"
|
|
1176
|
-
: `${pluginReport.configured} plugin(s) ready`
|
|
1177
|
-
: `${invalidPlugins.map((plugin) => plugin.name).join(", ")} · fix: lynxship plugin doctor`,
|
|
1178
|
-
},
|
|
1179
|
-
{
|
|
1180
|
-
name: "credential-store",
|
|
1181
|
-
ok: true,
|
|
1182
|
-
status: nativeCredentialStore ? "pass" : "warn",
|
|
1183
|
-
value: nativeCredentialStore
|
|
1184
|
-
? credentialStore
|
|
1185
|
-
: `${credentialStore} · use CI secret variables or install Linux Secret Service`,
|
|
1186
|
-
},
|
|
1187
|
-
...(doctorPlatform === "android"
|
|
1188
|
-
? [
|
|
1189
|
-
{
|
|
1190
|
-
name: "android-host",
|
|
1191
|
-
ok: androidHost,
|
|
1192
|
-
status: androidHost ? "pass" : "fail",
|
|
1193
|
-
value: androidHost
|
|
1194
|
-
? "Gradle host found"
|
|
1195
|
-
: "missing · fix: lynxship android host init --application-id com.example.myapp",
|
|
1196
|
-
},
|
|
1197
|
-
...(androidToolchain?.checks ?? []).map((toolchainCheck) => ({
|
|
1198
|
-
name: toolchainCheck.name,
|
|
1199
|
-
ok: toolchainCheck.ok,
|
|
1200
|
-
status: toolchainCheck.status,
|
|
1201
|
-
value: toolchainCheck.fix && toolchainCheck.status !== "pass"
|
|
1202
|
-
? `${toolchainCheck.value} · fix: ${toolchainCheck.fix}`
|
|
1203
|
-
: toolchainCheck.value,
|
|
1204
|
-
})),
|
|
1205
|
-
]
|
|
1206
|
-
: doctorPlatform === "ios"
|
|
1207
|
-
? (iosToolchain?.checks ?? [])
|
|
1208
|
-
.filter((toolchainCheck) => toolchainCheck.name !== "ios-host")
|
|
1209
|
-
.map((toolchainCheck) => ({
|
|
1210
|
-
name: toolchainCheck.name,
|
|
1211
|
-
ok: toolchainCheck.ok,
|
|
1212
|
-
status: toolchainCheck.status,
|
|
1213
|
-
value: toolchainCheck.fix && toolchainCheck.status !== "pass"
|
|
1214
|
-
? `${toolchainCheck.value} · fix: ${toolchainCheck.fix}`
|
|
1215
|
-
: toolchainCheck.value,
|
|
1216
|
-
}))
|
|
1217
|
-
: (targetToolchain?.checks ?? []).map((toolchainCheck) => ({
|
|
1218
|
-
name: toolchainCheck.name,
|
|
1219
|
-
ok: toolchainCheck.ok,
|
|
1220
|
-
status: toolchainCheck.status,
|
|
1221
|
-
value: toolchainCheck.fix && toolchainCheck.status !== "pass"
|
|
1222
|
-
? `${toolchainCheck.value} · fix: ${toolchainCheck.fix}`
|
|
1223
|
-
: toolchainCheck.value,
|
|
1224
|
-
}))),
|
|
1225
|
-
{
|
|
1226
|
-
name: "cloudflare-r2",
|
|
1227
|
-
ok: doctorPlatform === "ios" && doctorProfile.ios?.simulator
|
|
1228
|
-
? true
|
|
1229
|
-
: configuration.r2,
|
|
1230
|
-
status: doctorPlatform === "ios" && doctorProfile.ios?.simulator
|
|
1231
|
-
? "pass"
|
|
1232
|
-
: configuration.r2
|
|
1233
|
-
? "pass"
|
|
1234
|
-
: "fail",
|
|
1235
|
-
value: doctorPlatform === "ios" && doctorProfile.ios?.simulator
|
|
1236
|
-
? "not required for iOS Simulator"
|
|
1237
|
-
: configuration.r2
|
|
1238
|
-
? "configured"
|
|
1239
|
-
: "missing · fix: lynxship storage configure",
|
|
1240
|
-
},
|
|
1241
|
-
...(doctorPlatform === "android"
|
|
1242
|
-
? [
|
|
1243
|
-
{
|
|
1244
|
-
name: "android-signing",
|
|
1245
|
-
ok: configuration.android,
|
|
1246
|
-
status: configuration.android
|
|
1247
|
-
? "pass"
|
|
1248
|
-
: "fail",
|
|
1249
|
-
value: configuration.android
|
|
1250
|
-
? "configured"
|
|
1251
|
-
: "missing · fix: lynxship android configure",
|
|
1252
|
-
},
|
|
1253
|
-
]
|
|
1254
|
-
: doctorPlatform === "ios"
|
|
1255
|
-
? [
|
|
1256
|
-
{
|
|
1257
|
-
name: "ios-host",
|
|
1258
|
-
ok: process.platform === "darwin" && hasIosHost(root),
|
|
1259
|
-
status: process.platform === "darwin" && hasIosHost(root)
|
|
1260
|
-
? "pass"
|
|
1261
|
-
: "fail",
|
|
1262
|
-
value: process.platform === "darwin" && hasIosHost(root)
|
|
1263
|
-
? "Xcode host found"
|
|
1264
|
-
: "missing · fix: use macOS, then lynxship ios host init --bundle-identifier com.example.myapp",
|
|
1265
|
-
},
|
|
1266
|
-
]
|
|
1267
|
-
: []),
|
|
1268
|
-
...(autolinkForPlatform
|
|
1269
|
-
? [
|
|
1270
|
-
{
|
|
1271
|
-
name: `lynx-autolink-${doctorPlatform}`,
|
|
1272
|
-
ok: autolinkForPlatform.ready,
|
|
1273
|
-
status: autolinkForPlatform.ready
|
|
1274
|
-
? "pass"
|
|
1275
|
-
: "fail",
|
|
1276
|
-
value: autolinkForPlatform.ready
|
|
1277
|
-
? autolinkForPlatform.reason
|
|
1278
|
-
: `${autolinkForPlatform.reason} · fix: install the native plugin, then run autolink codegen`,
|
|
1279
|
-
},
|
|
1280
|
-
]
|
|
1281
|
-
: []),
|
|
1282
|
-
];
|
|
1283
|
-
const result = {
|
|
1284
|
-
ok: checks.every((check) => check.status !== "fail"),
|
|
1285
|
-
checks,
|
|
1286
|
-
};
|
|
1287
|
-
const hasWarnings = checks.some((check) => check.status === "warn");
|
|
1288
|
-
if (!result.ok)
|
|
1289
|
-
ui.warn("One or more environment checks failed");
|
|
1290
|
-
else if (hasWarnings)
|
|
1291
|
-
ui.warn("Environment is usable, but one recommendation needs attention");
|
|
1292
|
-
if (!result.ok)
|
|
1293
|
-
process.exitCode = 1;
|
|
1294
|
-
printValue(result, {
|
|
1295
|
-
title: "Doctor result",
|
|
1296
|
-
rows: checks.map((check) => ({
|
|
1297
|
-
label: check.name,
|
|
1298
|
-
value: `${check.status} · ${check.value}`,
|
|
1299
|
-
valueColor: check.status === "pass"
|
|
1300
|
-
? "green"
|
|
1301
|
-
: check.status === "warn"
|
|
1302
|
-
? "yellow"
|
|
1303
|
-
: "red",
|
|
1304
|
-
})),
|
|
1305
|
-
done: result.ok
|
|
1306
|
-
? hasWarnings
|
|
1307
|
-
? "Environment is usable; review the recommendation when convenient."
|
|
1308
|
-
: "Environment looks ready."
|
|
1309
|
-
: "Fix the failed checks before building.",
|
|
1310
|
-
});
|
|
1311
|
-
return;
|
|
1312
|
-
}
|
|
1313
|
-
if (["dev", "preview", "inspect"].includes(command)) {
|
|
1314
|
-
await runRspeedyCommand(command);
|
|
1315
|
-
return;
|
|
1316
|
-
}
|
|
1317
|
-
if (command === "profile") {
|
|
1318
|
-
await runRspeedyCommand("build", {
|
|
1319
|
-
...process.env,
|
|
1320
|
-
RSPACK_PROFILE: process.env.RSPACK_PROFILE ?? "ALL",
|
|
1321
|
-
});
|
|
1322
|
-
return;
|
|
1323
|
-
}
|
|
1324
|
-
if (["devtool", "trace", "recorder"].includes(command)) {
|
|
1325
|
-
const subcommand = args.shift() ?? "doctor";
|
|
1326
|
-
assert(subcommand === "doctor", "CLI_DEVTOOL_COMMAND", "Use `lynxship devtool doctor`, `lynxship trace doctor` or `lynxship recorder doctor`.");
|
|
1327
|
-
await runDevToolDoctor(command);
|
|
1328
|
-
return;
|
|
1329
|
-
}
|
|
1330
|
-
if (command === "autolink") {
|
|
1331
|
-
const subcommand = args.shift() ?? "check";
|
|
1332
|
-
assert(["check", "codegen"].includes(subcommand), "CLI_AUTOLINK_COMMAND", "Use `lynxship autolink check` or `lynxship autolink codegen`");
|
|
1333
|
-
if (subcommand === "codegen") {
|
|
1334
|
-
await runAutolinkCodegen();
|
|
1335
|
-
return;
|
|
1336
|
-
}
|
|
1337
|
-
const platform = mobilePlatformValue(flag("--platform", "android"));
|
|
1338
|
-
const status = (await inspectAutolink(root))[platform];
|
|
1339
|
-
printValue(status, {
|
|
1340
|
-
title: `Lynx Autolink · ${platform}`,
|
|
1341
|
-
rows: [
|
|
1342
|
-
{
|
|
1343
|
-
label: "Required",
|
|
1344
|
-
value: String(status.required),
|
|
1345
|
-
valueColor: "blue",
|
|
1346
|
-
},
|
|
1347
|
-
{
|
|
1348
|
-
label: "Ready",
|
|
1349
|
-
value: String(status.ready),
|
|
1350
|
-
valueColor: status.ready ? "green" : "red",
|
|
1351
|
-
},
|
|
1352
|
-
{
|
|
1353
|
-
label: "Status",
|
|
1354
|
-
value: status.reason,
|
|
1355
|
-
valueColor: status.ready ? "green" : "yellow",
|
|
1356
|
-
},
|
|
1357
|
-
],
|
|
1358
|
-
done: status.ready
|
|
1359
|
-
? "Autolink host integration is ready."
|
|
1360
|
-
: "Autolink host integration needs attention.",
|
|
1361
|
-
});
|
|
1362
|
-
return;
|
|
1363
|
-
}
|
|
1364
|
-
if (command === "ota") {
|
|
1365
|
-
assert((args.shift() ?? "doctor") === "doctor", "CLI_OTA_COMMAND", "Only `lynxship ota doctor` is available");
|
|
1366
|
-
const platform = mobilePlatformValue(flag("--platform", "android"));
|
|
1367
|
-
const status = await inspectOtaHost(root, platform);
|
|
1368
|
-
printValue(status, {
|
|
1369
|
-
title: `OTA host · ${platform}`,
|
|
1370
|
-
rows: [
|
|
1371
|
-
{
|
|
1372
|
-
label: "Native files",
|
|
1373
|
-
value: String(status.files.length),
|
|
1374
|
-
valueColor: "blue",
|
|
1375
|
-
},
|
|
1376
|
-
{
|
|
1377
|
-
label: "Missing hooks",
|
|
1378
|
-
value: status.missing.length ? status.missing.join(", ") : "none",
|
|
1379
|
-
valueColor: status.missing.length ? "red" : "green",
|
|
1380
|
-
},
|
|
1381
|
-
],
|
|
1382
|
-
done: status.missing.length === 0
|
|
1383
|
-
? "Native OTA integration looks ready."
|
|
1384
|
-
: "Integrate the LynxShip OTA client before using device OTA.",
|
|
1385
|
-
});
|
|
1386
|
-
return;
|
|
1387
|
-
}
|
|
1388
|
-
if (command === "run") {
|
|
1389
|
-
await requireProjectRoot();
|
|
1390
|
-
await runDevice();
|
|
1391
|
-
return;
|
|
1392
|
-
}
|
|
1393
|
-
if (command === "logs") {
|
|
1394
|
-
await requireProjectRoot();
|
|
1395
|
-
await streamNativeLogs();
|
|
1396
|
-
return;
|
|
1397
|
-
}
|
|
1398
|
-
if (command === "self-host") {
|
|
1399
|
-
assert((args.shift() ?? "init") === "init", "CLI_SELF_HOST_COMMAND", "Only self-host init is available in this package");
|
|
1400
|
-
ui.info("Preparing local self-host credentials…");
|
|
1401
|
-
const result = await initSelfHost();
|
|
1402
|
-
ui.success(result.status === "created"
|
|
1403
|
-
? "Created protected local environment file"
|
|
1404
|
-
: "Existing environment file preserved");
|
|
1405
|
-
printValue(result, {
|
|
1406
|
-
title: "Self-host setup",
|
|
1407
|
-
rows: [
|
|
1408
|
-
{
|
|
1409
|
-
label: "Status",
|
|
1410
|
-
value: result.status,
|
|
1411
|
-
valueColor: result.status === "created" ? "green" : "yellow",
|
|
1412
|
-
},
|
|
1413
|
-
{ label: "Environment", value: result.file, valueColor: "muted" },
|
|
1414
|
-
],
|
|
1415
|
-
done: "Self-host environment is ready.",
|
|
1416
|
-
});
|
|
1417
|
-
return;
|
|
1418
|
-
}
|
|
1419
|
-
if (command === "storage") {
|
|
1420
|
-
assert(ui.interactive, "CLI_INTERACTIVE_REQUIRED", "Run `lynxship storage configure` in an interactive terminal");
|
|
1421
|
-
assert((args.shift() ?? "configure") === "configure", "CLI_STORAGE_COMMAND", "Only storage configure is available");
|
|
1422
|
-
ui.info("Configuring Cloudflare R2. Secret fields will stay invisible…");
|
|
1423
|
-
const config = await configureR2(root);
|
|
1424
|
-
ui.success(`R2 bucket verified: ${config.bucket}`);
|
|
1425
|
-
printValue({
|
|
1426
|
-
status: "configured",
|
|
1427
|
-
provider: "cloudflare-r2",
|
|
1428
|
-
bucket: config.bucket,
|
|
1429
|
-
}, {
|
|
1430
|
-
title: "Cloudflare R2",
|
|
1431
|
-
rows: [
|
|
1432
|
-
{ label: "Provider", value: "Cloudflare R2", valueColor: "orange" },
|
|
1433
|
-
{ label: "Bucket", value: config.bucket, valueColor: "blue" },
|
|
1434
|
-
{
|
|
1435
|
-
label: "Credentials",
|
|
1436
|
-
value: credentialStorageDescription(),
|
|
1437
|
-
valueColor: "green",
|
|
1438
|
-
},
|
|
1439
|
-
],
|
|
1440
|
-
done: "R2 is ready for signed build artifacts.",
|
|
1441
|
-
});
|
|
1442
|
-
return;
|
|
1443
|
-
}
|
|
1444
|
-
if (command === "ios") {
|
|
1445
|
-
const subcommand = args.shift() ?? "host";
|
|
1446
|
-
assert(subcommand === "host" && (args.shift() ?? "init") === "init", "CLI_IOS_HOST_COMMAND", "Use `lynxship ios host init` to create an iOS host.");
|
|
1447
|
-
await initializeBuildProject();
|
|
1448
|
-
const suggestedId = suggestedIosBundleIdentifier(root);
|
|
1449
|
-
const bundleIdentifier = flag("--bundle-identifier") ??
|
|
1450
|
-
(await assertInteractivePrompt("iOS bundle identifier", suggestedId, "--bundle-identifier"));
|
|
1451
|
-
const result = await initializeIosHost(root, {
|
|
1452
|
-
bundleIdentifier,
|
|
1453
|
-
appName: basename(root),
|
|
1454
|
-
appIcon: flag("--icon") ?? undefined,
|
|
1455
|
-
});
|
|
1456
|
-
ui.success(`iOS host created: ${result.directory}`);
|
|
1457
|
-
printValue({
|
|
1458
|
-
status: "created",
|
|
1459
|
-
platform: "ios",
|
|
1460
|
-
directory: result.directory,
|
|
1461
|
-
bundleIdentifier: result.bundleIdentifier,
|
|
1462
|
-
project: result.project,
|
|
1463
|
-
scheme: result.scheme,
|
|
1464
|
-
configUpdated: result.configUpdated,
|
|
1465
|
-
}, {
|
|
1466
|
-
title: "iOS host",
|
|
1467
|
-
rows: [
|
|
1468
|
-
{
|
|
1469
|
-
label: "Bundle identifier",
|
|
1470
|
-
value: result.bundleIdentifier,
|
|
1471
|
-
valueColor: "blue",
|
|
1472
|
-
},
|
|
1473
|
-
{
|
|
1474
|
-
label: "Xcode project",
|
|
1475
|
-
value: result.project,
|
|
1476
|
-
valueColor: "green",
|
|
1477
|
-
},
|
|
1478
|
-
{ label: "Scheme", value: result.scheme, valueColor: "purple" },
|
|
1479
|
-
{
|
|
1480
|
-
label: "CocoaPods",
|
|
1481
|
-
value: "Run pod install on macOS before the first build",
|
|
1482
|
-
valueColor: "yellow",
|
|
1483
|
-
},
|
|
1484
|
-
],
|
|
1485
|
-
done: "iOS host is ready. Run lynxship doctor --platform ios on macOS.",
|
|
1486
|
-
});
|
|
1487
|
-
return;
|
|
1488
|
-
}
|
|
1489
|
-
if (command === "android") {
|
|
1490
|
-
const subcommand = args.shift() ?? "configure";
|
|
1491
|
-
if (subcommand === "host") {
|
|
1492
|
-
assert((args.shift() ?? "init") === "init", "CLI_ANDROID_HOST_COMMAND", "Use `lynxship android host init` to create an Android host.");
|
|
1493
|
-
await initializeBuildProject();
|
|
1494
|
-
const suggestedId = suggestedAndroidApplicationId(root);
|
|
1495
|
-
const applicationId = flag("--application-id") ??
|
|
1496
|
-
(await assertInteractivePrompt("Android application ID", suggestedId, "--application-id"));
|
|
1497
|
-
const result = await initializeAndroidHost(root, {
|
|
1498
|
-
applicationId,
|
|
1499
|
-
appName: basename(root),
|
|
1500
|
-
});
|
|
1501
|
-
ui.success(`Android host created: ${result.directory}`);
|
|
1502
|
-
printValue({
|
|
1503
|
-
status: "created",
|
|
1504
|
-
platform: "android",
|
|
1505
|
-
directory: result.directory,
|
|
1506
|
-
applicationId: result.applicationId,
|
|
1507
|
-
}, {
|
|
1508
|
-
title: "Android host",
|
|
1509
|
-
rows: [
|
|
1510
|
-
{
|
|
1511
|
-
label: "Application ID",
|
|
1512
|
-
value: result.applicationId,
|
|
1513
|
-
valueColor: "blue",
|
|
1514
|
-
},
|
|
1515
|
-
{
|
|
1516
|
-
label: "Gradle wrapper",
|
|
1517
|
-
value: join(result.directory, "gradlew"),
|
|
1518
|
-
valueColor: "green",
|
|
1519
|
-
},
|
|
1520
|
-
],
|
|
1521
|
-
done: "Android host is ready. Run lynxship doctor, then lynxship build.",
|
|
1522
|
-
});
|
|
1523
|
-
return;
|
|
1524
|
-
}
|
|
1525
|
-
assert(ui.interactive, "CLI_INTERACTIVE_REQUIRED", "Run `lynxship android configure` in an interactive terminal");
|
|
1526
|
-
assert(subcommand === "configure", "CLI_ANDROID_COMMAND", "Only android configure is available");
|
|
1527
|
-
ui.info("Configuring Android signing. Secret fields will stay invisible…");
|
|
1528
|
-
const result = await configureAndroid(root);
|
|
1529
|
-
ui.success(result.generated
|
|
1530
|
-
? `Android keystore generated: ${result.keystorePath}`
|
|
1531
|
-
: `Android signing credentials saved in ${credentialStorageDescription()}`);
|
|
1532
|
-
printValue({ status: "configured", provider: "android-keystore" }, {
|
|
1533
|
-
title: "Android signing",
|
|
1534
|
-
rows: [
|
|
1535
|
-
{ label: "Keystore", value: "Configured", valueColor: "green" },
|
|
1536
|
-
{
|
|
1537
|
-
label: "Storage",
|
|
1538
|
-
value: credentialStorageDescription(),
|
|
1539
|
-
valueColor: "green",
|
|
1540
|
-
},
|
|
1541
|
-
],
|
|
1542
|
-
done: "Android signing is ready for the next build.",
|
|
1543
|
-
});
|
|
1544
|
-
return;
|
|
1545
|
-
}
|
|
1546
|
-
if (command === "store") {
|
|
1547
|
-
assert(ui.interactive, "CLI_INTERACTIVE_REQUIRED", "Run store configure in an interactive terminal");
|
|
1548
|
-
assert((args.shift() ?? "configure") === "configure", "CLI_STORE_COMMAND", "Only store configure is available");
|
|
1549
|
-
const platform = mobilePlatformValue(flag("--platform", "android"));
|
|
1550
|
-
ui.info(platform === "android"
|
|
1551
|
-
? "Configuring Google Play submission. Secret fields will stay invisible…"
|
|
1552
|
-
: "Configuring App Store Connect submission. Private key contents stay protected…");
|
|
1553
|
-
if (platform === "android")
|
|
1554
|
-
await configureGooglePlay(root);
|
|
1555
|
-
else
|
|
1556
|
-
await configureAppStoreConnect(root);
|
|
1557
|
-
ui.success(platform === "android"
|
|
1558
|
-
? "Google Play submission credentials saved securely"
|
|
1559
|
-
: "App Store Connect submission credentials saved securely");
|
|
1560
|
-
printValue({
|
|
1561
|
-
status: "configured",
|
|
1562
|
-
provider: platform === "android" ? "google-play" : "app-store-connect",
|
|
1563
|
-
storage: credentialStorageDescription(),
|
|
1564
|
-
}, {
|
|
1565
|
-
title: platform === "android"
|
|
1566
|
-
? "Google Play submission"
|
|
1567
|
-
: "App Store Connect submission",
|
|
1568
|
-
rows: [
|
|
1569
|
-
{
|
|
1570
|
-
label: "Provider",
|
|
1571
|
-
value: platform === "android" ? "Google Play" : "App Store Connect",
|
|
1572
|
-
valueColor: "blue",
|
|
1573
|
-
},
|
|
1574
|
-
{
|
|
1575
|
-
label: "Credentials",
|
|
1576
|
-
value: credentialStorageDescription(),
|
|
1577
|
-
valueColor: "green",
|
|
1578
|
-
},
|
|
1579
|
-
],
|
|
1580
|
-
done: "Store submission is ready for the next build.",
|
|
1581
|
-
});
|
|
1582
|
-
return;
|
|
1583
|
-
}
|
|
1584
|
-
if (command === "build")
|
|
1585
|
-
await initializeBuildProject();
|
|
1586
|
-
const { state, repository, builds, submissions } = await loadState();
|
|
1587
|
-
if (command === "submit") {
|
|
1588
|
-
const config = await loadConfig(root);
|
|
1589
|
-
const platform = mobilePlatformValue(flag("--platform", "android"));
|
|
1590
|
-
await requireOperationalConfiguration(platform);
|
|
1591
|
-
const credentials = await loadCredentials(root);
|
|
1592
|
-
const localMode = args.includes("--local") || process.env.LYNXSHIP_SUBMIT_MODE === "mock";
|
|
1593
|
-
const storeConfigured = platform === "android"
|
|
1594
|
-
? Boolean(credentials.googlePlay)
|
|
1595
|
-
: Boolean(credentials.appStoreConnect);
|
|
1596
|
-
assert(localMode || storeConfigured, "STORE_SUBMISSION_REQUIRED", platform === "android"
|
|
1597
|
-
? "Google Play is not configured. Run store configure --platform android."
|
|
1598
|
-
: "App Store Connect is not configured. Run store configure --platform ios.");
|
|
1599
|
-
const candidate = builds
|
|
1600
|
-
.list()
|
|
1601
|
-
.filter((job) => job.platform === platform && job.state === "success")
|
|
1602
|
-
.at(-1);
|
|
1603
|
-
assert(candidate, "BUILD_REQUIRED", "A successful build is required");
|
|
1604
|
-
assert(localMode || candidate.artifact?.path, "STORE_ARTIFACT_REQUIRED", "A local signed artifact path is required for store submission");
|
|
1605
|
-
const latest = args.includes("--latest");
|
|
1606
|
-
const spinner = ui.spinner("Submitting artifact…");
|
|
1607
|
-
try {
|
|
1608
|
-
const controlPlaneSubmission = candidate.artifact?.path
|
|
1609
|
-
? await submitRealArtifact(config, state, candidate, latest)
|
|
1610
|
-
: await submissions.submit({
|
|
1611
|
-
projectId: configuredProjectId(config),
|
|
1612
|
-
organizationId: "local_org",
|
|
1613
|
-
platform,
|
|
1614
|
-
artifact: candidate.artifact ?? { hash: `local-${candidate.id}` },
|
|
1615
|
-
latest,
|
|
1616
|
-
buildId: latest ? null : candidate.id,
|
|
1617
|
-
});
|
|
1618
|
-
const storeResult = !localMode && candidate.artifact?.path
|
|
1619
|
-
? platform === "android"
|
|
1620
|
-
? await new GooglePlayApiProvider(credentials.googlePlay).submit({
|
|
1621
|
-
platform,
|
|
1622
|
-
path: candidate.artifact.path,
|
|
1623
|
-
hash: candidate.artifact.hash,
|
|
1624
|
-
})
|
|
1625
|
-
: await new AppStoreConnectApiProvider(credentials.appStoreConnect).submit({
|
|
1626
|
-
platform,
|
|
1627
|
-
path: candidate.artifact.path,
|
|
1628
|
-
hash: candidate.artifact.hash,
|
|
1629
|
-
})
|
|
1630
|
-
: undefined;
|
|
1631
|
-
const submission = storeResult
|
|
1632
|
-
? {
|
|
1633
|
-
...controlPlaneSubmission,
|
|
1634
|
-
store: storeResult,
|
|
1635
|
-
}
|
|
1636
|
-
: controlPlaneSubmission;
|
|
1637
|
-
const result = submission;
|
|
1638
|
-
spinner.succeed(storeResult
|
|
1639
|
-
? "Artifact submitted to the configured app store"
|
|
1640
|
-
: "Local submission job accepted");
|
|
1641
|
-
await saveState(state, repository, builds, submissions);
|
|
1642
|
-
printValue(submission, {
|
|
1643
|
-
title: "Submission result",
|
|
1644
|
-
rows: [
|
|
1645
|
-
{
|
|
1646
|
-
label: "Submission ID",
|
|
1647
|
-
value: result.id ?? "remote",
|
|
1648
|
-
valueColor: "purple",
|
|
1649
|
-
},
|
|
1650
|
-
{
|
|
1651
|
-
label: "Platform",
|
|
1652
|
-
value: result.platform ?? platform,
|
|
1653
|
-
valueColor: "blue",
|
|
1654
|
-
},
|
|
1655
|
-
{
|
|
1656
|
-
label: "Status",
|
|
1657
|
-
value: result.status ?? "accepted",
|
|
1658
|
-
valueColor: "green",
|
|
1659
|
-
},
|
|
1660
|
-
],
|
|
1661
|
-
done: "App submitted to the configured provider.",
|
|
1662
|
-
});
|
|
1663
|
-
if (result.downloadUrl)
|
|
1664
|
-
ui.downloadArtifact(result.downloadUrl, result.downloadExpiresAt);
|
|
1665
|
-
}
|
|
1666
|
-
catch (error) {
|
|
1667
|
-
spinner.fail(error instanceof Error ? error.message : "Submission failed");
|
|
1668
|
-
throw error;
|
|
1669
|
-
}
|
|
1670
|
-
return;
|
|
1671
|
-
}
|
|
1672
|
-
if (command === "update" && args[0] !== "rollback") {
|
|
1673
|
-
const config = await loadConfig(root);
|
|
1674
|
-
const platform = mobilePlatformValue(flag("--platform", "android"));
|
|
1675
|
-
await requireOperationalConfiguration(platform);
|
|
1676
|
-
const projectId = configuredProjectId(config);
|
|
1677
|
-
const localMode = args.includes("--local") || process.env.LYNXSHIP_SUBMIT_MODE === "mock";
|
|
1678
|
-
const explicitBundles = flag("--bundle");
|
|
1679
|
-
const runtime = await inspectRuntimeFingerprint(root, platform, config);
|
|
1680
|
-
const progress = ui.progress("Sign manifest");
|
|
1681
|
-
try {
|
|
1682
|
-
if (localMode) {
|
|
1683
|
-
const key = state.signingKey ?? createSigningKey();
|
|
1684
|
-
const data = flag("--bundle", "local-bundle");
|
|
1685
|
-
const manifest = {
|
|
1686
|
-
protocolVersion: 1,
|
|
1687
|
-
projectId,
|
|
1688
|
-
channel: config.update?.channel ?? "production",
|
|
1689
|
-
platform,
|
|
1690
|
-
runtimeVersion: runtime.value,
|
|
1691
|
-
sequence: state.releases.length + 1,
|
|
1692
|
-
keyId: key.keyId,
|
|
1693
|
-
assets: [
|
|
1694
|
-
{
|
|
1695
|
-
path: "main.js",
|
|
1696
|
-
hash: sha256(data),
|
|
1697
|
-
size: Buffer.byteLength(data),
|
|
1698
|
-
},
|
|
1699
|
-
],
|
|
1700
|
-
};
|
|
1701
|
-
const release = {
|
|
1702
|
-
id: createId("rel"),
|
|
1703
|
-
manifest,
|
|
1704
|
-
signature: signManifest(manifest, key.privateKey),
|
|
1705
|
-
message: flag("--message", "local update"),
|
|
1706
|
-
createdAt: new Date().toISOString(),
|
|
1707
|
-
};
|
|
1708
|
-
state.releases.push(release);
|
|
1709
|
-
await saveState(state, repository, builds, submissions);
|
|
1710
|
-
progress.update(100);
|
|
1711
|
-
printValue(release, {
|
|
1712
|
-
title: "OTA update published locally",
|
|
1713
|
-
rows: [
|
|
1714
|
-
{ label: "Release ID", value: release.id, valueColor: "purple" },
|
|
1715
|
-
{
|
|
1716
|
-
label: "Platform",
|
|
1717
|
-
value: release.manifest.platform,
|
|
1718
|
-
valueColor: "blue",
|
|
1719
|
-
},
|
|
1720
|
-
{
|
|
1721
|
-
label: "Signature",
|
|
1722
|
-
value: "Ed25519 signed",
|
|
1723
|
-
valueColor: "muted",
|
|
1724
|
-
},
|
|
1725
|
-
],
|
|
1726
|
-
done: "Local update created. Use a real API and bundle for devices.",
|
|
1727
|
-
});
|
|
1728
|
-
return;
|
|
1729
|
-
}
|
|
1730
|
-
const bundlePaths = await otaAssetPaths(root, explicitBundles);
|
|
1731
|
-
for (const bundlePath of bundlePaths)
|
|
1732
|
-
assert(await exists(bundlePath), "OTA_BUNDLE_REQUIRED", `Bundle not found: ${bundlePath}. Build the Lynx bundle first or pass --bundle.`);
|
|
1733
|
-
assertCompatibleBinaryBuild(builds, platform, runtime.value);
|
|
1734
|
-
const releaseId = createId("ota");
|
|
1735
|
-
progress.update(25, `Uploading ${bundlePaths.length} Lynx asset(s) to Cloudflare R2…`);
|
|
1736
|
-
const uploadedAssets = [];
|
|
1737
|
-
for (const [index, bundlePath] of bundlePaths.entries()) {
|
|
1738
|
-
const uploaded = await uploadR2Artifact(root, projectId, releaseId, bundlePath, "application/octet-stream", otaAssetName(root, bundlePath));
|
|
1739
|
-
uploadedAssets.push({
|
|
1740
|
-
path: otaAssetName(root, bundlePath),
|
|
1741
|
-
hash: uploaded.hash,
|
|
1742
|
-
size: uploaded.size,
|
|
1743
|
-
url: uploaded.url,
|
|
1744
|
-
});
|
|
1745
|
-
progress.update(25 + Math.round(((index + 1) / bundlePaths.length) * 35), `Uploaded ${index + 1}/${bundlePaths.length} Lynx asset(s)…`);
|
|
1746
|
-
}
|
|
1747
|
-
progress.update(65, "Publishing signed OTA release through LynxShip API…");
|
|
1748
|
-
const remoteRelease = (await publishOtaRelease(config, state, {
|
|
1749
|
-
projectId,
|
|
1750
|
-
organizationId: "local_org",
|
|
1751
|
-
channel: config.update?.channel ?? "production",
|
|
1752
|
-
platform,
|
|
1753
|
-
runtimeVersion: runtime.value,
|
|
1754
|
-
assets: uploadedAssets,
|
|
1755
|
-
message: flag("--message", "OTA update"),
|
|
1756
|
-
rollout: config.update?.rollout?.defaultPercentage ?? 100,
|
|
1757
|
-
policyApprovalId: flag("--policy-approval-id"),
|
|
1758
|
-
}));
|
|
1759
|
-
const publicKey = await fetchOtaPublicKey(config);
|
|
1760
|
-
state.releases.push(remoteRelease);
|
|
1761
|
-
await saveState(state, repository, builds, submissions);
|
|
1762
|
-
progress.update(100, "OTA release signed and published");
|
|
1763
|
-
printValue({ ...remoteRelease, signingKey: publicKey }, {
|
|
1764
|
-
title: "OTA update published",
|
|
1765
|
-
rows: [
|
|
1766
|
-
{
|
|
1767
|
-
label: "Release ID",
|
|
1768
|
-
value: remoteRelease.id,
|
|
1769
|
-
valueColor: "purple",
|
|
1770
|
-
},
|
|
1771
|
-
{
|
|
1772
|
-
label: "Platform",
|
|
1773
|
-
value: remoteRelease.manifest.platform,
|
|
1774
|
-
valueColor: "blue",
|
|
1775
|
-
},
|
|
1776
|
-
{ label: "Bundle", value: "Cloudflare R2", valueColor: "orange" },
|
|
1777
|
-
{
|
|
1778
|
-
label: "Signature",
|
|
1779
|
-
value: "Ed25519 signed by API",
|
|
1780
|
-
valueColor: "green",
|
|
1781
|
-
},
|
|
1782
|
-
],
|
|
1783
|
-
done: "Devices can check and install this compatible OTA release.",
|
|
1784
|
-
});
|
|
1785
|
-
}
|
|
1786
|
-
finally {
|
|
1787
|
-
progress.stop();
|
|
1788
|
-
}
|
|
1789
|
-
return;
|
|
1790
|
-
}
|
|
1791
|
-
if (command === "rollback" ||
|
|
1792
|
-
(command === "update" && args[0] === "rollback")) {
|
|
1793
|
-
if (command === "update")
|
|
1794
|
-
args.shift();
|
|
1795
|
-
const config = await loadConfig(root);
|
|
1796
|
-
const platform = mobilePlatformValue(flag("--platform", "android"));
|
|
1797
|
-
const releaseId = flag("--release-id");
|
|
1798
|
-
const reason = flag("--reason");
|
|
1799
|
-
const channel = config.update?.channel ?? "production";
|
|
1800
|
-
const localMode = args.includes("--local");
|
|
1801
|
-
assert(Boolean(releaseId && releaseId !== "true"), "ROLLBACK_RELEASE_REQUIRED", "Pass the release to restore with `--release-id <id>`.");
|
|
1802
|
-
assert(Boolean(reason?.trim()) && reason !== "true", "ROLLBACK_REASON_REQUIRED", 'Pass an audit reason with `--reason "..."`.');
|
|
1803
|
-
await requireR2Configuration();
|
|
1804
|
-
const progress = ui.progress("OTA rollback");
|
|
1805
|
-
try {
|
|
1806
|
-
progress.update(10, `Selecting ${releaseId} for ${channel}…`);
|
|
1807
|
-
if (localMode) {
|
|
1808
|
-
const release = state.releases.find((candidate) => candidate.id === releaseId &&
|
|
1809
|
-
candidate.manifest.channel === channel &&
|
|
1810
|
-
candidate.manifest.platform === platform);
|
|
1811
|
-
assert(release, "RELEASE_NOT_FOUND", `Local release ${releaseId} was not found in channel ${channel} for ${platform}.`);
|
|
1812
|
-
state.lastRollback = {
|
|
1813
|
-
releaseId: release.id,
|
|
1814
|
-
reason: reason,
|
|
1815
|
-
at: new Date().toISOString(),
|
|
1816
|
-
};
|
|
1817
|
-
await saveState(state, repository, builds, submissions);
|
|
1818
|
-
progress.update(100, "Local OTA channel rolled back");
|
|
1819
|
-
printValue({ status: "rolled_back", release, rollback: state.lastRollback }, {
|
|
1820
|
-
title: "OTA rollback",
|
|
1821
|
-
rows: [
|
|
1822
|
-
{ label: "Release ID", value: release.id, valueColor: "purple" },
|
|
1823
|
-
{ label: "Platform", value: platform, valueColor: "blue" },
|
|
1824
|
-
{ label: "Channel", value: channel, valueColor: "orange" },
|
|
1825
|
-
{ label: "Reason", value: reason, valueColor: "muted" },
|
|
1826
|
-
],
|
|
1827
|
-
done: "Local OTA channel now points to the selected release.",
|
|
1828
|
-
});
|
|
1829
|
-
return;
|
|
1830
|
-
}
|
|
1831
|
-
progress.update(45, "Requesting rollback through LynxShip API…");
|
|
1832
|
-
const release = (await rollbackOtaRelease(config, state, {
|
|
1833
|
-
projectId: configuredProjectId(config),
|
|
1834
|
-
channel,
|
|
1835
|
-
platform,
|
|
1836
|
-
releaseId: releaseId,
|
|
1837
|
-
reason: reason,
|
|
1838
|
-
}));
|
|
1839
|
-
state.lastRollback = {
|
|
1840
|
-
releaseId: release.id,
|
|
1841
|
-
reason: reason,
|
|
1842
|
-
at: new Date().toISOString(),
|
|
1843
|
-
};
|
|
1844
|
-
await saveState(state, repository, builds, submissions);
|
|
1845
|
-
progress.update(100, "OTA channel rolled back");
|
|
1846
|
-
printValue({ status: "rolled_back", release, rollback: state.lastRollback }, {
|
|
1847
|
-
title: "OTA rollback",
|
|
1848
|
-
rows: [
|
|
1849
|
-
{ label: "Release ID", value: release.id, valueColor: "purple" },
|
|
1850
|
-
{ label: "Platform", value: platform, valueColor: "blue" },
|
|
1851
|
-
{ label: "Channel", value: channel, valueColor: "orange" },
|
|
1852
|
-
{ label: "Reason", value: reason, valueColor: "muted" },
|
|
1853
|
-
],
|
|
1854
|
-
done: "Devices will receive the selected compatible release on their next OTA check.",
|
|
1855
|
-
});
|
|
1856
|
-
}
|
|
1857
|
-
finally {
|
|
1858
|
-
progress.stop();
|
|
1859
|
-
}
|
|
1860
|
-
return;
|
|
1861
|
-
}
|
|
1862
|
-
assert(command === "build", "CLI_COMMAND", `Unknown command: ${command}`);
|
|
1863
|
-
const subcommand = args[0] && !args[0].startsWith("--") ? args.shift() : "create";
|
|
1864
|
-
const platformArgument = flag("--platform", "android");
|
|
1865
|
-
const buildAll = subcommand === "all" || platformArgument === "all";
|
|
1866
|
-
const platform = buildAll ? "android" : platformValue(platformArgument);
|
|
1867
|
-
const simulator = args.includes("--simulator");
|
|
1868
|
-
const skipUpload = args.includes("--no-upload") || simulator;
|
|
1869
|
-
assert(!simulator || platformArgument === "ios", "IOS_SIMULATOR_PLATFORM", "The --simulator option is only available with --platform ios.");
|
|
1870
|
-
const allowUnsigned = args.includes("--allow-unsigned");
|
|
1871
|
-
assert(!allowUnsigned || skipUpload, "CLI_UNSIGNED_UPLOAD_BLOCKED", "--allow-unsigned is only available with --no-upload and can never upload an unsigned Desktop artifact.");
|
|
1872
|
-
await requireOperationalConfiguration(platform, { requireR2: !skipUpload });
|
|
1873
|
-
if (subcommand === "list") {
|
|
1874
|
-
printValue(builds.list());
|
|
1875
|
-
return;
|
|
1876
|
-
}
|
|
1877
|
-
const id = args[0];
|
|
1878
|
-
if (subcommand === "status") {
|
|
1879
|
-
printValue(builds.get(id ?? ""));
|
|
1880
|
-
return;
|
|
1881
|
-
}
|
|
1882
|
-
if (subcommand === "cancel") {
|
|
1883
|
-
const job = builds.cancel(id ?? "");
|
|
1884
|
-
await saveState(state, repository, builds, submissions);
|
|
1885
|
-
printValue(job);
|
|
1886
|
-
return;
|
|
1887
|
-
}
|
|
1888
|
-
if (subcommand === "retry") {
|
|
1889
|
-
const job = builds.retry(id ?? "");
|
|
1890
|
-
await saveState(state, repository, builds, submissions);
|
|
1891
|
-
printValue(job);
|
|
1892
|
-
return;
|
|
1893
|
-
}
|
|
1894
|
-
assert(subcommand === "create" || buildAll, "CLI_BUILD_COMMAND", `Unknown build command: ${subcommand}`);
|
|
1895
|
-
const config = await loadConfig(root);
|
|
1896
|
-
const profile = flag("--profile", simulator ? "simulator" : "production");
|
|
1897
|
-
const simulatorDevice = flag("--device") ?? undefined;
|
|
1898
|
-
const simulatorAutostart = simulator &&
|
|
1899
|
-
!args.includes("--no-autostart") &&
|
|
1900
|
-
(args.includes("--autostart") || (!json && ui.interactive));
|
|
1901
|
-
const wait = !args.includes("--no-wait");
|
|
1902
|
-
const local = args.includes("--local");
|
|
1903
|
-
const platforms = buildAll
|
|
1904
|
-
? ["android", "ios", "harmony", "web", "desktop"]
|
|
1905
|
-
: [platform];
|
|
1906
|
-
if (buildAll && wait && !local) {
|
|
1907
|
-
assert(process.platform === "darwin", "BUILD_ALL_MACOS_REQUIRED", "A real all-target build includes iOS and therefore requires macOS locally. Run supported targets individually on Windows/Linux, or use a macOS CI worker for the complete matrix.");
|
|
1908
|
-
for (const target of platforms)
|
|
1909
|
-
await ensureNativeHostForBuild(target, profile, config);
|
|
1910
|
-
}
|
|
1911
|
-
if (!buildAll) {
|
|
1912
|
-
await executeBuild({
|
|
1913
|
-
config,
|
|
1914
|
-
profile,
|
|
1915
|
-
platform,
|
|
1916
|
-
skipUpload,
|
|
1917
|
-
allowUnsigned,
|
|
1918
|
-
wait,
|
|
1919
|
-
local,
|
|
1920
|
-
simulator,
|
|
1921
|
-
simulatorDevice,
|
|
1922
|
-
simulatorAutostart,
|
|
1923
|
-
state,
|
|
1924
|
-
repository,
|
|
1925
|
-
builds,
|
|
1926
|
-
submissions,
|
|
1927
|
-
});
|
|
1928
|
-
return;
|
|
1929
|
-
}
|
|
1930
|
-
if (buildAll && wait && !local)
|
|
1931
|
-
await buildSharedLynxBundle(resolveProfile(config, profile).miso);
|
|
1932
|
-
const progress = ui.progress("All Lynx targets build");
|
|
1933
|
-
const progressValues = {};
|
|
1934
|
-
const progressLabels = {};
|
|
1935
|
-
const platformName = (target) => target === "android"
|
|
1936
|
-
? "Android"
|
|
1937
|
-
: target === "ios"
|
|
1938
|
-
? "iOS"
|
|
1939
|
-
: target === "harmony"
|
|
1940
|
-
? "HarmonyOS"
|
|
1941
|
-
: target === "web"
|
|
1942
|
-
? "Web"
|
|
1943
|
-
: "Desktop";
|
|
1944
|
-
const outcomes = await Promise.allSettled(platforms.map((target) => executeBuild({
|
|
1945
|
-
config,
|
|
1946
|
-
profile,
|
|
1947
|
-
platform: target,
|
|
1948
|
-
skipUpload,
|
|
1949
|
-
allowUnsigned,
|
|
1950
|
-
wait,
|
|
1951
|
-
local,
|
|
1952
|
-
simulator,
|
|
1953
|
-
simulatorDevice,
|
|
1954
|
-
simulatorAutostart,
|
|
1955
|
-
state,
|
|
1956
|
-
repository,
|
|
1957
|
-
builds,
|
|
1958
|
-
submissions,
|
|
1959
|
-
progress,
|
|
1960
|
-
progressPrefix: platformName(target),
|
|
1961
|
-
skipBundleBuild: wait && !local && target !== "web",
|
|
1962
|
-
onEvent: (message) => progress.event(`${platformName(target)} · ${message}`),
|
|
1963
|
-
onProgress: (value, label) => {
|
|
1964
|
-
if (value !== undefined)
|
|
1965
|
-
progressValues[target] = value;
|
|
1966
|
-
if (label)
|
|
1967
|
-
progressLabels[target] = label;
|
|
1968
|
-
const average = platforms.reduce((total, current) => total + (progressValues[current] ?? 0), 0) / platforms.length;
|
|
1969
|
-
progress.update(average, `${platformName(target)} · ${progressLabels[target]}`);
|
|
1970
|
-
},
|
|
1971
|
-
}, false)));
|
|
1972
|
-
progress.update(100, "All Lynx target builds finished");
|
|
1973
|
-
progress.stop();
|
|
1974
|
-
const summaryBuilds = outcomes.map((outcome, index) => {
|
|
1975
|
-
if (outcome.status === "fulfilled")
|
|
1976
|
-
return outcome.value;
|
|
1977
|
-
const error = outcome.reason;
|
|
1978
|
-
const buildId = error && typeof error === "object" && "buildId" in error
|
|
1979
|
-
? String(error.buildId)
|
|
1980
|
-
: undefined;
|
|
1981
|
-
const job = buildId ? builds.jobs.get(buildId) : undefined;
|
|
1982
|
-
return (job ?? {
|
|
1983
|
-
platform: platforms[index] ?? "android",
|
|
1984
|
-
state: "failed",
|
|
1985
|
-
error: error instanceof Error ? error.message : String(error),
|
|
1986
|
-
});
|
|
1987
|
-
});
|
|
1988
|
-
const failed = summaryBuilds.some((result) => result.state === "failed");
|
|
1989
|
-
printValue({
|
|
1990
|
-
status: failed
|
|
1991
|
-
? "failed"
|
|
1992
|
-
: summaryBuilds.every((result) => result.state === "success")
|
|
1993
|
-
? "success"
|
|
1994
|
-
: "queued",
|
|
1995
|
-
builds: summaryBuilds,
|
|
1996
|
-
}, {
|
|
1997
|
-
title: "Build all result",
|
|
1998
|
-
rows: summaryBuilds.map((result) => ({
|
|
1999
|
-
label: platformName(result.platform),
|
|
2000
|
-
value: `${result.state}${"id" in result ? ` · ${result.id}` : ""}`,
|
|
2001
|
-
valueColor: result.state === "success"
|
|
2002
|
-
? "green"
|
|
2003
|
-
: result.state === "failed"
|
|
2004
|
-
? "red"
|
|
2005
|
-
: "yellow",
|
|
2006
|
-
})),
|
|
2007
|
-
done: failed
|
|
2008
|
-
? "At least one platform build failed. Review its events and retry that platform."
|
|
2009
|
-
: "All Lynx target build jobs completed. Submit supported store artifacts separately.",
|
|
2010
|
-
});
|
|
2011
|
-
for (const result of summaryBuilds)
|
|
2012
|
-
if (result.state === "success" &&
|
|
2013
|
-
"artifact" in result &&
|
|
2014
|
-
result.artifact?.url)
|
|
2015
|
-
ui.downloadArtifact(result.artifact.url, result.artifact.expiresAt);
|
|
2016
|
-
if (failed)
|
|
2017
|
-
process.exitCode = 5;
|
|
2018
|
-
}
|
|
2019
|
-
function exitCode(error) {
|
|
2020
|
-
const code = error.code;
|
|
2021
|
-
if (code?.startsWith("CLI_") || code?.startsWith("CONFIG_"))
|
|
2022
|
-
return 2;
|
|
2023
|
-
if (code === "BUILD_SIGNING_REQUIRED")
|
|
2024
|
-
return 2;
|
|
2025
|
-
if (code === "DESKTOP_SIGNING_REQUIRED")
|
|
2026
|
-
return 2;
|
|
2027
|
-
if (code?.startsWith("AUTH_"))
|
|
2028
|
-
return 4;
|
|
2029
|
-
if (code?.startsWith("BUILD_"))
|
|
2030
|
-
return 5;
|
|
2031
|
-
if (code?.startsWith("SUBMISSION_"))
|
|
2032
|
-
return 6;
|
|
2033
|
-
if (code?.startsWith("OTA_"))
|
|
2034
|
-
return 7;
|
|
2035
|
-
return 1;
|
|
2036
|
-
}
|
|
2037
|
-
void main()
|
|
2038
|
-
.catch((error) => {
|
|
2039
|
-
const message = error instanceof Error ? error.message : "Unknown error";
|
|
2040
|
-
const code = error.code ?? "CLI_ERROR";
|
|
2041
|
-
const nextSteps = guidanceForError(error, { args: rawArgs });
|
|
2042
|
-
if (json) {
|
|
2043
|
-
console.log(JSON.stringify({
|
|
2044
|
-
error: message,
|
|
2045
|
-
code,
|
|
2046
|
-
...(nextSteps.commands.length > 0
|
|
2047
|
-
? {
|
|
2048
|
-
nextSteps: nextSteps.commands,
|
|
2049
|
-
...(nextSteps.note ? { note: nextSteps.note } : {}),
|
|
2050
|
-
...(nextSteps.environment
|
|
2051
|
-
? { environment: nextSteps.environment }
|
|
2052
|
-
: {}),
|
|
2053
|
-
}
|
|
2054
|
-
: {}),
|
|
2055
|
-
}));
|
|
2056
|
-
}
|
|
2057
|
-
else {
|
|
2058
|
-
ui.error(message);
|
|
2059
|
-
ui.nextSteps(nextSteps);
|
|
2060
|
-
}
|
|
2061
|
-
process.exitCode = exitCode(error);
|
|
2062
|
-
})
|
|
2063
|
-
.finally(async () => {
|
|
2064
|
-
try {
|
|
2065
|
-
await renderConfigurationFooter();
|
|
2066
|
-
}
|
|
2067
|
-
catch {
|
|
2068
|
-
// Configuration status must never hide the original command result.
|
|
2069
|
-
}
|
|
2070
|
-
});
|
|
2
|
+
// Keep the executable entrypoint intentionally small. Command orchestration
|
|
3
|
+
// lives in cli.ts so the package entrypoint is not coupled to every command
|
|
4
|
+
// implementation and can remain a stable binary boundary.
|
|
5
|
+
import "./cli.js";
|