@livx.cc/appwrap 0.43.0 → 0.46.2
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/runtime/App_Resources/Android/src/main/AndroidManifest.xml +1 -0
- package/runtime-desktop/bridge-shim/build.sh +9 -0
- package/runtime-desktop/bridge-shim/content-entry.ts +87 -0
- package/runtime-desktop/bridge-shim/shim.ts +132 -0
- package/runtime-desktop/crates/webview-control/Cargo.lock +330 -0
- package/runtime-desktop/crates/webview-control/Cargo.toml +28 -0
- package/runtime-desktop/crates/webview-control/examples/host_harness.rs +245 -0
- package/runtime-desktop/crates/webview-control/src/embedded.rs +100 -0
- package/runtime-desktop/crates/webview-control/src/lib.rs +95 -0
- package/runtime-desktop/crates/webview-control/src/popup.rs +519 -0
- package/runtime-desktop/crates/webview-control/src/profiles.rs +45 -0
- package/runtime-desktop/crates/webview-control/src/wkwebview_backend.rs +497 -0
- package/runtime-desktop/src-tauri/Cargo.lock +15 -0
- package/runtime-desktop/src-tauri/Cargo.toml +5 -0
- package/runtime-desktop/src-tauri/src/bridge_mac.rs +306 -0
- package/runtime-desktop/src-tauri/src/browser_tab.rs +193 -0
- package/runtime-desktop/src-tauri/src/main.rs +206 -3
- package/runtime-desktop/src-tauri/src/popup_mac.rs +5 -466
- package/runtime-desktop/src-tauri/src/server.rs +156 -0
- package/runtime-desktop/src-tauri/tauri.conf.json +1 -0
- package/src/cli.ts +86 -13
- package/src/config.ts +46 -1
- package/src/desktop.ts +48 -1
package/src/cli.ts
CHANGED
|
@@ -1064,6 +1064,17 @@ function stampAndroidAppName(outDir: string, cfg: AppwrapConfig, req: NativeReqs
|
|
|
1064
1064
|
if (existsSync(manifest)) {
|
|
1065
1065
|
let src = readFileSync(manifest, 'utf8');
|
|
1066
1066
|
if (cfg.urlScheme) src = src.replace(/android:scheme="[^"]*"/, `android:scheme="${cfg.urlScheme}"`);
|
|
1067
|
+
// Extra OAuth redirect schemes (e.g. Google's `com.googleusercontent.apps.<client>`) as additional
|
|
1068
|
+
// BROWSABLE <data> on the deep-link filter, so the Custom Tab redirect re-enters the app instead of
|
|
1069
|
+
// web-searching an unhandled scheme. Idempotent marker → re-sync-safe. See config.oauthRedirectSchemes.
|
|
1070
|
+
{
|
|
1071
|
+
const extra = (cfg.oauthRedirectSchemes ?? []).filter(Boolean);
|
|
1072
|
+
const body = extra.map((s) => `\n\t\t\t<data android:scheme="${esc(s)}" />`).join('') + (extra.length ? '\n\t\t\t' : '');
|
|
1073
|
+
src = src.replace(
|
|
1074
|
+
/<!-- appwrap:oauth-redirect-schemes -->[\s\S]*?<!-- \/appwrap:oauth-redirect-schemes -->/,
|
|
1075
|
+
`<!-- appwrap:oauth-redirect-schemes -->${body}<!-- /appwrap:oauth-redirect-schemes -->`
|
|
1076
|
+
);
|
|
1077
|
+
}
|
|
1067
1078
|
// Supported orientation (config > manifest) on the main <activity>. Skipped when unset → keep
|
|
1068
1079
|
// the template default (free); 'any' removes the attribute, so re-sync stays idempotent.
|
|
1069
1080
|
if (cfg.orientation) src = stampAndroidOrientation(src, androidScreenOrientation(cfg.orientation));
|
|
@@ -1979,6 +1990,30 @@ function desktopCopyFilter(src: string): boolean {
|
|
|
1979
1990
|
return !/(^|\/)(target|gen|node_modules)(\/|$)/.test(rel);
|
|
1980
1991
|
}
|
|
1981
1992
|
|
|
1993
|
+
/** Resolve a `desktop.server.command` to an absolute path: an absolute/relative path is resolved
|
|
1994
|
+
* against the app root; a bare name is looked up on `path` (build-time PATH). Falls back to the bare
|
|
1995
|
+
* name (the Rust shell will surface the spawn ENOENT) when it's nowhere. */
|
|
1996
|
+
function resolveServerCommand(command: string, cwd: string, path: string): string {
|
|
1997
|
+
if (command.includes('/')) return resolve(cwd, command);
|
|
1998
|
+
for (const dir of path.split(pathDelimiter).filter(Boolean)) {
|
|
1999
|
+
const abs = join(dir, command);
|
|
2000
|
+
if (existsSync(abs)) return abs;
|
|
2001
|
+
}
|
|
2002
|
+
return command;
|
|
2003
|
+
}
|
|
2004
|
+
|
|
2005
|
+
/** The bundled splash shown while a local-server app boots (before the shell navigates to the live
|
|
2006
|
+
* server). A dependency-free, theme-neutral holding page. */
|
|
2007
|
+
function desktopSplashHtml(name: string): string {
|
|
2008
|
+
const safe = name.replace(/</g, '<').replace(/&/g, '&');
|
|
2009
|
+
return `<!doctype html><html><head><meta charset="utf-8"><title>${safe}</title>
|
|
2010
|
+
<style>html,body{height:100%;margin:0}body{display:flex;align-items:center;justify-content:center;
|
|
2011
|
+
flex-direction:column;gap:20px;background:#0b0b0f;color:#e5e5e5;font:14px -apple-system,system-ui,sans-serif}
|
|
2012
|
+
.s{width:34px;height:34px;border:3px solid #333;border-top-color:#888;border-radius:50%;animation:r .8s linear infinite}
|
|
2013
|
+
@keyframes r{to{transform:rotate(360deg)}}#e{color:#f87171;max-width:70%;text-align:center;line-height:1.5}</style>
|
|
2014
|
+
</head><body><div class="s" id="spin"></div><div id="m">Starting ${safe}…</div><div id="e"></div></body></html>`;
|
|
2015
|
+
}
|
|
2016
|
+
|
|
1982
2017
|
/** Copy the desktop template into `native-desktop/` and stamp it from the config: the Rust-read
|
|
1983
2018
|
* `shell_config.json` (identity + window), `tauri.conf.json` (productName/version/identifier +
|
|
1984
2019
|
* frontendDist), and the staged web dist (`native-desktop/dist`, referenced as `../dist`). */
|
|
@@ -1990,20 +2025,45 @@ function regenerateDesktop(cwd: string, cfg: AppwrapConfig, outDir: string, push
|
|
|
1990
2025
|
const srcTauri = join(outDir, 'src-tauri');
|
|
1991
2026
|
mkdirSync(outDir, { recursive: true });
|
|
1992
2027
|
cpSync(DESKTOP_TEMPLATE_DIR, outDir, { recursive: true, force: true, filter: desktopCopyFilter });
|
|
1993
|
-
|
|
1994
|
-
//
|
|
1995
|
-
//
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
console.error(`✖ Web dist not found at ${dist} — build the PWA first (or check pwaDist).`);
|
|
1999
|
-
process.exit(1);
|
|
2000
|
-
}
|
|
2001
|
-
const stagedDist = join(outDir, 'dist');
|
|
2002
|
-
rmSync(stagedDist, { recursive: true, force: true });
|
|
2003
|
-
cpSync(dist, stagedDist, { recursive: true, force: true });
|
|
2028
|
+
// `native-desktop/` is generated + disposable (regenerated from the template on every build) — mark
|
|
2029
|
+
// the whole tree git-invisible so a consuming repo never commits generated native code (incl. the
|
|
2030
|
+
// copied `crates/` + `src-tauri/`). A self-ignoring `*` .gitignore works regardless of the app's root
|
|
2031
|
+
// .gitignore; mirrors the mobile lane's disposable-`native/` convention. Written AFTER cpSync so it wins.
|
|
2032
|
+
writeFileSync(join(outDir, '.gitignore'), '*\n');
|
|
2004
2033
|
|
|
2005
2034
|
const shell = deriveDesktopConfig(cfg);
|
|
2006
2035
|
shell.pushSigned = pushSigned;
|
|
2036
|
+
|
|
2037
|
+
// Stage the frontendDist INTO the wrapper (mirrors mobile copyPwa) so it's a stable relative path
|
|
2038
|
+
// independent of where the app project sits. Local-server apps (`desktop.server`) have no meaningful
|
|
2039
|
+
// web dist — the window loads the live server — so we stage a tiny bundled SPLASH instead (shown
|
|
2040
|
+
// while the server boots, then the shell navigates away from it).
|
|
2041
|
+
const stagedDist = join(outDir, 'dist');
|
|
2042
|
+
rmSync(stagedDist, { recursive: true, force: true });
|
|
2043
|
+
if (shell.server) {
|
|
2044
|
+
mkdirSync(stagedDist, { recursive: true });
|
|
2045
|
+
writeFileSync(join(stagedDist, 'index.html'), desktopSplashHtml(shell.name));
|
|
2046
|
+
} else {
|
|
2047
|
+
const dist = resolve(cwd, cfg.pwaDist);
|
|
2048
|
+
if (!existsSync(dist)) {
|
|
2049
|
+
console.error(`✖ Web dist not found at ${dist} — build the PWA first (or check pwaDist).`);
|
|
2050
|
+
process.exit(1);
|
|
2051
|
+
}
|
|
2052
|
+
cpSync(dist, stagedDist, { recursive: true, force: true });
|
|
2053
|
+
}
|
|
2054
|
+
|
|
2055
|
+
// Local-server mode: resolve `command` (bare name → PATH lookup) and `cwd` to ABSOLUTE, and stamp
|
|
2056
|
+
// the build-time PATH — a GUI-launched .app inherits the minimal launchd PATH (no ~/.bun/bin), so
|
|
2057
|
+
// a bare-name spawn (or a child that itself shells out to `bun`/`node`) would fail there.
|
|
2058
|
+
if (shell.server) {
|
|
2059
|
+
const s = shell.server;
|
|
2060
|
+
s.cwd = s.cwd ? resolve(cwd, s.cwd) : cwd;
|
|
2061
|
+
s.command = resolveServerCommand(s.command, cwd, process.env.PATH ?? '');
|
|
2062
|
+
s.path = process.env.PATH ?? '';
|
|
2063
|
+
if (!existsSync(s.command)) {
|
|
2064
|
+
console.warn(`⚠ desktop.server.command not found (${s.command}) — the server will fail to spawn at launch.`);
|
|
2065
|
+
}
|
|
2066
|
+
}
|
|
2007
2067
|
// Resolve the handlers path to ABSOLUTE (against the app root): the shell binary runs from a
|
|
2008
2068
|
// different cwd, and for `build desktop` the .app runs from wherever it's installed — an absolute
|
|
2009
2069
|
// path is the only one that stays valid. The handler file is NOT copied into the wrapper/.app; the
|
|
@@ -2347,7 +2407,9 @@ async function buildDesktop(cwd: string, flags: Record<string, string>): Promise
|
|
|
2347
2407
|
ensureDesktopMacOS();
|
|
2348
2408
|
const cfg = await loadConfig(cwd, flags);
|
|
2349
2409
|
const outDir = resolve(cwd, flags.out ?? 'native-desktop');
|
|
2350
|
-
|
|
2410
|
+
// Local-server apps load the live server, not a bundled web build — skip it (regenerateDesktop
|
|
2411
|
+
// stages a boot splash for them). Otherwise build/stage the PWA as usual.
|
|
2412
|
+
if (!cfg.desktop?.server) buildWebIfBundled(cwd, cfg, flags);
|
|
2351
2413
|
// Resolve the signing lane BEFORE regeneration: shell_config.json is embedded at compile time,
|
|
2352
2414
|
// and the Rust shell's push capability must only claim 'native' when the .app really ships the
|
|
2353
2415
|
// profile-backed aps-environment entitlement (signed lane + installed macOS profile).
|
|
@@ -3524,10 +3586,21 @@ async function logs(cwd: string, flags: Record<string, string>, positionals: str
|
|
|
3524
3586
|
console.log(`▶ watching web logs from ${cfg.id} on ${device.name} (pull every 3s) — Ctrl-C to stop.`);
|
|
3525
3587
|
console.log(' [appwrap-web] = forwarded WebView console/errors. (--once = snapshot, --native = OS firehose.)');
|
|
3526
3588
|
let shown = 0;
|
|
3589
|
+
// Self-terminate when orphaned: if our controlling session dies, macOS reparents us to launchd and
|
|
3590
|
+
// this loop would otherwise poll `devicectl` FOREVER — every stale session stacking load until
|
|
3591
|
+
// CoreDeviceService pins a core at 100%. NOTE: `process.ppid` is cached at startup and does NOT
|
|
3592
|
+
// update on reparent, so probe the ORIGINAL parent's liveness with signal 0 (throws once it's gone);
|
|
3593
|
+
// also stop on a broken stdout pipe (consumer gone).
|
|
3594
|
+
const parentPid = process.ppid;
|
|
3595
|
+
const orphaned = (): boolean => { try { process.kill(parentPid, 0); return false; } catch { return true; } };
|
|
3527
3596
|
for (;;) {
|
|
3597
|
+
if (orphaned()) break; // original parent gone → we've been reparented → stop
|
|
3528
3598
|
const all = pull();
|
|
3529
3599
|
if (all.length < shown) shown = 0; // app relaunched → file reset; reprint
|
|
3530
|
-
if (all.length > shown) {
|
|
3600
|
+
if (all.length > shown) {
|
|
3601
|
+
try { process.stdout.write(all.slice(shown)); } catch { break; } // pipe closed → consumer gone
|
|
3602
|
+
shown = all.length;
|
|
3603
|
+
}
|
|
3531
3604
|
try { execFileSync('sleep', ['3']); } catch { break; }
|
|
3532
3605
|
}
|
|
3533
3606
|
}
|
package/src/config.ts
CHANGED
|
@@ -89,6 +89,42 @@ export interface AppwrapConfig {
|
|
|
89
89
|
* the app dir, so `build desktop` apps must keep their repo (+ node_modules) around at runtime —
|
|
90
90
|
* embedding the handler file into the `.app` is out of scope for phase-0. macOS-only. */
|
|
91
91
|
handlers?: string;
|
|
92
|
+
/** Local-server mode: boot a command that serves the app locally, then load it LIVE in the window
|
|
93
|
+
* (instead of the bundled `pwaDist`). The shell shows a bundled splash, spawns `command` (+`args`)
|
|
94
|
+
* in a background thread, polls `http://localhost:<port><healthPath>` until it answers, then
|
|
95
|
+
* navigates the window there; on window-close it kills the child so the stack never outlives the
|
|
96
|
+
* shell. Use for a desktop app that IS a local dev/server stack (e.g. `para-chat`). Absent → the
|
|
97
|
+
* desktop lane loads the bundled `pwaDist` as usual. The CLI resolves `command` (when a bare name)
|
|
98
|
+
* and `cwd` to absolute paths at stamp time (a GUI-launched .app inherits the minimal launchd PATH,
|
|
99
|
+
* so a bare `bun`/PATH lookup would fail). `build desktop` apps must keep the server (+ its repo)
|
|
100
|
+
* present at runtime — bundling the server INTO the .app is a future phase. macOS-only. */
|
|
101
|
+
server?: {
|
|
102
|
+
/** Command to spawn (argv[0]). A bare name is PATH-resolved at stamp time; an absolute path is used as-is. */
|
|
103
|
+
command: string;
|
|
104
|
+
/** Extra arguments passed to the command. */
|
|
105
|
+
args?: string[];
|
|
106
|
+
/** Working directory to spawn in (default: the app project root). */
|
|
107
|
+
cwd?: string;
|
|
108
|
+
/** How the shell learns the URL to load. Provide EXACTLY ONE:
|
|
109
|
+
* - `urlMarker` (preferred for servers that print their address, e.g. vite/para-chat): the shell
|
|
110
|
+
* pipes the server's stdout and, on the first line CONTAINING this marker, extracts the first
|
|
111
|
+
* `http(s)://…` token and loads it. This tracks whatever port the server auto-picked — no
|
|
112
|
+
* hardcoded port, no mismatch. e.g. `"Frontend:"` matches `Frontend: https://localhost:5050`.
|
|
113
|
+
* - `port` (+ optional `https`) for a silent server on a KNOWN fixed port: the shell TCP-polls
|
|
114
|
+
* `localhost:<port>` and loads `<http|https>://localhost:<port>`. */
|
|
115
|
+
urlMarker?: string;
|
|
116
|
+
/** Fixed port the server listens on (used only when `urlMarker` is absent). */
|
|
117
|
+
port?: number;
|
|
118
|
+
/** Load over https in fixed-`port` mode (default false = http). Ignored in `urlMarker` mode (the
|
|
119
|
+
* captured URL carries its own scheme). The server's TLS cert CA must be trusted by the system
|
|
120
|
+
* keychain and its SANs cover `localhost`/`127.0.0.1`, or the WebView shows a cert error. */
|
|
121
|
+
https?: boolean;
|
|
122
|
+
/** Health path polled for readiness (default `/`). Ready = any HTTP response (even 4xx/5xx). */
|
|
123
|
+
healthPath?: string;
|
|
124
|
+
/** Max ms to wait for the port before showing an error in the splash (default 120000 — a first-run
|
|
125
|
+
* production build can be slow). */
|
|
126
|
+
readyTimeoutMs?: number;
|
|
127
|
+
};
|
|
92
128
|
};
|
|
93
129
|
/** Custom URL scheme for deep links (e.g. "hellowrap" → hellowrap://...). */
|
|
94
130
|
urlScheme?: string;
|
|
@@ -105,6 +141,15 @@ export interface AppwrapConfig {
|
|
|
105
141
|
* `<package android:name="..."/>`. For scheme probes prefer `queryUrlSchemes` (cross-platform).
|
|
106
142
|
* No-op when absent. */
|
|
107
143
|
queryPackages?: string[];
|
|
144
|
+
/** Extra custom URL schemes the system browser (Android Chrome Custom Tabs) may redirect back to
|
|
145
|
+
* during `kit.oauth` flows — e.g. Google's reversed iOS-client id scheme
|
|
146
|
+
* `com.googleusercontent.apps.<client>`. On Android the Custom Tab does NOT auto-close; the provider
|
|
147
|
+
* redirect re-enters the app only via a manifest deep-link intent-filter, so each OAuth redirect
|
|
148
|
+
* scheme MUST be registered as a BROWSABLE VIEW `<data>` on the main activity — otherwise the
|
|
149
|
+
* redirect has no handler and the browser web-searches it (lands on google.com). These stamp as
|
|
150
|
+
* additional `<data android:scheme="..."/>` siblings of `urlScheme`'s deep-link filter. iOS needs no
|
|
151
|
+
* equivalent — ASWebAuthenticationSession intercepts the callbackScheme directly. No-op when absent. */
|
|
152
|
+
oauthRedirectSchemes?: string[];
|
|
108
153
|
/** App icon source (≥512px square png). Defaults to the largest icon in the PWA manifest. */
|
|
109
154
|
icon?: string;
|
|
110
155
|
/** Optional centered logo for the iOS launch splash — a TRANSPARENT-background png (a wordmark or
|
|
@@ -281,7 +326,7 @@ export function defineConfig(config: AppwrapConfig): AppwrapConfig {
|
|
|
281
326
|
export const KNOWN_CONFIG_KEYS: ReadonlySet<string> = new Set([
|
|
282
327
|
'appBoundDomains', 'backendOrigin', 'backgroundAudio', 'backgroundColor', 'backgroundTasks', 'buildNumber', 'debug',
|
|
283
328
|
'debugLog', 'desktop', 'devMenu', 'edgeToEdge', 'entry', 'icon', 'id', 'iosKeyboardExtraLift', 'loader', 'modules', 'name',
|
|
284
|
-
'iosEntitlements', 'neutralizeServiceWorker', 'openNewWindowsInBrowser', 'orientation', 'overrides', 'permissions',
|
|
329
|
+
'iosEntitlements', 'neutralizeServiceWorker', 'oauthRedirectSchemes', 'openNewWindowsInBrowser', 'orientation', 'overrides', 'permissions',
|
|
285
330
|
'plugins', 'push', 'pwaDist', 'queryPackages', 'queryUrlSchemes', 'serverUrl', 'signing', 'signingProfiles', 'statusBarStyle',
|
|
286
331
|
'splashIcon', 'storekitConfig', 'targetedDevices', 'teamId', 'themeColor', 'trackingDomains', 'urlScheme',
|
|
287
332
|
'usesNonExemptEncryption', 'vendorPaths', 'version',
|
package/src/desktop.ts
CHANGED
|
@@ -44,6 +44,31 @@ export interface DesktopShellConfig {
|
|
|
44
44
|
* ~/.bun/bin, so a bare `bun` spawn fails. The Rust shell falls back to `bun` when this is
|
|
45
45
|
* missing/nonexistent. */
|
|
46
46
|
handlersRuntime: string;
|
|
47
|
+
/** True when `modules` includes 'media' → the .app carries an NSMicrophoneUsageDescription so
|
|
48
|
+
* `getUserMedia` (voice mode) can pass macOS TCC. The WebKit-layer grant (popup_mac) is
|
|
49
|
+
* unconditional; this only adds the OS usage string an app that actually uses the mic needs. */
|
|
50
|
+
microphone: boolean;
|
|
51
|
+
/** Local-server mode. When present the Rust shell boots `command` (+`args`) in a background thread,
|
|
52
|
+
* polls `http://localhost:<port><healthPath>` until it answers, then navigates the window there
|
|
53
|
+
* (instead of the bundled dist); it kills the child on window-close. Absent → bundled-PWA load.
|
|
54
|
+
* `command`/`cwd` are resolved to ABSOLUTE by the CLI (see `regenerateDesktop`) because a
|
|
55
|
+
* GUI-launched .app has the minimal launchd PATH (no ~/.bun/bin). Empty when the app isn't a
|
|
56
|
+
* local-server app. */
|
|
57
|
+
server?: {
|
|
58
|
+
command: string;
|
|
59
|
+
args: string[];
|
|
60
|
+
cwd: string;
|
|
61
|
+
/** Stdout marker → capture the URL the server prints (dynamic port). '' when using fixed `port`. */
|
|
62
|
+
urlMarker: string;
|
|
63
|
+
/** Fixed port (used only when `urlMarker` is ''). 0 when unused. */
|
|
64
|
+
port: number;
|
|
65
|
+
https: boolean;
|
|
66
|
+
healthPath: string;
|
|
67
|
+
readyTimeoutMs: number;
|
|
68
|
+
/** PATH the child is spawned with — the CLI stamps its own (build-time) PATH so a GUI launch can
|
|
69
|
+
* still resolve neighbouring tools (bun, node) the launchd PATH omits. */
|
|
70
|
+
path: string;
|
|
71
|
+
};
|
|
47
72
|
}
|
|
48
73
|
|
|
49
74
|
/** Resolve the desktop shell config from an appwrap config: identity + `urlScheme` come from the
|
|
@@ -65,6 +90,21 @@ export function deriveDesktopConfig(cfg: AppwrapConfig): DesktopShellConfig {
|
|
|
65
90
|
handlers: d.handlers ?? '',
|
|
66
91
|
pushSigned: false, // stamped by the CLI when a signing identity + macOS profile resolve — see buildDesktop
|
|
67
92
|
handlersRuntime: '', // stamped by the CLI (process.execPath) when handlers is set — see regenerateDesktop
|
|
93
|
+
microphone: (cfg.modules ?? []).includes('media'),
|
|
94
|
+
// `command`/`cwd`/`path` are resolved to absolute by the CLI (regenerateDesktop); carried as authored here.
|
|
95
|
+
server: d.server
|
|
96
|
+
? {
|
|
97
|
+
command: d.server.command,
|
|
98
|
+
args: d.server.args ?? [],
|
|
99
|
+
cwd: d.server.cwd ?? '',
|
|
100
|
+
urlMarker: d.server.urlMarker ?? '',
|
|
101
|
+
port: d.server.port ?? 0,
|
|
102
|
+
https: d.server.https ?? false,
|
|
103
|
+
healthPath: d.server.healthPath ?? '/',
|
|
104
|
+
readyTimeoutMs: d.server.readyTimeoutMs ?? 120_000,
|
|
105
|
+
path: '',
|
|
106
|
+
}
|
|
107
|
+
: undefined,
|
|
68
108
|
};
|
|
69
109
|
}
|
|
70
110
|
|
|
@@ -134,6 +174,13 @@ export function buildInfoPlist(
|
|
|
134
174
|
<key>CFBundleIconName</key>
|
|
135
175
|
<string>${xmlEscape(iconName)}</string>`
|
|
136
176
|
: '';
|
|
177
|
+
// NSMicrophoneUsageDescription: required for getUserMedia (voice mode) to pass macOS TCC — without
|
|
178
|
+
// it the OS silently denies mic access (no prompt). Added only when the app declares the `media` module.
|
|
179
|
+
const mic = shell.microphone
|
|
180
|
+
? `
|
|
181
|
+
<key>NSMicrophoneUsageDescription</key>
|
|
182
|
+
<string>${xmlEscape(shell.name)} uses the microphone for voice input.</string>`
|
|
183
|
+
: '';
|
|
137
184
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
138
185
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
139
186
|
<plist version="1.0">
|
|
@@ -153,7 +200,7 @@ export function buildInfoPlist(
|
|
|
153
200
|
<key>CFBundlePackageType</key>
|
|
154
201
|
<string>APPL</string>
|
|
155
202
|
<key>NSHighResolutionCapable</key>
|
|
156
|
-
<true/>${icon}${iconAsset}${urlTypes}
|
|
203
|
+
<true/>${icon}${iconAsset}${mic}${urlTypes}
|
|
157
204
|
</dict>
|
|
158
205
|
</plist>
|
|
159
206
|
`;
|