@lynxship/cli 0.1.10 → 0.1.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -25,6 +25,33 @@ npx @lynxship/cli doctor --project-dir ./my-lynx-app
25
25
 
26
26
  The package exposes the `lynxship` executable.
27
27
 
28
+ ## Vue Lynx projects
29
+
30
+ LynxShip supports projects created with the official Vue Lynx scaffold. Vue
31
+ Lynx is integrated through the project's Rspeedy configuration, so LynxShip
32
+ does not swap plugins or mutate `lynx.config.*`; it invokes the project's
33
+ package-manager `build` script and then passes the generated Lynx bundle to
34
+ the selected native or non-native target pipeline.
35
+
36
+ ```bash
37
+ npm create vue-lynx@latest my-vue-lynx-app
38
+ cd my-vue-lynx-app
39
+ npm install
40
+ npx @lynxship/cli@latest init
41
+ npx @lynxship/cli@latest doctor
42
+ npx @lynxship/cli@latest build --platform android --profile production
43
+ ```
44
+
45
+ For iOS Simulator on macOS:
46
+
47
+ ```bash
48
+ npx @lynxship/cli@latest build --platform ios --simulator --no-upload
49
+ ```
50
+
51
+ The CLI generator itself creates ReactLynx templates by default. Use the
52
+ official `create-vue-lynx` scaffold when Vue is required, then add LynxShip to
53
+ that existing project with `init`.
54
+
28
55
  ## Requirements
29
56
 
30
57
  - Node.js 22 or 24 LTS, or Node.js 26 Current; Node.js 24 LTS is the
@@ -386,6 +413,18 @@ script. On macOS, `lynxship build --platform ios` installs CocoaPods before
386
413
  archiving. Xcode, CocoaPods and real Apple signing credentials are still
387
414
  required for a signed IPA; the CLI never fabricates them.
388
415
 
416
+ For an iOS Simulator build, use the dedicated simulator profile:
417
+
418
+ ```bash
419
+ lynxship doctor --project-dir ./my-lynx-app --platform ios --profile simulator
420
+ lynxship build --project-dir ./my-lynx-app --platform ios --simulator --profile simulator --no-upload
421
+ ```
422
+
423
+ This uses Xcode's `iphonesimulator` SDK, boots an available Simulator, creates
424
+ and installs a local `.app`, and does not require a physical iPhone, Apple
425
+ Distribution certificate, provisioning profile or R2 upload. Use the
426
+ production profile only for a signed device IPA.
427
+
389
428
  Run the complete first-use diagnosis before building:
390
429
 
391
430
  ```bash
package/dist/config.d.ts CHANGED
@@ -9,6 +9,7 @@ export interface BuildProfile {
9
9
  ios?: {
10
10
  configuration?: string;
11
11
  distribution?: string;
12
+ simulator?: boolean;
12
13
  workspace?: string;
13
14
  project?: string;
14
15
  scheme?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAEA,OAAO,EAAU,KAAK,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAE5D,MAAM,WAAW,YAAY;IAC3B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAChC,GAAG,CAAC,EAAE;QACJ,aAAa,CAAC,EAAE,MAAM,CAAC;QACvB,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,kBAAkB,CAAC,EAAE,MAAM,CAAC;QAC5B,YAAY,CAAC,EAAE,MAAM,CAAC;KACvB,CAAC;IACF,OAAO,CAAC,EAAE;QACR,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,IAAI,CAAC,EAAE,SAAS,GAAG,QAAQ,CAAC;QAC5B,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,SAAS,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;QAChC,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;KACnB,CAAC;IACF,GAAG,CAAC,EAAE;QACJ,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;KACnB,CAAC;IACF,OAAO,CAAC,EAAE;QACR,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;KACnB,CAAC;CACH;AAED,MAAM,WAAW,cAAc;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE;QACJ,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,cAAc,CAAC,EAAE,MAAM,CAAC;QACxB,KAAK,CAAC,EAAE,MAAM,CAAC;KAChB,CAAC;IACF,cAAc,CAAC,EAAE;QAAE,MAAM,EAAE,aAAa,GAAG,QAAQ,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACtE,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IACrC,MAAM,CAAC,EAAE;QACP,eAAe,CAAC,EAAE,MAAM,CAAC;QACzB,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,OAAO,CAAC,EAAE;YAAE,iBAAiB,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC;KAC1C,CAAC;IACF,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,eAAO,MAAM,cAAc,EAAE,cAU5B,CAAC;AAaF,wBAAgB,cAAc,CAC5B,MAAM,EAAE,OAAO,EACf,OAAO,GAAE;IAAE,EAAE,CAAC,EAAE,OAAO,CAAA;CAAO,GAC7B,cAAc,CAyChB;AAED,wBAAsB,UAAU,CAC9B,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE;IAAE,EAAE,CAAC,EAAE,OAAO,CAAA;CAAO,GAC7B,OAAO,CAAC,cAAc,CAAC,CAazB;AAED,wBAAgB,cAAc,CAC5B,MAAM,EAAE,cAAc,EACtB,IAAI,SAAe,GAClB,YAAY,GAAG;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAIjC;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,QAAQ,CAOrD"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAEA,OAAO,EAAU,KAAK,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAE5D,MAAM,WAAW,YAAY;IAC3B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAChC,GAAG,CAAC,EAAE;QACJ,aAAa,CAAC,EAAE,MAAM,CAAC;QACvB,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,SAAS,CAAC,EAAE,OAAO,CAAC;QACpB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,kBAAkB,CAAC,EAAE,MAAM,CAAC;QAC5B,YAAY,CAAC,EAAE,MAAM,CAAC;KACvB,CAAC;IACF,OAAO,CAAC,EAAE;QACR,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,IAAI,CAAC,EAAE,SAAS,GAAG,QAAQ,CAAC;QAC5B,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,SAAS,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;QAChC,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;KACnB,CAAC;IACF,GAAG,CAAC,EAAE;QACJ,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;KACnB,CAAC;IACF,OAAO,CAAC,EAAE;QACR,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;KACnB,CAAC;CACH;AAED,MAAM,WAAW,cAAc;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE;QACJ,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,cAAc,CAAC,EAAE,MAAM,CAAC;QACxB,KAAK,CAAC,EAAE,MAAM,CAAC;KAChB,CAAC;IACF,cAAc,CAAC,EAAE;QAAE,MAAM,EAAE,aAAa,GAAG,QAAQ,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACtE,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IACrC,MAAM,CAAC,EAAE;QACP,eAAe,CAAC,EAAE,MAAM,CAAC;QACzB,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,OAAO,CAAC,EAAE;YAAE,iBAAiB,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC;KAC1C,CAAC;IACF,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,eAAO,MAAM,cAAc,EAAE,cAsB5B,CAAC;AAaF,wBAAgB,cAAc,CAC5B,MAAM,EAAE,OAAO,EACf,OAAO,GAAE;IAAE,EAAE,CAAC,EAAE,OAAO,CAAA;CAAO,GAC7B,cAAc,CAyChB;AAED,wBAAsB,UAAU,CAC9B,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE;IAAE,EAAE,CAAC,EAAE,OAAO,CAAA;CAAO,GAC7B,OAAO,CAAC,cAAc,CAAC,CAazB;AAED,wBAAgB,cAAc,CAC5B,MAAM,EAAE,cAAc,EACtB,IAAI,SAAe,GAClB,YAAY,GAAG;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAejC;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,QAAQ,CAOrD"}
package/dist/config.js CHANGED
@@ -4,6 +4,18 @@ import { assert } from "@lynxship/contracts";
4
4
  export const DEFAULT_CONFIG = {
5
5
  runtimeVersion: { policy: "fingerprint" },
6
6
  build: {
7
+ development: {
8
+ distribution: "development",
9
+ channel: "development",
10
+ environment: "development",
11
+ ios: { configuration: "Debug" },
12
+ },
13
+ simulator: {
14
+ distribution: "development",
15
+ channel: "development",
16
+ environment: "development",
17
+ ios: { configuration: "Debug", simulator: true },
18
+ },
7
19
  production: {
8
20
  distribution: "store",
9
21
  channel: "production",
@@ -52,9 +64,20 @@ export async function loadConfig(root, options = {}) {
52
64
  }
53
65
  }
54
66
  export function resolveProfile(config, name = "production") {
55
- const profile = config.build?.[name];
67
+ const profile = config.build?.[name] ?? DEFAULT_CONFIG.build?.[name];
56
68
  assert(profile, "PROFILE_NOT_FOUND", `Build profile '${name}' was not found`);
57
- return { name, ...profile };
69
+ const inherited = DEFAULT_CONFIG.build?.[name] ?? {};
70
+ const productionIos = config.build?.production?.ios ?? {};
71
+ return {
72
+ name,
73
+ ...inherited,
74
+ ...profile,
75
+ ios: {
76
+ ...(inherited.ios ?? {}),
77
+ ...(name !== "production" ? productionIos : {}),
78
+ ...(profile.ios ?? {}),
79
+ },
80
+ };
58
81
  }
59
82
  export function platformValue(value) {
60
83
  assert(["android", "ios", "harmony", "web", "desktop"].includes(value), "PLATFORM_INVALID", "Platform must be android, ios, harmony, web or desktop");
@@ -0,0 +1,3 @@
1
+ export declare function extractDevServerUrl(line: string): string | undefined;
2
+ export declare function shouldPrintDevServerQr(line: string): boolean;
3
+ //# sourceMappingURL=dev-qr.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dev-qr.d.ts","sourceRoot":"","sources":["../src/dev-qr.ts"],"names":[],"mappings":"AAMA,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAMpE;AAED,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAE5D"}
package/dist/dev-qr.js ADDED
@@ -0,0 +1,12 @@
1
+ const URL_PATTERN = /https?:\/\/[^\s<>"'`]+/g;
2
+ function cleanUrl(value) {
3
+ return value.replace(/[),.;!?]+$/u, "");
4
+ }
5
+ export function extractDevServerUrl(line) {
6
+ const urls = (line.match(URL_PATTERN) ?? []).map(cleanUrl);
7
+ return (urls.find((url) => url.includes("fullscreen=true")) ??
8
+ urls.find((url) => url.includes(".lynx.bundle")));
9
+ }
10
+ export function shouldPrintDevServerQr(line) {
11
+ return /(?:default:|fullscreen=true|\bready\b)/iu.test(line);
12
+ }
@@ -1,6 +1,11 @@
1
1
  export interface CliGuidance {
2
2
  commands: string[];
3
3
  note?: string;
4
+ environment?: string;
4
5
  }
5
- export declare function guidanceForError(error: unknown): CliGuidance;
6
+ export interface GuidanceContext {
7
+ args?: readonly string[];
8
+ hostPlatform?: NodeJS.Platform;
9
+ }
10
+ export declare function guidanceForError(error: unknown, context?: GuidanceContext): CliGuidance;
6
11
  //# sourceMappingURL=guidance.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"guidance.d.ts","sourceRoot":"","sources":["../src/guidance.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAiXD,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,WAAW,CAiG5D"}
1
+ {"version":3,"file":"guidance.d.ts","sourceRoot":"","sources":["../src/guidance.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACzB,YAAY,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC;CAChC;AAumBD,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,OAAO,EACd,OAAO,GAAE,eAAoB,GAC5B,WAAW,CAEb"}
package/dist/guidance.js CHANGED
@@ -223,6 +223,35 @@ const guidance = {
223
223
  ],
224
224
  note: "The iOS doctor checks macOS, Xcode, xcrun, CocoaPods, Xcode settings, Apple signing identities, provisioning and export options without printing certificate contents.",
225
225
  },
226
+ IOS_SIMULATOR_PLATFORM: {
227
+ commands: [
228
+ "lynxship doctor --platform ios --profile simulator",
229
+ "lynxship build --platform ios --simulator --profile simulator --no-upload",
230
+ ],
231
+ note: "The iOS Simulator target is selected with --platform ios; Android, Web and Desktop targets cannot use this flag.",
232
+ },
233
+ IOS_SIMULATOR_RUNTIME_REQUIRED: {
234
+ commands: [
235
+ "xcode-select --install",
236
+ "lynxship doctor --platform ios --profile simulator",
237
+ "lynxship build --platform ios --simulator --profile simulator --no-upload",
238
+ ],
239
+ note: "Install an iOS Simulator runtime in Xcode > Settings > Components, then rerun the doctor.",
240
+ },
241
+ IOS_SIMULATOR_ARTIFACT_MISSING: {
242
+ commands: [
243
+ "lynxship doctor --platform ios --profile simulator",
244
+ "lynxship build --platform ios --simulator --profile simulator --no-upload",
245
+ ],
246
+ note: "The simulator build must produce an .app under DerivedData before it can be installed.",
247
+ },
248
+ IOS_SIMULATOR_UPLOAD_BLOCKED: {
249
+ commands: [
250
+ "lynxship build --platform ios --simulator --profile simulator --no-upload",
251
+ "lynxship build --platform ios --profile production",
252
+ ],
253
+ note: "Simulator .app artifacts stay local; use a signed production device build for R2 upload and distribution.",
254
+ },
226
255
  IOS_COCOAPODS_REQUIRED: {
227
256
  commands: ["brew install cocoapods", "lynxship build --platform ios"],
228
257
  note: "CocoaPods is required when the iOS host contains a Podfile.",
@@ -234,15 +263,20 @@ const guidance = {
234
263
  ],
235
264
  },
236
265
  IOS_SCHEME_REQUIRED: {
237
- commands: ["lynxship inspect", "lynxship doctor --platform ios"],
266
+ commands: [
267
+ "lynxship inspect",
268
+ "lynxship doctor --platform ios --profile simulator",
269
+ "lynxship doctor --platform ios --profile production",
270
+ ],
238
271
  note: "Set build.<profile>.ios.scheme in lynxship.json.",
239
272
  },
240
273
  IOS_EXPORT_OPTIONS_REQUIRED: {
241
274
  commands: [
242
275
  "lynxship ios host init --bundle-identifier com.example.myapp",
243
276
  "lynxship doctor --platform ios",
277
+ "lynxship build --platform ios --simulator --profile simulator --no-upload",
244
278
  ],
245
- note: "Set build.<profile>.ios.exportOptionsPlist to a valid export options file.",
279
+ note: "Set build.<profile>.ios.exportOptionsPlist for a device IPA; simulator .app builds do not require export options.",
246
280
  },
247
281
  IOS_DEVICE_REQUIRED: {
248
282
  commands: [
@@ -364,7 +398,7 @@ const guidance = {
364
398
  note: "Lynx Trace and Recorder are operated from Lynx DevTool Desktop after the native -dev runtime is integrated.",
365
399
  },
366
400
  };
367
- export function guidanceForError(error) {
401
+ function guidanceForErrorBase(error) {
368
402
  const code = error.code;
369
403
  if (typeof code === "string" && guidance[code])
370
404
  return guidance[code];
@@ -461,3 +495,96 @@ export function guidanceForError(error) {
461
495
  }
462
496
  return { commands: [] };
463
497
  }
498
+ function argumentValue(args, name) {
499
+ const index = args.indexOf(name);
500
+ if (index >= 0)
501
+ return args[index + 1];
502
+ const inline = args.find((value) => value.startsWith(`${name}=`));
503
+ return inline?.slice(name.length + 1);
504
+ }
505
+ function targetFromCode(code) {
506
+ if (!code)
507
+ return undefined;
508
+ if (code.startsWith("ANDROID_"))
509
+ return "android";
510
+ if (code.startsWith("IOS_"))
511
+ return "ios";
512
+ if (code.startsWith("HARMONY_"))
513
+ return "harmony";
514
+ if (code.startsWith("WEB_"))
515
+ return "web";
516
+ if (code.startsWith("DESKTOP_"))
517
+ return "desktop";
518
+ if (code === "BUILD_ALL_MACOS_REQUIRED")
519
+ return "all";
520
+ return undefined;
521
+ }
522
+ function targetFromCommand(command) {
523
+ const match = command.match(/--platform\s+(android|ios|harmony|web|desktop|all)/);
524
+ return match?.[1];
525
+ }
526
+ function targetCommand(platform, simulator, action) {
527
+ if (platform === "all")
528
+ return action === "doctor"
529
+ ? "lynxship doctor --platform android"
530
+ : "lynxship build --platform all --profile production";
531
+ if (platform === "ios" && simulator) {
532
+ return action === "doctor"
533
+ ? "lynxship doctor --platform ios --profile simulator"
534
+ : "lynxship build --platform ios --simulator --profile simulator --no-upload";
535
+ }
536
+ if (action === "doctor")
537
+ return `lynxship doctor --platform ${platform}`;
538
+ return `lynxship build --platform ${platform} --profile production`;
539
+ }
540
+ function adaptGuidance(guidance, error, context) {
541
+ const code = error.code;
542
+ const errorCode = typeof code === "string" ? code : undefined;
543
+ const args = context.args ?? [];
544
+ const requested = argumentValue(args, "--platform");
545
+ const target = requested ?? targetFromCode(errorCode);
546
+ const simulator = args.includes("--simulator");
547
+ let commands = guidance.commands;
548
+ // Generic errors must preserve the platform the developer actually asked
549
+ // for. The old fallback to Android was misleading for iOS/Web/Desktop jobs.
550
+ if (target &&
551
+ errorCode !== "BUILD_ALL_MACOS_REQUIRED" &&
552
+ ["BUILD_", "PROFILE_NOT_FOUND"].some((prefix) => errorCode?.startsWith(prefix))) {
553
+ commands = [
554
+ targetCommand(target, target === "ios" && simulator, "doctor"),
555
+ targetCommand(target, target === "ios" && simulator, "build"),
556
+ ];
557
+ }
558
+ const hostPlatform = context.hostPlatform ?? process.platform;
559
+ const macOnly = hostPlatform !== "darwin";
560
+ let requiredEnvironment;
561
+ commands = commands.map((command) => {
562
+ const commandTarget = targetFromCommand(command);
563
+ const isIosCommand = commandTarget === "ios" ||
564
+ command.includes("ios host") ||
565
+ command.startsWith("xcode-select") ||
566
+ command.startsWith("xcrun ") ||
567
+ command.startsWith("brew ");
568
+ const isAllCommand = commandTarget === "all";
569
+ if (macOnly && isIosCommand) {
570
+ requiredEnvironment = "macOS or a macOS CI runner";
571
+ return command;
572
+ }
573
+ if (macOnly && isAllCommand) {
574
+ requiredEnvironment = "macOS or a macOS CI runner";
575
+ return command;
576
+ }
577
+ return command;
578
+ });
579
+ const notes = guidance.note ? [guidance.note] : [];
580
+ if (requiredEnvironment)
581
+ notes.push("These commands must be run on macOS or a macOS CI runner; Android remains supported on Windows, macOS and Linux.");
582
+ return {
583
+ commands,
584
+ note: notes.length > 0 ? notes.join(" ") : undefined,
585
+ environment: requiredEnvironment,
586
+ };
587
+ }
588
+ export function guidanceForError(error, context = {}) {
589
+ return adaptGuidance(guidanceForErrorBase(error), error, context);
590
+ }
package/dist/index.js CHANGED
@@ -33,6 +33,7 @@ import { formatDevToolFailure, inspectLynxDevTool } from "./lynx-devtool.js";
33
33
  import { hasHarmonyHost, runRealHarmonyBuild } from "./harmony-build.js";
34
34
  import { hasWebConfiguration, runRealWebBuild } from "./web-build.js";
35
35
  import { hasDesktopHost, runRealDesktopBuild } from "./desktop-build.js";
36
+ import { extractDevServerUrl, shouldPrintDevServerQr } from "./dev-qr.js";
36
37
  import { inspectDesktopTarget, inspectHarmonyTarget, inspectWebTarget, } from "./target-toolchain.js";
37
38
  const rawArgs = process.argv.slice(2);
38
39
  const args = [...rawArgs];
@@ -149,6 +150,12 @@ function mobilePlatformValue(value) {
149
150
  async function renderConfigurationFooter() {
150
151
  if (!ui.interactive || ui.options.quiet)
151
152
  return;
153
+ if (rawArgs[0] === "inspect")
154
+ return;
155
+ const profileIndex = rawArgs.indexOf("--profile");
156
+ const simulatorProfile = profileIndex >= 0 && rawArgs[profileIndex + 1] === "simulator";
157
+ if (rawArgs.includes("--simulator") || simulatorProfile)
158
+ return;
152
159
  const status = await readConfigurationStatus();
153
160
  const ready = status.r2 && status.android;
154
161
  if (ready)
@@ -202,14 +209,19 @@ async function saveState(state, repository, builds, submissions) {
202
209
  }
203
210
  let stateSaveQueue = Promise.resolve();
204
211
  async function executeBuild(options, showResult = true) {
205
- const { config, profile, platform, skipUpload, wait, local, state, repository, builds, submissions, progress: sharedProgress, progressPrefix, onProgress, onEvent, skipBundleBuild, } = options;
212
+ const { config, profile, platform, skipUpload, wait, local, state, repository, builds, submissions, progress: sharedProgress, progressPrefix, onProgress, onEvent, skipBundleBuild, simulator = false, simulatorDevice, } = options;
206
213
  if (wait && !local)
207
214
  await ensureNativeHostForBuild(platform, profile, config);
215
+ // Host initialization can write lynxship.json (for example, the generated
216
+ // iOS scheme). Always read it again before resolving the build profile so a
217
+ // first build uses the host that was just created in the same invocation.
218
+ const effectiveConfig = wait && !local ? await loadConfig(root) : config;
208
219
  if (platform === "android" || platform === "ios")
209
220
  await requireAutolinkReady(root, platform);
210
- const runtime = await inspectRuntimeFingerprint(root, platform, config);
221
+ const runtime = await inspectRuntimeFingerprint(root, platform, effectiveConfig);
222
+ const resolvedBuildProfile = resolveProfile(effectiveConfig, profile);
211
223
  const job = await builds.create({
212
- projectId: configuredProjectId(config),
224
+ projectId: configuredProjectId(effectiveConfig),
213
225
  organizationId: "local_org",
214
226
  platform,
215
227
  profile,
@@ -242,7 +254,8 @@ async function executeBuild(options, showResult = true) {
242
254
  const realAndroid = platform === "android" &&
243
255
  isSupportedAndroidPlatform() &&
244
256
  (await hasAndroidHost(root));
245
- const realIos = platform === "ios" && hasIosHost(root, resolveProfile(config, profile));
257
+ const realIos = platform === "ios" &&
258
+ hasIosHost(root, resolveProfile(effectiveConfig, profile));
246
259
  if (platform === "ios" && !realIos && !local)
247
260
  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.");
248
261
  if (platform === "android" && !realAndroid && !local)
@@ -252,7 +265,7 @@ async function executeBuild(options, showResult = true) {
252
265
  assert(toolchain.ok, "ANDROID_TOOLCHAIN_REQUIRED", `Android toolchain is not ready: ${formatAndroidToolchainFailure(toolchain)}`);
253
266
  await runRealAndroidBuild(job, {
254
267
  root,
255
- profile: config.build?.[profile] ?? {},
268
+ profile: resolvedBuildProfile,
256
269
  uploadArtifacts: !skipUpload,
257
270
  skipBundleBuild,
258
271
  quiet: json,
@@ -261,11 +274,13 @@ async function executeBuild(options, showResult = true) {
261
274
  });
262
275
  }
263
276
  else if (realIos) {
264
- const toolchain = await inspectIosToolchain(root, resolveProfile(config, profile));
277
+ const toolchain = await inspectIosToolchain(root, resolveProfile(effectiveConfig, profile), simulator ? "simulator" : "device");
265
278
  assert(toolchain.ok, "IOS_TOOLCHAIN_REQUIRED", `iOS toolchain is not ready: ${formatIosToolchainFailure(toolchain)}`);
266
279
  await runRealIosBuild(job, {
267
280
  root,
268
- profile: config.build?.[profile] ?? {},
281
+ profile: resolvedBuildProfile,
282
+ simulator,
283
+ simulatorDevice,
269
284
  uploadArtifacts: !skipUpload,
270
285
  skipBundleBuild,
271
286
  quiet: json,
@@ -276,7 +291,7 @@ async function executeBuild(options, showResult = true) {
276
291
  else if (!local && platform === "web") {
277
292
  await runRealWebBuild(job, {
278
293
  root,
279
- profile: resolveProfile(config, profile),
294
+ profile: resolveProfile(effectiveConfig, profile),
280
295
  uploadArtifacts: !skipUpload,
281
296
  skipBundleBuild,
282
297
  quiet: json,
@@ -287,7 +302,7 @@ async function executeBuild(options, showResult = true) {
287
302
  else if (!local && platform === "harmony") {
288
303
  await runRealHarmonyBuild(job, {
289
304
  root,
290
- profile: resolveProfile(config, profile),
305
+ profile: resolveProfile(effectiveConfig, profile),
291
306
  uploadArtifacts: !skipUpload,
292
307
  skipBundleBuild,
293
308
  quiet: json,
@@ -298,7 +313,7 @@ async function executeBuild(options, showResult = true) {
298
313
  else if (!local && platform === "desktop") {
299
314
  await runRealDesktopBuild(job, {
300
315
  root,
301
- profile: resolveProfile(config, profile),
316
+ profile: resolveProfile(effectiveConfig, profile),
302
317
  uploadArtifacts: !skipUpload,
303
318
  allowUnsigned: options.allowUnsigned,
304
319
  skipBundleBuild,
@@ -439,9 +454,11 @@ Commands:
439
454
 
440
455
  Build options:
441
456
  --platform <p> Target android, ios, harmony, web, desktop or all (default: android)
442
- --profile <name> Build profile (default: production)
457
+ --profile <name> Build profile (default: production; simulator uses simulator)
443
458
  --no-wait Queue the build without executing it locally
444
459
  --no-upload Keep the artifact local and skip R2 (CI verification)
460
+ --simulator Build and install an iOS Simulator .app locally
461
+ --device <id> Select the iOS Simulator device for a simulator build
445
462
  --allow-unsigned Allow an unsigned Desktop artifact only with --no-upload (local tests)
446
463
  --local Use the contract-only build path for tests
447
464
 
@@ -487,7 +504,7 @@ Global options:
487
504
  --application-id <id> Android package/application ID for host init
488
505
  --bundle-identifier <id> iOS bundle identifier for host init
489
506
  --library-dir <path> Native library directory for autolink codegen
490
- --simulator Install an iOS .app on a simulator with simctl
507
+ --simulator Build/install an iOS .app or install one with simctl
491
508
  --help Show this complete command reference
492
509
 
493
510
  Node support: Node 22/24 LTS or Node 26 Current. Use Node 24 LTS for production.`;
@@ -507,7 +524,7 @@ async function looksLikeLynxProject() {
507
524
  ...packageJson.dependencies,
508
525
  ...packageJson.devDependencies,
509
526
  };
510
- return (Object.keys(dependencies).some((name) => name.startsWith("@lynx-js/")) ||
527
+ return (Object.keys(dependencies).some((name) => name.startsWith("@lynx-js/") || name === "vue-lynx") ||
511
528
  Object.values(packageJson.scripts ?? {}).some((script) => script.includes("rspeedy")));
512
529
  }
513
530
  catch {
@@ -618,10 +635,34 @@ async function runRspeedyCommand(subcommand, environment) {
618
635
  if (subcommand === "dev") {
619
636
  ui.info("Lynx Explorer mode: no Android or iOS native host is required. Scan the QR code printed by Rspeedy; source changes reload automatically.");
620
637
  }
638
+ let devUrl;
639
+ let devQrPrinted = false;
640
+ let devQrTimer;
641
+ const printDevQr = () => {
642
+ if (!devUrl || devQrPrinted || json || ui.options.quiet)
643
+ return;
644
+ devQrPrinted = true;
645
+ if (devQrTimer)
646
+ clearTimeout(devQrTimer);
647
+ ui.devServerQr(devUrl);
648
+ };
621
649
  await runRspeedy(root, subcommand, forwarded, {
622
650
  env: environment,
623
651
  quiet: json,
624
- onOutput: (line) => ui.info(`│ ${line}`),
652
+ onOutput: (line) => {
653
+ ui.info(`│ ${line}`);
654
+ if (subcommand !== "dev")
655
+ return;
656
+ const url = extractDevServerUrl(line);
657
+ if (url && (!devUrl || url.includes("fullscreen=true"))) {
658
+ devUrl = url;
659
+ if (devQrTimer)
660
+ clearTimeout(devQrTimer);
661
+ devQrTimer = setTimeout(printDevQr, 500);
662
+ }
663
+ if (devUrl && shouldPrintDevServerQr(line))
664
+ printDevQr();
665
+ },
625
666
  });
626
667
  printValue({ status: "success", command: `rspeedy ${subcommand}` }, {
627
668
  title: `Rspeedy ${subcommand}`,
@@ -860,12 +901,7 @@ async function main() {
860
901
  const nativeCredentialStore = !credentialStore.includes("owner-only");
861
902
  const androidHost = doctorPlatform === "android" ? await hasAndroidHost(root) : false;
862
903
  const doctorProfileName = flag("--profile", "production");
863
- const doctorProfile = config.build?.[doctorProfileName]
864
- ? resolveProfile(config, doctorProfileName)
865
- : {
866
- name: doctorProfileName,
867
- ...(DEFAULT_CONFIG.build?.production ?? {}),
868
- };
904
+ const doctorProfile = resolveProfile(config, doctorProfileName);
869
905
  const targetToolchain = doctorPlatform === "web"
870
906
  ? await inspectWebTarget(root, doctorProfile)
871
907
  : doctorPlatform === "harmony"
@@ -879,7 +915,9 @@ async function main() {
879
915
  ? await inspectAndroidToolchain(root)
880
916
  : undefined;
881
917
  const iosToolchain = doctorPlatform === "ios"
882
- ? await inspectIosToolchain(root, doctorProfile)
918
+ ? await inspectIosToolchain(root, doctorProfile, doctorProfile.ios?.simulator || doctorProfileName === "simulator"
919
+ ? "simulator"
920
+ : "device")
883
921
  : undefined;
884
922
  if (androidToolchain && args.includes("--fix")) {
885
923
  await fixAndroidToolchain(root, androidToolchain, (message) => confirm(message), (line) => ui.info(line));
@@ -963,11 +1001,19 @@ async function main() {
963
1001
  }))),
964
1002
  {
965
1003
  name: "cloudflare-r2",
966
- ok: configuration.r2,
967
- status: configuration.r2 ? "pass" : "fail",
968
- value: configuration.r2
969
- ? "configured"
970
- : "missing · fix: lynxship storage configure",
1004
+ ok: doctorPlatform === "ios" && doctorProfile.ios?.simulator
1005
+ ? true
1006
+ : configuration.r2,
1007
+ status: doctorPlatform === "ios" && doctorProfile.ios?.simulator
1008
+ ? "pass"
1009
+ : configuration.r2
1010
+ ? "pass"
1011
+ : "fail",
1012
+ value: doctorPlatform === "ios" && doctorProfile.ios?.simulator
1013
+ ? "not required for iOS Simulator"
1014
+ : configuration.r2
1015
+ ? "configured"
1016
+ : "missing · fix: lynxship storage configure",
971
1017
  },
972
1018
  ...(doctorPlatform === "android"
973
1019
  ? [
@@ -1594,7 +1640,9 @@ async function main() {
1594
1640
  const platformArgument = flag("--platform", "android");
1595
1641
  const buildAll = subcommand === "all" || platformArgument === "all";
1596
1642
  const platform = buildAll ? "android" : platformValue(platformArgument);
1597
- const skipUpload = args.includes("--no-upload");
1643
+ const simulator = args.includes("--simulator");
1644
+ const skipUpload = args.includes("--no-upload") || simulator;
1645
+ assert(!simulator || platformArgument === "ios", "IOS_SIMULATOR_PLATFORM", "The --simulator option is only available with --platform ios.");
1598
1646
  const allowUnsigned = args.includes("--allow-unsigned");
1599
1647
  assert(!allowUnsigned || skipUpload, "CLI_UNSIGNED_UPLOAD_BLOCKED", "--allow-unsigned is only available with --no-upload and can never upload an unsigned Desktop artifact.");
1600
1648
  await requireOperationalConfiguration(platform, { requireR2: !skipUpload });
@@ -1621,7 +1669,8 @@ async function main() {
1621
1669
  }
1622
1670
  assert(subcommand === "create" || buildAll, "CLI_BUILD_COMMAND", `Unknown build command: ${subcommand}`);
1623
1671
  const config = await loadConfig(root);
1624
- const profile = flag("--profile", "production");
1672
+ const profile = flag("--profile", simulator ? "simulator" : "production");
1673
+ const simulatorDevice = flag("--device") ?? undefined;
1625
1674
  const wait = !args.includes("--no-wait");
1626
1675
  const local = args.includes("--local");
1627
1676
  const platforms = buildAll
@@ -1641,6 +1690,8 @@ async function main() {
1641
1690
  allowUnsigned,
1642
1691
  wait,
1643
1692
  local,
1693
+ simulator,
1694
+ simulatorDevice,
1644
1695
  state,
1645
1696
  repository,
1646
1697
  builds,
@@ -1670,6 +1721,8 @@ async function main() {
1670
1721
  allowUnsigned,
1671
1722
  wait,
1672
1723
  local,
1724
+ simulator,
1725
+ simulatorDevice,
1673
1726
  state,
1674
1727
  repository,
1675
1728
  builds,
@@ -1756,7 +1809,7 @@ void main()
1756
1809
  .catch((error) => {
1757
1810
  const message = error instanceof Error ? error.message : "Unknown error";
1758
1811
  const code = error.code ?? "CLI_ERROR";
1759
- const nextSteps = guidanceForError(error);
1812
+ const nextSteps = guidanceForError(error, { args: rawArgs });
1760
1813
  if (json) {
1761
1814
  console.log(JSON.stringify({
1762
1815
  error: message,
@@ -1765,6 +1818,9 @@ void main()
1765
1818
  ? {
1766
1819
  nextSteps: nextSteps.commands,
1767
1820
  ...(nextSteps.note ? { note: nextSteps.note } : {}),
1821
+ ...(nextSteps.environment
1822
+ ? { environment: nextSteps.environment }
1823
+ : {}),
1768
1824
  }
1769
1825
  : {}),
1770
1826
  }));
@@ -4,6 +4,8 @@ interface IosBuildOptions {
4
4
  root: string;
5
5
  profile: BuildProfile;
6
6
  uploadArtifacts?: boolean;
7
+ simulator?: boolean;
8
+ simulatorDevice?: string;
7
9
  skipBundleBuild?: boolean;
8
10
  quiet?: boolean;
9
11
  onEvent?: (message: string) => void;
@@ -1 +1 @@
1
- {"version":3,"file":"ios-build.d.ts","sourceRoot":"","sources":["../src/ios-build.ts"],"names":[],"mappings":"AAGA,OAAO,EAAkB,KAAK,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAEpE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAMhD,UAAU,eAAe;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,YAAY,CAAC;IACtB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,UAAU,CAAC,EAAE,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;CACvD;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,YAAY,GAAG,OAAO,CAcxE;AAsDD,wBAAsB,eAAe,CACnC,GAAG,EAAE,QAAQ,EACb,OAAO,EAAE,eAAe,GACvB,OAAO,CAAC,QAAQ,CAAC,CAuMnB"}
1
+ {"version":3,"file":"ios-build.d.ts","sourceRoot":"","sources":["../src/ios-build.ts"],"names":[],"mappings":"AAIA,OAAO,EAAkB,KAAK,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAEpE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAMhD,UAAU,eAAe;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,YAAY,CAAC;IACtB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,UAAU,CAAC,EAAE,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;CACvD;AAwRD,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,YAAY,GAAG,OAAO,CAcxE;AAsDD,wBAAsB,eAAe,CACnC,GAAG,EAAE,QAAQ,EACb,OAAO,EAAE,eAAe,GACvB,OAAO,CAAC,QAAQ,CAAC,CAwMnB"}
package/dist/ios-build.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { createHash } from "node:crypto";
1
2
  import { copyFile, mkdir, readdir, readFile } from "node:fs/promises";
2
3
  import { existsSync, readdirSync } from "node:fs";
3
4
  import { join, resolve } from "node:path";
@@ -5,8 +6,177 @@ import { assert, sha256 } from "@lynxship/contracts";
5
6
  import { transitionBuild } from "@lynxship/build-orchestrator";
6
7
  import { loadR2, uploadR2Artifact } from "./r2.js";
7
8
  import { nativeArtifactName } from "./artifact-name.js";
8
- import { commandExists, runProcess } from "./process-runner.js";
9
+ import { captureProcess, commandExists, runProcess } from "./process-runner.js";
9
10
  import { buildLynxBundle } from "./bundle-build.js";
11
+ async function listSimulatorDevices(root, filter) {
12
+ const result = await captureProcess("xcrun", ["simctl", "list", "devices", filter, "--json"], { cwd: root });
13
+ if (result.code !== 0)
14
+ return [];
15
+ try {
16
+ const parsed = JSON.parse(result.stdout);
17
+ return Object.values(parsed.devices ?? {}).flat();
18
+ }
19
+ catch {
20
+ return [];
21
+ }
22
+ }
23
+ async function selectSimulatorDevice(root, requested) {
24
+ if (requested)
25
+ return requested;
26
+ const booted = (await listSimulatorDevices(root, "booted"))[0];
27
+ if (booted)
28
+ return booted.udid;
29
+ const available = (await listSimulatorDevices(root, "available")).find((device) => device.isAvailable !== false);
30
+ assert(available, "IOS_SIMULATOR_RUNTIME_REQUIRED", "No available iOS Simulator device was found. Install an iOS Simulator runtime in Xcode, then rerun the build.");
31
+ return available.udid;
32
+ }
33
+ async function ensureSimulatorBooted(root, device, onEvent, quiet) {
34
+ const booted = (await listSimulatorDevices(root, "booted")).some((entry) => entry.udid === device);
35
+ if (!booted) {
36
+ onEvent?.(`Booting iOS Simulator ${device}…`);
37
+ await runProcess("xcrun", ["simctl", "boot", device], {
38
+ cwd: root,
39
+ quiet,
40
+ onOutput: onEvent,
41
+ });
42
+ }
43
+ await runProcess("xcrun", ["simctl", "bootstatus", device, "-b"], {
44
+ cwd: root,
45
+ quiet,
46
+ onOutput: onEvent,
47
+ });
48
+ }
49
+ async function findSimulatorApp(root, derivedData, configuration, scheme) {
50
+ const products = join(derivedData, "Build", "Products", `${configuration}-iphonesimulator`);
51
+ const expected = join(products, `${scheme}.app`);
52
+ if (existsSync(expected))
53
+ return expected;
54
+ const entries = await readdir(products, { withFileTypes: true }).catch(() => []);
55
+ const app = entries.find((entry) => entry.isDirectory() && entry.name.endsWith(".app"));
56
+ assert(app, "IOS_SIMULATOR_ARTIFACT_MISSING", `Xcode did not produce a Simulator .app under ${products}`);
57
+ return join(products, app.name);
58
+ }
59
+ async function hashDirectory(directory) {
60
+ const hash = createHash("sha256");
61
+ let size = 0;
62
+ async function visit(current, relative) {
63
+ const entries = await readdir(current, { withFileTypes: true });
64
+ entries.sort((left, right) => left.name.localeCompare(right.name));
65
+ for (const entry of entries) {
66
+ const entryRelative = relative ? `${relative}/${entry.name}` : entry.name;
67
+ const entryPath = join(current, entry.name);
68
+ if (entry.isDirectory()) {
69
+ hash.update(`dir:${entryRelative}\n`);
70
+ await visit(entryPath, entryRelative);
71
+ }
72
+ else {
73
+ const content = await readFile(entryPath);
74
+ size += content.length;
75
+ hash.update(`file:${entryRelative}:${content.length}\n`);
76
+ hash.update(content.toString("latin1"), "latin1");
77
+ }
78
+ }
79
+ }
80
+ await visit(directory, "");
81
+ return { hash: hash.digest("hex"), size };
82
+ }
83
+ async function runRealIosSimulatorBuild(job, options) {
84
+ assert(process.platform === "darwin", "IOS_MACOS_REQUIRED", "iOS Simulator builds require macOS with Xcode");
85
+ assert(commandExists("xcodebuild"), "IOS_XCODE_REQUIRED", "xcodebuild was not found. Install Xcode and select its command-line tools.");
86
+ assert(commandExists("xcrun"), "IOS_XCRUN_REQUIRED", "xcrun was not found. Install Xcode command-line tools.");
87
+ const project = findProject(options.root, options.profile);
88
+ const ios = options.profile.ios ?? {};
89
+ assert(ios.scheme, "IOS_SCHEME_REQUIRED", "Configure ios.scheme in the selected Simulator build profile");
90
+ const projectFlag = project.endsWith(".xcworkspace")
91
+ ? "-workspace"
92
+ : "-project";
93
+ const configuration = ios.configuration ?? "Debug";
94
+ const device = await selectSimulatorDevice(options.root, options.simulatorDevice);
95
+ await installCocoaPods(options.root, options.quiet, options.onEvent);
96
+ const directory = join(options.root, ".lynxship", "ios", job.id, "simulator");
97
+ const derivedData = join(directory, "derived-data");
98
+ const step = (message, progress) => {
99
+ options.onEvent?.(message);
100
+ options.onProgress?.(progress, message);
101
+ };
102
+ try {
103
+ assert(options.uploadArtifacts !== true, "IOS_SIMULATOR_UPLOAD_BLOCKED", "iOS Simulator .app builds stay local. Use --no-upload; simulator artifacts are not store artifacts.");
104
+ await mkdir(directory, { recursive: true });
105
+ transitionBuild(job, "uploading_source", "iOS Simulator source prepared");
106
+ if (options.skipBundleBuild) {
107
+ step("Using shared Lynx bundle", 5);
108
+ }
109
+ else {
110
+ step("Building Lynx bundle with Rspeedy…", 5);
111
+ await buildLynxBundle(options.root, {
112
+ quiet: options.quiet,
113
+ onOutput: options.onEvent,
114
+ });
115
+ }
116
+ if (ios.bundleScript) {
117
+ step("Syncing bundle into the iOS host…", 8);
118
+ await runProcess(process.execPath, [resolve(options.root, ios.bundleScript)], {
119
+ cwd: options.root,
120
+ quiet: options.quiet,
121
+ onOutput: options.onEvent,
122
+ });
123
+ }
124
+ transitionBuild(job, "queued", "iOS Simulator build queued locally");
125
+ transitionBuild(job, "provisioning", "iOS Simulator destination selected");
126
+ transitionBuild(job, "installing_dependencies", "iOS Simulator dependencies ready");
127
+ await ensureSimulatorBooted(options.root, device, options.onEvent, options.quiet);
128
+ step("Building iOS Simulator .app…", 15);
129
+ transitionBuild(job, "building", "iOS Simulator build started");
130
+ await runProcess("xcodebuild", [
131
+ projectFlag,
132
+ project,
133
+ "-scheme",
134
+ ios.scheme,
135
+ "-configuration",
136
+ configuration,
137
+ "-sdk",
138
+ "iphonesimulator",
139
+ "-destination",
140
+ `id=${device}`,
141
+ "-derivedDataPath",
142
+ derivedData,
143
+ "CODE_SIGNING_ALLOWED=NO",
144
+ "CODE_SIGNING_REQUIRED=NO",
145
+ "build",
146
+ ], { cwd: options.root, quiet: options.quiet, onOutput: options.onEvent });
147
+ const appPath = await findSimulatorApp(options.root, derivedData, configuration, ios.scheme);
148
+ step("Installing .app in iOS Simulator…", 75);
149
+ await runProcess("xcrun", ["simctl", "install", device, appPath], {
150
+ cwd: options.root,
151
+ quiet: options.quiet,
152
+ onOutput: options.onEvent,
153
+ });
154
+ const artifact = await hashDirectory(appPath);
155
+ const artifactName = nativeArtifactName("app");
156
+ job.attempts += 1;
157
+ job.artifact = {
158
+ name: artifactName,
159
+ hash: artifact.hash,
160
+ path: appPath,
161
+ size: artifact.size,
162
+ contentType: "application/octet-stream",
163
+ };
164
+ transitionBuild(job, "signing", "Simulator app verification completed");
165
+ transitionBuild(job, "uploading_artifacts", "Simulator app collected locally");
166
+ step(`Simulator app ready: ${artifactName}`, 100);
167
+ return transitionBuild(job, "success", "iOS Simulator app created");
168
+ }
169
+ catch (error) {
170
+ if (!["success", "failed", "canceled", "timed_out"].includes(job.state))
171
+ transitionBuild(job, "failed", error instanceof Error ? error.message : "iOS Simulator build failed");
172
+ job.logs.push({
173
+ level: "error",
174
+ message: error instanceof Error ? error.message : "iOS Simulator build failed",
175
+ at: new Date().toISOString(),
176
+ });
177
+ throw error;
178
+ }
179
+ }
10
180
  export function hasIosHost(root, profile) {
11
181
  if (process.platform !== "darwin")
12
182
  return false;
@@ -66,6 +236,8 @@ async function findIpa(directory) {
66
236
  return join(directory, ipa.name);
67
237
  }
68
238
  export async function runRealIosBuild(job, options) {
239
+ if (options.simulator)
240
+ return runRealIosSimulatorBuild(job, options);
69
241
  assert(process.platform === "darwin", "IOS_MACOS_REQUIRED", "iOS builds require macOS with Xcode");
70
242
  assert(commandExists("xcodebuild"), "IOS_XCODE_REQUIRED", "xcodebuild was not found. Install Xcode and select its command-line tools.");
71
243
  assert(commandExists("xcrun"), "IOS_XCRUN_REQUIRED", "xcrun was not found. Install Xcode command-line tools.");
@@ -14,6 +14,7 @@ export interface IosToolchainReport {
14
14
  scheme?: string;
15
15
  automaticSigning: boolean;
16
16
  }
17
- export declare function inspectIosToolchain(root: string, profile: BuildProfile): Promise<IosToolchainReport>;
17
+ export type IosBuildTarget = "device" | "simulator";
18
+ export declare function inspectIosToolchain(root: string, profile: BuildProfile, target?: IosBuildTarget): Promise<IosToolchainReport>;
18
19
  export declare function formatIosToolchainFailure(report: IosToolchainReport): string;
19
20
  //# sourceMappingURL=ios-toolchain.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ios-toolchain.d.ts","sourceRoot":"","sources":["../src/ios-toolchain.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAOhD,MAAM,MAAM,kBAAkB,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;AAE1D,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,kBAAkB,CAAC;IAC3B,EAAE,EAAE,OAAO,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,EAAE,iBAAiB,EAAE,CAAC;IAC5B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,gBAAgB,EAAE,OAAO,CAAC;CAC3B;AAoKD,wBAAsB,mBAAmB,CACvC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,YAAY,GACpB,OAAO,CAAC,kBAAkB,CAAC,CA4T7B;AAED,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,kBAAkB,GAAG,MAAM,CAQ5E"}
1
+ {"version":3,"file":"ios-toolchain.d.ts","sourceRoot":"","sources":["../src/ios-toolchain.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAOhD,MAAM,MAAM,kBAAkB,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;AAE1D,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,kBAAkB,CAAC;IAC3B,EAAE,EAAE,OAAO,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,EAAE,iBAAiB,EAAE,CAAC;IAC5B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,gBAAgB,EAAE,OAAO,CAAC;CAC3B;AAED,MAAM,MAAM,cAAc,GAAG,QAAQ,GAAG,WAAW,CAAC;AAuKpD,wBAAsB,mBAAmB,CACvC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,YAAY,EACrB,MAAM,GAAE,cAAyB,GAChC,OAAO,CAAC,kBAAkB,CAAC,CA2W7B;AAED,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,kBAAkB,GAAG,MAAM,CAQ5E"}
@@ -86,10 +86,11 @@ async function hasValidProvisioningProfile(root, bundleIdentifier) {
86
86
  }
87
87
  return false;
88
88
  }
89
- async function probeProject(root, context, profile) {
89
+ async function probeProject(root, context, profile, target) {
90
90
  if (!context.path || !context.scheme || !commandExists("xcodebuild"))
91
91
  return context;
92
- const configuration = profile.ios?.configuration ?? "Release";
92
+ const configuration = profile.ios?.configuration ??
93
+ (target === "simulator" ? "Debug" : "Release");
93
94
  const settings = await captureProcess("xcodebuild", [
94
95
  context.flag,
95
96
  context.path,
@@ -124,16 +125,19 @@ async function probeCommand(command, args, cwd) {
124
125
  return false;
125
126
  }
126
127
  }
127
- export async function inspectIosToolchain(root, profile) {
128
+ export async function inspectIosToolchain(root, profile, target = "device") {
128
129
  const checks = [];
129
130
  const isMac = platform() === "darwin";
130
131
  const host = findHost(root, profile);
131
132
  const hostExists = Boolean(host && existsSync(host));
132
133
  const scheme = profile.ios?.scheme;
134
+ const simulator = target === "simulator";
133
135
  const exportOptions = profile.ios?.exportOptionsPlist
134
136
  ? resolve(root, profile.ios.exportOptionsPlist)
135
137
  : undefined;
136
- checks.push(check("ios-platform", isMac ? "pass" : "fail", isMac ? "macOS" : `${platform()} · macOS is required for an IPA`, isMac
138
+ checks.push(check("ios-platform", isMac ? "pass" : "fail", isMac
139
+ ? "macOS"
140
+ : `${platform()} · macOS is required for an iOS ${simulator ? "Simulator app" : "IPA"}`, isMac
137
141
  ? undefined
138
142
  : "Run the iOS build on a macOS machine or macOS CI runner"));
139
143
  if (!isMac) {
@@ -168,12 +172,12 @@ export async function inspectIosToolchain(root, profile) {
168
172
  checks.push(check("ios-host", hostExists ? "pass" : "fail", hostExists ? host : "Xcode project or workspace not found", hostExists
169
173
  ? undefined
170
174
  : "lynxship ios host init --bundle-identifier com.example.myapp"));
171
- checks.push(check("ios-scheme", scheme ? "pass" : "fail", scheme ?? "missing from the production build profile", scheme ? undefined : "Set build.production.ios.scheme in lynxship.json"));
175
+ checks.push(check("ios-scheme", scheme ? "pass" : "fail", scheme ?? "missing from the selected build profile", scheme ? undefined : "Set ios.scheme in the selected build profile"));
172
176
  const context = await probeProject(root, {
173
177
  path: hostExists ? host : undefined,
174
178
  flag: host ? projectFlag(host) : "-project",
175
179
  scheme,
176
- }, profile);
180
+ }, profile, target);
177
181
  const settingsOutput = context.settings
178
182
  ? commandOutput(context.settings)
179
183
  : "";
@@ -190,29 +194,40 @@ export async function inspectIosToolchain(root, profile) {
190
194
  : "Open the project in Xcode and verify the scheme is shared and buildable"));
191
195
  const automaticSigning = setting(settingsOutput, "CODE_SIGN_STYLE") === "Automatic";
192
196
  const team = setting(settingsOutput, "DEVELOPMENT_TEAM");
193
- checks.push(check("apple-team", context.settings?.code !== 0 || !context.settings
194
- ? "warn"
195
- : team
196
- ? "pass"
197
- : "fail", team ?? "team not resolved from Xcode build settings", team
197
+ checks.push(check("apple-team", simulator
198
+ ? "pass"
199
+ : context.settings?.code !== 0 || !context.settings
200
+ ? "warn"
201
+ : team
202
+ ? "pass"
203
+ : "fail", simulator
204
+ ? "not required for iOS Simulator"
205
+ : (team ?? "team not resolved from Xcode build settings"), simulator || team
198
206
  ? undefined
199
207
  : "Select the Apple Developer Team in Xcode Signing & Capabilities"));
200
208
  const distribution = profile.distribution ?? profile.ios?.distribution ?? "store";
201
- const identityResult = await (commandExists("security")
202
- ? captureProcess("security", ["find-identity", "-v", "-p", "codesigning"], {
203
- cwd: root,
204
- })
205
- : undefined);
209
+ const identityResult = simulator
210
+ ? undefined
211
+ : await (commandExists("security")
212
+ ? captureProcess("security", ["find-identity", "-v", "-p", "codesigning"], {
213
+ cwd: root,
214
+ })
215
+ : undefined);
206
216
  const identityOutput = identityResult ? commandOutput(identityResult) : "";
207
217
  const identityCount = Number(identityOutput.match(/(\d+) valid identities found/)?.[1] ?? 0);
208
218
  const requiredIdentity = distribution === "store" ? "Apple Distribution" : "Apple Development";
209
- checks.push(check("apple-signing-identity", identityCount === 0 || !identityOutput.includes(requiredIdentity)
210
- ? "fail"
211
- : "pass", identityCount === 0
212
- ? "no valid code-signing identity found"
213
- : identityOutput.includes(requiredIdentity)
214
- ? `${requiredIdentity} identity available`
215
- : `${requiredIdentity} identity not found`, identityCount > 0 && identityOutput.includes(requiredIdentity)
219
+ checks.push(check("apple-signing-identity", simulator
220
+ ? "pass"
221
+ : identityCount === 0 || !identityOutput.includes(requiredIdentity)
222
+ ? "fail"
223
+ : "pass", simulator
224
+ ? "not required for iOS Simulator"
225
+ : identityCount === 0
226
+ ? "no valid code-signing identity found"
227
+ : identityOutput.includes(requiredIdentity)
228
+ ? `${requiredIdentity} identity available`
229
+ : `${requiredIdentity} identity not found`, simulator ||
230
+ (identityCount > 0 && identityOutput.includes(requiredIdentity))
216
231
  ? undefined
217
232
  : "Install or select the Apple certificate in Xcode Settings > Accounts, then rerun doctor"));
218
233
  const podfile = join(root, "ios", "Podfile");
@@ -241,40 +256,49 @@ export async function inspectIosToolchain(root, profile) {
241
256
  "development",
242
257
  "enterprise",
243
258
  ]);
244
- checks.push(check("export-options", !exportOptions
245
- ? "fail"
246
- : !existsSync(exportOptions)
259
+ checks.push(check("export-options", simulator
260
+ ? "pass"
261
+ : !exportOptions
247
262
  ? "fail"
248
- : !exportMethod || !supportedExportMethods.has(exportMethod)
263
+ : !existsSync(exportOptions)
249
264
  ? "fail"
250
- : "pass", !exportOptions
251
- ? "missing from the production build profile"
252
- : !existsSync(exportOptions)
253
- ? `${exportOptions} not found`
254
- : exportMethod && supportedExportMethods.has(exportMethod)
255
- ? `method: ${exportMethod}`
256
- : "method missing or invalid for an iOS export", exportOptions &&
257
- existsSync(exportOptions) &&
258
- exportMethod &&
259
- supportedExportMethods.has(exportMethod)
265
+ : !exportMethod || !supportedExportMethods.has(exportMethod)
266
+ ? "fail"
267
+ : "pass", simulator
268
+ ? "not required for iOS Simulator"
269
+ : !exportOptions
270
+ ? "missing from the production build profile"
271
+ : !existsSync(exportOptions)
272
+ ? `${exportOptions} not found`
273
+ : exportMethod && supportedExportMethods.has(exportMethod)
274
+ ? `method: ${exportMethod}`
275
+ : "method missing or invalid for an iOS export", simulator ||
276
+ (exportOptions &&
277
+ existsSync(exportOptions) &&
278
+ exportMethod &&
279
+ supportedExportMethods.has(exportMethod))
260
280
  ? undefined
261
281
  : "Set build.production.ios.exportOptionsPlist to a valid ExportOptions.plist"));
262
282
  const manualProvisioning = setting(settingsOutput, "CODE_SIGN_STYLE") === "Manual" ||
263
283
  exportOptionsContent?.includes("<key>provisioningProfiles</key>") === true;
264
284
  const profilesAvailable = await hasValidProvisioningProfile(root, setting(settingsOutput, "PRODUCT_BUNDLE_IDENTIFIER"));
265
- checks.push(check("ios-provisioning", manualProvisioning && !profilesAvailable
266
- ? "fail"
267
- : automaticSigning && !profilesAvailable
268
- ? "warn"
269
- : "pass", manualProvisioning && !profilesAvailable
270
- ? "manual signing selected but no local provisioning profile was found"
271
- : automaticSigning && !profilesAvailable
272
- ? "Xcode-managed provisioning will be requested during the build"
273
- : profilesAvailable
274
- ? "local provisioning profile available"
275
- : "not required by the detected signing mode", manualProvisioning && !profilesAvailable
276
- ? "Download a matching .mobileprovision/.provisionprofile from Apple Developer and install it in Xcode"
277
- : undefined));
285
+ checks.push(check("ios-provisioning", simulator
286
+ ? "pass"
287
+ : manualProvisioning && !profilesAvailable
288
+ ? "fail"
289
+ : automaticSigning && !profilesAvailable
290
+ ? "warn"
291
+ : "pass", simulator
292
+ ? "not required for iOS Simulator"
293
+ : manualProvisioning && !profilesAvailable
294
+ ? "manual signing selected but no local provisioning profile was found"
295
+ : automaticSigning && !profilesAvailable
296
+ ? "Xcode-managed provisioning will be requested during the build"
297
+ : profilesAvailable
298
+ ? "local provisioning profile available"
299
+ : "not required by the detected signing mode", simulator || !(manualProvisioning && !profilesAvailable)
300
+ ? undefined
301
+ : "Download a matching .mobileprovision/.provisionprofile from Apple Developer and install it in Xcode"));
278
302
  const simctlAvailable = await probeCommand("xcrun", ["--find", "simctl"], root);
279
303
  const devicectlAvailable = await probeCommand("xcrun", ["--find", "devicectl"], root);
280
304
  checks.push(check("ios-device-tools", simctlAvailable || devicectlAvailable ? "pass" : "warn", simctlAvailable && devicectlAvailable
@@ -286,6 +310,18 @@ export async function inspectIosToolchain(root, profile) {
286
310
  : "simctl and devicectl unavailable", simctlAvailable || devicectlAvailable
287
311
  ? undefined
288
312
  : "Install the full Xcode toolchain, then rerun lynxship doctor --platform ios"));
313
+ if (simulator) {
314
+ const runtimeResult = await (commandExists("xcrun")
315
+ ? captureProcess("xcrun", ["simctl", "list", "runtimes"], { cwd: root })
316
+ : undefined);
317
+ const runtimeOutput = runtimeResult ? commandOutput(runtimeResult) : "";
318
+ const hasRuntime = /iOS\s+[0-9]/.test(runtimeOutput);
319
+ checks.push(check("ios-simulator-runtime", hasRuntime ? "pass" : "fail", hasRuntime
320
+ ? "iOS Simulator runtime available"
321
+ : "no iOS Simulator runtime found", hasRuntime
322
+ ? undefined
323
+ : "Open Xcode > Settings > Components and install an iOS Simulator runtime"));
324
+ }
289
325
  const failures = checks.filter((item) => item.status === "fail");
290
326
  return {
291
327
  ok: failures.length === 0,
@@ -33,6 +33,7 @@ export declare const pill: {
33
33
  error: (text: string) => string;
34
34
  };
35
35
  export declare function finalLine(message: string, success?: boolean): void;
36
- export declare function nextSteps(commands: string[], note?: string): void;
36
+ export declare function nextSteps(commands: string[], note?: string, environment?: string): void;
37
37
  export declare function downloadArtifact(url: string, expiresAt?: string, enabled?: boolean): void;
38
+ export declare function devServerQr(url: string, enabled?: boolean): void;
38
39
  //# sourceMappingURL=components.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"components.d.ts","sourceRoot":"","sources":["../../src/ui/components.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAI7C,MAAM,WAAW,MAAM;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,SAAS,CAAC;CACxB;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAEjD;AAED,eAAO,MAAM,GAAG;qBACG,MAAM,GAAG,IAAI;kBAGhB,MAAM,GAAG,IAAI;mBAGZ,MAAM,GAAG,IAAI;kBAGd,MAAM,GAAG,IAAI;mBAGZ,MAAM,GAAG,IAAI;CAG7B,CAAC;AAgBF,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,IAAI,CA6C9D;AAED,MAAM,WAAW,cAAc;IAC7B,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,MAAM,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7C,IAAI,IAAI,IAAI,CAAC;CACd;AA6CD,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAoBnD;AA6BD,wBAAgB,cAAc,CAC5B,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,OAAO,GACf,cAAc,CAuEhB;AAED,MAAM,WAAW,aAAa;IAC5B,OAAO,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,IAAI,IAAI,IAAI,CAAC;CACd;AAED,wBAAgB,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,aAAa,CA2BlE;AAYD,eAAO,MAAM,IAAI;oBACC,MAAM;iBACT,MAAM;iBACN,MAAM;kBACL,MAAM;CACrB,CAAC;AAEF,wBAAgB,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,UAAO,GAAG,IAAI,CAE/D;AAED,wBAAgB,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAOjE;AAED,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE,MAAM,EACX,SAAS,CAAC,EAAE,MAAM,EAClB,OAAO,UAAO,GACb,IAAI,CAQN"}
1
+ {"version":3,"file":"components.d.ts","sourceRoot":"","sources":["../../src/ui/components.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAI7C,MAAM,WAAW,MAAM;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,SAAS,CAAC;CACxB;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAEjD;AAED,eAAO,MAAM,GAAG;qBACG,MAAM,GAAG,IAAI;kBAGhB,MAAM,GAAG,IAAI;mBAGZ,MAAM,GAAG,IAAI;kBAGd,MAAM,GAAG,IAAI;mBAGZ,MAAM,GAAG,IAAI;CAG7B,CAAC;AAgBF,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,IAAI,CA6C9D;AAED,MAAM,WAAW,cAAc;IAC7B,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,MAAM,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7C,IAAI,IAAI,IAAI,CAAC;CACd;AA6CD,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAoBnD;AA6BD,wBAAgB,cAAc,CAC5B,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,OAAO,GACf,cAAc,CAuEhB;AAED,MAAM,WAAW,aAAa;IAC5B,OAAO,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,IAAI,IAAI,IAAI,CAAC;CACd;AAED,wBAAgB,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,aAAa,CA2BlE;AAYD,eAAO,MAAM,IAAI;oBACC,MAAM;iBACT,MAAM;iBACN,MAAM;kBACL,MAAM;CACrB,CAAC;AAEF,wBAAgB,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,UAAO,GAAG,IAAI,CAE/D;AAED,wBAAgB,SAAS,CACvB,QAAQ,EAAE,MAAM,EAAE,EAClB,IAAI,CAAC,EAAE,MAAM,EACb,WAAW,CAAC,EAAE,MAAM,GACnB,IAAI,CAQN;AAED,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE,MAAM,EACX,SAAS,CAAC,EAAE,MAAM,EAClB,OAAO,UAAO,GACb,IAAI,CAQN;AAED,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,UAAO,GAAG,IAAI,CAK7D"}
@@ -265,10 +265,12 @@ export const pill = {
265
265
  export function finalLine(message, success = true) {
266
266
  console.log(`\n${success ? c.brand("◆") : c.red("◆")} ${c.text(message)}`);
267
267
  }
268
- export function nextSteps(commands, note) {
268
+ export function nextSteps(commands, note, environment) {
269
269
  if (commands.length === 0)
270
270
  return;
271
271
  console.error(`\n${c.yellow("Next steps")}`);
272
+ if (environment)
273
+ console.error(` ${c.muted(`Run on ${environment}:`)}`);
272
274
  commands.forEach((command, index) => {
273
275
  console.error(` ${c.muted(`${index + 1}.`)} ${c.teal(command)}`);
274
276
  });
@@ -286,3 +288,10 @@ export function downloadArtifact(url, expiresAt, enabled = true) {
286
288
  if (expiresAt)
287
289
  console.log(` ${c.muted(`Link expires at ${expiresAt}`)}`);
288
290
  }
291
+ export function devServerQr(url, enabled = true) {
292
+ if (!enabled)
293
+ return;
294
+ console.log(`\n${c.brandBold("Lynx Explorer")}`);
295
+ qrcode.generate(url, { small: true }, (code) => console.log(code));
296
+ console.log(` ${c.teal("URL")} ${url}\n`);
297
+ }
@@ -19,6 +19,7 @@ export declare class CliUi {
19
19
  spinner(text: string): SpinnerHandle;
20
20
  done(message: string, success?: boolean): void;
21
21
  downloadArtifact(url: string, expiresAt?: string): void;
22
+ devServerQr(url: string): void;
22
23
  }
23
24
  export declare function createCliUi(args: string[]): CliUi;
24
25
  export { terminalOptions };
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/ui/index.ts"],"names":[],"mappings":"AAAA,OAAO,EASL,KAAK,MAAM,EACX,KAAK,cAAc,EACnB,KAAK,aAAa,EACnB,MAAM,iBAAiB,CAAC;AAGzB,OAAO,EAEL,eAAe,EACf,KAAK,eAAe,EACrB,MAAM,eAAe,CAAC;AACvB,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAElD,qBAAa,KAAK;IAChB,QAAQ,CAAC,OAAO,EAAE,eAAe,CAAC;IAElC,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;gBAElB,IAAI,EAAE,MAAM,EAAE;IAU1B,MAAM,IAAI,IAAI;IAId,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAI3B,OAAO,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAI9B,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAI3B,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAI3B,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAK5B,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAI5B,SAAS,CAAC,QAAQ,EAAE,WAAW,GAAG,IAAI;IAKtC,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,IAAI;IAI5C,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,IAAI;IAKzC,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,cAAc;IAIvC,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,aAAa;IAIpC,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,UAAO,GAAG,IAAI;IAI3C,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI;CAGxD;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,KAAK,CAEjD;AAED,OAAO,EAAE,eAAe,EAAE,CAAC;AAE3B,YAAY,EAAE,MAAM,EAAE,cAAc,EAAE,eAAe,EAAE,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/ui/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAUL,KAAK,MAAM,EACX,KAAK,cAAc,EACnB,KAAK,aAAa,EACnB,MAAM,iBAAiB,CAAC;AAGzB,OAAO,EAEL,eAAe,EACf,KAAK,eAAe,EACrB,MAAM,eAAe,CAAC;AACvB,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAElD,qBAAa,KAAK;IAChB,QAAQ,CAAC,OAAO,EAAE,eAAe,CAAC;IAElC,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;gBAElB,IAAI,EAAE,MAAM,EAAE;IAU1B,MAAM,IAAI,IAAI;IAId,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAI3B,OAAO,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAI9B,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAI3B,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAI3B,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAK5B,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAI5B,SAAS,CAAC,QAAQ,EAAE,WAAW,GAAG,IAAI;IAKtC,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,IAAI;IAI5C,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,IAAI;IAKzC,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,cAAc;IAIvC,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,aAAa;IAIpC,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,UAAO,GAAG,IAAI;IAI3C,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI;IAIvD,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;CAG/B;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,KAAK,CAEjD;AAED,OAAO,EAAE,eAAe,EAAE,CAAC;AAE3B,YAAY,EAAE,MAAM,EAAE,cAAc,EAAE,eAAe,EAAE,CAAC"}
package/dist/ui/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { finalLine, log, sectionHeader, summaryBox, nextSteps, createProgress, spin, downloadArtifact, } from "./components.js";
1
+ import { finalLine, log, sectionHeader, summaryBox, nextSteps, createProgress, spin, downloadArtifact, devServerQr, } from "./components.js";
2
2
  import { printBanner } from "./logo.js";
3
3
  import { setColors } from "./state.js";
4
4
  import { isInteractive, terminalOptions, } from "./terminal.js";
@@ -43,7 +43,7 @@ export class CliUi {
43
43
  nextSteps(guidance) {
44
44
  if (this.options.json || this.options.quiet)
45
45
  return;
46
- nextSteps(guidance.commands, guidance.note);
46
+ nextSteps(guidance.commands, guidance.note, guidance.environment);
47
47
  }
48
48
  summary(title, rows) {
49
49
  if (this.interactive)
@@ -66,6 +66,9 @@ export class CliUi {
66
66
  downloadArtifact(url, expiresAt) {
67
67
  downloadArtifact(url, expiresAt, this.interactive);
68
68
  }
69
+ devServerQr(url) {
70
+ devServerQr(url, this.interactive && !this.options.quiet);
71
+ }
69
72
  }
70
73
  export function createCliUi(args) {
71
74
  return new CliUi(args);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lynxship/cli",
3
- "version": "0.1.10",
3
+ "version": "0.1.12",
4
4
  "description": "Build, sign, store, submit and update LynxJS applications from the terminal.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -69,8 +69,8 @@
69
69
  "ora": "8.2.0",
70
70
  "qrcode-terminal": "0.12.0",
71
71
  "@lynxship/build-orchestrator": "0.1.0",
72
- "@lynxship/submit": "0.1.0",
73
72
  "@lynxship/db": "0.1.0",
73
+ "@lynxship/submit": "0.1.0",
74
74
  "@lynxship/contracts": "0.1.0",
75
75
  "@lynxship/signing": "0.1.0"
76
76
  },