@livx.cc/appwrap 0.20.4 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@livx.cc/appwrap",
3
- "version": "0.20.4",
3
+ "version": "0.21.0",
4
4
  "description": "Wrap any PWA into a native app with native capabilities (appwrap runtime + @livx.cc/native-kit).",
5
5
  "license": "MIT",
6
6
  "author": "Elya Livshitz",
@@ -11,6 +11,7 @@ import { registerAndroidHandlers } from './shell/handlers-android';
11
11
  import { registerOptionalHandlers } from './shell/optional-handlers.generated';
12
12
  import './shell/fcm-bootstrap.generated'; // side-effect: registers the FCM service when push is wired
13
13
  import { startEventForwarding } from './shell/events';
14
+ import { startDevMenu } from './shell/devmenu';
14
15
  import { SHELL_CONFIG } from './shell/config';
15
16
  import { bindStatusBarPage, setStatusBarStyle, enableAndroidEdgeToEdge, wireAndroidSafeArea } from './shell/status-bar';
16
17
  import { CustomWebView } from './shell/custom-webview';
@@ -44,6 +45,8 @@ export function onPageLoaded(args: EventData): void {
44
45
  if (isAndroid) registerAndroidHandlers();
45
46
  // Opt-in modules that own their own handler file (health, …) — generated to only the active set.
46
47
  registerOptionalHandlers();
48
+ // Shake-to-open developer menu (App Info / Reload). On by default, incl. prod.
49
+ if (SHELL_CONFIG.devMenu) startDevMenu();
47
50
 
48
51
  const webView = page.getViewById<CustomWebView>('webview');
49
52
  bridge.attach(webView);
@@ -0,0 +1,149 @@
1
+ import { Application, Utils, isAndroid, isIOS } from '@nativescript/core';
2
+ import { bridge } from './bridge';
3
+
4
+ /**
5
+ * Persistent bottom banner (vs. the auto-dismiss `toast`). Used for the "new version available —
6
+ * tap to reload" update prompt. **Tap** emits `toast.action` { id } (the web side decides what to
7
+ * do) and dismisses; **swipe down** dismisses without acting.
8
+ */
9
+
10
+ let currentId: string | null = null;
11
+ let iosBanner: UIView | null = null;
12
+ let iosTapHandler: any = null;
13
+ let androidBanner: android.view.View | null = null;
14
+
15
+ function onTap(): void {
16
+ const id = currentId;
17
+ dismissBanner();
18
+ if (id) bridge.emit('toast.action', { id });
19
+ }
20
+
21
+ export function showBanner(opts: { id: string; message: string }): void {
22
+ dismissBanner(); // only one at a time
23
+ currentId = opts.id;
24
+ if (isIOS) Utils.dispatchToMainThread(() => showIOSBanner(opts.message));
25
+ else if (isAndroid) runOnAndroidUi(() => showAndroidBanner(opts.message));
26
+ }
27
+
28
+ export function dismissBanner(): void {
29
+ currentId = null;
30
+ if (isIOS && iosBanner) {
31
+ const b = iosBanner;
32
+ iosBanner = null;
33
+ iosTapHandler = null;
34
+ Utils.dispatchToMainThread(() => b.removeFromSuperview());
35
+ }
36
+ if (isAndroid && androidBanner) {
37
+ const v = androidBanner;
38
+ androidBanner = null;
39
+ runOnAndroidUi(() => (v.getParent() as android.view.ViewGroup)?.removeView(v));
40
+ }
41
+ }
42
+
43
+ // ── iOS ──────────────────────────────────────────────────────────────
44
+ // Built lazily (and only on iOS) — `NSObject`/`interop` don't exist on Android, and this file is
45
+ // imported on both platforms via handlers.ts, so a top-level `.extend` would crash the Android shell.
46
+ let IOSGestureHandler: any = null;
47
+ function iosGestureHandlerClass(): any {
48
+ if (!IOSGestureHandler) {
49
+ IOSGestureHandler = (NSObject as any).extend(
50
+ { bannerTapped() { onTap(); }, bannerDismissed() { dismissBanner(); } },
51
+ {
52
+ exposedMethods: {
53
+ bannerTapped: { returns: interop.types.void },
54
+ bannerDismissed: { returns: interop.types.void },
55
+ },
56
+ }
57
+ );
58
+ }
59
+ return IOSGestureHandler;
60
+ }
61
+
62
+ function showIOSBanner(message: string): void {
63
+ const rootVC = Utils.ios.getRootViewController();
64
+ if (!rootVC?.view) return;
65
+ const screen = UIScreen.mainScreen.bounds;
66
+ const safeBottom = rootVC.view.safeAreaInsets ? rootVC.view.safeAreaInsets.bottom : 0;
67
+ const width = screen.size.width - 24;
68
+
69
+ const container = UIView.alloc().initWithFrame(CGRectMake(12, 0, width, 52));
70
+ container.backgroundColor = UIColor.colorWithRedGreenBlueAlpha(0.08, 0.08, 0.1, 0.85);
71
+ container.layer.cornerRadius = 12;
72
+ container.clipsToBounds = true;
73
+ container.userInteractionEnabled = true;
74
+
75
+ const blur = UIVisualEffectView.alloc().initWithEffect(UIBlurEffect.effectWithStyle(UIBlurEffectStyle.Dark));
76
+ blur.frame = container.bounds;
77
+ blur.autoresizingMask = UIViewAutoresizing.FlexibleWidth | UIViewAutoresizing.FlexibleHeight;
78
+ blur.userInteractionEnabled = false;
79
+ container.addSubview(blur);
80
+
81
+ const label = UILabel.alloc().initWithFrame(CGRectMake(16, 0, width - 32, 52));
82
+ label.text = message;
83
+ label.textColor = UIColor.whiteColor;
84
+ label.textAlignment = NSTextAlignment.Center;
85
+ label.font = UIFont.systemFontOfSizeWeight(15, UIFontWeightSemibold);
86
+ blur.contentView.addSubview(label);
87
+
88
+ iosTapHandler = iosGestureHandlerClass().alloc().init();
89
+ container.addGestureRecognizer(UITapGestureRecognizer.alloc().initWithTargetAction(iosTapHandler, 'bannerTapped'));
90
+ const swipe = UISwipeGestureRecognizer.alloc().initWithTargetAction(iosTapHandler, 'bannerDismissed');
91
+ swipe.direction = UISwipeGestureRecognizerDirection.Down;
92
+ container.addGestureRecognizer(swipe);
93
+
94
+ container.center = CGPointMake(screen.size.width / 2, screen.size.height - safeBottom - 38);
95
+ container.alpha = 0;
96
+ rootVC.view.addSubview(container);
97
+ iosBanner = container;
98
+ UIView.animateWithDurationAnimations(0.3, () => (container.alpha = 1));
99
+ }
100
+
101
+ // ── Android ──────────────────────────────────────────────────────────
102
+ function showAndroidBanner(message: string): void {
103
+ const activity = Application.android.foregroundActivity || Application.android.startActivity;
104
+ if (!activity) return;
105
+ const density = activity.getResources().getDisplayMetrics().density;
106
+ const pad = Math.round(16 * density);
107
+
108
+ const tv = new android.widget.TextView(activity);
109
+ tv.setText(message);
110
+ tv.setTextColor(android.graphics.Color.WHITE);
111
+ tv.setTextSize(15);
112
+ tv.setPadding(pad, pad, pad, pad);
113
+ tv.setGravity(android.view.Gravity.CENTER);
114
+ tv.setBackgroundColor(android.graphics.Color.argb(230, 20, 20, 26));
115
+
116
+ // Distinguish a tap (→ reload) from a downward swipe (→ dismiss) on one touch listener.
117
+ const SWIPE = 40 * density;
118
+ let downX = 0, downY = 0, downT = 0;
119
+ tv.setOnTouchListener(new android.view.View.OnTouchListener({
120
+ onTouch(_v: any, e: any) {
121
+ switch (e.getActionMasked()) {
122
+ case android.view.MotionEvent.ACTION_DOWN:
123
+ downX = e.getRawX(); downY = e.getRawY(); downT = e.getEventTime();
124
+ return true;
125
+ case android.view.MotionEvent.ACTION_UP: {
126
+ const dy = e.getRawY() - downY;
127
+ const dx = Math.abs(e.getRawX() - downX);
128
+ const dt = e.getEventTime() - downT;
129
+ if (dy > SWIPE) dismissBanner(); // swipe down → dismiss
130
+ else if (Math.abs(dy) < SWIPE && dx < SWIPE && dt < 400) onTap(); // tap → reload
131
+ return true;
132
+ }
133
+ default:
134
+ return true;
135
+ }
136
+ },
137
+ }));
138
+
139
+ const lp = new android.widget.FrameLayout.LayoutParams(-1, -2); // MATCH_PARENT × WRAP_CONTENT
140
+ lp.gravity = android.view.Gravity.BOTTOM;
141
+ activity.addContentView(tv, lp);
142
+ androidBanner = tv;
143
+ }
144
+
145
+ function runOnAndroidUi(fn: () => void): void {
146
+ const activity = Application.android.foregroundActivity || Application.android.startActivity;
147
+ if (activity) activity.runOnUiThread(new java.lang.Runnable({ run: fn }));
148
+ else fn();
149
+ }
@@ -39,6 +39,11 @@ export class Bridge {
39
39
  this.webView = null;
40
40
  }
41
41
 
42
+ /** The attached WebView (for handlers that drive it directly, e.g. app.reload). */
43
+ getWebView(): CustomWebView | null {
44
+ return this.webView;
45
+ }
46
+
42
47
  emit(event: string, payload?: unknown): void {
43
48
  this.deliver(JSON.stringify({ v: 1, kind: 'event', event, payload }));
44
49
  }
@@ -75,7 +80,8 @@ export class Bridge {
75
80
  this.evalJs(js).catch((e) => console.error('Bridge: deliver failed', e));
76
81
  }
77
82
 
78
- private evalJs(script: string): Promise<any> {
83
+ /** Evaluate JS in the WebView and resolve its value (used by deliver + the dev-menu version probe). */
84
+ evalJs(script: string): Promise<any> {
79
85
  const wv = this.webView;
80
86
  return new Promise((resolve, reject) => {
81
87
  if (!wv) return reject(new Error('no webview'));
@@ -68,7 +68,7 @@ export const MODULES: ModuleManifest[] = [
68
68
  { name: 'haptics', core: true, group: 'core', capabilities: { haptics: 'native' } },
69
69
  { name: 'share', core: true, group: 'core', capabilities: { share: 'native', shareFiles: 'native' } },
70
70
  { name: 'storage', core: true, group: 'core', capabilities: { storage: 'native', secureStorage: 'native' } },
71
- { name: 'toast', core: true, group: 'core', capabilities: { toast: 'native' } },
71
+ { name: 'toast', core: true, group: 'core', capabilities: { toast: 'native', banner: 'native', updates: 'native' } },
72
72
  { name: 'statusBar', core: true, group: 'core', capabilities: { statusBar: 'native', themeColor: 'native' } },
73
73
  { name: 'device', core: true, group: 'extended', capabilities: { device: 'native' } },
74
74
  { name: 'clipboard', core: true, group: 'extended', capabilities: { clipboard: 'native' } },
@@ -27,6 +27,8 @@ export const SHELL_CONFIG = {
27
27
  debug: false,
28
28
  /** Value written to `localStorage.DEBUG` in debug mode so the PWA logger goes verbose ('*' = all). */
29
29
  debugLog: '*',
30
+ /** Shake-to-open developer menu (App Info / Reload). On by default, including store builds. */
31
+ devMenu: true,
30
32
  /** Remote push configured, per platform (iOS aps-environment entitlement / Android FCM). Drives the
31
33
  * `push` capability flag at runtime by platform — off unless `appwrap.json.push` enables it, so an
32
34
  * un-provisioned build honestly reports 'none' (and a personal-team iOS build keeps `pushIos:false`). */
@@ -0,0 +1,144 @@
1
+ import { Device, Dialogs, Utils, isAndroid, isIOS } from '@nativescript/core';
2
+ import { bridge } from './bridge';
3
+ import { SHELL_CONFIG } from './config';
4
+ import { SHELL_BUILD, reloadWebView } from './handlers';
5
+
6
+ /**
7
+ * Shake-to-open developer menu (enabled in prod too, gated by `SHELL_CONFIG.devMenu`).
8
+ * A shake raises a native action sheet → "App Info" shows non-sensitive diagnostics
9
+ * (ids, versions, loader, remote host) including the running webapp's version vs. the
10
+ * latest deployed — so you can tell at a glance whether the device got the update.
11
+ */
12
+
13
+ /** Last version info the web side reported (native-kit's updates module). */
14
+ let webInfo: { current?: string; latest?: string; build?: string | number; updateAvailable?: boolean } = {};
15
+
16
+ export function startDevMenu(): void {
17
+ // The web side (native-kit updates) pushes its version + the latest-deployed version here.
18
+ bridge.register('app.reportWebVersion', (p: any) => {
19
+ webInfo = p || {};
20
+ });
21
+ if (isIOS) startIOSShake();
22
+ else if (isAndroid) startAndroidShake();
23
+ }
24
+
25
+ // ── shake detection (total acceleration magnitude in g incl. gravity, debounced) ──
26
+ const SHAKE_G = 1.8; // spike vs. ~1g rest
27
+ let shakeMm: any = null; // hold a ref so CoreMotion isn't GC'd
28
+ let firstSpikeAt = 0;
29
+ let spikes = 0;
30
+ let lastMenuAt = 0;
31
+
32
+ function onAccelMagnitude(g: number): void {
33
+ if (g < SHAKE_G) return;
34
+ const now = Date.now();
35
+ if (now - lastMenuAt < 1500) return; // don't re-open right after a menu
36
+ if (now - firstSpikeAt > 800) {
37
+ firstSpikeAt = now; // start a fresh window
38
+ spikes = 1;
39
+ return;
40
+ }
41
+ if (++spikes >= 2) {
42
+ spikes = 0;
43
+ firstSpikeAt = 0;
44
+ lastMenuAt = now;
45
+ void showDevMenu();
46
+ }
47
+ }
48
+
49
+ function startIOSShake(): void {
50
+ // Poll `mm.deviceMotion` on a JS timer — CoreMotion's queue-handler block is fragile under
51
+ // NativeScript (can silently stop firing on-device); mirrors the motion.start handler.
52
+ const mm = CMMotionManager.new();
53
+ if (!mm.deviceMotionAvailable) return; // no sensors (simulator)
54
+ shakeMm = mm;
55
+ mm.deviceMotionUpdateInterval = 0.1;
56
+ mm.startDeviceMotionUpdates();
57
+ setInterval(() => {
58
+ const m = mm.deviceMotion;
59
+ if (!m) return; // first sample not ready
60
+ const x = m.userAcceleration.x + m.gravity.x; // total accel in g (rest ≈ 1)
61
+ const y = m.userAcceleration.y + m.gravity.y;
62
+ const z = m.userAcceleration.z + m.gravity.z;
63
+ onAccelMagnitude(Math.sqrt(x * x + y * y + z * z));
64
+ }, 100);
65
+ }
66
+
67
+ function startAndroidShake(): void {
68
+ const sm = Utils.android
69
+ .getApplicationContext()
70
+ .getSystemService(android.content.Context.SENSOR_SERVICE) as android.hardware.SensorManager;
71
+ const accel = sm.getDefaultSensor(android.hardware.Sensor.TYPE_ACCELEROMETER);
72
+ if (!accel) return;
73
+ const G = 9.80665;
74
+ const listener = new android.hardware.SensorEventListener({
75
+ onAccuracyChanged() {},
76
+ onSensorChanged(e: any) {
77
+ const v = e.values;
78
+ onAccelMagnitude(Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]) / G);
79
+ },
80
+ });
81
+ sm.registerListener(listener, accel, android.hardware.SensorManager.SENSOR_DELAY_UI);
82
+ }
83
+
84
+ // ── menu + info ──────────────────────────────────────────────────────
85
+ let menuOpen = false;
86
+ async function showDevMenu(): Promise<void> {
87
+ if (menuOpen) return; // sensor keeps firing while the sheet is up — don't stack dialogs
88
+ menuOpen = true;
89
+ try {
90
+ const action = await Dialogs.action({
91
+ title: SHELL_CONFIG.name,
92
+ message: 'Developer menu',
93
+ cancelButtonText: 'Cancel',
94
+ actions: ['App Info', 'Reload'],
95
+ });
96
+ if (action === 'App Info') await showInfo();
97
+ else if (action === 'Reload') reloadWebView();
98
+ } finally {
99
+ menuOpen = false;
100
+ }
101
+ }
102
+
103
+ async function showInfo(): Promise<void> {
104
+ // Running web version: prefer what kit.updates reported, else read the page's embedded global
105
+ // directly — so the line shows for any server-loader app exposing __APP_VERSION__, even if its
106
+ // native-kit is too old to ship the updates module.
107
+ const current = webInfo.current || (await readPageVersion());
108
+ const lines = [
109
+ `App: ${SHELL_CONFIG.name}`,
110
+ `ID: ${SHELL_CONFIG.appId}`,
111
+ `Shell: ${SHELL_CONFIG.version} (${SHELL_BUILD})`,
112
+ `Platform: ${isIOS ? 'iOS' : 'Android'} ${Device.osVersion}`,
113
+ `Loader: ${SHELL_CONFIG.loader}`,
114
+ ];
115
+ if (SHELL_CONFIG.loader === 'server') lines.push(`Remote: ${safeHost(SHELL_CONFIG.serverUrl)}`);
116
+
117
+ const build = webInfo.build ? ` · build ${webInfo.build}` : '';
118
+ lines.push(`Web version: ${current || 'unknown'}${build}`);
119
+ if (webInfo.latest && current && webInfo.latest !== current) lines.push(`⚠️ Update available: ${webInfo.latest} — Reload to apply`);
120
+ else if (webInfo.latest) lines.push('✓ Up to date');
121
+
122
+ Dialogs.alert({ title: 'App Info', message: lines.join('\n'), okButtonText: 'Close' });
123
+ }
124
+
125
+ /** Read the running page's embedded version directly from the WebView: `window.__APP_VERSION__`,
126
+ * then a `<meta name="app-version">` fallback. '' if neither is present (or eval fails). */
127
+ async function readPageVersion(): Promise<string> {
128
+ try {
129
+ const v = await bridge.evalJs(
130
+ 'window.__APP_VERSION__ || document.querySelector(\'meta[name="app-version"]\')?.getAttribute("content") || ""'
131
+ );
132
+ return v ? String(v) : '';
133
+ } catch {
134
+ return '';
135
+ }
136
+ }
137
+
138
+ /** Host[:port] only — strips scheme, path, query, fragment AND any `user:pass@` userinfo, so no
139
+ * credentials or tokens embedded in the URL ever surface in diagnostics. */
140
+ function safeHost(url: string): string {
141
+ const afterScheme = String(url || '').replace(/^[a-z][a-z0-9+.-]*:\/\//i, '');
142
+ const authority = afterScheme.split(/[/?#]/)[0]; // drop path/query/fragment
143
+ return authority.split('@').pop() || authority; // drop userinfo
144
+ }
@@ -3,12 +3,13 @@ import { bridge } from './bridge';
3
3
  import { SHELL_CONFIG } from './config';
4
4
  import { onPwaHandshake } from './events';
5
5
  import { showToast } from './toast';
6
+ import { showBanner, dismissBanner } from './banner';
6
7
  import { setStatusBarStyle } from './status-bar';
7
8
  import { buildCapabilityMap } from './capabilities.manifest';
8
9
  import { ACTIVE_MODULE_NAMES } from './active-modules.generated';
9
10
 
10
11
  /** Build identifier for the native shell bundle — bump per deploy to spot stale bundles. */
11
- export const SHELL_BUILD = 'oauth-media-speaker-1';
12
+ export const SHELL_BUILD = 'updates-devmenu-2';
12
13
 
13
14
  /** Register all protocol-v1 handlers. */
14
15
  export function registerHandlers(): void {
@@ -23,7 +24,7 @@ export function registerHandlers(): void {
23
24
  return {
24
25
  protocol: 1,
25
26
  platform: isIOS ? 'ios' : 'android',
26
- app: { id: SHELL_CONFIG.appId, name: SHELL_CONFIG.name, version: SHELL_CONFIG.version, build: SHELL_BUILD },
27
+ app: { id: SHELL_CONFIG.appId, name: SHELL_CONFIG.name, version: SHELL_CONFIG.version, build: SHELL_BUILD, loader: SHELL_CONFIG.loader },
27
28
  debug: { lastNotifTap: safeJson(ApplicationSettings.getString('kit:__notifTap', '')) },
28
29
  capabilities,
29
30
  };
@@ -127,11 +128,36 @@ export function registerHandlers(): void {
127
128
  showToast(String(message ?? ''), duration ?? 'short')
128
129
  );
129
130
 
131
+ // Persistent, tappable banner (e.g. the remote-update "tap to reload" prompt). Tap emits
132
+ // `toast.action` { id } back to the web side.
133
+ bridge.register('toast.banner', ({ id, message }: { id: string; message: string }) =>
134
+ showBanner({ id: String(id ?? 'banner'), message: String(message ?? '') })
135
+ );
136
+ bridge.register('toast.dismissBanner', () => dismissBanner());
137
+
138
+ // Hard reload the WebView, bypassing cache — used by the update banner + dev menu.
139
+ bridge.register('app.reload', () => reloadWebView());
140
+
130
141
  bridge.register('ui.statusBar.setStyle', ({ style }: { style: 'light' | 'dark' }) =>
131
142
  setStatusBarStyle(style)
132
143
  );
133
144
  }
134
145
 
146
+ /** Reload the attached WebView from origin, bypassing the HTTP cache (iOS `reloadFromOrigin`,
147
+ * Android `clearCache` + `reload`). No-op if no WebView is attached yet. */
148
+ export function reloadWebView(): void {
149
+ const wv = bridge.getWebView();
150
+ if (!wv) return;
151
+ Utils.dispatchToMainThread(() => {
152
+ if (isIOS && wv.ios) {
153
+ (wv.ios as WKWebView).reloadFromOrigin();
154
+ } else if (isAndroid && wv.android) {
155
+ wv.android.clearCache(true);
156
+ wv.android.reload();
157
+ }
158
+ });
159
+ }
160
+
135
161
  /** Parse a stored JSON breadcrumb; null if absent/unparseable (diagnostic, never throws). */
136
162
  function safeJson(s: string): unknown {
137
163
  if (!s) return null;
package/src/cli.ts CHANGED
@@ -358,6 +358,7 @@ export const SHELL_CONFIG = {
358
358
  backendOrigin: ${JSON.stringify(cfg.backendOrigin ?? '')},
359
359
  debug: ${JSON.stringify(cfg.debug ?? false)},
360
360
  debugLog: ${JSON.stringify(cfg.debugLog ?? '*')},
361
+ devMenu: ${JSON.stringify(cfg.devMenu ?? true)},
361
362
  pushIos: ${JSON.stringify(!!cfg.push?.enabled && cfg.push?.ios !== false)},
362
363
  pushAndroid: ${JSON.stringify(!!cfg.push?.enabled && cfg.push?.android !== false)},
363
364
  };
package/src/config.ts CHANGED
@@ -57,6 +57,10 @@ export interface AppwrapConfig {
57
57
  /** In debug mode, the value written to `localStorage.DEBUG` at startup so the PWA's logger goes
58
58
  * verbose (common convention — `'*'` = all, or comma-separated module names). Default `'*'`. */
59
59
  debugLog?: string;
60
+ /** Shake-to-open developer menu (App Info / Reload). Default `true` — ON in store builds too, since
61
+ * it only exposes non-sensitive diagnostics (ids, versions, loader, remote host). Set `false` to
62
+ * disable. Remote-update detection (native-kit `kit.updates`) is independent of this flag. */
63
+ devMenu?: boolean;
60
64
  /** Apple Development Team ID for device builds (Xcode → Settings → Accounts). */
61
65
  teamId?: string;
62
66
  /** Path (relative to the PWA project) to a StoreKit configuration file for LOCAL IAP
@@ -11,7 +11,7 @@ jobs:
11
11
  - uses: oven-sh/setup-bun@v2
12
12
  - run: bun install
13
13
  - run: bun test || echo "no tests"
14
- - run: bun run build.ts # → dist/ (the PWA bundle the wrapper ships)
14
+ - run: bun run build # → dist/ (the PWA bundle the wrapper ships)
15
15
 
16
16
  ios-build:
17
17
  runs-on: macos-15
@@ -21,7 +21,7 @@ jobs:
21
21
  - uses: oven-sh/setup-bun@v2
22
22
  - uses: actions/setup-node@v4
23
23
  with: { node-version: 22 }
24
- - run: bun install && bun run build.ts
24
+ - run: bun install && bun run build
25
25
  # Pin global tools — unpinned installs can grab a breaking release mid-flight. Bump deliberately.
26
26
  - run: npm i -g nativescript@9.0.6
27
27
  - run: bunx @livx.cc/appwrap init # native/ is generated (gitignored) — regenerate it fresh in CI
@@ -29,7 +29,7 @@ jobs:
29
29
  - uses: actions/setup-java@v4
30
30
  with: { distribution: temurin, java-version: 17 }
31
31
  - uses: android-actions/setup-android@v3
32
- - run: bun install && bun run build.ts # → dist/ (the PWA bundle the wrapper ships)
32
+ - run: bun install && bun run build # → dist/ (the PWA bundle the wrapper ships)
33
33
  # Pin global tools — unpinned installs can grab a breaking release mid-flight. Bump deliberately.
34
34
  - run: npm i -g nativescript@9.0.6
35
35
  - run: bunx @livx.cc/appwrap init # native/ is generated (gitignored) — regenerate fresh in CI
@@ -31,7 +31,7 @@ jobs:
31
31
  - uses: oven-sh/setup-bun@v2
32
32
  - uses: actions/setup-node@v4
33
33
  with: { node-version: 22 }
34
- - run: bun install && bun run build.ts
34
+ - run: bun install && bun run build
35
35
  # Pin global tools — unpinned installs can grab a breaking release mid-flight. Bump deliberately.
36
36
  - run: npm i -g nativescript@9.0.6
37
37
  - run: bunx @livx.cc/appwrap init # native/ is generated (gitignored) — regenerate it fresh in CI