@lynxship/cli 0.1.13 → 0.1.15

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
@@ -212,6 +212,11 @@ Next steps
212
212
  ```
213
213
 
214
214
  `lynxship dev` is for Lynx Explorer and live QR/HMR development. The Android
215
+ Rspeedy URLs are rendered by LynxShip's own compact QR adapter so the QR
216
+ remains visible when child-process output is captured. It uses the WISA
217
+ renderer settings (H-level correction, dots, dotted corners and 50-degree
218
+ linear gradient) with LynxShip pink-to-cyan colors and no center logo;
219
+ non-interactive output keeps the URL and omits terminal decoration. The Android
215
220
  host command creates the native Gradle project needed for a real APK/AAB. An
216
221
  interactive real build creates a missing host after asking for the application
217
222
  ID; CI must pass `--application-id`. Existing native directories are never
@@ -277,6 +282,10 @@ Use the official Lynx Harmony integration under `harmony/`. The project must
277
282
  provide `hvigorw`, `hvigorfile.ts`, `build-profile.json5` and
278
283
  `oh-package.json5`. LynxShip runs `ohpm install`, the pinned wrapper in release
279
284
  mode, then verifies the signed HAP with the official `hap-sign-tool.jar`.
285
+ Before Hvigor runs, it copies root `.lynx.bundle` files plus `dist/static` and
286
+ `dist/async` into the configured HarmonyOS `rawfile` directory. This keeps
287
+ imported images and lazy bundles inside the HAP instead of leaving them only in
288
+ the JavaScript build output.
280
289
 
281
290
  Optional profile overrides are explicit and remain project-owned:
282
291
 
@@ -406,6 +415,19 @@ with:
406
415
  lynxship ios host init --bundle-identifier com.example.myapp
407
416
  ```
408
417
 
418
+ If the project already has its app icon, provide the required high-resolution
419
+ PNG during host creation:
420
+
421
+ ```bash
422
+ lynxship ios host init --bundle-identifier com.example.myapp --icon ./assets/icon.png
423
+ ```
424
+
425
+ The icon must be a square 1024x1024 PNG. For an existing host, set
426
+ `build.production.ios.appIcon` in `lynxship.json`, or add `icon.png` in the
427
+ project root; LynxShip discovers it before the next native build. Simulator
428
+ builds may fall back to a square `lynx-logo*.png` emitted by Rspeedy for local
429
+ preview only; provide the project icon for an IPA or store release.
430
+
409
431
  The command refuses to overwrite an existing `ios/` directory and creates a
410
432
  Swift host based on Lynx's official integration shape: `LynxEnv`, `LynxView`,
411
433
  `LynxTemplateProvider`, `Podfile`, `ExportOptions.plist` and a bundle sync
@@ -421,9 +443,18 @@ lynxship build --project-dir ./my-lynx-app --platform ios --simulator --profile
421
443
  ```
422
444
 
423
445
  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
446
+ and installs a local `.app`, opens the Simulator and launches the app in an
447
+ interactive terminal, and does not require a physical iPhone, Apple
425
448
  Distribution certificate, provisioning profile or R2 upload. Use the
426
- production profile only for a signed device IPA.
449
+ production profile only for a signed device IPA. Pass `--no-autostart` to
450
+ install without launching, or use `run --platform ios --simulator --launch
451
+ --artifact <app>` to launch an already-built app.
452
+
453
+ Rspeedy can emit external files beside the main bundle. The iOS adapter copies
454
+ all root `.lynx.bundle` files plus `dist/static` and `dist/async` into the
455
+ compiled `.app` before installation/export, so imported images and lazy assets
456
+ are included in Simulator and device builds. The same synchronization is
457
+ applied to older LynxShip-generated iOS hosts.
427
458
 
428
459
  Run the complete first-use diagnosis before building:
429
460
 
package/dist/config.d.ts CHANGED
@@ -15,6 +15,7 @@ export interface BuildProfile {
15
15
  scheme?: string;
16
16
  exportOptionsPlist?: string;
17
17
  bundleScript?: string;
18
+ appIcon?: string;
18
19
  };
19
20
  harmony?: {
20
21
  task?: 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,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"}
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;QACtB,OAAO,CAAC,EAAE,MAAM,CAAC;KAClB,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"}
@@ -1 +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"}
1
+ {"version":3,"file":"dev-qr.d.ts","sourceRoot":"","sources":["../src/dev-qr.ts"],"names":[],"mappings":"AAOA,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAOpE;AAED,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAE5D"}
package/dist/dev-qr.js CHANGED
@@ -1,12 +1,14 @@
1
- const URL_PATTERN = /https?:\/\/[^\s<>"'`]+/g;
1
+ // Rspeedy's QR plugin accepts custom schemas, so a dev URL is not always HTTP.
2
+ const URL_PATTERN = /[a-z][a-z\d+.-]*:\/\/[^\s<>"'`]+/gi;
2
3
  function cleanUrl(value) {
3
4
  return value.replace(/[),.;!?]+$/u, "");
4
5
  }
5
6
  export function extractDevServerUrl(line) {
6
7
  const urls = (line.match(URL_PATTERN) ?? []).map(cleanUrl);
7
8
  return (urls.find((url) => url.includes("fullscreen=true")) ??
8
- urls.find((url) => url.includes(".lynx.bundle")));
9
+ urls.find((url) => url.includes(".lynx.bundle")) ??
10
+ urls[0]);
9
11
  }
10
12
  export function shouldPrintDevServerQr(line) {
11
- return /(?:default:|fullscreen=true|\bready\b)/iu.test(line);
13
+ return /(?:default:|fullscreen|\bready\b|\blynx\b)/iu.test(line);
12
14
  }
@@ -17,6 +17,7 @@ export declare function harmonyToolchain(root: string): {
17
17
  hdc: boolean;
18
18
  message: string;
19
19
  };
20
+ export declare function syncHarmonyAssets(root: string, profile: BuildProfile): Promise<string[]>;
20
21
  export declare function runRealHarmonyBuild(job: BuildJob, options: HarmonyBuildOptions): Promise<BuildJob>;
21
22
  export {};
22
23
  //# sourceMappingURL=harmony-build.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"harmony-build.d.ts","sourceRoot":"","sources":["../src/harmony-build.ts"],"names":[],"mappings":"AAIA,OAAO,EAAU,KAAK,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAE5D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAKhD,UAAU,mBAAmB;IAC3B,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,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAOpD;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG;IAC9C,EAAE,EAAE,OAAO,CAAC;IACZ,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,OAAO,CAAC;IACd,GAAG,EAAE,OAAO,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB,CAcA;AAmID,wBAAsB,mBAAmB,CACvC,GAAG,EAAE,QAAQ,EACb,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,QAAQ,CAAC,CAkGnB"}
1
+ {"version":3,"file":"harmony-build.d.ts","sourceRoot":"","sources":["../src/harmony-build.ts"],"names":[],"mappings":"AAIA,OAAO,EAAU,KAAK,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAE5D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAKhD,UAAU,mBAAmB;IAC3B,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,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAOpD;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG;IAC9C,EAAE,EAAE,OAAO,CAAC;IACZ,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,OAAO,CAAC;IACd,GAAG,EAAE,OAAO,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB,CAcA;AAED,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,YAAY,GACpB,OAAO,CAAC,MAAM,EAAE,CAAC,CA4BnB;AA2GD,wBAAsB,mBAAmB,CACvC,GAAG,EAAE,QAAQ,EACb,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,QAAQ,CAAC,CAoGnB"}
@@ -1,4 +1,4 @@
1
- import { copyFile, mkdir, mkdtemp, readdir, rm } from "node:fs/promises";
1
+ import { copyFile, cp, mkdir, mkdtemp, readdir, rm } from "node:fs/promises";
2
2
  import { existsSync } from "node:fs";
3
3
  import { homedir, tmpdir } from "node:os";
4
4
  import { join, resolve } from "node:path";
@@ -37,7 +37,7 @@ export function harmonyToolchain(root) {
37
37
  : "Harmony host requires the project hvigorw wrapper and ohpm from DevEco Studio",
38
38
  };
39
39
  }
40
- async function syncBundle(root, profile) {
40
+ export async function syncHarmonyAssets(root, profile) {
41
41
  const dist = join(root, "dist");
42
42
  const entries = await readdir(dist, { withFileTypes: true }).catch(() => []);
43
43
  const bundles = entries
@@ -46,9 +46,20 @@ async function syncBundle(root, profile) {
46
46
  assert(bundles.length > 0, "LYNX_BUNDLE_MISSING", `No .lynx.bundle was found in ${dist}. Check the Rspeedy output configuration.`);
47
47
  const target = resolve(root, profile.harmony?.bundleDir ?? "harmony/entry/src/main/resources/rawfile");
48
48
  await mkdir(target, { recursive: true });
49
- for (const bundle of bundles)
50
- await copyFile(join(dist, bundle), join(target, bundle));
51
- return bundles;
49
+ const outputNames = [...bundles, "async", "static"];
50
+ const copied = [];
51
+ for (const name of outputNames) {
52
+ const source = join(dist, name);
53
+ if (!existsSync(source))
54
+ continue;
55
+ await rm(join(target, name), { recursive: true, force: true });
56
+ if (name.endsWith(".lynx.bundle"))
57
+ await copyFile(source, join(target, name));
58
+ else
59
+ await cp(source, join(target, name), { recursive: true, force: true });
60
+ copied.push(name);
61
+ }
62
+ return copied;
52
63
  }
53
64
  async function findHap(root, configured) {
54
65
  if (configured) {
@@ -133,7 +144,9 @@ export async function runRealHarmonyBuild(job, options) {
133
144
  });
134
145
  }
135
146
  step("Syncing bundle into the HarmonyOS HAP resources…", 20);
136
- await syncBundle(options.root, options.profile);
147
+ const copiedAssets = await syncHarmonyAssets(options.root, options.profile);
148
+ for (const name of copiedAssets)
149
+ options.onEvent?.(`Synced dist/${name} into HarmonyOS rawfile resources`);
137
150
  transitionBuild(job, "queued", "HarmonyOS build queued locally");
138
151
  transitionBuild(job, "provisioning", "HarmonyOS Hvigor toolchain selected");
139
152
  transitionBuild(job, "installing_dependencies", "HarmonyOS dependencies selected");
package/dist/index.js CHANGED
@@ -12,7 +12,7 @@ import { DEFAULT_CONFIG, loadConfig, platformValue, resolveProfile, } from "./co
12
12
  import { hasAndroidHost, isSupportedAndroidPlatform, runRealAndroidBuild, } from "./android-build.js";
13
13
  import { initializeAndroidHost, suggestedAndroidApplicationId, } from "./android-host.js";
14
14
  import { initializeIosHost, suggestedIosBundleIdentifier } from "./ios-host.js";
15
- import { hasIosHost, runRealIosBuild } from "./ios-build.js";
15
+ import { hasIosHost, launchIosSimulatorApp, runRealIosBuild, } from "./ios-build.js";
16
16
  import { configureAndroid, configureAppStoreConnect, configureGooglePlay, configureR2, } from "./configure.js";
17
17
  import { fetchOtaPublicKey, publishOtaRelease, rollbackOtaRelease, submitRealArtifact, } from "./remote.js";
18
18
  import { uploadR2Artifact } from "./r2.js";
@@ -209,7 +209,7 @@ async function saveState(state, repository, builds, submissions) {
209
209
  }
210
210
  let stateSaveQueue = Promise.resolve();
211
211
  async function executeBuild(options, showResult = true) {
212
- const { config, profile, platform, skipUpload, wait, local, state, repository, builds, submissions, progress: sharedProgress, progressPrefix, onProgress, onEvent, skipBundleBuild, simulator = false, simulatorDevice, } = options;
212
+ const { config, profile, platform, skipUpload, wait, local, state, repository, builds, submissions, progress: sharedProgress, progressPrefix, onProgress, onEvent, skipBundleBuild, simulator = false, simulatorDevice, simulatorAutostart = false, } = options;
213
213
  if (wait && !local)
214
214
  await ensureNativeHostForBuild(platform, profile, config);
215
215
  // Host initialization can write lynxship.json (for example, the generated
@@ -281,6 +281,7 @@ async function executeBuild(options, showResult = true) {
281
281
  profile: resolvedBuildProfile,
282
282
  simulator,
283
283
  simulatorDevice,
284
+ simulatorAutostart,
284
285
  uploadArtifacts: !skipUpload,
285
286
  skipBundleBuild,
286
287
  quiet: json,
@@ -459,6 +460,8 @@ Build options:
459
460
  --no-upload Keep the artifact local and skip R2 (CI verification)
460
461
  --simulator Build and install an iOS Simulator .app locally
461
462
  --device <id> Select the iOS Simulator device for a simulator build
463
+ --autostart Open and launch the iOS Simulator app after install
464
+ --no-autostart Install the Simulator app without launching it
462
465
  --allow-unsigned Allow an unsigned Desktop artifact only with --no-upload (local tests)
463
466
  --local Use the contract-only build path for tests
464
467
 
@@ -490,6 +493,7 @@ Device and diagnostics options:
490
493
  run --artifact <path> Install a specific APK, IPA, app or signed HAP
491
494
  run --device <id> Select an Android, iOS or HarmonyOS device
492
495
  run --simulator Install an iOS .app with simctl
496
+ run --launch Launch the installed iOS Simulator app after install
493
497
  logs --device <id> Select the device or simulator for native logs
494
498
 
495
499
  Global options:
@@ -503,6 +507,7 @@ Global options:
503
507
  --project-id <id> Project ID used by init
504
508
  --application-id <id> Android package/application ID for host init
505
509
  --bundle-identifier <id> iOS bundle identifier for host init
510
+ --icon <path> 1024x1024 PNG app icon for iOS host init
506
511
  --library-dir <path> Native library directory for autolink codegen
507
512
  --simulator Build/install an iOS .app or install one with simctl
508
513
  --help Show this complete command reference
@@ -594,6 +599,7 @@ async function ensureNativeHostForBuild(platform, profile, config) {
594
599
  const result = await initializeIosHost(root, {
595
600
  bundleIdentifier,
596
601
  appName: basename(root),
602
+ appIcon: flag("--icon") ?? undefined,
597
603
  });
598
604
  ui.success(`iOS host created: ${result.directory}`);
599
605
  }
@@ -638,13 +644,13 @@ async function runRspeedyCommand(subcommand, environment) {
638
644
  let devUrl;
639
645
  let devQrPrinted = false;
640
646
  let devQrTimer;
641
- const printDevQr = () => {
647
+ const printDevQr = async () => {
642
648
  if (!devUrl || devQrPrinted || json || ui.options.quiet)
643
649
  return;
644
650
  devQrPrinted = true;
645
651
  if (devQrTimer)
646
652
  clearTimeout(devQrTimer);
647
- ui.devServerQr(devUrl);
653
+ await ui.devServerQr(devUrl);
648
654
  };
649
655
  await runRspeedy(root, subcommand, forwarded, {
650
656
  env: environment,
@@ -658,10 +664,12 @@ async function runRspeedyCommand(subcommand, environment) {
658
664
  devUrl = url;
659
665
  if (devQrTimer)
660
666
  clearTimeout(devQrTimer);
661
- devQrTimer = setTimeout(printDevQr, 500);
667
+ devQrTimer = setTimeout(() => {
668
+ void printDevQr();
669
+ }, 500);
662
670
  }
663
671
  if (devUrl && shouldPrintDevServerQr(line))
664
- printDevQr();
672
+ void printDevQr();
665
673
  },
666
674
  });
667
675
  printValue({ status: "success", command: `rspeedy ${subcommand}` }, {
@@ -753,6 +761,12 @@ async function runDevice() {
753
761
  quiet: json,
754
762
  onOutput: (line) => ui.info(`│ ${line}`),
755
763
  });
764
+ if (args.includes("--launch")) {
765
+ await launchIosSimulatorApp(root, device, artifactPath, {
766
+ quiet: json,
767
+ onEvent: (line) => ui.info(`│ ${line}`),
768
+ });
769
+ }
756
770
  }
757
771
  else {
758
772
  assert(device !== "booted", "IOS_DEVICE_REQUIRED", "A physical iOS install requires --device <device-identifier>; use --simulator for a booted simulator.");
@@ -1228,6 +1242,7 @@ async function main() {
1228
1242
  const result = await initializeIosHost(root, {
1229
1243
  bundleIdentifier,
1230
1244
  appName: basename(root),
1245
+ appIcon: flag("--icon") ?? undefined,
1231
1246
  });
1232
1247
  ui.success(`iOS host created: ${result.directory}`);
1233
1248
  printValue({
@@ -1671,6 +1686,9 @@ async function main() {
1671
1686
  const config = await loadConfig(root);
1672
1687
  const profile = flag("--profile", simulator ? "simulator" : "production");
1673
1688
  const simulatorDevice = flag("--device") ?? undefined;
1689
+ const simulatorAutostart = simulator &&
1690
+ !args.includes("--no-autostart") &&
1691
+ (args.includes("--autostart") || (!json && ui.interactive));
1674
1692
  const wait = !args.includes("--no-wait");
1675
1693
  const local = args.includes("--local");
1676
1694
  const platforms = buildAll
@@ -1692,6 +1710,7 @@ async function main() {
1692
1710
  local,
1693
1711
  simulator,
1694
1712
  simulatorDevice,
1713
+ simulatorAutostart,
1695
1714
  state,
1696
1715
  repository,
1697
1716
  builds,
@@ -1723,6 +1742,7 @@ async function main() {
1723
1742
  local,
1724
1743
  simulator,
1725
1744
  simulatorDevice,
1745
+ simulatorAutostart,
1726
1746
  state,
1727
1747
  repository,
1728
1748
  builds,
@@ -6,11 +6,32 @@ interface IosBuildOptions {
6
6
  uploadArtifacts?: boolean;
7
7
  simulator?: boolean;
8
8
  simulatorDevice?: string;
9
+ simulatorAutostart?: boolean;
9
10
  skipBundleBuild?: boolean;
10
11
  quiet?: boolean;
11
12
  onEvent?: (message: string) => void;
12
13
  onProgress?: (value?: number, label?: string) => void;
13
14
  }
15
+ /**
16
+ * Rspeedy leaves external resources beside the Lynx bundle. Xcode does not
17
+ * know about those generated files, so copy the complete output into the
18
+ * final app bundle after the native host has been compiled. This also makes
19
+ * older LynxShip-generated iOS hosts behave correctly without editing their
20
+ * Xcode project files.
21
+ */
22
+ export declare function syncIosRuntimeResources(root: string, appBundle: string): Promise<string[]>;
23
+ /**
24
+ * Apply a project-owned 1024x1024 PNG to the generated or existing Xcode
25
+ * AppIcon set. A missing icon is reported as a warning by the caller rather
26
+ * than silently inventing product branding.
27
+ */
28
+ export declare function prepareIosAppIcon(root: string, configured?: string, options?: {
29
+ allowFallback?: boolean;
30
+ }): Promise<string | undefined>;
31
+ export declare function launchIosSimulatorApp(root: string, device: string, appBundle: string, options?: {
32
+ quiet?: boolean;
33
+ onEvent?: (message: string) => void;
34
+ }): Promise<void>;
14
35
  export declare function hasIosHost(root: string, profile?: BuildProfile): boolean;
15
36
  export declare function runRealIosBuild(job: BuildJob, options: IosBuildOptions): Promise<BuildJob>;
16
37
  export {};
@@ -1 +1 @@
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;AA+DD,wBAAsB,eAAe,CACnC,GAAG,EAAE,QAAQ,EACb,OAAO,EAAE,eAAe,GACvB,OAAO,CAAC,QAAQ,CAAC,CAwMnB"}
1
+ {"version":3,"file":"ios-build.d.ts","sourceRoot":"","sources":["../src/ios-build.ts"],"names":[],"mappings":"AAYA,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,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,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;AAyHD;;;;;;GAMG;AACH,wBAAsB,uBAAuB,CAC3C,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,MAAM,EAAE,CAAC,CAoBnB;AAoED;;;;GAIG;AACH,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,MAAM,EACZ,UAAU,CAAC,EAAE,MAAM,EACnB,OAAO,GAAE;IAAE,aAAa,CAAC,EAAE,OAAO,CAAA;CAAO,GACxC,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAuD7B;AAgCD,wBAAsB,qBAAqB,CACzC,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,EACjB,OAAO,GAAE;IACP,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CAChC,GACL,OAAO,CAAC,IAAI,CAAC,CAcf;AAkND,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,YAAY,GAAG,OAAO,CAcxE;AA+DD,wBAAsB,eAAe,CACnC,GAAG,EAAE,QAAQ,EACb,OAAO,EAAE,eAAe,GACvB,OAAO,CAAC,QAAQ,CAAC,CAqNnB"}
package/dist/ios-build.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
- import { copyFile, mkdir, readdir, readFile } from "node:fs/promises";
2
+ import { copyFile, cp, mkdir, readdir, readFile, rm, writeFile, } from "node:fs/promises";
3
3
  import { existsSync, readdirSync } from "node:fs";
4
4
  import { join, resolve } from "node:path";
5
5
  import { assert, sha256 } from "@lynxship/contracts";
@@ -56,6 +56,159 @@ async function findSimulatorApp(root, derivedData, configuration, scheme) {
56
56
  assert(app, "IOS_SIMULATOR_ARTIFACT_MISSING", `Xcode did not produce a Simulator .app under ${products}`);
57
57
  return join(products, app.name);
58
58
  }
59
+ async function findArchiveApp(archivePath) {
60
+ const products = join(archivePath, "Products", "Applications");
61
+ const entries = await readdir(products, { withFileTypes: true }).catch(() => []);
62
+ const app = entries.find((entry) => entry.isDirectory() && entry.name.endsWith(".app"));
63
+ assert(app, "IOS_ARCHIVE_ARTIFACT_MISSING", `Xcode did not produce an app bundle under ${products}`);
64
+ return join(products, app.name);
65
+ }
66
+ async function copyIosOutput(source, target) {
67
+ if (!existsSync(source))
68
+ return false;
69
+ await rm(target, { recursive: true, force: true });
70
+ await cp(source, target, { recursive: true, force: true });
71
+ return true;
72
+ }
73
+ /**
74
+ * Rspeedy leaves external resources beside the Lynx bundle. Xcode does not
75
+ * know about those generated files, so copy the complete output into the
76
+ * final app bundle after the native host has been compiled. This also makes
77
+ * older LynxShip-generated iOS hosts behave correctly without editing their
78
+ * Xcode project files.
79
+ */
80
+ export async function syncIosRuntimeResources(root, appBundle) {
81
+ const distRoot = join(root, "dist");
82
+ const entries = await readdir(distRoot, { withFileTypes: true }).catch(() => []);
83
+ const bundles = entries
84
+ .filter((entry) => entry.isFile() && entry.name.endsWith(".lynx.bundle"))
85
+ .map((entry) => entry.name);
86
+ assert(bundles.length > 0, "LYNX_BUNDLE_MISSING", `No .lynx.bundle was found in ${distRoot}. Check the Rspeedy output configuration.`);
87
+ const copied = [];
88
+ for (const name of [...bundles, "async", "static"]) {
89
+ if (await copyIosOutput(join(distRoot, name), join(appBundle, name)))
90
+ copied.push(name);
91
+ }
92
+ return copied;
93
+ }
94
+ async function findAppIconSet(directory) {
95
+ const entries = await readdir(directory, { withFileTypes: true }).catch(() => []);
96
+ for (const entry of entries) {
97
+ const path = join(directory, entry.name);
98
+ if (entry.isDirectory() && entry.name === "AppIcon.appiconset") {
99
+ if (existsSync(join(path, "Contents.json")))
100
+ return path;
101
+ }
102
+ if (entry.isDirectory() &&
103
+ !entry.name.endsWith(".xcodeproj") &&
104
+ !entry.name.endsWith(".xcworkspace")) {
105
+ const result = await findAppIconSet(path);
106
+ if (result)
107
+ return result;
108
+ }
109
+ }
110
+ return undefined;
111
+ }
112
+ function pngDimensions(content) {
113
+ assert(content.length >= 24 &&
114
+ content.subarray(0, 8).toString("hex") === "89504e470d0a1a0a", "IOS_APP_ICON_INVALID", "The iOS app icon must be a valid PNG file.");
115
+ return { width: content.readUInt32BE(16), height: content.readUInt32BE(20) };
116
+ }
117
+ async function findConfiguredIosIcon(root, configured, allowFallback = false) {
118
+ const candidates = [
119
+ configured,
120
+ "icon.png",
121
+ "app-icon.png",
122
+ "assets/icon.png",
123
+ "assets/app-icon.png",
124
+ "src/assets/icon.png",
125
+ "src/assets/app-icon.png",
126
+ "public/icon.png",
127
+ ].filter((value) => Boolean(value));
128
+ for (const candidate of candidates) {
129
+ const path = resolve(root, candidate);
130
+ if (existsSync(path))
131
+ return { path, fallback: false };
132
+ }
133
+ if (allowFallback) {
134
+ const staticImages = join(root, "dist", "static", "image");
135
+ const entries = await readdir(staticImages, { withFileTypes: true }).catch(() => []);
136
+ const logo = entries.find((entry) => entry.isFile() &&
137
+ entry.name.endsWith(".png") &&
138
+ /(?:lynx|logo|icon)/i.test(entry.name));
139
+ if (logo)
140
+ return { path: join(staticImages, logo.name), fallback: true };
141
+ }
142
+ return undefined;
143
+ }
144
+ /**
145
+ * Apply a project-owned 1024x1024 PNG to the generated or existing Xcode
146
+ * AppIcon set. A missing icon is reported as a warning by the caller rather
147
+ * than silently inventing product branding.
148
+ */
149
+ export async function prepareIosAppIcon(root, configured, options = {}) {
150
+ const source = await findConfiguredIosIcon(root, configured, options.allowFallback);
151
+ const iconSet = await findAppIconSet(join(root, "ios"));
152
+ if (!source || !iconSet)
153
+ return undefined;
154
+ const dimensions = pngDimensions(await readFile(source.path));
155
+ const filename = "AppIcon.png";
156
+ const destination = join(iconSet, filename);
157
+ if (dimensions.width === 1024 && dimensions.height === 1024) {
158
+ await copyFile(source.path, destination);
159
+ }
160
+ else {
161
+ assert(source.fallback &&
162
+ dimensions.width === dimensions.height &&
163
+ dimensions.width >= 512, "IOS_APP_ICON_INVALID", `The iOS app icon must be exactly 1024x1024 PNG; received ${dimensions.width}x${dimensions.height} from ${source.path}.`);
164
+ assert(commandExists("sips"), "IOS_SIPS_REQUIRED", "The fallback Simulator icon needs Apple's sips tool. Install Xcode command-line tools, or provide a 1024x1024 icon.png.");
165
+ await runProcess("sips", ["-z", "1024", "1024", source.path, "--out", destination], {
166
+ cwd: root,
167
+ quiet: true,
168
+ });
169
+ }
170
+ await writeFile(join(iconSet, "Contents.json"), `${JSON.stringify({
171
+ images: [
172
+ {
173
+ filename,
174
+ idiom: "universal",
175
+ platform: "ios",
176
+ size: "1024x1024",
177
+ },
178
+ ],
179
+ info: { author: "xcode", version: 1 },
180
+ }, null, 2)}\n`, "utf8");
181
+ return source.path;
182
+ }
183
+ async function appBundleIdentifier(root, appBundle) {
184
+ assert(commandExists("plutil"), "IOS_PLUTIL_REQUIRED", "plutil was not found. Install Xcode command-line tools before launching the Simulator app.");
185
+ const result = await captureProcess("plutil", [
186
+ "-extract",
187
+ "CFBundleIdentifier",
188
+ "raw",
189
+ "-o",
190
+ "-",
191
+ join(appBundle, "Info.plist"),
192
+ ], { cwd: root });
193
+ const identifier = result.stdout.trim();
194
+ assert(result.code === 0 && identifier, "IOS_BUNDLE_IDENTIFIER_MISSING", `Could not read CFBundleIdentifier from ${join(appBundle, "Info.plist")}.`);
195
+ return identifier;
196
+ }
197
+ export async function launchIosSimulatorApp(root, device, appBundle, options = {}) {
198
+ const identifier = await appBundleIdentifier(root, appBundle);
199
+ options.onEvent?.("Opening iOS Simulator…");
200
+ await runProcess("open", ["-a", "Simulator"], {
201
+ cwd: root,
202
+ quiet: options.quiet,
203
+ onOutput: options.onEvent,
204
+ });
205
+ options.onEvent?.(`Launching ${identifier} in iOS Simulator…`);
206
+ await runProcess("xcrun", ["simctl", "launch", device, identifier], {
207
+ cwd: root,
208
+ quiet: options.quiet,
209
+ onOutput: options.onEvent,
210
+ });
211
+ }
59
212
  async function hashDirectory(directory) {
60
213
  const hash = createHash("sha256");
61
214
  let size = 0;
@@ -121,6 +274,13 @@ async function runRealIosSimulatorBuild(job, options) {
121
274
  onOutput: options.onEvent,
122
275
  });
123
276
  }
277
+ const icon = await prepareIosAppIcon(options.root, ios.appIcon, {
278
+ allowFallback: true,
279
+ });
280
+ if (icon)
281
+ step(`Using iOS app icon: ${icon}`, 9);
282
+ else
283
+ options.onEvent?.("No 1024x1024 PNG app icon was found; configure ios.appIcon or add icon.png before distribution.");
124
284
  transitionBuild(job, "queued", "iOS Simulator build queued locally");
125
285
  transitionBuild(job, "provisioning", "iOS Simulator destination selected");
126
286
  transitionBuild(job, "installing_dependencies", "iOS Simulator dependencies ready");
@@ -145,12 +305,26 @@ async function runRealIosSimulatorBuild(job, options) {
145
305
  "build",
146
306
  ], { cwd: options.root, quiet: options.quiet, onOutput: options.onEvent });
147
307
  const appPath = await findSimulatorApp(options.root, derivedData, configuration, ios.scheme);
308
+ const copiedResources = await syncIosRuntimeResources(options.root, appPath);
309
+ for (const name of copiedResources)
310
+ options.onEvent?.(`Packaged ${name} in the iOS Simulator app`);
148
311
  step("Installing .app in iOS Simulator…", 75);
149
312
  await runProcess("xcrun", ["simctl", "install", device, appPath], {
150
313
  cwd: options.root,
151
314
  quiet: options.quiet,
152
315
  onOutput: options.onEvent,
153
316
  });
317
+ if (options.simulatorAutostart) {
318
+ try {
319
+ await launchIosSimulatorApp(options.root, device, appPath, {
320
+ quiet: options.quiet,
321
+ onEvent: options.onEvent,
322
+ });
323
+ }
324
+ catch (error) {
325
+ options.onEvent?.(`Simulator app installed but could not be launched automatically: ${error instanceof Error ? error.message : String(error)}`);
326
+ }
327
+ }
154
328
  const artifact = await hashDirectory(appPath);
155
329
  const artifactName = nativeArtifactName("app");
156
330
  job.attempts += 1;
@@ -290,6 +464,11 @@ export async function runRealIosBuild(job, options) {
290
464
  onOutput: options.onEvent,
291
465
  });
292
466
  }
467
+ const icon = await prepareIosAppIcon(options.root, ios.appIcon);
468
+ if (icon)
469
+ step(`Using iOS app icon: ${icon}`, 9);
470
+ else
471
+ options.onEvent?.("No 1024x1024 PNG app icon was found; configure ios.appIcon or add icon.png before distribution.");
293
472
  step("Preparing Xcode archive…", 10);
294
473
  await runProcess("xcodebuild", [
295
474
  projectFlag,
@@ -304,6 +483,10 @@ export async function runRealIosBuild(job, options) {
304
483
  archivePath,
305
484
  "archive",
306
485
  ], { cwd: options.root, quiet: options.quiet, onOutput: options.onEvent });
486
+ const archivedApp = await findArchiveApp(archivePath);
487
+ const copiedResources = await syncIosRuntimeResources(options.root, archivedApp);
488
+ for (const name of copiedResources)
489
+ options.onEvent?.(`Packaged ${name} in the iOS archive`);
307
490
  transitionBuild(job, "queued", "iOS build queued locally");
308
491
  transitionBuild(job, "provisioning", "Xcode signing configuration selected");
309
492
  step("Exporting signed IPA…", 55);
@@ -1,6 +1,7 @@
1
1
  export interface IosHostOptions {
2
2
  bundleIdentifier: string;
3
3
  appName: string;
4
+ appIcon?: string;
4
5
  }
5
6
  export interface IosHostResult {
6
7
  directory: string;
@@ -10,6 +11,7 @@ export interface IosHostResult {
10
11
  scheme: string;
11
12
  exportOptionsPlist: string;
12
13
  bundleScript: string;
14
+ appIcon?: string;
13
15
  configUpdated: boolean;
14
16
  }
15
17
  export declare function initializeIosHost(root: string, options: IosHostOptions): Promise<IosHostResult>;
@@ -1 +1 @@
1
- {"version":3,"file":"ios-host.d.ts","sourceRoot":"","sources":["../src/ios-host.ts"],"names":[],"mappings":"AAiBA,MAAM,WAAW,cAAc;IAC7B,gBAAgB,EAAE,MAAM,CAAC;IACzB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,gBAAgB,EAAE,MAAM,CAAC;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,kBAAkB,EAAE,MAAM,CAAC;IAC3B,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,OAAO,CAAC;CACxB;AAoED,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,cAAc,GACtB,OAAO,CAAC,aAAa,CAAC,CA0CxB;AAED,wBAAgB,4BAA4B,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAKjE"}
1
+ {"version":3,"file":"ios-host.d.ts","sourceRoot":"","sources":["../src/ios-host.ts"],"names":[],"mappings":"AAiBA,MAAM,WAAW,cAAc;IAC7B,gBAAgB,EAAE,MAAM,CAAC;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,gBAAgB,EAAE,MAAM,CAAC;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,kBAAkB,EAAE,MAAM,CAAC;IAC3B,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,OAAO,CAAC;CACxB;AAqED,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,cAAc,GACtB,OAAO,CAAC,aAAa,CAAC,CA2CxB;AAED,wBAAgB,4BAA4B,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAKjE"}
package/dist/ios-host.js CHANGED
@@ -51,6 +51,7 @@ async function updateProjectConfig(root, result) {
51
51
  configuration: "Release",
52
52
  exportOptionsPlist: "ios/ExportOptions.plist",
53
53
  bundleScript: "ios/sync-bundle.mjs",
54
+ ...(result.appIcon ? { appIcon: result.appIcon } : {}),
54
55
  };
55
56
  await writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
56
57
  return true;
@@ -81,6 +82,7 @@ export async function initializeIosHost(root, options) {
81
82
  scheme,
82
83
  exportOptionsPlist: "ios/ExportOptions.plist",
83
84
  bundleScript: "ios/sync-bundle.mjs",
85
+ ...(options.appIcon ? { appIcon: options.appIcon } : {}),
84
86
  };
85
87
  const configUpdated = await updateProjectConfig(root, result);
86
88
  return { ...result, configUpdated };
@@ -35,5 +35,5 @@ export declare const pill: {
35
35
  export declare function finalLine(message: string, success?: boolean): void;
36
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
+ export declare function devServerQr(url: string, enabled?: boolean): Promise<void>;
39
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,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"}
1
+ {"version":3,"file":"components.d.ts","sourceRoot":"","sources":["../../src/ui/components.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAK7C,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,wBAAsB,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,UAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAmB5E"}
@@ -5,6 +5,7 @@ import ora from "ora";
5
5
  import qrcode from "qrcode-terminal";
6
6
  import { c, colorsEnabled } from "./state.js";
7
7
  import { supportsUnicode, terminalWidth } from "./terminal.js";
8
+ import { renderTerminalQr } from "./qr.js";
8
9
  export function sectionHeader(title) {
9
10
  console.log(`\n${c.brandBold(`◆ LynxShip — ${title}`)}`);
10
11
  }
@@ -288,10 +289,25 @@ export function downloadArtifact(url, expiresAt, enabled = true) {
288
289
  if (expiresAt)
289
290
  console.log(` ${c.muted(`Link expires at ${expiresAt}`)}`);
290
291
  }
291
- export function devServerQr(url, enabled = true) {
292
+ export async function devServerQr(url, enabled = true) {
292
293
  if (!enabled)
293
294
  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`);
295
+ const rail = c.teal("");
296
+ const printRail = (value = "") => console.log(` ${rail}${value}`);
297
+ printRail();
298
+ printRail(` ${c.brandBold("Lynx Explorer")}`);
299
+ if (colorsEnabled && supportsUnicode()) {
300
+ (await renderTerminalQr(url))
301
+ .split("\n")
302
+ .forEach((line) => printRail(` ${line}`));
303
+ }
304
+ else {
305
+ qrcode.generate(url, { small: true }, (code) => {
306
+ code.split("\n").forEach((line) => printRail(` ${line}`));
307
+ });
308
+ }
309
+ printRail();
310
+ printRail(` ${c.teal("URL")} ${url}`);
311
+ printRail();
312
+ console.log();
297
313
  }
@@ -19,7 +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
+ devServerQr(url: string): Promise<void>;
23
23
  }
24
24
  export declare function createCliUi(args: string[]): CliUi;
25
25
  export { terminalOptions };
@@ -1 +1 @@
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"}
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;IAIjD,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAS9C;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
@@ -66,8 +66,11 @@ 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);
69
+ async devServerQr(url) {
70
+ // Rspeedy suppresses its own QR when its stdout is piped. LynxShip
71
+ // captures that output, so render the fallback unless the caller asked
72
+ // for machine JSON, quiet output, or an explicitly non-interactive run.
73
+ await devServerQr(url, !this.options.json && !this.options.quiet && !this.options.nonInteractive);
71
74
  }
72
75
  }
73
76
  export function createCliUi(args) {
@@ -0,0 +1,61 @@
1
+ export declare const LYNXSHIP_QR_STYLE: {
2
+ readonly type: "svg";
3
+ readonly width: 300;
4
+ readonly height: 300;
5
+ readonly margin: 0;
6
+ readonly qrOptions: {
7
+ readonly errorCorrectionLevel: "H";
8
+ };
9
+ readonly dotsOptions: {
10
+ readonly type: "dots";
11
+ readonly gradient: {
12
+ readonly type: "linear";
13
+ readonly rotation: number;
14
+ readonly colorStops: readonly [{
15
+ readonly offset: 0;
16
+ readonly color: "#ff6b9d";
17
+ }, {
18
+ readonly offset: 1;
19
+ readonly color: "#45b7d1";
20
+ }];
21
+ };
22
+ };
23
+ readonly cornersSquareOptions: {
24
+ readonly type: "dot";
25
+ readonly gradient: {
26
+ readonly type: "linear";
27
+ readonly rotation: number;
28
+ readonly colorStops: readonly [{
29
+ readonly offset: 0;
30
+ readonly color: "#ff6b9d";
31
+ }, {
32
+ readonly offset: 1;
33
+ readonly color: "#45b7d1";
34
+ }];
35
+ };
36
+ };
37
+ readonly cornersDotOptions: {
38
+ readonly type: "dot";
39
+ readonly gradient: {
40
+ readonly type: "linear";
41
+ readonly rotation: number;
42
+ readonly colorStops: readonly [{
43
+ readonly offset: 0;
44
+ readonly color: "#ff6b9d";
45
+ }, {
46
+ readonly offset: 1;
47
+ readonly color: "#45b7d1";
48
+ }];
49
+ };
50
+ };
51
+ readonly backgroundOptions: {
52
+ readonly color: "#FFFFFF";
53
+ };
54
+ };
55
+ /**
56
+ * Generate the QR with qr-code-styling itself, exactly like the WISA renderer.
57
+ * The SVG is then rasterized only to make the library's real rounded shapes
58
+ * displayable in a normal terminal; no QR modules are recreated by LynxShip.
59
+ */
60
+ export declare function renderTerminalQr(data: string): Promise<string>;
61
+ //# sourceMappingURL=qr.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"qr.d.ts","sourceRoot":"","sources":["../../src/ui/qr.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAwCpB,CAAC;AAyLX;;;;GAIG;AACH,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAmBpE"}
package/dist/ui/qr.js ADDED
@@ -0,0 +1,180 @@
1
+ import { LYNX_BRAND } from "./colors.js";
2
+ import { terminalWidth } from "./terminal.js";
3
+ import { createRequire } from "node:module";
4
+ export const LYNXSHIP_QR_STYLE = {
5
+ type: "svg",
6
+ width: 300,
7
+ height: 300,
8
+ margin: 0,
9
+ qrOptions: { errorCorrectionLevel: "H" },
10
+ dotsOptions: {
11
+ type: "dots",
12
+ gradient: {
13
+ type: "linear",
14
+ rotation: (50 * Math.PI) / 180,
15
+ colorStops: [
16
+ { offset: 0, color: LYNX_BRAND.pink },
17
+ { offset: 1, color: LYNX_BRAND.cyan },
18
+ ],
19
+ },
20
+ },
21
+ cornersSquareOptions: {
22
+ type: "dot",
23
+ gradient: {
24
+ type: "linear",
25
+ rotation: (50 * Math.PI) / 180,
26
+ colorStops: [
27
+ { offset: 0, color: LYNX_BRAND.pink },
28
+ { offset: 1, color: LYNX_BRAND.cyan },
29
+ ],
30
+ },
31
+ },
32
+ cornersDotOptions: {
33
+ type: "dot",
34
+ gradient: {
35
+ type: "linear",
36
+ rotation: (50 * Math.PI) / 180,
37
+ colorStops: [
38
+ { offset: 0, color: LYNX_BRAND.pink },
39
+ { offset: 1, color: LYNX_BRAND.cyan },
40
+ ],
41
+ },
42
+ },
43
+ backgroundOptions: { color: "#FFFFFF" },
44
+ };
45
+ async function loadDependencies() {
46
+ const require = createRequire(import.meta.url);
47
+ const qrModule = require("qr-code-styling");
48
+ const jsdomModule = require("jsdom");
49
+ const resvgModule = require("@resvg/resvg-js");
50
+ const pngModule = require("pngjs");
51
+ return {
52
+ QRCodeStyling: "default" in qrModule && qrModule.default
53
+ ? qrModule.default
54
+ : qrModule,
55
+ JSDOM: jsdomModule.JSDOM,
56
+ Resvg: resvgModule.Resvg,
57
+ PNG: pngModule.PNG,
58
+ };
59
+ }
60
+ function ansiColor(channel, red, green, blue) {
61
+ return "\u001b[" + channel + ";2;" + red + ";" + green + ";" + blue + "m";
62
+ }
63
+ function pixelAt(image, x, y) {
64
+ const offset = (y * image.width + x) * 4;
65
+ return [
66
+ image.data[offset] ?? 255,
67
+ image.data[offset + 1] ?? 255,
68
+ image.data[offset + 2] ?? 255,
69
+ image.data[offset + 3] ?? 255,
70
+ ];
71
+ }
72
+ function isBackground([red, green, blue, alpha]) {
73
+ return alpha < 16 || (red > 248 && green > 248 && blue > 248);
74
+ }
75
+ function cropToQrContent(image) {
76
+ let left = image.width;
77
+ let top = image.height;
78
+ let right = -1;
79
+ let bottom = -1;
80
+ for (let y = 0; y < image.height; y += 1) {
81
+ for (let x = 0; x < image.width; x += 1) {
82
+ if (!isBackground(pixelAt(image, x, y))) {
83
+ left = Math.min(left, x);
84
+ top = Math.min(top, y);
85
+ right = Math.max(right, x);
86
+ bottom = Math.max(bottom, y);
87
+ }
88
+ }
89
+ }
90
+ if (right < left || bottom < top)
91
+ return image;
92
+ const padding = Math.max(3, Math.round(Math.min(image.width, image.height) * 0.02));
93
+ const cropLeft = Math.max(0, left - padding);
94
+ const cropTop = Math.max(0, top - padding);
95
+ const cropRight = Math.min(image.width - 1, right + padding);
96
+ const cropBottom = Math.min(image.height - 1, bottom + padding);
97
+ const width = cropRight - cropLeft + 1;
98
+ const height = cropBottom - cropTop + 1;
99
+ const data = new Uint8Array(width * height * 4);
100
+ for (let y = 0; y < height; y += 1) {
101
+ for (let x = 0; x < width; x += 1) {
102
+ const source = ((cropTop + y) * image.width + cropLeft + x) * 4;
103
+ const target = (y * width + x) * 4;
104
+ data[target] = image.data[source] ?? 255;
105
+ data[target + 1] = image.data[source + 1] ?? 255;
106
+ data[target + 2] = image.data[source + 2] ?? 255;
107
+ data[target + 3] = image.data[source + 3] ?? 255;
108
+ }
109
+ }
110
+ return { width, height, data };
111
+ }
112
+ function renderRasterInTerminal(image) {
113
+ const columns = Math.max(35, Math.min(72, terminalWidth() - 8));
114
+ const scale = image.width / columns;
115
+ const rows = Math.ceil(image.height / scale / 2);
116
+ const lines = [];
117
+ const whiteBackground = ansiColor("48", 255, 255, 255);
118
+ const reset = "\u001b[0m";
119
+ for (let row = 0; row < rows; row += 1) {
120
+ const topY = Math.min(image.height - 1, Math.floor(row * 2 * scale));
121
+ const bottomY = Math.min(image.height - 1, Math.floor((row * 2 + 1) * scale));
122
+ let line = whiteBackground;
123
+ for (let column = 0; column < columns; column += 1) {
124
+ const x = Math.min(image.width - 1, Math.floor(column * scale));
125
+ const top = pixelAt(image, x, topY);
126
+ const bottom = pixelAt(image, x, bottomY);
127
+ const topBackground = isBackground(top);
128
+ const bottomBackground = isBackground(bottom);
129
+ if (topBackground && bottomBackground) {
130
+ line += " ";
131
+ }
132
+ else if (!topBackground && bottomBackground) {
133
+ line +=
134
+ ansiColor("38", top[0], top[1], top[2]) +
135
+ ansiColor("48", 255, 255, 255) +
136
+ "▀";
137
+ }
138
+ else if (topBackground && !bottomBackground) {
139
+ line +=
140
+ ansiColor("38", bottom[0], bottom[1], bottom[2]) +
141
+ ansiColor("48", 255, 255, 255) +
142
+ "▄";
143
+ }
144
+ else {
145
+ line +=
146
+ ansiColor("38", top[0], top[1], top[2]) +
147
+ ansiColor("48", bottom[0], bottom[1], bottom[2]) +
148
+ "▀";
149
+ }
150
+ }
151
+ lines.push(line + reset);
152
+ }
153
+ return lines.join("\n");
154
+ }
155
+ /**
156
+ * Generate the QR with qr-code-styling itself, exactly like the WISA renderer.
157
+ * The SVG is then rasterized only to make the library's real rounded shapes
158
+ * displayable in a normal terminal; no QR modules are recreated by LynxShip.
159
+ */
160
+ export async function renderTerminalQr(data) {
161
+ const { QRCodeStyling, JSDOM, Resvg, PNG } = await loadDependencies();
162
+ const qrCode = new QRCodeStyling({
163
+ jsdom: JSDOM,
164
+ data,
165
+ ...LYNXSHIP_QR_STYLE,
166
+ });
167
+ const svg = await qrCode.getRawData("svg");
168
+ if (!svg)
169
+ throw new Error("qr-code-styling returned no SVG data");
170
+ const svgText = Buffer.isBuffer(svg)
171
+ ? svg.toString("utf8")
172
+ : new TextDecoder().decode(svg);
173
+ // jsdom serializes SVG references as url('#id'). Normalize only those
174
+ // references for resvg; the SVG generated by qr-code-styling is unchanged
175
+ // for browsers and for the file returned by the library.
176
+ const rasterSvg = svgText.replaceAll("url('#", "url(#").replaceAll("')", ")");
177
+ const png = new Resvg(rasterSvg).render().asPng();
178
+ const image = cropToQrContent(PNG.sync.read(Buffer.from(png)));
179
+ return renderRasterInTerminal(image);
180
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lynxship/cli",
3
- "version": "0.1.13",
3
+ "version": "0.1.15",
4
4
  "description": "Build, sign, store, submit and update LynxJS applications from the terminal.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -68,14 +68,19 @@
68
68
  "cli-progress": "3.12.0",
69
69
  "ora": "8.2.0",
70
70
  "qrcode-terminal": "0.12.0",
71
+ "qr-code-styling": "1.9.2",
72
+ "@resvg/resvg-js": "2.6.2",
73
+ "jsdom": "29.1.1",
74
+ "pngjs": "7.0.0",
71
75
  "@lynxship/build-orchestrator": "0.1.0",
72
- "@lynxship/contracts": "0.1.0",
73
76
  "@lynxship/db": "0.1.0",
74
- "@lynxship/submit": "0.1.0",
75
- "@lynxship/signing": "0.1.0"
77
+ "@lynxship/contracts": "0.1.0",
78
+ "@lynxship/signing": "0.1.0",
79
+ "@lynxship/submit": "0.1.0"
76
80
  },
77
81
  "devDependencies": {
78
82
  "@types/cli-progress": "3.11.6",
83
+ "@types/pngjs": "6.0.4",
79
84
  "@types/qrcode-terminal": "0.12.2"
80
85
  },
81
86
  "scripts": {