@livx.cc/appwrap 0.39.13 → 0.39.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/package.json +1 -1
- package/src/cli.ts +134 -20
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -1714,14 +1714,16 @@ function buildFingerprint(cwd: string, cfg: { pwaDist?: string }, _outDir: strin
|
|
|
1714
1714
|
return h.toString(36);
|
|
1715
1715
|
}
|
|
1716
1716
|
|
|
1717
|
-
|
|
1718
|
-
|
|
1717
|
+
// Per-platform build cache (iOS .ipa / Android .apk) — separate files so the two don't clobber each
|
|
1718
|
+
// other's fingerprint (that's what enables the build-skip on BOTH platforms).
|
|
1719
|
+
const buildCacheFile = (platform: string) => `.appwrap-build-cache-${platform}.json`;
|
|
1720
|
+
interface BuildCache { fingerprint: string; artifactPath: string; builtAt: string }
|
|
1719
1721
|
|
|
1720
|
-
function readBuildCache(outDir: string): BuildCache | null {
|
|
1721
|
-
try { return JSON.parse(readFileSync(join(outDir,
|
|
1722
|
+
function readBuildCache(outDir: string, platform: string): BuildCache | null {
|
|
1723
|
+
try { return JSON.parse(readFileSync(join(outDir, buildCacheFile(platform)), 'utf8')); } catch { return null; }
|
|
1722
1724
|
}
|
|
1723
|
-
function writeBuildCache(outDir: string, cache: BuildCache): void {
|
|
1724
|
-
try { writeFileSync(join(outDir,
|
|
1725
|
+
function writeBuildCache(outDir: string, platform: string, cache: BuildCache): void {
|
|
1726
|
+
try { writeFileSync(join(outDir, buildCacheFile(platform)), JSON.stringify(cache, null, 2)); } catch { /* non-fatal */ }
|
|
1725
1727
|
}
|
|
1726
1728
|
|
|
1727
1729
|
/** Discover usable physical iOS devices via devicectl (USB + network). Excludes 'unavailable'
|
|
@@ -1776,6 +1778,40 @@ function pickDevice(devices: DeviceInfo[], explicitId?: string): DeviceInfo {
|
|
|
1776
1778
|
* distribution signing) — for testing on your own device. Run the PWA build first (or via the script).
|
|
1777
1779
|
* iOS has a bespoke Debug-IPA path (below); Android delegates to `run` (NativeScript builds + installs
|
|
1778
1780
|
* + launches), so the `deploy <platform>` surface is symmetric across both. */
|
|
1781
|
+
/** The project's web-build command: explicit `webBuild` in config, else `bun run build` if the
|
|
1782
|
+
* project's package.json has a "build" script. null when there's nothing to run. */
|
|
1783
|
+
function detectWebBuildCmd(cwd: string, cfg: AppwrapConfig): string[] | null {
|
|
1784
|
+
const explicit = (cfg as { webBuild?: string }).webBuild;
|
|
1785
|
+
if (explicit) return explicit.trim().split(/\s+/);
|
|
1786
|
+
try {
|
|
1787
|
+
const pj = JSON.parse(readFileSync(join(cwd, 'package.json'), 'utf8'));
|
|
1788
|
+
if (pj.scripts?.build) return ['bun', 'run', 'build'];
|
|
1789
|
+
} catch { /* no package.json */ }
|
|
1790
|
+
return null;
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
/** Build the web bundle before a device deploy, but ONLY for a bundled loader — a `loader:'server'`
|
|
1794
|
+
* app loads the live serverUrl, so its dist is unused (building it would be wasteful + misleading).
|
|
1795
|
+
* Prints exactly what it's doing either way. `--no-web-build` skips it (ship the current dist as-is,
|
|
1796
|
+
* e.g. to hit the native build-skip fast-path when only the wrapper changed). */
|
|
1797
|
+
function buildWebIfBundled(cwd: string, cfg: AppwrapConfig, flags: Record<string, string>): void {
|
|
1798
|
+
if ((cfg.loader ?? 'app') === 'server') {
|
|
1799
|
+
console.log('ℹ loader:server — NOT building the web (the app loads serverUrl live; the bundle is unused).');
|
|
1800
|
+
return;
|
|
1801
|
+
}
|
|
1802
|
+
if ('no-web-build' in flags) {
|
|
1803
|
+
console.log('ℹ --no-web-build — shipping the CURRENT dist/ as-is (web NOT rebuilt).');
|
|
1804
|
+
return;
|
|
1805
|
+
}
|
|
1806
|
+
const cmd = detectWebBuildCmd(cwd, cfg);
|
|
1807
|
+
if (!cmd) {
|
|
1808
|
+
console.warn('⚠ Bundled loader but no web-build command (no package.json "build" script / `webBuild` config). Shipping the CURRENT dist/ — it may be STALE.');
|
|
1809
|
+
return;
|
|
1810
|
+
}
|
|
1811
|
+
console.log(`▶ ${cmd.join(' ')} (bundled loader → fresh web bundle so dist isn't stale)`);
|
|
1812
|
+
execFileSync(cmd[0], cmd.slice(1), { cwd, stdio: 'inherit' });
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1779
1815
|
/** Resolve adb: $ANDROID_HOME/$ANDROID_SDK_ROOT platform-tools, else `adb` on PATH. */
|
|
1780
1816
|
function androidAdb(): string {
|
|
1781
1817
|
const home = process.env.ANDROID_HOME || process.env.ANDROID_SDK_ROOT;
|
|
@@ -1833,21 +1869,42 @@ async function deployAndroid(cwd: string, flags: Record<string, string>): Promis
|
|
|
1833
1869
|
const adb = androidAdb();
|
|
1834
1870
|
const device = pickAndroidDevice(adb, flags.device || undefined); // fail fast before the build
|
|
1835
1871
|
|
|
1872
|
+
buildWebIfBundled(cwd, cfg, flags); // bundled → fresh web bundle; server → skip (both printed)
|
|
1836
1873
|
await sync(cwd, flags); // re-stamp config + copy latest PWA dist
|
|
1837
1874
|
stampShellConfig(outDir, { ...cfg, debug: true }); // debug: keep-awake + WebView inspector (parity with deploy ios)
|
|
1838
1875
|
|
|
1839
|
-
console.log('▶ ns build android (debug)');
|
|
1840
|
-
runNs(outDir, ['build', 'android']); // bun-installs deps if needed, then gradle build
|
|
1841
1876
|
const apk = join(outDir, 'platforms/android/app/build/outputs/apk/debug/app-debug.apk');
|
|
1842
|
-
|
|
1877
|
+
// Fingerprint build-skip (parity with deploy ios): skip the gradle build when dist + config are
|
|
1878
|
+
// unchanged since the last build. --force/-f always rebuilds. (Rebuilding the web above usually
|
|
1879
|
+
// bumps the fingerprint; pair with --no-web-build to actually hit this fast-path.)
|
|
1880
|
+
const force = 'force' in flags || 'f' in flags;
|
|
1881
|
+
const fp = buildFingerprint(cwd, cfg, outDir);
|
|
1882
|
+
const cache = readBuildCache(outDir, 'android');
|
|
1883
|
+
if (!force && existsSync(apk) && cache?.fingerprint === fp && cache?.artifactPath === apk) {
|
|
1884
|
+
console.log(`⚡ Skipping build — inputs unchanged since last build (${apk.split('/').pop()})`);
|
|
1885
|
+
} else {
|
|
1886
|
+
console.log(`▶ ns build android (debug)${force ? ' [--force]' : ''}`);
|
|
1887
|
+
runNs(outDir, ['build', 'android']); // bun-installs deps if needed, then gradle build
|
|
1888
|
+
if (!existsSync(apk)) { console.error(`✖ No APK produced at ${apk}`); process.exit(1); }
|
|
1889
|
+
writeBuildCache(outDir, 'android', { fingerprint: fp, artifactPath: apk, builtAt: new Date().toISOString() });
|
|
1890
|
+
}
|
|
1843
1891
|
|
|
1844
1892
|
console.log(`▶ installing → ${device}`);
|
|
1845
1893
|
try {
|
|
1846
|
-
|
|
1894
|
+
// Capture (not inherit) so we can recognize the MIUI install-restriction and print guidance.
|
|
1895
|
+
const out = execFileSync(adb, ['-s', device, 'install', '-r', apk], { encoding: 'utf8', stdio: ['inherit', 'pipe', 'pipe'] });
|
|
1896
|
+
process.stdout.write(out);
|
|
1847
1897
|
} catch (e: unknown) {
|
|
1848
1898
|
const log = execErrText(e);
|
|
1849
|
-
|
|
1850
|
-
|
|
1899
|
+
process.stderr.write(log);
|
|
1900
|
+
if (/INSTALL_FAILED_USER_RESTRICTED|user is restricted|canceled by user/i.test(log)) {
|
|
1901
|
+
console.error(
|
|
1902
|
+
'\n✖ Install blocked/canceled by the device (MIUI/Xiaomi restriction — NOT a build problem).\n' +
|
|
1903
|
+
' → Unlock the phone and watch for the "Install via USB?" prompt → tap OK/Install.\n' +
|
|
1904
|
+
' → Developer options: enable "Install via USB" AND "USB debugging (Security settings)".\n' +
|
|
1905
|
+
' (Toggling these pings Xiaomi servers — needs mobile data + a SIM, no VPN.)\n' +
|
|
1906
|
+
` The built APK is ready: ${apk}`
|
|
1907
|
+
);
|
|
1851
1908
|
}
|
|
1852
1909
|
process.exit(1);
|
|
1853
1910
|
}
|
|
@@ -1883,6 +1940,7 @@ async function deploy(cwd: string, flags: Record<string, string>, positionals: s
|
|
|
1883
1940
|
// Pick the device up front so we fail fast before a long build if nothing's connected.
|
|
1884
1941
|
const device = pickDevice(listIosDevices(), flags.device || undefined);
|
|
1885
1942
|
|
|
1943
|
+
buildWebIfBundled(cwd, cfg, flags); // bundled → fresh web bundle (no stale dist); server → skip (printed)
|
|
1886
1944
|
await sync(cwd, flags); // re-stamp config + copy latest PWA dist (+ vendor backend assets)
|
|
1887
1945
|
// Dev deploy → debug mode: keep-awake + WebView inspector for continuous troubleshooting.
|
|
1888
1946
|
stampShellConfig(outDir, { ...cfg, debug: true });
|
|
@@ -1896,11 +1954,11 @@ async function deploy(cwd: string, flags: Record<string, string>, positionals: s
|
|
|
1896
1954
|
const resume = 'resume' in flags || 'r' in flags;
|
|
1897
1955
|
const force = 'force' in flags || 'f' in flags;
|
|
1898
1956
|
const fp = buildFingerprint(cwd, cfg, outDir);
|
|
1899
|
-
const cache = readBuildCache(outDir);
|
|
1957
|
+
const cache = readBuildCache(outDir, 'ios');
|
|
1900
1958
|
const existingIpa = existsSync(ipaDir)
|
|
1901
1959
|
? readdirSync(ipaDir).find((f) => f.endsWith('.ipa'))
|
|
1902
1960
|
: undefined;
|
|
1903
|
-
const fingerprintMatch = !force && existingIpa && cache?.fingerprint === fp && cache?.
|
|
1961
|
+
const fingerprintMatch = !force && existingIpa && cache?.fingerprint === fp && cache?.artifactPath === join(ipaDir, existingIpa);
|
|
1904
1962
|
// --resume also accepts a missing cache file (e.g. after a manual Xcode build or first run),
|
|
1905
1963
|
// but ONLY when the fingerprint matches what's currently on disk — never skips a needed build.
|
|
1906
1964
|
const noCache = existingIpa && !cache;
|
|
@@ -1932,7 +1990,7 @@ async function deploy(cwd: string, flags: Record<string, string>, positionals: s
|
|
|
1932
1990
|
const ipa = builtIpa;
|
|
1933
1991
|
if (!ipa) { console.error(`✖ No .ipa produced in ${ipaDir}`); process.exit(1); }
|
|
1934
1992
|
const ipaPath = join(ipaDir, ipa);
|
|
1935
|
-
if (!canSkipBuild) writeBuildCache(outDir, { fingerprint: fp, ipaPath, builtAt: new Date().toISOString() });
|
|
1993
|
+
if (!canSkipBuild) writeBuildCache(outDir, 'ios', { fingerprint: fp, artifactPath: ipaPath, builtAt: new Date().toISOString() });
|
|
1936
1994
|
|
|
1937
1995
|
console.log(`▶ installing ${ipa} → ${device.name} [${device.transport}]`);
|
|
1938
1996
|
let installedViaUsbmux = false;
|
|
@@ -2061,12 +2119,28 @@ function libimobiledeviceUdid(): { udid: string; network: boolean } | null {
|
|
|
2061
2119
|
* Headless-friendly: redirect to a file and read it. */
|
|
2062
2120
|
async function logs(cwd: string, flags: Record<string, string>, positionals: string[]): Promise<void> {
|
|
2063
2121
|
const platform = positionals[0] ?? 'ios';
|
|
2064
|
-
if (platform !== 'ios') {
|
|
2065
|
-
console.error('Usage: appwrap logs ios [--once] [--native] [--device <id|name>]');
|
|
2122
|
+
if (platform !== 'ios' && platform !== 'android') {
|
|
2123
|
+
console.error('Usage: appwrap logs <ios|android> [--once] [--native] [--device <id|name>]');
|
|
2066
2124
|
process.exit(1);
|
|
2067
2125
|
}
|
|
2068
2126
|
const cfg = await loadConfig(cwd, flags);
|
|
2069
2127
|
|
|
2128
|
+
// ── Android: adb logcat — WebView console (chromium tag) by default; --native = full app logcat ──
|
|
2129
|
+
if (platform === 'android') {
|
|
2130
|
+
const adb = androidAdb();
|
|
2131
|
+
const device = pickAndroidDevice(adb, flags.device || undefined);
|
|
2132
|
+
const once = 'once' in flags;
|
|
2133
|
+
if ('native' in flags) {
|
|
2134
|
+
const pid = (() => { try { return execFileSync(adb, ['-s', device, 'shell', 'pidof', cfg.id], { encoding: 'utf8' }).trim().split(/\s+/)[0]; } catch { return ''; } })();
|
|
2135
|
+
console.log(`▶ logcat for ${cfg.id}${pid ? ` (pid ${pid})` : ' (not running — full stream)'}${once ? ' [once]' : ''} — Ctrl-C to stop.`);
|
|
2136
|
+
try { execFileSync(adb, ['-s', device, 'logcat', ...(once ? ['-d'] : []), ...(pid ? ['--pid', pid] : [])], { stdio: 'inherit' }); } catch { process.exit(1); }
|
|
2137
|
+
return;
|
|
2138
|
+
}
|
|
2139
|
+
console.log(`▶ WebView console (chromium) for ${cfg.id}${once ? ' [once]' : ''} — Ctrl-C to stop. (--native for full app logcat)`);
|
|
2140
|
+
try { execFileSync(adb, ['-s', device, 'logcat', ...(once ? ['-d'] : []), '-s', 'chromium:I'], { stdio: 'inherit' }); } catch { process.exit(1); }
|
|
2141
|
+
return;
|
|
2142
|
+
}
|
|
2143
|
+
|
|
2070
2144
|
if ('native' in flags) {
|
|
2071
2145
|
const li = libimobiledeviceUdid();
|
|
2072
2146
|
if (!li) {
|
|
@@ -2116,6 +2190,40 @@ async function logs(cwd: string, flags: Record<string, string>, positionals: str
|
|
|
2116
2190
|
|
|
2117
2191
|
/** CLI dispatch. Guarded by `import.meta.main` so importing this module (e.g. for the `AppwrapConfig`
|
|
2118
2192
|
* type via the package entry) doesn't run a command. */
|
|
2193
|
+
/** `appwrap debug <ios|android>` — point at the WebView inspector for the (debug-built) app, then
|
|
2194
|
+
* stream its console. Debug builds enable the inspector (deploy installs one); this just opens the
|
|
2195
|
+
* door + tails logs — no rebuild. Android: adb-forwards the WebView devtools socket → chrome://inspect.
|
|
2196
|
+
* iOS: prints the Safari Web Inspector path. Both then fall through to `logs` for the live console. */
|
|
2197
|
+
async function debug(cwd: string, flags: Record<string, string>, positionals: string[]): Promise<void> {
|
|
2198
|
+
const platform = positionals[0];
|
|
2199
|
+
if (platform !== 'ios' && platform !== 'android') {
|
|
2200
|
+
console.error('Usage: appwrap debug <ios|android> [--device <id|name>]');
|
|
2201
|
+
process.exit(1);
|
|
2202
|
+
}
|
|
2203
|
+
const cfg = await loadConfig(cwd, flags);
|
|
2204
|
+
if (platform === 'android') {
|
|
2205
|
+
const adb = androidAdb();
|
|
2206
|
+
const device = pickAndroidDevice(adb, flags.device || undefined);
|
|
2207
|
+
const pid = (() => { try { return execFileSync(adb, ['-s', device, 'shell', 'pidof', cfg.id], { encoding: 'utf8' }).trim().split(/\s+/)[0]; } catch { return ''; } })();
|
|
2208
|
+
if (pid) {
|
|
2209
|
+
try {
|
|
2210
|
+
execFileSync(adb, ['-s', device, 'forward', 'tcp:9222', `localabstract:webview_devtools_remote_${pid}`], { stdio: 'pipe' });
|
|
2211
|
+
console.log('✓ WebView devtools forwarded → open chrome://inspect (or http://localhost:9222) in desktop Chrome to inspect the page.');
|
|
2212
|
+
} catch {
|
|
2213
|
+
console.log('⚠ Could not forward the devtools socket — open chrome://inspect and look for the device there.');
|
|
2214
|
+
}
|
|
2215
|
+
} else {
|
|
2216
|
+
console.log(`⚠ ${cfg.id} not running — launch it (or \`appwrap deploy android\`), then open chrome://inspect.`);
|
|
2217
|
+
}
|
|
2218
|
+
console.log(' (Needs a DEBUG build — `appwrap deploy android` installs one with the inspector enabled.)\n');
|
|
2219
|
+
} else {
|
|
2220
|
+
console.log('▶ iOS WebView inspector: Safari → Develop → [your iPhone] → [the app]. Enable it first in iOS Settings → Safari → Advanced → Web Inspector.');
|
|
2221
|
+
console.log(' (Needs a DEBUG build — `appwrap deploy ios` installs one.)\n');
|
|
2222
|
+
}
|
|
2223
|
+
// Tail the live console (reuses logs' per-platform streaming).
|
|
2224
|
+
await logs(cwd, flags, positionals);
|
|
2225
|
+
}
|
|
2226
|
+
|
|
2119
2227
|
async function main(): Promise<void> {
|
|
2120
2228
|
const { command, flags, positionals } = parseArgs(process.argv.slice(2));
|
|
2121
2229
|
const cwd = process.cwd();
|
|
@@ -2148,14 +2256,20 @@ async function main(): Promise<void> {
|
|
|
2148
2256
|
case 'logs':
|
|
2149
2257
|
await logs(cwd, flags, positionals);
|
|
2150
2258
|
break;
|
|
2259
|
+
case 'debug':
|
|
2260
|
+
await debug(cwd, flags, positionals);
|
|
2261
|
+
break;
|
|
2151
2262
|
default:
|
|
2152
|
-
console.log('Usage: appwrap <init|sync|dev|run|build|deploy|release|submit|logs> [--config <path>] [--out native]\n' +
|
|
2263
|
+
console.log('Usage: appwrap <init|sync|dev|run|build|deploy|release|submit|logs|debug> [--config <path>] [--out native]\n' +
|
|
2153
2264
|
' config: appwrap.config.ts (preferred) → .js → appwrap.json\n' +
|
|
2154
2265
|
' run <ios|android> [--device <id|name>] (compile + boot in a simulator/emulator, live reload)\n' +
|
|
2155
|
-
'
|
|
2266
|
+
' deploy <ios|android> [--device <id|name>] [--no-launch] [--no-web-build] [-f] (build → install to device → launch; builds web if bundled)\n' +
|
|
2267
|
+
' build <ios|android> [--release] [--aab] (store artifact)\n' +
|
|
2156
2268
|
' release ios [--server-url <url>] [--env <name>] [--build-number <n>] (build+sign+upload to TestFlight)\n' +
|
|
2157
2269
|
' submit ios [--build-number <n>] [--submit-for-review] (promote the binary to the App Store; metadata stays in ASC)\n' +
|
|
2158
|
-
' logs ios [--once] [--native]
|
|
2270
|
+
' logs <ios|android> [--once] [--native] (stream WebView console; --native = full OS log)\n' +
|
|
2271
|
+
' debug <ios|android> [--device <id|name>] (open the WebView inspector + stream console)\n' +
|
|
2272
|
+
' dev [--url <url> | --port <p>]');
|
|
2159
2273
|
process.exit(command ? 1 : 0);
|
|
2160
2274
|
}
|
|
2161
2275
|
}
|