@livx.cc/appwrap 0.39.12 → 0.39.14
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 +209 -16
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,11 +1778,145 @@ 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
|
+
|
|
1815
|
+
/** Resolve adb: $ANDROID_HOME/$ANDROID_SDK_ROOT platform-tools, else `adb` on PATH. */
|
|
1816
|
+
function androidAdb(): string {
|
|
1817
|
+
const home = process.env.ANDROID_HOME || process.env.ANDROID_SDK_ROOT;
|
|
1818
|
+
if (home) {
|
|
1819
|
+
const p = join(home, 'platform-tools', 'adb');
|
|
1820
|
+
if (existsSync(p)) return p;
|
|
1821
|
+
}
|
|
1822
|
+
return 'adb';
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
/** Authorized (`device` state) adb serials. Skips `unauthorized`/`offline`. */
|
|
1826
|
+
function listAndroidDevices(adb: string): string[] {
|
|
1827
|
+
try {
|
|
1828
|
+
return execFileSync(adb, ['devices'], { encoding: 'utf8' })
|
|
1829
|
+
.split('\n').slice(1)
|
|
1830
|
+
.filter((l) => /\tdevice\b/.test(l))
|
|
1831
|
+
.map((l) => l.split('\t')[0].trim())
|
|
1832
|
+
.filter(Boolean);
|
|
1833
|
+
} catch {
|
|
1834
|
+
return [];
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1838
|
+
/** Pick the target Android device (fail fast, like iOS's pickDevice): explicit --device, else the
|
|
1839
|
+
* single connected one; clear errors for none / multiple / unauthorized. */
|
|
1840
|
+
function pickAndroidDevice(adb: string, flag?: string): string {
|
|
1841
|
+
const devices = listAndroidDevices(adb);
|
|
1842
|
+
if (flag) {
|
|
1843
|
+
if (devices.includes(flag)) return flag;
|
|
1844
|
+
console.error(`✖ Device "${flag}" not connected/authorized. Authorized: ${devices.join(', ') || '(none)'}`);
|
|
1845
|
+
process.exit(1);
|
|
1846
|
+
}
|
|
1847
|
+
if (devices.length === 0) {
|
|
1848
|
+
console.error('✖ No authorized Android device. Connect via USB + accept the "Allow USB debugging" prompt (check with `adb devices`).');
|
|
1849
|
+
process.exit(1);
|
|
1850
|
+
}
|
|
1851
|
+
if (devices.length > 1) {
|
|
1852
|
+
console.error(`✖ Multiple devices — pass --device <id>: ${devices.join(', ')}`);
|
|
1853
|
+
process.exit(1);
|
|
1854
|
+
}
|
|
1855
|
+
return devices[0];
|
|
1856
|
+
}
|
|
1857
|
+
|
|
1858
|
+
/** `appwrap deploy android` — ONE-SHOT device deploy, the Android twin of `deploy ios`: sync + debug
|
|
1859
|
+
* config → `ns build android` (debug APK) → `adb install -r` to the device → launch (unless --no-launch)
|
|
1860
|
+
* → exit. Unlike `run android` (ns watch-mode), it doesn't stay attached. NOTE: like `deploy ios`, it
|
|
1861
|
+
* ships the CURRENT `dist/` — build the web first (`bun run build`, or use the `bun run android` script). */
|
|
1862
|
+
async function deployAndroid(cwd: string, flags: Record<string, string>): Promise<void> {
|
|
1863
|
+
const cfg = await loadConfig(cwd, flags);
|
|
1864
|
+
const outDir = resolve(cwd, flags.out ?? 'native');
|
|
1865
|
+
if (!existsSync(outDir)) {
|
|
1866
|
+
console.error(`✖ Wrapper not found at ${outDir} — run \`appwrap init\` first`);
|
|
1867
|
+
process.exit(1);
|
|
1868
|
+
}
|
|
1869
|
+
const adb = androidAdb();
|
|
1870
|
+
const device = pickAndroidDevice(adb, flags.device || undefined); // fail fast before the build
|
|
1871
|
+
|
|
1872
|
+
buildWebIfBundled(cwd, cfg, flags); // bundled → fresh web bundle; server → skip (both printed)
|
|
1873
|
+
await sync(cwd, flags); // re-stamp config + copy latest PWA dist
|
|
1874
|
+
stampShellConfig(outDir, { ...cfg, debug: true }); // debug: keep-awake + WebView inspector (parity with deploy ios)
|
|
1875
|
+
|
|
1876
|
+
const apk = join(outDir, 'platforms/android/app/build/outputs/apk/debug/app-debug.apk');
|
|
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
|
+
}
|
|
1891
|
+
|
|
1892
|
+
console.log(`▶ installing → ${device}`);
|
|
1893
|
+
try {
|
|
1894
|
+
execFileSync(adb, ['-s', device, 'install', '-r', apk], { stdio: 'inherit' });
|
|
1895
|
+
} catch (e: unknown) {
|
|
1896
|
+
const log = execErrText(e);
|
|
1897
|
+
if (/INSTALL_FAILED_USER_RESTRICTED|user is restricted/i.test(log)) {
|
|
1898
|
+
console.error('\n✖ Install blocked by the device. On MIUI/Xiaomi: Developer options → enable "Install via USB" (may need mobile data + a SIM, no VPN).');
|
|
1899
|
+
}
|
|
1900
|
+
process.exit(1);
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
if (!('no-launch' in flags)) {
|
|
1904
|
+
console.log(`▶ launching ${cfg.id}`);
|
|
1905
|
+
try {
|
|
1906
|
+
execFileSync(adb, ['-s', device, 'shell', 'monkey', '-p', cfg.id, '-c', 'android.intent.category.LAUNCHER', '1'], { stdio: 'ignore' });
|
|
1907
|
+
} catch {
|
|
1908
|
+
console.error('⚠ Launch failed — the app is installed; tap its icon.');
|
|
1909
|
+
}
|
|
1910
|
+
}
|
|
1911
|
+
console.log(`✓ Deployed to ${device}.`);
|
|
1912
|
+
}
|
|
1913
|
+
|
|
1779
1914
|
async function deploy(cwd: string, flags: Record<string, string>, positionals: string[]): Promise<void> {
|
|
1780
1915
|
const platform = positionals[0];
|
|
1781
1916
|
if (platform === 'android') {
|
|
1782
|
-
//
|
|
1783
|
-
|
|
1917
|
+
// Parity with `deploy ios`: a clean ONE-SHOT build → install-to-device → launch → exit (NOT `ns run`,
|
|
1918
|
+
// which is watch-mode + hangs on some devices). Mirrors the iOS path with the adb toolchain.
|
|
1919
|
+
return deployAndroid(cwd, flags);
|
|
1784
1920
|
}
|
|
1785
1921
|
if (platform !== 'ios') {
|
|
1786
1922
|
console.error('Usage: appwrap deploy <ios|android> [--device <id|name>] [--no-launch]');
|
|
@@ -1795,6 +1931,7 @@ async function deploy(cwd: string, flags: Record<string, string>, positionals: s
|
|
|
1795
1931
|
// Pick the device up front so we fail fast before a long build if nothing's connected.
|
|
1796
1932
|
const device = pickDevice(listIosDevices(), flags.device || undefined);
|
|
1797
1933
|
|
|
1934
|
+
buildWebIfBundled(cwd, cfg, flags); // bundled → fresh web bundle (no stale dist); server → skip (printed)
|
|
1798
1935
|
await sync(cwd, flags); // re-stamp config + copy latest PWA dist (+ vendor backend assets)
|
|
1799
1936
|
// Dev deploy → debug mode: keep-awake + WebView inspector for continuous troubleshooting.
|
|
1800
1937
|
stampShellConfig(outDir, { ...cfg, debug: true });
|
|
@@ -1808,11 +1945,11 @@ async function deploy(cwd: string, flags: Record<string, string>, positionals: s
|
|
|
1808
1945
|
const resume = 'resume' in flags || 'r' in flags;
|
|
1809
1946
|
const force = 'force' in flags || 'f' in flags;
|
|
1810
1947
|
const fp = buildFingerprint(cwd, cfg, outDir);
|
|
1811
|
-
const cache = readBuildCache(outDir);
|
|
1948
|
+
const cache = readBuildCache(outDir, 'ios');
|
|
1812
1949
|
const existingIpa = existsSync(ipaDir)
|
|
1813
1950
|
? readdirSync(ipaDir).find((f) => f.endsWith('.ipa'))
|
|
1814
1951
|
: undefined;
|
|
1815
|
-
const fingerprintMatch = !force && existingIpa && cache?.fingerprint === fp && cache?.
|
|
1952
|
+
const fingerprintMatch = !force && existingIpa && cache?.fingerprint === fp && cache?.artifactPath === join(ipaDir, existingIpa);
|
|
1816
1953
|
// --resume also accepts a missing cache file (e.g. after a manual Xcode build or first run),
|
|
1817
1954
|
// but ONLY when the fingerprint matches what's currently on disk — never skips a needed build.
|
|
1818
1955
|
const noCache = existingIpa && !cache;
|
|
@@ -1844,7 +1981,7 @@ async function deploy(cwd: string, flags: Record<string, string>, positionals: s
|
|
|
1844
1981
|
const ipa = builtIpa;
|
|
1845
1982
|
if (!ipa) { console.error(`✖ No .ipa produced in ${ipaDir}`); process.exit(1); }
|
|
1846
1983
|
const ipaPath = join(ipaDir, ipa);
|
|
1847
|
-
if (!canSkipBuild) writeBuildCache(outDir, { fingerprint: fp, ipaPath, builtAt: new Date().toISOString() });
|
|
1984
|
+
if (!canSkipBuild) writeBuildCache(outDir, 'ios', { fingerprint: fp, artifactPath: ipaPath, builtAt: new Date().toISOString() });
|
|
1848
1985
|
|
|
1849
1986
|
console.log(`▶ installing ${ipa} → ${device.name} [${device.transport}]`);
|
|
1850
1987
|
let installedViaUsbmux = false;
|
|
@@ -1973,12 +2110,28 @@ function libimobiledeviceUdid(): { udid: string; network: boolean } | null {
|
|
|
1973
2110
|
* Headless-friendly: redirect to a file and read it. */
|
|
1974
2111
|
async function logs(cwd: string, flags: Record<string, string>, positionals: string[]): Promise<void> {
|
|
1975
2112
|
const platform = positionals[0] ?? 'ios';
|
|
1976
|
-
if (platform !== 'ios') {
|
|
1977
|
-
console.error('Usage: appwrap logs ios [--once] [--native] [--device <id|name>]');
|
|
2113
|
+
if (platform !== 'ios' && platform !== 'android') {
|
|
2114
|
+
console.error('Usage: appwrap logs <ios|android> [--once] [--native] [--device <id|name>]');
|
|
1978
2115
|
process.exit(1);
|
|
1979
2116
|
}
|
|
1980
2117
|
const cfg = await loadConfig(cwd, flags);
|
|
1981
2118
|
|
|
2119
|
+
// ── Android: adb logcat — WebView console (chromium tag) by default; --native = full app logcat ──
|
|
2120
|
+
if (platform === 'android') {
|
|
2121
|
+
const adb = androidAdb();
|
|
2122
|
+
const device = pickAndroidDevice(adb, flags.device || undefined);
|
|
2123
|
+
const once = 'once' in flags;
|
|
2124
|
+
if ('native' in flags) {
|
|
2125
|
+
const pid = (() => { try { return execFileSync(adb, ['-s', device, 'shell', 'pidof', cfg.id], { encoding: 'utf8' }).trim().split(/\s+/)[0]; } catch { return ''; } })();
|
|
2126
|
+
console.log(`▶ logcat for ${cfg.id}${pid ? ` (pid ${pid})` : ' (not running — full stream)'}${once ? ' [once]' : ''} — Ctrl-C to stop.`);
|
|
2127
|
+
try { execFileSync(adb, ['-s', device, 'logcat', ...(once ? ['-d'] : []), ...(pid ? ['--pid', pid] : [])], { stdio: 'inherit' }); } catch { process.exit(1); }
|
|
2128
|
+
return;
|
|
2129
|
+
}
|
|
2130
|
+
console.log(`▶ WebView console (chromium) for ${cfg.id}${once ? ' [once]' : ''} — Ctrl-C to stop. (--native for full app logcat)`);
|
|
2131
|
+
try { execFileSync(adb, ['-s', device, 'logcat', ...(once ? ['-d'] : []), '-s', 'chromium:I'], { stdio: 'inherit' }); } catch { process.exit(1); }
|
|
2132
|
+
return;
|
|
2133
|
+
}
|
|
2134
|
+
|
|
1982
2135
|
if ('native' in flags) {
|
|
1983
2136
|
const li = libimobiledeviceUdid();
|
|
1984
2137
|
if (!li) {
|
|
@@ -2028,6 +2181,40 @@ async function logs(cwd: string, flags: Record<string, string>, positionals: str
|
|
|
2028
2181
|
|
|
2029
2182
|
/** CLI dispatch. Guarded by `import.meta.main` so importing this module (e.g. for the `AppwrapConfig`
|
|
2030
2183
|
* type via the package entry) doesn't run a command. */
|
|
2184
|
+
/** `appwrap debug <ios|android>` — point at the WebView inspector for the (debug-built) app, then
|
|
2185
|
+
* stream its console. Debug builds enable the inspector (deploy installs one); this just opens the
|
|
2186
|
+
* door + tails logs — no rebuild. Android: adb-forwards the WebView devtools socket → chrome://inspect.
|
|
2187
|
+
* iOS: prints the Safari Web Inspector path. Both then fall through to `logs` for the live console. */
|
|
2188
|
+
async function debug(cwd: string, flags: Record<string, string>, positionals: string[]): Promise<void> {
|
|
2189
|
+
const platform = positionals[0];
|
|
2190
|
+
if (platform !== 'ios' && platform !== 'android') {
|
|
2191
|
+
console.error('Usage: appwrap debug <ios|android> [--device <id|name>]');
|
|
2192
|
+
process.exit(1);
|
|
2193
|
+
}
|
|
2194
|
+
const cfg = await loadConfig(cwd, flags);
|
|
2195
|
+
if (platform === 'android') {
|
|
2196
|
+
const adb = androidAdb();
|
|
2197
|
+
const device = pickAndroidDevice(adb, flags.device || undefined);
|
|
2198
|
+
const pid = (() => { try { return execFileSync(adb, ['-s', device, 'shell', 'pidof', cfg.id], { encoding: 'utf8' }).trim().split(/\s+/)[0]; } catch { return ''; } })();
|
|
2199
|
+
if (pid) {
|
|
2200
|
+
try {
|
|
2201
|
+
execFileSync(adb, ['-s', device, 'forward', 'tcp:9222', `localabstract:webview_devtools_remote_${pid}`], { stdio: 'pipe' });
|
|
2202
|
+
console.log('✓ WebView devtools forwarded → open chrome://inspect (or http://localhost:9222) in desktop Chrome to inspect the page.');
|
|
2203
|
+
} catch {
|
|
2204
|
+
console.log('⚠ Could not forward the devtools socket — open chrome://inspect and look for the device there.');
|
|
2205
|
+
}
|
|
2206
|
+
} else {
|
|
2207
|
+
console.log(`⚠ ${cfg.id} not running — launch it (or \`appwrap deploy android\`), then open chrome://inspect.`);
|
|
2208
|
+
}
|
|
2209
|
+
console.log(' (Needs a DEBUG build — `appwrap deploy android` installs one with the inspector enabled.)\n');
|
|
2210
|
+
} else {
|
|
2211
|
+
console.log('▶ iOS WebView inspector: Safari → Develop → [your iPhone] → [the app]. Enable it first in iOS Settings → Safari → Advanced → Web Inspector.');
|
|
2212
|
+
console.log(' (Needs a DEBUG build — `appwrap deploy ios` installs one.)\n');
|
|
2213
|
+
}
|
|
2214
|
+
// Tail the live console (reuses logs' per-platform streaming).
|
|
2215
|
+
await logs(cwd, flags, positionals);
|
|
2216
|
+
}
|
|
2217
|
+
|
|
2031
2218
|
async function main(): Promise<void> {
|
|
2032
2219
|
const { command, flags, positionals } = parseArgs(process.argv.slice(2));
|
|
2033
2220
|
const cwd = process.cwd();
|
|
@@ -2060,14 +2247,20 @@ async function main(): Promise<void> {
|
|
|
2060
2247
|
case 'logs':
|
|
2061
2248
|
await logs(cwd, flags, positionals);
|
|
2062
2249
|
break;
|
|
2250
|
+
case 'debug':
|
|
2251
|
+
await debug(cwd, flags, positionals);
|
|
2252
|
+
break;
|
|
2063
2253
|
default:
|
|
2064
|
-
console.log('Usage: appwrap <init|sync|dev|run|build|deploy|release|submit|logs> [--config <path>] [--out native]\n' +
|
|
2254
|
+
console.log('Usage: appwrap <init|sync|dev|run|build|deploy|release|submit|logs|debug> [--config <path>] [--out native]\n' +
|
|
2065
2255
|
' config: appwrap.config.ts (preferred) → .js → appwrap.json\n' +
|
|
2066
2256
|
' run <ios|android> [--device <id|name>] (compile + boot in a simulator/emulator, live reload)\n' +
|
|
2067
|
-
'
|
|
2257
|
+
' deploy <ios|android> [--device <id|name>] [--no-launch] [--no-web-build] [-f] (build → install to device → launch; builds web if bundled)\n' +
|
|
2258
|
+
' build <ios|android> [--release] [--aab] (store artifact)\n' +
|
|
2068
2259
|
' release ios [--server-url <url>] [--env <name>] [--build-number <n>] (build+sign+upload to TestFlight)\n' +
|
|
2069
2260
|
' submit ios [--build-number <n>] [--submit-for-review] (promote the binary to the App Store; metadata stays in ASC)\n' +
|
|
2070
|
-
' logs ios [--once] [--native]
|
|
2261
|
+
' logs <ios|android> [--once] [--native] (stream WebView console; --native = full OS log)\n' +
|
|
2262
|
+
' debug <ios|android> [--device <id|name>] (open the WebView inspector + stream console)\n' +
|
|
2263
|
+
' dev [--url <url> | --port <p>]');
|
|
2071
2264
|
process.exit(command ? 1 : 0);
|
|
2072
2265
|
}
|
|
2073
2266
|
}
|