@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.
Files changed (191) hide show
  1. package/README.md +66 -5
  2. package/dist/android/signing.d.ts +16 -0
  3. package/dist/android/signing.d.ts.map +1 -0
  4. package/dist/android/signing.js +131 -0
  5. package/dist/android/types.d.ts +12 -0
  6. package/dist/android/types.d.ts.map +1 -0
  7. package/dist/android/types.js +1 -0
  8. package/dist/android-build.d.ts +4 -13
  9. package/dist/android-build.d.ts.map +1 -1
  10. package/dist/android-build.js +14 -133
  11. package/dist/android-toolchain/inspection.d.ts +3 -0
  12. package/dist/android-toolchain/inspection.d.ts.map +1 -0
  13. package/dist/android-toolchain/inspection.js +122 -0
  14. package/dist/android-toolchain/probes.d.ts +23 -0
  15. package/dist/android-toolchain/probes.d.ts.map +1 -0
  16. package/dist/android-toolchain/probes.js +183 -0
  17. package/dist/android-toolchain/types.d.ts +20 -0
  18. package/dist/android-toolchain/types.d.ts.map +1 -0
  19. package/dist/android-toolchain/types.js +1 -0
  20. package/dist/android-toolchain.d.ts +4 -20
  21. package/dist/android-toolchain.d.ts.map +1 -1
  22. package/dist/android-toolchain.js +4 -300
  23. package/dist/app-config.d.ts +25 -0
  24. package/dist/app-config.d.ts.map +1 -0
  25. package/dist/app-config.js +106 -0
  26. package/dist/bundle-build.d.ts.map +1 -1
  27. package/dist/bundle-build.js +11 -0
  28. package/dist/cli.d.ts +3 -0
  29. package/dist/cli.d.ts.map +1 -0
  30. package/dist/cli.js +66 -0
  31. package/dist/commands/build-execution.d.ts +41 -0
  32. package/dist/commands/build-execution.d.ts.map +1 -0
  33. package/dist/commands/build-execution.js +182 -0
  34. package/dist/commands/configuration.d.ts +19 -0
  35. package/dist/commands/configuration.d.ts.map +1 -0
  36. package/dist/commands/configuration.js +174 -0
  37. package/dist/commands/development.d.ts +22 -0
  38. package/dist/commands/development.d.ts.map +1 -0
  39. package/dist/commands/development.js +132 -0
  40. package/dist/commands/device.d.ts +3 -0
  41. package/dist/commands/device.d.ts.map +1 -0
  42. package/dist/commands/device.js +86 -0
  43. package/dist/commands/doctor.d.ts +18 -0
  44. package/dist/commands/doctor.d.ts.map +1 -0
  45. package/dist/commands/doctor.js +320 -0
  46. package/dist/commands/help.d.ts +2 -0
  47. package/dist/commands/help.d.ts.map +1 -0
  48. package/dist/commands/help.js +118 -0
  49. package/dist/commands/i18n.d.ts +14 -0
  50. package/dist/commands/i18n.d.ts.map +1 -0
  51. package/dist/commands/i18n.js +88 -0
  52. package/dist/commands/logs.d.ts +3 -0
  53. package/dist/commands/logs.d.ts.map +1 -0
  54. package/dist/commands/logs.js +51 -0
  55. package/dist/commands/metadata.d.ts +3 -0
  56. package/dist/commands/metadata.d.ts.map +1 -0
  57. package/dist/commands/metadata.js +45 -0
  58. package/dist/commands/ota.d.ts +28 -0
  59. package/dist/commands/ota.d.ts.map +1 -0
  60. package/dist/commands/ota.js +203 -0
  61. package/dist/commands/project.d.ts +14 -0
  62. package/dist/commands/project.d.ts.map +1 -0
  63. package/dist/commands/project.js +122 -0
  64. package/dist/commands/remote-build.d.ts +35 -0
  65. package/dist/commands/remote-build.d.ts.map +1 -0
  66. package/dist/commands/remote-build.js +73 -0
  67. package/dist/commands/submit.d.ts +27 -0
  68. package/dist/commands/submit.d.ts.map +1 -0
  69. package/dist/commands/submit.js +92 -0
  70. package/dist/ecosystem.d.ts +9 -0
  71. package/dist/ecosystem.d.ts.map +1 -0
  72. package/dist/ecosystem.js +95 -0
  73. package/dist/frameworks.d.ts.map +1 -1
  74. package/dist/frameworks.js +5 -1
  75. package/dist/guidance/catalog.d.ts +3 -0
  76. package/dist/guidance/catalog.d.ts.map +1 -0
  77. package/dist/guidance/catalog.js +504 -0
  78. package/dist/guidance/types.d.ts +10 -0
  79. package/dist/guidance/types.d.ts.map +1 -0
  80. package/dist/guidance/types.js +1 -0
  81. package/dist/guidance.d.ts +2 -9
  82. package/dist/guidance.d.ts.map +1 -1
  83. package/dist/guidance.js +1 -502
  84. package/dist/i18n/discovery.d.ts +14 -0
  85. package/dist/i18n/discovery.d.ts.map +1 -0
  86. package/dist/i18n/discovery.js +106 -0
  87. package/dist/i18n/plan.d.ts +12 -0
  88. package/dist/i18n/plan.d.ts.map +1 -0
  89. package/dist/i18n/plan.js +95 -0
  90. package/dist/i18n/setup.d.ts +11 -0
  91. package/dist/i18n/setup.d.ts.map +1 -0
  92. package/dist/i18n/setup.js +47 -0
  93. package/dist/i18n/types.d.ts +17 -0
  94. package/dist/i18n/types.d.ts.map +1 -0
  95. package/dist/i18n/types.js +1 -0
  96. package/dist/index.d.ts +1 -1
  97. package/dist/index.d.ts.map +1 -1
  98. package/dist/index.js +4 -2069
  99. package/dist/ios/assets.d.ts +19 -0
  100. package/dist/ios/assets.d.ts.map +1 -0
  101. package/dist/ios/assets.js +128 -0
  102. package/dist/ios/production-build.d.ts +6 -0
  103. package/dist/ios/production-build.d.ts.map +1 -0
  104. package/dist/ios/production-build.js +200 -0
  105. package/dist/ios/project.d.ts +4 -0
  106. package/dist/ios/project.d.ts.map +1 -0
  107. package/dist/ios/project.js +43 -0
  108. package/dist/ios/simulator-build.d.ts +8 -0
  109. package/dist/ios/simulator-build.d.ts.map +1 -0
  110. package/dist/ios/simulator-build.js +188 -0
  111. package/dist/ios/simulator.d.ts +5 -0
  112. package/dist/ios/simulator.d.ts.map +1 -0
  113. package/dist/ios/simulator.js +60 -0
  114. package/dist/ios/types.d.ts +14 -0
  115. package/dist/ios/types.d.ts.map +1 -0
  116. package/dist/ios/types.js +1 -0
  117. package/dist/ios-build.d.ts +4 -37
  118. package/dist/ios-build.d.ts.map +1 -1
  119. package/dist/ios-build.js +3 -575
  120. package/dist/ios-host.d.ts.map +1 -1
  121. package/dist/ios-toolchain/inspection.d.ts +4 -0
  122. package/dist/ios-toolchain/inspection.d.ts.map +1 -0
  123. package/dist/ios-toolchain/inspection.js +211 -0
  124. package/dist/ios-toolchain/probes.d.ts +24 -0
  125. package/dist/ios-toolchain/probes.d.ts.map +1 -0
  126. package/dist/ios-toolchain/probes.js +127 -0
  127. package/dist/ios-toolchain/types.d.ts +17 -0
  128. package/dist/ios-toolchain/types.d.ts.map +1 -0
  129. package/dist/ios-toolchain/types.js +1 -0
  130. package/dist/ios-toolchain.d.ts +3 -18
  131. package/dist/ios-toolchain.d.ts.map +1 -1
  132. package/dist/ios-toolchain.js +1 -333
  133. package/dist/plugins/contracts.d.ts +28 -0
  134. package/dist/plugins/contracts.d.ts.map +1 -0
  135. package/dist/plugins/contracts.js +1 -0
  136. package/dist/plugins/discovery.d.ts +19 -0
  137. package/dist/plugins/discovery.d.ts.map +1 -0
  138. package/dist/plugins/discovery.js +250 -0
  139. package/dist/plugins/native-operations.d.ts +28 -0
  140. package/dist/plugins/native-operations.d.ts.map +1 -0
  141. package/dist/plugins/native-operations.js +217 -0
  142. package/dist/plugins/operations.d.ts +9 -0
  143. package/dist/plugins/operations.d.ts.map +1 -0
  144. package/dist/plugins/operations.js +76 -0
  145. package/dist/plugins/validation.d.ts +9 -0
  146. package/dist/plugins/validation.d.ts.map +1 -0
  147. package/dist/plugins/validation.js +52 -0
  148. package/dist/plugins.d.ts +3 -34
  149. package/dist/plugins.d.ts.map +1 -1
  150. package/dist/plugins.js +2 -595
  151. package/dist/process-runner.d.ts +4 -0
  152. package/dist/process-runner.d.ts.map +1 -1
  153. package/dist/process-runner.js +14 -0
  154. package/dist/remote.d.ts +22 -1
  155. package/dist/remote.d.ts.map +1 -1
  156. package/dist/remote.js +141 -2
  157. package/dist/rspeedy-app-config.d.ts +4 -0
  158. package/dist/rspeedy-app-config.d.ts.map +1 -0
  159. package/dist/rspeedy-app-config.js +32 -0
  160. package/dist/runtime/args.d.ts +3 -0
  161. package/dist/runtime/args.d.ts.map +1 -0
  162. package/dist/runtime/args.js +11 -0
  163. package/dist/runtime/build-commands.d.ts +3 -0
  164. package/dist/runtime/build-commands.d.ts.map +1 -0
  165. package/dist/runtime/build-commands.js +282 -0
  166. package/dist/runtime/context.d.ts +63 -0
  167. package/dist/runtime/context.d.ts.map +1 -0
  168. package/dist/runtime/context.js +179 -0
  169. package/dist/runtime/early-commands.d.ts +4 -0
  170. package/dist/runtime/early-commands.d.ts.map +1 -0
  171. package/dist/runtime/early-commands.js +284 -0
  172. package/dist/runtime/project.d.ts +8 -0
  173. package/dist/runtime/project.d.ts.map +1 -0
  174. package/dist/runtime/project.js +38 -0
  175. package/dist/runtime/state.d.ts +47 -0
  176. package/dist/runtime/state.d.ts.map +1 -0
  177. package/dist/runtime/state.js +33 -0
  178. package/dist/secure-store/backends.d.ts +20 -0
  179. package/dist/secure-store/backends.d.ts.map +1 -0
  180. package/dist/secure-store/backends.js +248 -0
  181. package/dist/secure-store/types.d.ts +27 -0
  182. package/dist/secure-store/types.d.ts.map +1 -0
  183. package/dist/secure-store/types.js +1 -0
  184. package/dist/secure-store.d.ts +2 -26
  185. package/dist/secure-store.d.ts.map +1 -1
  186. package/dist/secure-store.js +3 -245
  187. package/package.json +7 -4
  188. package/templates/android-host/app/build.gradle +1 -0
  189. package/templates/android-host/app/src/main/java/template/MainActivity.java +98 -3
  190. package/templates/ios-host/__IOS_TARGET_NAME__/DemoLynxProvider.swift +26 -16
  191. 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
- import { access, mkdir, readFile, writeFile } from "node:fs/promises";
3
- import { existsSync } from "node:fs";
4
- import { createHash, randomBytes, randomUUID } from "node:crypto";
5
- import { basename, dirname, join, resolve } from "node:path";
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";