Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 12:43:05 +08:00

478 lines
14 KiB
JavaScript

#!/usr/bin/env node
import { spawnSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
import process from "node:process";
import { fileURLToPath } from "node:url";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const pluginRoot = path.resolve(__dirname, "..");
const defaultRepoRoot = path.resolve(pluginRoot, "..", "..", "..");
export const IOS_SCREEN_TIME_REQUIREMENTS = Object.freeze({
entitlements: Object.freeze({
familyControls: "com.apple.developer.family-controls",
}),
frameworks: Object.freeze(["FamilyControls", "DeviceActivity"]),
extensionPoints: Object.freeze({
deviceActivityMonitor: "com.apple.deviceactivity.monitor-extension",
deviceActivityReport: "com.apple.deviceactivityui.report-extension",
}),
extensionTargets: Object.freeze({
deviceActivityMonitor: "DeviceActivityMonitorExtension",
deviceActivityReport: "DeviceActivityReportExtension",
}),
appEntitlementsRelativePath: path.join("App", "App.entitlements"),
});
export function defaultIosScreenTimeValidationPaths({
repoRootValue = defaultRepoRoot,
} = {}) {
return {
entitlementsPath: path.join(
repoRootValue,
"packages",
"app-core",
"platforms",
"ios",
"App",
"App",
"App.entitlements",
),
projectPath: path.join(
repoRootValue,
"packages",
"app-core",
"platforms",
"ios",
"App",
"App.xcodeproj",
"project.pbxproj",
),
appRootPath: path.join(
repoRootValue,
"packages",
"app-core",
"platforms",
"ios",
"App",
"App",
),
podspecPath: path.join(
repoRootValue,
"packages",
"native-plugins",
"mobile-signals",
"ElizaosCapacitorMobileSignals.podspec",
),
};
}
function escapeRegExp(value) {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function addCheck(checks, id, ok, message, skipped = false) {
checks.push({ id, ok, skipped, message });
}
function readRequiredText(filePath, label) {
if (!filePath || !fs.existsSync(filePath)) {
throw new Error(`${label} does not exist at ${filePath ?? "(not set)"}`);
}
return fs.readFileSync(filePath, "utf8");
}
function findKeyEnd(plist, key) {
const pattern = new RegExp(`<key>\\s*${escapeRegExp(key)}\\s*</key>`, "m");
const match = pattern.exec(plist);
return match ? match.index + match[0].length : -1;
}
function extractNextDict(plist, startIndex) {
const dictStart = plist.indexOf("<dict>", startIndex);
if (dictStart === -1) return null;
const tokenPattern = /<\/?dict>/g;
tokenPattern.lastIndex = dictStart;
let depth = 0;
for (;;) {
const match = tokenPattern.exec(plist);
if (!match) return null;
depth += match[0] === "<dict>" ? 1 : -1;
if (depth === 0) {
return plist.slice(dictStart, tokenPattern.lastIndex);
}
}
}
function extractDictAfterKey(plist, key) {
const keyEnd = findKeyEnd(plist, key);
if (keyEnd === -1) return null;
return extractNextDict(plist, keyEnd);
}
function plistStringValue(plist, key, { enclosingKey } = {}) {
const source = enclosingKey
? extractDictAfterKey(plist, enclosingKey)
: plist;
if (!source) return null;
const pattern = new RegExp(
`<key>\\s*${escapeRegExp(key)}\\s*</key>\\s*<string>\\s*([^<]+?)\\s*</string>`,
"m",
);
const match = pattern.exec(source);
return match ? match[1].trim() : null;
}
function plistBooleanIsTrue(plist, key, { enclosingKey } = {}) {
const source = enclosingKey
? extractDictAfterKey(plist, enclosingKey)
: plist;
if (!source) return false;
const pattern = new RegExp(
`<key>\\s*${escapeRegExp(key)}\\s*</key>\\s*<true\\s*/>`,
"m",
);
return pattern.test(source);
}
function missingRequiredEntitlements(plist, { enclosingKey } = {}) {
return Object.values(IOS_SCREEN_TIME_REQUIREMENTS.entitlements).filter(
(key) => !plistBooleanIsTrue(plist, key, { enclosingKey }),
);
}
function decodeProvisioningProfile(profilePath) {
const raw = fs.readFileSync(profilePath);
const text = raw.toString("utf8");
if (text.includes("<plist")) {
return text;
}
if (process.platform !== "darwin") {
throw new Error(
"binary provisioning profiles can only be decoded on macOS with the security tool",
);
}
const result = spawnSync("security", ["cms", "-D", "-i", profilePath], {
encoding: "utf8",
});
if (result.status !== 0) {
throw new Error(
result.stderr.trim() ||
`security cms failed with exit code ${result.status ?? 1}`,
);
}
return result.stdout;
}
function walkFiles(rootPath) {
if (!rootPath || !fs.existsSync(rootPath)) return [];
const entries = fs.readdirSync(rootPath, { withFileTypes: true });
const files = [];
for (const entry of entries) {
const entryPath = path.join(rootPath, entry.name);
if (entry.isDirectory()) {
files.push(...walkFiles(entryPath));
} else if (entry.isFile()) {
files.push(entryPath);
}
}
return files;
}
function findDeviceActivityExtensionInfoPlists(appRootPath) {
const expectedPoints = IOS_SCREEN_TIME_REQUIREMENTS.extensionPoints;
const matches = {
deviceActivityMonitor: [],
deviceActivityReport: [],
};
for (const filePath of walkFiles(appRootPath)) {
if (path.basename(filePath) !== "Info.plist") continue;
const plist = fs.readFileSync(filePath, "utf8");
const extensionPoint = plistStringValue(
plist,
"NSExtensionPointIdentifier",
{
enclosingKey: "NSExtension",
},
);
for (const [key, expected] of Object.entries(expectedPoints)) {
if (extensionPoint === expected) {
matches[key].push(filePath);
}
}
}
return matches;
}
function missingDeviceActivityExtensions(matches) {
return Object.keys(IOS_SCREEN_TIME_REQUIREMENTS.extensionPoints).filter(
(key) => !matches[key]?.length,
);
}
function relativeList(filePaths, basePath) {
return filePaths.map((filePath) => path.relative(basePath, filePath));
}
export function validateIosScreenTimeBuildWiring(options = {}) {
const defaults = defaultIosScreenTimeValidationPaths(options);
const entitlementsPath =
options.entitlementsPath ?? defaults.entitlementsPath;
const projectPath = options.projectPath ?? defaults.projectPath;
const appRootPath = options.appRootPath ?? defaults.appRootPath;
const podspecPath = options.podspecPath ?? defaults.podspecPath;
const provisioningProfilePath =
options.provisioningProfilePath ??
process.env.MOBILE_SIGNALS_IOS_PROVISIONING_PROFILE;
const requireProvisioningProfile =
options.requireProvisioningProfile ??
process.env.MOBILE_SIGNALS_REQUIRE_IOS_PROVISIONING_PROFILE === "1";
const checks = [];
try {
const entitlements = readRequiredText(entitlementsPath, "iOS entitlements");
const missing = missingRequiredEntitlements(entitlements);
addCheck(
checks,
"app-entitlements",
missing.length === 0,
missing.length === 0
? `App entitlements include ${Object.values(
IOS_SCREEN_TIME_REQUIREMENTS.entitlements,
).join(", ")}.`
: `App entitlements are missing required Screen Time keys: ${missing.join(
", ",
)}.`,
);
} catch (error) {
addCheck(checks, "app-entitlements", false, error.message);
}
try {
const project = readRequiredText(projectPath, "Xcode project");
const expected = `CODE_SIGN_ENTITLEMENTS = ${IOS_SCREEN_TIME_REQUIREMENTS.appEntitlementsRelativePath};`;
addCheck(
checks,
"xcode-entitlements-build-setting",
project.includes(expected),
project.includes(expected)
? "Xcode project signs the app target with App/App.entitlements."
: `Xcode project does not contain ${expected}`,
);
} catch (error) {
addCheck(checks, "xcode-entitlements-build-setting", false, error.message);
}
try {
const extensionPlists = findDeviceActivityExtensionInfoPlists(appRootPath);
const missing = missingDeviceActivityExtensions(extensionPlists);
const found = Object.values(extensionPlists).flat();
addCheck(
checks,
"deviceactivity-extension-info-plists",
missing.length === 0,
missing.length === 0
? `DeviceActivity extension Info.plists found: ${relativeList(
found,
appRootPath,
).join(", ")}.`
: `Missing DeviceActivity extension Info.plists for: ${missing.join(
", ",
)}. Expected NSExtensionPointIdentifier values ${Object.values(
IOS_SCREEN_TIME_REQUIREMENTS.extensionPoints,
).join(", ")} under ${appRootPath}.`,
);
} catch (error) {
addCheck(
checks,
"deviceactivity-extension-info-plists",
false,
error.message,
);
}
try {
const project = readRequiredText(projectPath, "Xcode project");
const missingTargets = Object.values(
IOS_SCREEN_TIME_REQUIREMENTS.extensionTargets,
).filter((targetName) => !project.includes(`/* ${targetName} */`));
addCheck(
checks,
"xcode-deviceactivity-extension-targets",
missingTargets.length === 0,
missingTargets.length === 0
? "Xcode project includes DeviceActivity monitor and report extension targets."
: `Xcode project is missing DeviceActivity extension targets: ${missingTargets.join(
", ",
)}.`,
);
const missingEmbeddedProducts = Object.values(
IOS_SCREEN_TIME_REQUIREMENTS.extensionTargets,
).filter((targetName) => !project.includes(`${targetName}.appex`));
addCheck(
checks,
"xcode-deviceactivity-embedded-products",
missingEmbeddedProducts.length === 0,
missingEmbeddedProducts.length === 0
? "Xcode project declares DeviceActivity extension products for app embedding."
: `Xcode project is missing embedded DeviceActivity .appex products: ${missingEmbeddedProducts
.map((targetName) => `${targetName}.appex`)
.join(", ")}.`,
);
} catch (error) {
addCheck(
checks,
"xcode-deviceactivity-extension-targets",
false,
error.message,
);
addCheck(
checks,
"xcode-deviceactivity-embedded-products",
false,
error.message,
);
}
try {
const podspec = readRequiredText(podspecPath, "mobile-signals podspec");
const missingFrameworks = IOS_SCREEN_TIME_REQUIREMENTS.frameworks.filter(
(framework) => !podspec.includes(framework),
);
addCheck(
checks,
"podspec-frameworks",
missingFrameworks.length === 0,
missingFrameworks.length === 0
? "mobile-signals podspec links FamilyControls and DeviceActivity."
: `mobile-signals podspec is missing frameworks: ${missingFrameworks.join(
", ",
)}.`,
);
} catch (error) {
addCheck(checks, "podspec-frameworks", false, error.message);
}
if (provisioningProfilePath) {
try {
if (!fs.existsSync(provisioningProfilePath)) {
throw new Error(
`Provisioning profile does not exist at ${provisioningProfilePath}`,
);
}
const profilePlist = decodeProvisioningProfile(provisioningProfilePath);
const missing = missingRequiredEntitlements(profilePlist, {
enclosingKey: "Entitlements",
});
addCheck(
checks,
"provisioning-entitlements",
missing.length === 0,
missing.length === 0
? "Provisioning profile includes required Screen Time entitlements."
: `Provisioning profile is missing required Screen Time keys: ${missing.join(
", ",
)}.`,
);
} catch (error) {
addCheck(checks, "provisioning-entitlements", false, error.message);
}
} else if (requireProvisioningProfile) {
addCheck(
checks,
"provisioning-entitlements",
false,
"MOBILE_SIGNALS_REQUIRE_IOS_PROVISIONING_PROFILE=1 but no provisioning profile was supplied.",
);
} else {
addCheck(
checks,
"provisioning-entitlements",
true,
"No provisioning profile supplied; skipping profile entitlement inspection.",
true,
);
}
const failures = checks.filter((check) => !check.ok);
return {
ok: failures.length === 0,
checks,
failures,
requirements: IOS_SCREEN_TIME_REQUIREMENTS,
};
}
export function assertIosScreenTimeBuildWiring(options = {}) {
const result = validateIosScreenTimeBuildWiring(options);
if (!result.ok) {
throw new Error(
[
"iOS Screen Time build wiring is invalid:",
...result.failures.map((failure) => `- ${failure.message}`),
].join("\n"),
);
}
return result;
}
function parseArgs(argv) {
const options = {};
for (let index = 0; index < argv.length; index += 1) {
const arg = argv[index];
const next = () => argv[++index];
if (arg === "--json") {
options.json = true;
} else if (arg === "--repo-root") {
options.repoRootValue = path.resolve(next());
} else if (arg === "--entitlements") {
options.entitlementsPath = path.resolve(next());
} else if (arg === "--project") {
options.projectPath = path.resolve(next());
} else if (arg === "--app-root") {
options.appRootPath = path.resolve(next());
} else if (arg === "--podspec") {
options.podspecPath = path.resolve(next());
} else if (arg === "--provisioning-profile") {
options.provisioningProfilePath = path.resolve(next());
} else if (arg === "--require-provisioning-profile") {
options.requireProvisioningProfile = true;
} else {
throw new Error(`Unknown argument: ${arg}`);
}
}
return options;
}
const isMain =
process.argv[1] &&
path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
if (isMain) {
try {
const options = parseArgs(process.argv.slice(2));
const result = validateIosScreenTimeBuildWiring(options);
if (options.json) {
console.log(JSON.stringify(result, null, 2));
} else {
for (const check of result.checks) {
const prefix = check.skipped ? "SKIP" : check.ok ? "OK" : "FAIL";
console.log(`[${prefix}] ${check.message}`);
}
}
if (!result.ok) {
process.exitCode = 1;
}
} catch (error) {
console.error(error instanceof Error ? error.message : String(error));
process.exitCode = 1;
}
}