426e9eeabd
Voice Workbench / headless workbench (mocked backends) (push) Has been cancelled
Voice Workbench / real acoustic lane (nightly, provisioned only) (push) Has been cancelled
ci / test (push) Has been cancelled
ci / lint-and-format (push) Has been cancelled
ci / build (push) Has been cancelled
ci / dev-startup (push) Has been cancelled
gitleaks / gitleaks (push) Has been cancelled
Markdown Links / Relative Markdown Links (push) Has been cancelled
Quality (Extended) / Homepage Build (PR smoke) (push) Has been cancelled
Quality (Extended) / Comment-only diff guard (push) Has been cancelled
Quality (Extended) / Format + Type Safety Ratchet (push) Has been cancelled
Quality (Extended) / Develop Gate (secret scan + UI determinism) (push) Has been cancelled
Quality (Extended) / Develop Gate (lint) (push) Has been cancelled
Chat shell gestures / Chat shell gesture + parity e2e (push) Has been cancelled
Cloud Gateway Discord / Test (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Has been cancelled
Build Agent Image / build-and-push (push) Has been cancelled
Dev Smoke / bun run dev onboarding chat (push) Has been cancelled
Dev Smoke / Vite HMR dependency-level smoke (push) Has been cancelled
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Has been cancelled
Publish @elizaos/example-code / check_npm (push) Has been cancelled
Publish @elizaos/example-code / publish_npm (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / verify_version (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / publish_npm (push) Has been cancelled
Sandbox Live Smoke / Sandbox live smoke (push) Has been cancelled
Snap Build & Test / Build Snap (amd64) (push) Has been cancelled
Snap Build & Test / Build Snap (arm64) (push) Has been cancelled
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Has been cancelled
Cloud Gateway Webhook / Test (push) Has been cancelled
Cloud Tests / lint-and-types (push) Has been cancelled
Cloud Tests / unit-tests (push) Has been cancelled
Cloud Tests / integration-tests (push) Has been cancelled
Cloud Tests / e2e-tests (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Apps Worker (Product 2) / Determine environment (push) Has been cancelled
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Has been cancelled
Deploy Eliza Provisioning Worker / Determine environment (push) Has been cancelled
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Has been cancelled
Dev Smoke / Classify changed paths (push) Has been cancelled
supply-chain / sbom (push) Has been cancelled
supply-chain / vulnerability-scan (push) Has been cancelled
Build, Push & Deploy to Phala Cloud / build-and-push (push) Has been cancelled
Test Packaging / Validate Packaging Configs (push) Has been cancelled
Test Packaging / Build & Test PyPI Package (push) Has been cancelled
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Has been cancelled
Test Packaging / Pack & Test JS Tarballs (push) Has been cancelled
UI Fixture E2E / ui-fixture-e2e (push) Has been cancelled
UI Fixture E2E / fixture-e2e (push) Has been cancelled
UI Story Gate / story-gate (push) Has been cancelled
vault-ci / test (macos-latest) (push) Has been cancelled
vault-ci / test (ubuntu-latest) (push) Has been cancelled
vault-ci / test (windows-latest) (push) Has been cancelled
vault-ci / app-core wiring tests (push) Has been cancelled
verify-patches / verify patches/CHECKSUMS.sha256 (push) Has been cancelled
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Has been cancelled
Voice Benchmark Smoke / voice bench smoke summary (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/app-core test bun run --cwd packages/elizaos test bun run --cwd packages/cloud/shared test], app-and-cli) (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/scenario-runner test bun run --cwd packages/vault test bun run --cwd packages/security test bun run --cwd plugins/plugin-coding-tools test], framework-packages) (push) Has been cancelled
Windows CI / windows ([bun run --cwd plugins/plugin-elizacloud test bun run --cwd plugins/plugin-discord test bun run --cwd plugins/plugin-anthropic test bun run --cwd plugins/plugin-openai test bun run --cwd plugins/plugin-app-control test bun run --cwd plugins/pl… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run build --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/agent --concurrency=4 node packages/scripts/run-bash-linux-only.mjs scripts/verify-riscv64-buildpaths.sh node packages/scripts/run… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run typecheck --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/cloud-shared --concurrency=4 bun run --cwd packages/core test bun run --cwd packages/shared test], core-runtime, 75) (push) Has been cancelled
199 lines
6.9 KiB
JavaScript
199 lines
6.9 KiB
JavaScript
#!/usr/bin/env node
|
|
// Patches the Capacitor-generated iOS Info.plist for continuous-chat
|
|
// support (R10 §6.1).
|
|
//
|
|
// Capacitor regenerates ios/App/App/Info.plist on `cap sync`; this script
|
|
// runs after `cap sync ios` and idempotently inserts the keys the voice
|
|
// stack needs:
|
|
//
|
|
// UIBackgroundModes = ["audio"]
|
|
// NSMicrophoneUsageDescription
|
|
// NSSpeechRecognitionUsageDescription
|
|
//
|
|
// Without `UIBackgroundModes = audio` the AVAudioSession the TalkMode
|
|
// plugin configures (.playAndRecord / .voiceChat with .mixWithOthers +
|
|
// .duckOthers) is paused when the screen locks. With it, the session
|
|
// survives lock and continuous-chat keeps working end-to-end.
|
|
//
|
|
// The script is intentionally a small XML-aware text patcher rather than
|
|
// pulling in a plist parser. The schema is well-defined and Capacitor
|
|
// generates the same `<dict>` layout every sync; we look for the keys we
|
|
// own, add them if missing, leave anything else untouched.
|
|
|
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
import { dirname, resolve } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import { ensurePlistUrlScheme } from "../../app-core/scripts/lib/ios-plist-url-scheme.mjs";
|
|
import { readAppIdentity } from "../../app-core/scripts/lib/read-app-identity.mjs";
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
const APP_DIR = resolve(__dirname, "..");
|
|
|
|
const MIC_PURPOSE = "Eliza listens when you talk to your agent.";
|
|
const SPEECH_PURPOSE =
|
|
"Eliza transcribes your speech so you can talk to the agent.";
|
|
|
|
const KEYS =
|
|
/** @type {Array<{ key: string; value: string | string[] | boolean }>} */ ([
|
|
{ key: "UIBackgroundModes", value: ["audio"] },
|
|
{ key: "NSMicrophoneUsageDescription", value: MIC_PURPOSE },
|
|
{ key: "NSSpeechRecognitionUsageDescription", value: SPEECH_PURPOSE },
|
|
// Live Activities for the voice/dictation session (#12185 D10). Kept in
|
|
// sync with the app-core merger (scripts/mobile/ios-plist.mjs) so the two
|
|
// plist patchers agree.
|
|
{ key: "NSSupportsLiveActivities", value: true },
|
|
]);
|
|
|
|
const TARGET_PATH = resolve(__dirname, "..", "ios", "App", "App", "Info.plist");
|
|
|
|
function findInsertionPoint(xml) {
|
|
// Insert before the closing `</dict>` of the top-level `<plist>`. The
|
|
// generated file always has exactly one top-level dict; we anchor on
|
|
// the last `</dict>` followed by `</plist>`.
|
|
const m = xml.match(/<\/dict>\s*<\/plist>/);
|
|
if (!m || typeof m.index !== "number") {
|
|
throw new Error("Info.plist: could not locate top-level </dict>");
|
|
}
|
|
return m.index;
|
|
}
|
|
|
|
function hasKey(xml, key) {
|
|
return new RegExp(`<key>${escapeKey(key)}</key>`).test(xml);
|
|
}
|
|
|
|
function escapeKey(key) {
|
|
return key.replace(/[-\\^$*+?.()|[\]{}]/g, "\\$&");
|
|
}
|
|
|
|
function renderEntry({ key, value }) {
|
|
if (Array.isArray(value)) {
|
|
const items = value
|
|
.map((v) => `\t\t<string>${escapeXml(v)}</string>`)
|
|
.join("\n");
|
|
return `\t<key>${key}</key>\n\t<array>\n${items}\n\t</array>\n`;
|
|
}
|
|
if (typeof value === "boolean") {
|
|
return `\t<key>${key}</key>\n\t<${value ? "true" : "false"}/>\n`;
|
|
}
|
|
return `\t<key>${key}</key>\n\t<string>${escapeXml(value)}</string>\n`;
|
|
}
|
|
|
|
function escapeXml(value) {
|
|
return String(value)
|
|
.replace(/&/g, "&")
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">")
|
|
.replace(/"/g, """)
|
|
.replace(/'/g, "'");
|
|
}
|
|
|
|
function ensureArrayStringValue(xml, key, value) {
|
|
const keyPattern = `<key>${escapeKey(key)}</key>`;
|
|
const blockPattern = new RegExp(
|
|
`(${keyPattern}\\s*<array>)([\\s\\S]*?)(\\s*</array>)`,
|
|
);
|
|
const match = xml.match(blockPattern);
|
|
if (!match) {
|
|
return { next: xml, changed: false };
|
|
}
|
|
const [, prefix, body, suffix] = match;
|
|
if (
|
|
new RegExp(`<string>${escapeKey(escapeXml(value))}</string>`).test(body)
|
|
) {
|
|
return { next: xml, changed: false };
|
|
}
|
|
const indent = body.match(/\n(\s*)<string>/)?.[1] ?? "\t\t";
|
|
const insertedBody = `${body}\n${indent}<string>${escapeXml(value)}</string>`;
|
|
return {
|
|
next: xml.replace(blockPattern, `${prefix}${insertedBody}${suffix}`),
|
|
changed: true,
|
|
};
|
|
}
|
|
|
|
function patchPlist(xml, urlScheme) {
|
|
let changed = false;
|
|
let next = xml;
|
|
for (const entry of KEYS) {
|
|
if (hasKey(next, entry.key)) {
|
|
if (Array.isArray(entry.value)) {
|
|
for (const value of entry.value) {
|
|
const result = ensureArrayStringValue(next, entry.key, value);
|
|
next = result.next;
|
|
changed = changed || result.changed;
|
|
}
|
|
}
|
|
continue;
|
|
}
|
|
const insertAt = findInsertionPoint(next);
|
|
next = next.slice(0, insertAt) + renderEntry(entry) + next.slice(insertAt);
|
|
changed = true;
|
|
}
|
|
const nextWithScheme = ensurePlistUrlScheme(next, urlScheme);
|
|
if (nextWithScheme !== next) {
|
|
next = nextWithScheme;
|
|
changed = true;
|
|
}
|
|
return { next, changed };
|
|
}
|
|
|
|
function main() {
|
|
if (!existsSync(TARGET_PATH)) {
|
|
// Not a failure: this script is wired into `cap sync ios`; on
|
|
// workspaces that haven't generated the iOS platform yet (CI Linux
|
|
// workers, fresh checkouts that only target web/desktop), there's
|
|
// nothing to patch and we exit cleanly.
|
|
console.log(
|
|
`[patch-ios-plist] no Info.plist found at ${TARGET_PATH} — skipping.`,
|
|
);
|
|
return;
|
|
}
|
|
const original = readFileSync(TARGET_PATH, "utf8");
|
|
const { urlScheme } = readAppIdentity(APP_DIR);
|
|
const { next, changed } = patchPlist(original, urlScheme);
|
|
if (!changed) {
|
|
console.log("[patch-ios-plist] all keys already present — no changes.");
|
|
return;
|
|
}
|
|
writeFileSync(TARGET_PATH, next);
|
|
console.log(
|
|
"[patch-ios-plist] patched UIBackgroundModes, microphone/speech usage descriptions, and URL scheme.",
|
|
);
|
|
}
|
|
|
|
// Allow `node patch-ios-plist.mjs --check` to verify without writing.
|
|
const checkOnly = process.argv.includes("--check");
|
|
if (checkOnly) {
|
|
if (!existsSync(TARGET_PATH)) {
|
|
console.log("[patch-ios-plist] no Info.plist; nothing to check.");
|
|
process.exit(0);
|
|
}
|
|
const xml = readFileSync(TARGET_PATH, "utf8");
|
|
const { urlScheme } = readAppIdentity(APP_DIR);
|
|
const patched = patchPlist(xml, urlScheme);
|
|
const missingUrlScheme = ensurePlistUrlScheme(xml, urlScheme) !== xml;
|
|
if (!patched.changed && !missingUrlScheme) {
|
|
console.log("[patch-ios-plist] OK — all required keys present.");
|
|
process.exit(0);
|
|
}
|
|
const missing = KEYS.filter((k) => !hasKey(xml, k.key));
|
|
const incompleteArrays = KEYS.flatMap((entry) => {
|
|
if (!Array.isArray(entry.value) || !hasKey(xml, entry.key)) return [];
|
|
return entry.value
|
|
.filter((value) => ensureArrayStringValue(xml, entry.key, value).changed)
|
|
.map((value) => `${entry.key}:${value}`);
|
|
});
|
|
console.error(
|
|
`[patch-ios-plist] missing keys: ${[
|
|
...missing.map((k) => k.key),
|
|
...incompleteArrays,
|
|
...(missingUrlScheme ? [`CFBundleURLTypes:${urlScheme}`] : []),
|
|
].join(", ")}`,
|
|
);
|
|
process.exit(1);
|
|
}
|
|
|
|
main();
|
|
|
|
// Exports for tests.
|
|
export { findInsertionPoint, hasKey, KEYS, patchPlist };
|