From 0debe775499c7f16be77e7a2afcf4fc16aa4d300 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Fri, 10 Jul 2026 10:12:46 +0000 Subject: [PATCH 1/9] refactor(command-descriptor): keep owner-file claims tooling-only Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../__tests__/command-explain.test.ts | 12 +- src/commands/command-explain.ts | 3 +- .../__tests__/owner-files.test.ts | 23 ++++ src/core/command-descriptor/owner-files.ts | 114 ++++++++++++++++++ src/core/command-descriptor/registry.ts | 79 ------------ src/core/command-descriptor/types.ts | 1 - 6 files changed, 150 insertions(+), 82 deletions(-) create mode 100644 src/core/command-descriptor/__tests__/owner-files.test.ts create mode 100644 src/core/command-descriptor/owner-files.ts diff --git a/src/commands/__tests__/command-explain.test.ts b/src/commands/__tests__/command-explain.test.ts index 7c22b7f02..3331e02ae 100644 --- a/src/commands/__tests__/command-explain.test.ts +++ b/src/commands/__tests__/command-explain.test.ts @@ -3,6 +3,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { describe, expect, test } from 'vitest'; import { commandDescriptors } from '../../core/command-descriptor/registry.ts'; +import { ownerFilesForCommand } from '../../core/command-descriptor/owner-files.ts'; import { getDaemonRouteOwnerFiles } from '../../daemon/request-handler-chain.ts'; import { explainCommand as explainCommandFromMetadata, @@ -165,7 +166,7 @@ describe('explainCommand table-driven coverage', () => { const result = explainCommand(descriptor.name, { fileExists }); expect(result.found).toBe(true); if (!result.found) continue; - for (const ownerFile of descriptor.ownerFiles) { + for (const ownerFile of ownerFilesForCommand(descriptor.name)) { expect(result.explanation.files).toContain(ownerFile); } for (const file of result.explanation.files) { @@ -174,6 +175,15 @@ describe('explainCommand table-driven coverage', () => { } }); + test('owner-file claims stay tooling-only: production descriptors do not carry them', () => { + for (const descriptor of commandDescriptors) { + expect( + Object.hasOwn(descriptor, 'ownerFiles'), + `${descriptor.name} still exposes ownerFiles on the runtime descriptor`, + ).toBe(false); + } + }); + test('every daemon explanation uses its production handler module owner', () => { for (const descriptor of commandDescriptors) { if (!('daemon' in descriptor) || !descriptor.daemon) continue; diff --git a/src/commands/command-explain.ts b/src/commands/command-explain.ts index 601c2a9b8..9e60930b3 100644 --- a/src/commands/command-explain.ts +++ b/src/commands/command-explain.ts @@ -3,6 +3,7 @@ import { cliAliasesForCommand, normalizeCliCommandAlias } from '../cli-command-a import { buildCommandUsage } from '../utils/cli-usage.ts'; import type { DaemonCommandRoute } from '../daemon/daemon-command-registry.ts'; import { commandDescriptors, type Command } from '../core/command-descriptor/registry.ts'; +import { ownerFilesForCommand } from '../core/command-descriptor/owner-files.ts'; import type { CommandCapability } from '../core/capabilities.ts'; import type { CommandTimeoutPolicy } from '../core/command-descriptor/types.ts'; import { @@ -121,7 +122,7 @@ function buildCommandExplanation( ...(cliSchema ? { cli: describeCliSurface(descriptor.name, cliSchema) } : {}), files: commandFiles( descriptor.name, - descriptor.ownerFiles, + ownerFilesForCommand(descriptor.name), family?.name, 'daemon' in descriptor ? descriptor.daemon?.route : undefined, Boolean('capability' in descriptor && descriptor.capability), diff --git a/src/core/command-descriptor/__tests__/owner-files.test.ts b/src/core/command-descriptor/__tests__/owner-files.test.ts new file mode 100644 index 000000000..e4e171f4f --- /dev/null +++ b/src/core/command-descriptor/__tests__/owner-files.test.ts @@ -0,0 +1,23 @@ +import { describe, expect, test } from 'vitest'; +import { commandDescriptors } from '../registry.ts'; +import { COMMAND_OWNER_FILES, ownerFilesForCommand } from '../owner-files.ts'; + +describe('command owner-file projection', () => { + test('covers exactly the registered commands (completeness, no extras)', () => { + const declared = Object.keys(COMMAND_OWNER_FILES).sort(); + const registered = commandDescriptors.map((descriptor) => descriptor.name).sort(); + expect(declared).toEqual(registered); + }); + + test('every command maps to a non-empty owner-file list', () => { + for (const descriptor of commandDescriptors) { + expect(ownerFilesForCommand(descriptor.name).length).toBeGreaterThan(0); + } + }); + + test('the projection is not reachable from the runtime descriptor objects', () => { + for (const descriptor of commandDescriptors) { + expect(Object.hasOwn(descriptor, 'ownerFiles')).toBe(false); + } + }); +}); diff --git a/src/core/command-descriptor/owner-files.ts b/src/core/command-descriptor/owner-files.ts new file mode 100644 index 000000000..ae666fb2b --- /dev/null +++ b/src/core/command-descriptor/owner-files.ts @@ -0,0 +1,114 @@ +import type { Command } from './registry.ts'; + +/** + * Development-only owner-file navigation claims for every command (ADR 0008 + * follow-up, https://github.com/callstack/agent-device/issues/1178). + * + * These paths point a reader at the module that owns each command's surface so + * `explain:command` can render "where does this live". They are pure tooling + * metadata: nothing in the daemon/CLI runtime reads them, so they were removed + * from the production {@link CommandDescriptor} objects (and therefore from the + * emitted bundles) and colocated here instead. + * + * This is a typed projection of the descriptor registry, NOT a parallel command + * registry: the key space is the descriptor-derived {@link Command} union, so + * `satisfies Record` makes a missing or misspelled command a + * compile error and forbids owner claims for commands that do not exist. Add or + * remove a command in {@link commandDescriptors} and the typechecker forces the + * matching edit here — the same completeness guarantee the colocated field gave, + * without shipping the strings. + * + * Only `command-explain.ts` and its tests import this module. Because the + * production import graph never reaches it, the bundler drops it entirely. + */ +export const COMMAND_OWNER_FILES = { + // -- lease -- + lease_allocate: ['src/daemon/handlers/lease.ts'], + lease_heartbeat: ['src/daemon/handlers/lease.ts'], + lease_release: ['src/daemon/handlers/lease.ts'], + artifacts: ['src/commands/management/artifacts.ts'], + // -- session / inventory -- + session_list: ['src/daemon/handlers/session-inventory.ts'], + devices: ['src/commands/management/device.ts'], + capabilities: ['src/commands/management/device.ts'], + doctor: ['src/commands/management/doctor.ts'], + apps: ['src/commands/management/app.ts'], + boot: ['src/commands/management/device.ts'], + shutdown: ['src/commands/management/device.ts'], + appstate: ['src/commands/system/index.ts'], + perf: ['src/commands/perf/index.ts'], + logs: ['src/commands/observability/index.ts'], + events: ['src/commands/observability/index.ts'], + network: ['src/commands/observability/index.ts'], + audio: ['src/commands/observability/index.ts'], + replay: ['src/commands/replay/index.ts'], + test: ['src/commands/replay/index.ts'], + runtime: ['src/daemon/handlers/session-runtime-command.ts'], + clipboard: ['src/commands/system/index.ts'], + keyboard: ['src/commands/system/index.ts'], + install: ['src/commands/management/install.ts'], + reinstall: ['src/commands/management/install.ts'], + install_source: ['src/daemon/handlers/install-source.ts'], + release_materialized_paths: ['src/daemon/handlers/install-source.ts'], + push: ['src/commands/management/push.ts'], + 'trigger-app-event': ['src/commands/management/push.ts'], + open: ['src/commands/management/app.ts'], + prepare: ['src/commands/management/prepare.ts'], + batch: ['src/commands/batch/index.ts'], + close: ['src/commands/management/app.ts'], + // -- capture -- + snapshot: ['src/commands/capture/snapshot.ts'], + diff: ['src/commands/capture/diff.ts'], + wait: ['src/commands/capture/wait.ts'], + alert: ['src/commands/capture/alert.ts'], + settings: ['src/commands/capture/settings.ts'], + 'react-native': ['src/commands/react-native/index.ts'], + record: ['src/commands/recording/index.ts'], + trace: ['src/commands/recording/index.ts'], + // -- interaction -- + find: ['src/commands/interaction/index.ts'], + click: ['src/commands/interaction/index.ts'], + fill: ['src/commands/interaction/index.ts'], + longpress: ['src/commands/interaction/index.ts'], + press: ['src/commands/interaction/index.ts'], + type: ['src/commands/interaction/index.ts'], + get: ['src/commands/interaction/index.ts'], + read: ['src/daemon/handlers/interaction.ts'], + is: ['src/commands/interaction/index.ts'], + back: ['src/commands/system/index.ts'], + gesture: ['src/commands/interaction/index.ts'], + home: ['src/commands/system/index.ts'], + 'tv-remote': ['src/commands/system/index.ts'], + rotate: ['src/commands/system/index.ts'], + scroll: ['src/commands/interaction/index.ts'], + swipe: ['src/commands/interaction/index.ts'], + 'swipe-preset': ['src/core/dispatch.ts'], + pinch: ['src/core/dispatch.ts'], + focus: ['src/commands/interaction/index.ts'], + screenshot: ['src/commands/capture/screenshot.ts'], + viewport: ['src/commands/management/viewport.ts'], + pan: ['src/core/dispatch.ts'], + fling: ['src/core/dispatch.ts'], + 'rotate-gesture': ['src/core/dispatch.ts'], + 'transform-gesture': ['src/core/dispatch.ts'], + 'app-switcher': ['src/commands/system/index.ts'], + 'install-from-source': ['src/commands/management/install.ts'], + debug: ['src/commands/debugging/index.ts'], + metro: ['src/commands/metro/index.ts'], + session: ['src/commands/management/session.ts'], + // -- schema-only local CLI -- + cdp: ['src/cli/commands/agent-cdp.ts'], + auth: ['src/cli/commands/auth.ts'], + connect: ['src/cli/commands/connection.ts'], + connection: ['src/cli/commands/connection.ts'], + disconnect: ['src/cli/commands/connection.ts'], + mcp: ['src/bin.ts'], + proxy: ['src/cli/commands/proxy.ts'], + 'react-devtools': ['src/cli/commands/react-devtools.ts'], + web: ['src/cli/commands/web.ts'], +} as const satisfies Record; + +/** The owner-file claims for one command (development-only navigation metadata). */ +export function ownerFilesForCommand(command: Command): readonly [string, ...string[]] { + return COMMAND_OWNER_FILES[command]; +} diff --git a/src/core/command-descriptor/registry.ts b/src/core/command-descriptor/registry.ts index 794470739..d08a73593 100644 --- a/src/core/command-descriptor/registry.ts +++ b/src/core/command-descriptor/registry.ts @@ -203,7 +203,6 @@ const RAW_COMMAND_DESCRIPTORS = [ // -- lease (route: lease) -- { name: 'lease_allocate', - ownerFiles: ['src/daemon/handlers/lease.ts'], catalog: { group: 'internal', key: 'leaseAllocate' }, daemon: { route: 'lease', ...ADMISSION_AND_LOCK_EXEMPT }, timeoutPolicy: DEFAULT_TIMEOUT_POLICY, @@ -211,7 +210,6 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'lease_heartbeat', - ownerFiles: ['src/daemon/handlers/lease.ts'], catalog: { group: 'internal', key: 'leaseHeartbeat' }, daemon: { route: 'lease', ...ADMISSION_AND_LOCK_EXEMPT }, timeoutPolicy: DEFAULT_TIMEOUT_POLICY, @@ -219,7 +217,6 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'lease_release', - ownerFiles: ['src/daemon/handlers/lease.ts'], catalog: { group: 'internal', key: 'leaseRelease' }, daemon: { route: 'lease', ...ADMISSION_AND_LOCK_EXEMPT }, timeoutPolicy: DEFAULT_TIMEOUT_POLICY, @@ -227,7 +224,6 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'artifacts', - ownerFiles: ['src/commands/management/artifacts.ts'], catalog: { group: 'public' }, daemon: { route: 'lease', ...ADMISSION_AND_LOCK_EXEMPT }, timeoutPolicy: DEFAULT_TIMEOUT_POLICY, @@ -237,7 +233,6 @@ const RAW_COMMAND_DESCRIPTORS = [ // -- session (route: session) -- { name: 'session_list', - ownerFiles: ['src/daemon/handlers/session-inventory.ts'], catalog: { group: 'internal', key: 'sessionList' }, daemon: { route: 'session', sessionKind: 'inventory', ...REQUEST_EXECUTION_EXEMPT }, timeoutPolicy: DEFAULT_TIMEOUT_POLICY, @@ -245,7 +240,6 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'devices', - ownerFiles: ['src/commands/management/device.ts'], catalog: { group: 'public' }, daemon: { route: 'session', @@ -258,7 +252,6 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'capabilities', - ownerFiles: ['src/commands/management/device.ts'], catalog: { group: 'public' }, daemon: { route: 'session', @@ -272,7 +265,6 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'doctor', - ownerFiles: ['src/commands/management/doctor.ts'], catalog: { group: 'public' }, daemon: { route: 'session', @@ -286,7 +278,6 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'apps', - ownerFiles: ['src/commands/management/app.ts'], catalog: { group: 'public' }, daemon: { route: 'session', @@ -300,7 +291,6 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'boot', - ownerFiles: ['src/commands/management/device.ts'], catalog: { group: 'public' }, daemon: { route: 'session', sessionKind: 'state' }, capability: { @@ -313,7 +303,6 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'shutdown', - ownerFiles: ['src/commands/management/device.ts'], catalog: { group: 'public' }, daemon: { route: 'session', sessionKind: 'state' }, capability: { @@ -326,7 +315,6 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'appstate', - ownerFiles: ['src/commands/system/index.ts'], catalog: { group: 'public', key: 'appState' }, daemon: { route: 'session', sessionKind: 'state' }, timeoutPolicy: DEFAULT_TIMEOUT_POLICY, @@ -334,7 +322,6 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'perf', - ownerFiles: ['src/commands/perf/index.ts'], catalog: { group: 'public' }, daemon: { route: 'session', sessionKind: 'observability' }, capability: { apple: APPLE_SIM_AND_DEVICE, android: ANDROID_ALL, linux: LINUX_NONE }, @@ -343,7 +330,6 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'logs', - ownerFiles: ['src/commands/observability/index.ts'], catalog: { group: 'public' }, daemon: { route: 'session', sessionKind: 'observability' }, capability: { apple: APPLE_SIM_AND_DEVICE, android: ANDROID_ALL, linux: LINUX_NONE }, @@ -352,7 +338,6 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'events', - ownerFiles: ['src/commands/observability/index.ts'], catalog: { group: 'public' }, daemon: { route: 'session', @@ -365,7 +350,6 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'network', - ownerFiles: ['src/commands/observability/index.ts'], catalog: { group: 'public' }, daemon: { route: 'session', sessionKind: 'observability' }, capability: { apple: APPLE_SIM_AND_DEVICE, android: ANDROID_ALL, linux: LINUX_NONE }, @@ -374,7 +358,6 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'audio', - ownerFiles: ['src/commands/observability/index.ts'], catalog: { group: 'public' }, daemon: { route: 'session', sessionKind: 'observability' }, capability: { @@ -387,7 +370,6 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'replay', - ownerFiles: ['src/commands/replay/index.ts'], catalog: { group: 'public' }, daemon: { route: 'session', @@ -400,7 +382,6 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'test', - ownerFiles: ['src/commands/replay/index.ts'], catalog: { group: 'public' }, daemon: { route: 'session', @@ -414,7 +395,6 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'runtime', - ownerFiles: ['src/daemon/handlers/session-runtime-command.ts'], catalog: { group: 'internal' }, daemon: { route: 'session' }, timeoutPolicy: DEFAULT_TIMEOUT_POLICY, @@ -422,7 +402,6 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'clipboard', - ownerFiles: ['src/commands/system/index.ts'], catalog: { group: 'public' }, daemon: { route: 'session', replayScopedAction: true }, dispatch: {}, @@ -436,7 +415,6 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'keyboard', - ownerFiles: ['src/commands/system/index.ts'], catalog: { group: 'public' }, daemon: { route: 'session', replayScopedAction: true, androidBlockingDialogGuard: true }, dispatch: {}, @@ -450,7 +428,6 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'install', - ownerFiles: ['src/commands/management/install.ts'], catalog: { group: 'public' }, daemon: { route: 'session' }, capability: APP_INSTALL_CAPABILITY, @@ -459,7 +436,6 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'reinstall', - ownerFiles: ['src/commands/management/install.ts'], catalog: { group: 'public' }, daemon: { route: 'session' }, capability: APP_INSTALL_CAPABILITY, @@ -468,7 +444,6 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'install_source', - ownerFiles: ['src/daemon/handlers/install-source.ts'], catalog: { group: 'internal', key: 'installSource' }, daemon: { route: 'session' }, timeoutPolicy: INSTALL_TIMEOUT_POLICY, @@ -476,7 +451,6 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'release_materialized_paths', - ownerFiles: ['src/daemon/handlers/install-source.ts'], catalog: { group: 'internal', key: 'releaseMaterializedPaths' }, daemon: { route: 'session', ...REQUEST_EXECUTION_EXEMPT }, timeoutPolicy: DEFAULT_TIMEOUT_POLICY, @@ -484,7 +458,6 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'push', - ownerFiles: ['src/commands/management/push.ts'], catalog: { group: 'public' }, daemon: { route: 'session' }, dispatch: {}, @@ -498,7 +471,6 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'trigger-app-event', - ownerFiles: ['src/commands/management/push.ts'], catalog: { group: 'public', key: 'triggerAppEvent' }, daemon: { route: 'session' }, dispatch: {}, @@ -508,7 +480,6 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'open', - ownerFiles: ['src/commands/management/app.ts'], catalog: { group: 'public' }, daemon: { route: 'session', allowSessionlessDefaultDevice: allowAnyDeviceSessionless }, dispatch: {}, @@ -518,7 +489,6 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'prepare', - ownerFiles: ['src/commands/management/prepare.ts'], catalog: { group: 'public' }, daemon: { route: 'session' }, // Runner warm-up builds are the longest fixed envelope; --timeout overrides. @@ -532,7 +502,6 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'batch', - ownerFiles: ['src/commands/batch/index.ts'], catalog: { group: 'public' }, daemon: { route: 'session' }, timeoutPolicy: DEFAULT_TIMEOUT_POLICY, @@ -540,7 +509,6 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'close', - ownerFiles: ['src/commands/management/app.ts'], catalog: { group: 'public' }, daemon: { route: 'session', allowInvalidRecording: true }, dispatch: {}, @@ -552,7 +520,6 @@ const RAW_COMMAND_DESCRIPTORS = [ // -- snapshot (route: snapshot) -- { name: 'snapshot', - ownerFiles: ['src/commands/capture/snapshot.ts'], catalog: { group: 'public' }, daemon: { route: 'snapshot', replayScopedAction: true }, dispatch: {}, @@ -564,7 +531,6 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'diff', - ownerFiles: ['src/commands/capture/diff.ts'], catalog: { group: 'public' }, daemon: { route: 'snapshot', replayScopedAction: true }, capability: ALL_DEVICE_COMMAND_CAPABILITY, @@ -573,7 +539,6 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'wait', - ownerFiles: ['src/commands/capture/wait.ts'], catalog: { group: 'public' }, daemon: { route: 'snapshot', replayScopedAction: true }, capability: ALL_DEVICE_COMMAND_CAPABILITY, @@ -587,7 +552,6 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'alert', - ownerFiles: ['src/commands/capture/alert.ts'], catalog: { group: 'public' }, daemon: { route: 'snapshot', replayScopedAction: true }, capability: { @@ -600,7 +564,6 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'settings', - ownerFiles: ['src/commands/capture/settings.ts'], catalog: { group: 'public' }, daemon: { route: 'snapshot', replayScopedAction: true }, dispatch: {}, @@ -616,7 +579,6 @@ const RAW_COMMAND_DESCRIPTORS = [ // -- specialized routes -- { name: 'react-native', - ownerFiles: ['src/commands/react-native/index.ts'], catalog: { group: 'public', key: 'reactNative' }, daemon: { route: 'reactNative', replayScopedAction: true }, capability: { apple: APPLE_SIM_AND_DEVICE, android: ANDROID_ALL, linux: LINUX_NONE }, @@ -625,7 +587,6 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'record', - ownerFiles: ['src/commands/recording/index.ts'], catalog: { group: 'public' }, daemon: { route: 'recordTrace', @@ -639,7 +600,6 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'trace', - ownerFiles: ['src/commands/recording/index.ts'], catalog: { group: 'public' }, daemon: { route: 'recordTrace' }, timeoutPolicy: DEFAULT_TIMEOUT_POLICY, @@ -647,7 +607,6 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'find', - ownerFiles: ['src/commands/interaction/index.ts'], catalog: { group: 'public' }, daemon: { route: 'find', replayScopedAction: true }, capability: ALL_DEVICE_COMMAND_CAPABILITY, @@ -664,7 +623,6 @@ const RAW_COMMAND_DESCRIPTORS = [ // stuck Apple runner work. { name: 'click', - ownerFiles: ['src/commands/interaction/index.ts'], catalog: { group: 'public' }, daemon: { route: 'interaction', replayScopedAction: true, androidBlockingDialogGuard: true }, capability: { apple: APPLE_SIM_AND_DEVICE, android: ANDROID_ALL, linux: LINUX_DEVICE }, @@ -675,7 +633,6 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'fill', - ownerFiles: ['src/commands/interaction/index.ts'], catalog: { group: 'public' }, daemon: { route: 'interaction', replayScopedAction: true, androidBlockingDialogGuard: true }, dispatch: {}, @@ -687,7 +644,6 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'longpress', - ownerFiles: ['src/commands/interaction/index.ts'], catalog: { group: 'public', key: 'longPress' }, daemon: { route: 'interaction', replayScopedAction: true, androidBlockingDialogGuard: true }, dispatch: {}, @@ -698,7 +654,6 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'press', - ownerFiles: ['src/commands/interaction/index.ts'], catalog: { group: 'public' }, daemon: { route: 'interaction', replayScopedAction: true, androidBlockingDialogGuard: true }, dispatch: {}, @@ -710,7 +665,6 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'type', - ownerFiles: ['src/commands/interaction/index.ts'], catalog: { group: 'public' }, daemon: { route: 'interaction', replayScopedAction: true, androidBlockingDialogGuard: true }, dispatch: {}, @@ -720,7 +674,6 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'get', - ownerFiles: ['src/commands/interaction/index.ts'], catalog: { group: 'public' }, daemon: { route: 'interaction', replayScopedAction: true }, capability: ALL_DEVICE_COMMAND_CAPABILITY, @@ -729,7 +682,6 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'read', - ownerFiles: ['src/daemon/handlers/interaction.ts'], catalog: { group: 'dispatch-alias' }, dispatch: {}, timeoutPolicy: DEFAULT_TIMEOUT_POLICY, @@ -737,7 +689,6 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'is', - ownerFiles: ['src/commands/interaction/index.ts'], catalog: { group: 'public' }, daemon: { route: 'interaction', replayScopedAction: true }, capability: ALL_DEVICE_COMMAND_CAPABILITY, @@ -748,7 +699,6 @@ const RAW_COMMAND_DESCRIPTORS = [ // -- generic (route: generic) -- { name: 'back', - ownerFiles: ['src/commands/system/index.ts'], catalog: { group: 'public' }, daemon: { route: 'generic', replayScopedAction: true, androidBlockingDialogGuard: true }, dispatch: {}, @@ -758,7 +708,6 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'gesture', - ownerFiles: ['src/commands/interaction/index.ts'], catalog: { group: 'public' }, daemon: { route: 'generic', replayScopedAction: true, androidBlockingDialogGuard: true }, timeoutPolicy: DEFAULT_TIMEOUT_POLICY, @@ -766,7 +715,6 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'home', - ownerFiles: ['src/commands/system/index.ts'], catalog: { group: 'public' }, daemon: { route: 'generic', replayScopedAction: true, androidBlockingDialogGuard: true }, dispatch: {}, @@ -780,7 +728,6 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'tv-remote', - ownerFiles: ['src/commands/system/index.ts'], catalog: { group: 'public', key: 'tvRemote' }, daemon: { route: 'generic', replayScopedAction: true, androidBlockingDialogGuard: true }, dispatch: {}, @@ -794,7 +741,6 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'rotate', - ownerFiles: ['src/commands/system/index.ts'], catalog: { group: 'public' }, daemon: { route: 'generic', replayScopedAction: true, androidBlockingDialogGuard: true }, dispatch: {}, @@ -808,7 +754,6 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'scroll', - ownerFiles: ['src/commands/interaction/index.ts'], catalog: { group: 'public' }, daemon: { route: 'generic', replayScopedAction: true, androidBlockingDialogGuard: true }, dispatch: {}, @@ -818,7 +763,6 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'swipe', - ownerFiles: ['src/commands/interaction/index.ts'], catalog: { group: 'public' }, daemon: { route: 'generic', replayScopedAction: true, androidBlockingDialogGuard: true }, dispatch: {}, @@ -828,7 +772,6 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'swipe-preset', - ownerFiles: ['src/core/dispatch.ts'], catalog: { group: 'dispatch-alias' }, dispatch: {}, timeoutPolicy: DEFAULT_TIMEOUT_POLICY, @@ -836,7 +779,6 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'pinch', - ownerFiles: ['src/core/dispatch.ts'], catalog: { group: 'dispatch-alias' }, daemon: { route: 'generic', replayScopedAction: true, androidBlockingDialogGuard: true }, dispatch: {}, @@ -850,7 +792,6 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'focus', - ownerFiles: ['src/commands/interaction/index.ts'], catalog: { group: 'public' }, daemon: { route: 'generic', androidBlockingDialogGuard: true }, dispatch: {}, @@ -860,7 +801,6 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'screenshot', - ownerFiles: ['src/commands/capture/screenshot.ts'], catalog: { group: 'public' }, daemon: { route: 'generic', replayScopedAction: true }, dispatch: {}, @@ -870,7 +810,6 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'viewport', - ownerFiles: ['src/commands/management/viewport.ts'], catalog: { group: 'public' }, daemon: { route: 'generic', replayScopedAction: true }, dispatch: {}, @@ -880,7 +819,6 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'pan', - ownerFiles: ['src/core/dispatch.ts'], catalog: { group: 'dispatch-alias' }, daemon: { route: 'generic', androidBlockingDialogGuard: true }, dispatch: {}, @@ -890,7 +828,6 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'fling', - ownerFiles: ['src/core/dispatch.ts'], catalog: { group: 'dispatch-alias' }, daemon: { route: 'generic', androidBlockingDialogGuard: true }, dispatch: {}, @@ -900,7 +837,6 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'rotate-gesture', - ownerFiles: ['src/core/dispatch.ts'], catalog: { group: 'dispatch-alias' }, daemon: { route: 'generic', androidBlockingDialogGuard: true }, dispatch: {}, @@ -914,7 +850,6 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'transform-gesture', - ownerFiles: ['src/core/dispatch.ts'], catalog: { group: 'dispatch-alias' }, daemon: { route: 'generic', androidBlockingDialogGuard: true }, dispatch: {}, @@ -930,7 +865,6 @@ const RAW_COMMAND_DESCRIPTORS = [ // -- capability/batch-only commands (no daemon route) -- { name: 'app-switcher', - ownerFiles: ['src/commands/system/index.ts'], catalog: { group: 'public', key: 'appSwitcher' }, dispatch: {}, capability: { @@ -943,7 +877,6 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'install-from-source', - ownerFiles: ['src/commands/management/install.ts'], catalog: { group: 'public', key: 'installFromSource' }, capability: APP_INSTALL_CAPABILITY, timeoutPolicy: DEFAULT_TIMEOUT_POLICY, @@ -953,28 +886,24 @@ const RAW_COMMAND_DESCRIPTORS = [ // -- local client-backed CLI/MCP commands (no daemon route/capability) -- { name: 'debug', - ownerFiles: ['src/commands/debugging/index.ts'], catalog: { group: 'local-cli' }, timeoutPolicy: DEFAULT_TIMEOUT_POLICY, batchable: false, }, { name: 'metro', - ownerFiles: ['src/commands/metro/index.ts'], catalog: { group: 'local-cli' }, timeoutPolicy: DEFAULT_TIMEOUT_POLICY, batchable: false, }, { name: 'session', - ownerFiles: ['src/commands/management/session.ts'], catalog: { group: 'local-cli' }, timeoutPolicy: DEFAULT_TIMEOUT_POLICY, batchable: false, }, { name: 'cdp', - ownerFiles: ['src/cli/commands/agent-cdp.ts'], catalog: { group: 'local-cli' }, timeoutPolicy: DEFAULT_TIMEOUT_POLICY, batchable: false, @@ -982,7 +911,6 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'auth', - ownerFiles: ['src/cli/commands/auth.ts'], catalog: { group: 'local-cli' }, timeoutPolicy: DEFAULT_TIMEOUT_POLICY, batchable: false, @@ -990,7 +918,6 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'connect', - ownerFiles: ['src/cli/commands/connection.ts'], catalog: { group: 'local-cli' }, timeoutPolicy: DEFAULT_TIMEOUT_POLICY, batchable: false, @@ -998,7 +925,6 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'connection', - ownerFiles: ['src/cli/commands/connection.ts'], catalog: { group: 'local-cli' }, timeoutPolicy: DEFAULT_TIMEOUT_POLICY, batchable: false, @@ -1006,7 +932,6 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'disconnect', - ownerFiles: ['src/cli/commands/connection.ts'], catalog: { group: 'local-cli' }, timeoutPolicy: DEFAULT_TIMEOUT_POLICY, batchable: false, @@ -1014,7 +939,6 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'mcp', - ownerFiles: ['src/bin.ts'], catalog: { group: 'local-cli' }, timeoutPolicy: DEFAULT_TIMEOUT_POLICY, batchable: false, @@ -1022,7 +946,6 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'proxy', - ownerFiles: ['src/cli/commands/proxy.ts'], catalog: { group: 'local-cli' }, timeoutPolicy: DEFAULT_TIMEOUT_POLICY, batchable: false, @@ -1030,7 +953,6 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'react-devtools', - ownerFiles: ['src/cli/commands/react-devtools.ts'], catalog: { group: 'local-cli', key: 'reactDevtools' }, timeoutPolicy: DEFAULT_TIMEOUT_POLICY, batchable: false, @@ -1038,7 +960,6 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'web', - ownerFiles: ['src/cli/commands/web.ts'], catalog: { group: 'local-cli' }, timeoutPolicy: DEFAULT_TIMEOUT_POLICY, batchable: false, diff --git a/src/core/command-descriptor/types.ts b/src/core/command-descriptor/types.ts index ea4e32673..b934c222a 100644 --- a/src/core/command-descriptor/types.ts +++ b/src/core/command-descriptor/types.ts @@ -129,7 +129,6 @@ export type CommandDispatchFacet = { */ export type CommandDescriptor = { name: string; - ownerFiles: readonly [string, ...string[]]; daemon?: DaemonCommandTraits; capability?: CommandCapability; batchable: boolean; From f1ccb767e3b3ddfeb01a24499fb6d6afb1de3536 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Fri, 10 Jul 2026 11:08:01 +0000 Subject: [PATCH 2/9] refactor(daemon): keep daemon-route owner-file claims tooling-only Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- scripts/explain-command.ts | 2 +- .../__tests__/command-explain.test.ts | 2 +- .../__tests__/request-handler-chain.test.ts | 25 +++++++-------- src/daemon/request-handler-chain.ts | 16 ---------- src/daemon/route-owner-files.ts | 32 +++++++++++++++++++ 5 files changed, 45 insertions(+), 32 deletions(-) create mode 100644 src/daemon/route-owner-files.ts diff --git a/scripts/explain-command.ts b/scripts/explain-command.ts index 0e964b023..973a573e5 100644 --- a/scripts/explain-command.ts +++ b/scripts/explain-command.ts @@ -1,7 +1,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { explainCommand, formatCommandExplanation } from '../src/commands/command-explain.ts'; -import { getDaemonRouteOwnerFiles } from '../src/daemon/request-handler-chain.ts'; +import { getDaemonRouteOwnerFiles } from '../src/daemon/route-owner-files.ts'; const repoRoot = path.resolve(import.meta.dirname, '..'); const args = process.argv.slice(2); diff --git a/src/commands/__tests__/command-explain.test.ts b/src/commands/__tests__/command-explain.test.ts index 3331e02ae..84f493990 100644 --- a/src/commands/__tests__/command-explain.test.ts +++ b/src/commands/__tests__/command-explain.test.ts @@ -4,7 +4,7 @@ import path from 'node:path'; import { describe, expect, test } from 'vitest'; import { commandDescriptors } from '../../core/command-descriptor/registry.ts'; import { ownerFilesForCommand } from '../../core/command-descriptor/owner-files.ts'; -import { getDaemonRouteOwnerFiles } from '../../daemon/request-handler-chain.ts'; +import { getDaemonRouteOwnerFiles } from '../../daemon/route-owner-files.ts'; import { explainCommand as explainCommandFromMetadata, formatCommandExplanation, diff --git a/src/daemon/__tests__/request-handler-chain.test.ts b/src/daemon/__tests__/request-handler-chain.test.ts index 286601d81..5050eeaf9 100644 --- a/src/daemon/__tests__/request-handler-chain.test.ts +++ b/src/daemon/__tests__/request-handler-chain.test.ts @@ -3,7 +3,8 @@ import fs from 'node:fs'; import { test } from 'vitest'; import { INTERNAL_COMMANDS } from '../../command-catalog.ts'; import { LeaseRegistry } from '../lease-registry.ts'; -import { getDaemonRouteOwnerFiles, runRequestHandlerChain } from '../request-handler-chain.ts'; +import { runRequestHandlerChain } from '../request-handler-chain.ts'; +import { getDaemonRouteOwnerFiles } from '../route-owner-files.ts'; import type { DaemonRequest, DaemonResponse } from '../types.ts'; import { makeIosSession } from '../../__tests__/test-utils/index.ts'; import { makeSessionStore } from '../../__tests__/test-utils/store-factory.ts'; @@ -36,28 +37,24 @@ function makeChainParams(req: DaemonRequest) { test('route owner files match the production module loaders', () => { const source = fs.readFileSync(new URL('../request-handler-chain.ts', import.meta.url), 'utf8'); const definitions = [ - ...source.matchAll( - /(\w+): defineDaemonRoute\(\{\s+ownerFile: '([^']+)',\s+load: \(\) => import\('([^']+)'\),/g, - ), + ...source.matchAll(/(\w+): defineDaemonRoute\(\{\s+load: \(\) => import\('([^']+)'\),/g), ]; const ownerFiles = getDaemonRouteOwnerFiles(); const genericModulePath = /import \* as genericRequestHandlerModule from '([^']+)'/.exec( source, )?.[1]; - const genericOwnerFile = - /generic: defineDaemonRoute\(\{\s+ownerFile: '([^']+)',\s+load: async \(\) => genericRequestHandlerModule,/.exec( + const genericRouteMatches = + /generic: defineDaemonRoute\(\{\s+load: async \(\) => genericRequestHandlerModule,/.test( source, - )?.[1]; + ); assert.equal(definitions.length + 1, Object.keys(ownerFiles).length); - for (const [, route, ownerFile, modulePath] of definitions) { - assert.ok(route && ownerFile && modulePath); - assert.equal(ownerFile, `src/daemon/${modulePath.slice(2)}`); - assert.equal(ownerFiles[route as keyof typeof ownerFiles], ownerFile); + for (const [, route, modulePath] of definitions) { + assert.ok(route && modulePath); + assert.equal(ownerFiles[route as keyof typeof ownerFiles], `src/daemon/${modulePath.slice(2)}`); } - assert.ok(genericModulePath && genericOwnerFile); - assert.equal(genericOwnerFile, `src/daemon/${genericModulePath.slice(2)}`); - assert.equal(ownerFiles.generic, genericOwnerFile); + assert.ok(genericModulePath && genericRouteMatches); + assert.equal(ownerFiles.generic, `src/daemon/${genericModulePath.slice(2)}`); }); test('request handler chain routes trace commands to the record-trace family', async () => { diff --git a/src/daemon/request-handler-chain.ts b/src/daemon/request-handler-chain.ts index 65dc2ce4d..dc0bba286 100644 --- a/src/daemon/request-handler-chain.ts +++ b/src/daemon/request-handler-chain.ts @@ -30,42 +30,34 @@ type RequestHandlerChainParams = { const DAEMON_ROUTE_HANDLERS = { lease: defineDaemonRoute({ - ownerFile: 'src/daemon/handlers/lease.ts', load: () => import('./handlers/lease.ts'), run: runLeaseHandler, }), session: defineDaemonRoute({ - ownerFile: 'src/daemon/handlers/session.ts', load: () => import('./handlers/session.ts'), run: runSessionHandler, }), snapshot: defineDaemonRoute({ - ownerFile: 'src/daemon/handlers/snapshot.ts', load: () => import('./handlers/snapshot.ts'), run: runSnapshotHandler, }), reactNative: defineDaemonRoute({ - ownerFile: 'src/daemon/handlers/react-native.ts', load: () => import('./handlers/react-native.ts'), run: runReactNativeHandler, }), recordTrace: defineDaemonRoute({ - ownerFile: 'src/daemon/handlers/record-trace.ts', load: () => import('./handlers/record-trace.ts'), run: runRecordTraceHandler, }), find: defineDaemonRoute({ - ownerFile: 'src/daemon/handlers/find.ts', load: () => import('./handlers/find.ts'), run: runFindHandler, }), interaction: defineDaemonRoute({ - ownerFile: 'src/daemon/handlers/interaction.ts', load: () => import('./handlers/interaction.ts'), run: runInteractionHandler, }), generic: defineDaemonRoute({ - ownerFile: 'src/daemon/request-generic-dispatch.ts', load: async () => genericRequestHandlerModule, run: async () => null, }), @@ -80,12 +72,6 @@ export async function runRequestHandlerChain( return await DAEMON_ROUTE_HANDLERS[route].run(params); } -export function getDaemonRouteOwnerFiles(): Record { - const routes = Object.keys(DAEMON_ROUTE_HANDLERS) as DaemonCommandRoute[]; - const entries = routes.map((route) => [route, DAEMON_ROUTE_HANDLERS[route].ownerFile] as const); - return Object.fromEntries(entries) as Record; -} - export async function loadGenericRequestHandlerModule(): Promise< typeof import('./request-generic-dispatch.ts') > { @@ -215,13 +201,11 @@ async function runInteractionHandler( } function defineDaemonRoute(definition: { - ownerFile: string; load: () => Promise; run: (module: TModule, params: RequestHandlerChainParams) => Promise; }) { const loadModule = lazyImport(definition.load); return { - ownerFile: definition.ownerFile, loadModule, run: async (params: RequestHandlerChainParams) => await definition.run(await loadModule(), params), diff --git a/src/daemon/route-owner-files.ts b/src/daemon/route-owner-files.ts new file mode 100644 index 000000000..bd39a3bc2 --- /dev/null +++ b/src/daemon/route-owner-files.ts @@ -0,0 +1,32 @@ +import type { DaemonCommandRoute } from './request-handler-chain.ts'; + +/** + * Development-only owner-file navigation claims for each daemon route (ADR 0008 + * follow-up, https://github.com/callstack/agent-device/issues/1178). + * + * Each route's runtime binding is its lazy `load` loader in + * {@link DAEMON_ROUTE_HANDLERS}; the owner-file path is pure tooling metadata + * that only `explain:command` consumes. Keeping it inline on the route object + * shipped these strings in `dist/src/internal/daemon.js`, so — like the + * per-command claims in `command-descriptor/owner-files.ts` — they live here in + * a module the production import graph never reaches, and the bundler drops them. + * + * `satisfies Record` keeps the map complete: adding + * or renaming a route in {@link DAEMON_ROUTE_HANDLERS} is a compile error until + * this map matches. The `request-handler-chain` parity test additionally asserts + * each path still points at the module that route's loader imports. + */ +const DAEMON_ROUTE_OWNER_FILES = { + lease: 'src/daemon/handlers/lease.ts', + session: 'src/daemon/handlers/session.ts', + snapshot: 'src/daemon/handlers/snapshot.ts', + reactNative: 'src/daemon/handlers/react-native.ts', + recordTrace: 'src/daemon/handlers/record-trace.ts', + find: 'src/daemon/handlers/find.ts', + interaction: 'src/daemon/handlers/interaction.ts', + generic: 'src/daemon/request-generic-dispatch.ts', +} as const satisfies Record; + +export function getDaemonRouteOwnerFiles(): Record { + return { ...DAEMON_ROUTE_OWNER_FILES }; +} From c7764653253b27dcb178e771ca48d28a89257543 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Fri, 10 Jul 2026 11:09:47 +0000 Subject: [PATCH 3/9] test(daemon): guard against re-adding owner-file paths to the production route chain Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- src/daemon/__tests__/request-handler-chain.test.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/daemon/__tests__/request-handler-chain.test.ts b/src/daemon/__tests__/request-handler-chain.test.ts index 5050eeaf9..7103a2af5 100644 --- a/src/daemon/__tests__/request-handler-chain.test.ts +++ b/src/daemon/__tests__/request-handler-chain.test.ts @@ -55,6 +55,11 @@ test('route owner files match the production module loaders', () => { } assert.ok(genericModulePath && genericRouteMatches); assert.equal(ownerFiles.generic, `src/daemon/${genericModulePath.slice(2)}`); + + assert.ok( + !/ownerFile/.test(source), + 'owner-file paths are tooling-only: keep them in route-owner-files.ts, not the production chain module', + ); }); test('request handler chain routes trace commands to the record-trace family', async () => { From cf415469e98db114e08ea28fedd89529a9b21622 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Fri, 10 Jul 2026 13:20:13 +0000 Subject: [PATCH 4/9] refactor(command-descriptor): derive owner-file projection from colocated RAW_COMMAND_DESCRIPTORS - Keep ownerFiles on each RAW_COMMAND_DESCRIPTORS entry as the source of truth. - Add tooling-only __OWNER_FILES__ build flag so production bundles omit the ownerFiles properties entirely. - Derive COMMAND_OWNER_FILES from RAW_COMMAND_DESCRIPTORS instead of a hand-maintained parallel table. - Guard command-explain tests against leaking ownerFiles into production descriptor objects. - Enable treeshake.propertyReadSideEffects: false in tsdown to help drop the dead ownerFiles branch from production bundles. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- src/core/command-descriptor/owner-files.ts | 122 ++++----------------- src/core/command-descriptor/registry.ts | 96 +++++++++++++++- src/global.d.ts | 2 + tsdown.config.ts | 4 + 4 files changed, 121 insertions(+), 103 deletions(-) create mode 100644 src/global.d.ts diff --git a/src/core/command-descriptor/owner-files.ts b/src/core/command-descriptor/owner-files.ts index ae666fb2b..eafce4f81 100644 --- a/src/core/command-descriptor/owner-files.ts +++ b/src/core/command-descriptor/owner-files.ts @@ -1,4 +1,4 @@ -import type { Command } from './registry.ts'; +import { RAW_COMMAND_DESCRIPTORS, type Command } from './registry.ts'; /** * Development-only owner-file navigation claims for every command (ADR 0008 @@ -8,105 +8,31 @@ import type { Command } from './registry.ts'; * `explain:command` can render "where does this live". They are pure tooling * metadata: nothing in the daemon/CLI runtime reads them, so they were removed * from the production {@link CommandDescriptor} objects (and therefore from the - * emitted bundles) and colocated here instead. + * emitted bundles) and kept as a derived view on the source-of-truth registry. * - * This is a typed projection of the descriptor registry, NOT a parallel command - * registry: the key space is the descriptor-derived {@link Command} union, so - * `satisfies Record` makes a missing or misspelled command a - * compile error and forbids owner claims for commands that do not exist. Add or - * remove a command in {@link commandDescriptors} and the typechecker forces the - * matching edit here — the same completeness guarantee the colocated field gave, - * without shipping the strings. - * - * Only `command-explain.ts` and its tests import this module. Because the - * production import graph never reaches it, the bundler drops it entirely. + * The key space is the descriptor-derived {@link Command} union, so deriving the + * projection from {@link RAW_COMMAND_DESCRIPTORS} makes a missing or misspelled + * command a type error and forbids owner claims for commands that do not exist. + * Only `command-explain.ts` and its tests import this module; the production + * import graph never reaches it, so the bundler drops it entirely. */ -export const COMMAND_OWNER_FILES = { - // -- lease -- - lease_allocate: ['src/daemon/handlers/lease.ts'], - lease_heartbeat: ['src/daemon/handlers/lease.ts'], - lease_release: ['src/daemon/handlers/lease.ts'], - artifacts: ['src/commands/management/artifacts.ts'], - // -- session / inventory -- - session_list: ['src/daemon/handlers/session-inventory.ts'], - devices: ['src/commands/management/device.ts'], - capabilities: ['src/commands/management/device.ts'], - doctor: ['src/commands/management/doctor.ts'], - apps: ['src/commands/management/app.ts'], - boot: ['src/commands/management/device.ts'], - shutdown: ['src/commands/management/device.ts'], - appstate: ['src/commands/system/index.ts'], - perf: ['src/commands/perf/index.ts'], - logs: ['src/commands/observability/index.ts'], - events: ['src/commands/observability/index.ts'], - network: ['src/commands/observability/index.ts'], - audio: ['src/commands/observability/index.ts'], - replay: ['src/commands/replay/index.ts'], - test: ['src/commands/replay/index.ts'], - runtime: ['src/daemon/handlers/session-runtime-command.ts'], - clipboard: ['src/commands/system/index.ts'], - keyboard: ['src/commands/system/index.ts'], - install: ['src/commands/management/install.ts'], - reinstall: ['src/commands/management/install.ts'], - install_source: ['src/daemon/handlers/install-source.ts'], - release_materialized_paths: ['src/daemon/handlers/install-source.ts'], - push: ['src/commands/management/push.ts'], - 'trigger-app-event': ['src/commands/management/push.ts'], - open: ['src/commands/management/app.ts'], - prepare: ['src/commands/management/prepare.ts'], - batch: ['src/commands/batch/index.ts'], - close: ['src/commands/management/app.ts'], - // -- capture -- - snapshot: ['src/commands/capture/snapshot.ts'], - diff: ['src/commands/capture/diff.ts'], - wait: ['src/commands/capture/wait.ts'], - alert: ['src/commands/capture/alert.ts'], - settings: ['src/commands/capture/settings.ts'], - 'react-native': ['src/commands/react-native/index.ts'], - record: ['src/commands/recording/index.ts'], - trace: ['src/commands/recording/index.ts'], - // -- interaction -- - find: ['src/commands/interaction/index.ts'], - click: ['src/commands/interaction/index.ts'], - fill: ['src/commands/interaction/index.ts'], - longpress: ['src/commands/interaction/index.ts'], - press: ['src/commands/interaction/index.ts'], - type: ['src/commands/interaction/index.ts'], - get: ['src/commands/interaction/index.ts'], - read: ['src/daemon/handlers/interaction.ts'], - is: ['src/commands/interaction/index.ts'], - back: ['src/commands/system/index.ts'], - gesture: ['src/commands/interaction/index.ts'], - home: ['src/commands/system/index.ts'], - 'tv-remote': ['src/commands/system/index.ts'], - rotate: ['src/commands/system/index.ts'], - scroll: ['src/commands/interaction/index.ts'], - swipe: ['src/commands/interaction/index.ts'], - 'swipe-preset': ['src/core/dispatch.ts'], - pinch: ['src/core/dispatch.ts'], - focus: ['src/commands/interaction/index.ts'], - screenshot: ['src/commands/capture/screenshot.ts'], - viewport: ['src/commands/management/viewport.ts'], - pan: ['src/core/dispatch.ts'], - fling: ['src/core/dispatch.ts'], - 'rotate-gesture': ['src/core/dispatch.ts'], - 'transform-gesture': ['src/core/dispatch.ts'], - 'app-switcher': ['src/commands/system/index.ts'], - 'install-from-source': ['src/commands/management/install.ts'], - debug: ['src/commands/debugging/index.ts'], - metro: ['src/commands/metro/index.ts'], - session: ['src/commands/management/session.ts'], - // -- schema-only local CLI -- - cdp: ['src/cli/commands/agent-cdp.ts'], - auth: ['src/cli/commands/auth.ts'], - connect: ['src/cli/commands/connection.ts'], - connection: ['src/cli/commands/connection.ts'], - disconnect: ['src/cli/commands/connection.ts'], - mcp: ['src/bin.ts'], - proxy: ['src/cli/commands/proxy.ts'], - 'react-devtools': ['src/cli/commands/react-devtools.ts'], - web: ['src/cli/commands/web.ts'], -} as const satisfies Record; +type OwnerFilesFromDescriptors< + T extends readonly { readonly name: string; readonly ownerFiles?: readonly [string, ...string[]] }[], +> = { + [K in keyof T & number as T[K]['name']]: NonNullable; +}; + +const buildOwnerFiles = < + const T extends readonly { readonly name: string; readonly ownerFiles?: readonly [string, ...string[]] }[], +>( + entries: T, +): OwnerFilesFromDescriptors => + Object.fromEntries(entries.map((entry) => [entry.name, entry.ownerFiles])) as OwnerFilesFromDescriptors; + +export const COMMAND_OWNER_FILES = buildOwnerFiles(RAW_COMMAND_DESCRIPTORS) satisfies Record< + Command, + readonly [string, ...string[]] +>; /** The owner-file claims for one command (development-only navigation metadata). */ export function ownerFilesForCommand(command: Command): readonly [string, ...string[]] { diff --git a/src/core/command-descriptor/registry.ts b/src/core/command-descriptor/registry.ts index d08a73593..d488a42f1 100644 --- a/src/core/command-descriptor/registry.ts +++ b/src/core/command-descriptor/registry.ts @@ -17,6 +17,7 @@ import type { type RawCommandDescriptor = Omit & { mcpExposed?: boolean; + ownerFiles?: readonly [string, ...string[]]; }; type RawCommandCatalogGroup = T extends { catalog: { group: infer Group } } ? Group : never; @@ -199,10 +200,13 @@ function postActionObservation(command: string): PostActionObservationSupport { // array rather than recreating command-name sets. // --------------------------------------------------------------------------- -const RAW_COMMAND_DESCRIPTORS = [ +const ownerFilesEnabled = typeof __OWNER_FILES__ === 'undefined' || __OWNER_FILES__; + +export const RAW_COMMAND_DESCRIPTORS = [ // -- lease (route: lease) -- { name: 'lease_allocate', + ...(ownerFilesEnabled ? { ownerFiles: ['src/daemon/handlers/lease.ts'] as const } : {}), catalog: { group: 'internal', key: 'leaseAllocate' }, daemon: { route: 'lease', ...ADMISSION_AND_LOCK_EXEMPT }, timeoutPolicy: DEFAULT_TIMEOUT_POLICY, @@ -210,6 +214,7 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'lease_heartbeat', + ...(ownerFilesEnabled ? { ownerFiles: ['src/daemon/handlers/lease.ts'] as const } : {}), catalog: { group: 'internal', key: 'leaseHeartbeat' }, daemon: { route: 'lease', ...ADMISSION_AND_LOCK_EXEMPT }, timeoutPolicy: DEFAULT_TIMEOUT_POLICY, @@ -217,6 +222,7 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'lease_release', + ...(ownerFilesEnabled ? { ownerFiles: ['src/daemon/handlers/lease.ts'] as const } : {}), catalog: { group: 'internal', key: 'leaseRelease' }, daemon: { route: 'lease', ...ADMISSION_AND_LOCK_EXEMPT }, timeoutPolicy: DEFAULT_TIMEOUT_POLICY, @@ -224,6 +230,7 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'artifacts', + ...(ownerFilesEnabled ? { ownerFiles: ['src/commands/management/artifacts.ts'] as const } : {}), catalog: { group: 'public' }, daemon: { route: 'lease', ...ADMISSION_AND_LOCK_EXEMPT }, timeoutPolicy: DEFAULT_TIMEOUT_POLICY, @@ -233,6 +240,7 @@ const RAW_COMMAND_DESCRIPTORS = [ // -- session (route: session) -- { name: 'session_list', + ...(ownerFilesEnabled ? { ownerFiles: ['src/daemon/handlers/session-inventory.ts'] as const } : {}), catalog: { group: 'internal', key: 'sessionList' }, daemon: { route: 'session', sessionKind: 'inventory', ...REQUEST_EXECUTION_EXEMPT }, timeoutPolicy: DEFAULT_TIMEOUT_POLICY, @@ -240,6 +248,7 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'devices', + ...(ownerFilesEnabled ? { ownerFiles: ['src/commands/management/device.ts'] as const } : {}), catalog: { group: 'public' }, daemon: { route: 'session', @@ -252,6 +261,7 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'capabilities', + ...(ownerFilesEnabled ? { ownerFiles: ['src/commands/management/device.ts'] as const } : {}), catalog: { group: 'public' }, daemon: { route: 'session', @@ -265,6 +275,7 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'doctor', + ...(ownerFilesEnabled ? { ownerFiles: ['src/commands/management/doctor.ts'] as const } : {}), catalog: { group: 'public' }, daemon: { route: 'session', @@ -278,6 +289,7 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'apps', + ...(ownerFilesEnabled ? { ownerFiles: ['src/commands/management/app.ts'] as const } : {}), catalog: { group: 'public' }, daemon: { route: 'session', @@ -291,6 +303,7 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'boot', + ...(ownerFilesEnabled ? { ownerFiles: ['src/commands/management/device.ts'] as const } : {}), catalog: { group: 'public' }, daemon: { route: 'session', sessionKind: 'state' }, capability: { @@ -303,6 +316,7 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'shutdown', + ...(ownerFilesEnabled ? { ownerFiles: ['src/commands/management/device.ts'] as const } : {}), catalog: { group: 'public' }, daemon: { route: 'session', sessionKind: 'state' }, capability: { @@ -315,6 +329,7 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'appstate', + ...(ownerFilesEnabled ? { ownerFiles: ['src/commands/system/index.ts'] as const } : {}), catalog: { group: 'public', key: 'appState' }, daemon: { route: 'session', sessionKind: 'state' }, timeoutPolicy: DEFAULT_TIMEOUT_POLICY, @@ -322,6 +337,7 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'perf', + ...(ownerFilesEnabled ? { ownerFiles: ['src/commands/perf/index.ts'] as const } : {}), catalog: { group: 'public' }, daemon: { route: 'session', sessionKind: 'observability' }, capability: { apple: APPLE_SIM_AND_DEVICE, android: ANDROID_ALL, linux: LINUX_NONE }, @@ -330,6 +346,7 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'logs', + ...(ownerFilesEnabled ? { ownerFiles: ['src/commands/observability/index.ts'] as const } : {}), catalog: { group: 'public' }, daemon: { route: 'session', sessionKind: 'observability' }, capability: { apple: APPLE_SIM_AND_DEVICE, android: ANDROID_ALL, linux: LINUX_NONE }, @@ -338,6 +355,7 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'events', + ...(ownerFilesEnabled ? { ownerFiles: ['src/commands/observability/index.ts'] as const } : {}), catalog: { group: 'public' }, daemon: { route: 'session', @@ -350,6 +368,7 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'network', + ...(ownerFilesEnabled ? { ownerFiles: ['src/commands/observability/index.ts'] as const } : {}), catalog: { group: 'public' }, daemon: { route: 'session', sessionKind: 'observability' }, capability: { apple: APPLE_SIM_AND_DEVICE, android: ANDROID_ALL, linux: LINUX_NONE }, @@ -358,6 +377,7 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'audio', + ...(ownerFilesEnabled ? { ownerFiles: ['src/commands/observability/index.ts'] as const } : {}), catalog: { group: 'public' }, daemon: { route: 'session', sessionKind: 'observability' }, capability: { @@ -370,6 +390,7 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'replay', + ...(ownerFilesEnabled ? { ownerFiles: ['src/commands/replay/index.ts'] as const } : {}), catalog: { group: 'public' }, daemon: { route: 'session', @@ -382,6 +403,7 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'test', + ...(ownerFilesEnabled ? { ownerFiles: ['src/commands/replay/index.ts'] as const } : {}), catalog: { group: 'public' }, daemon: { route: 'session', @@ -395,6 +417,7 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'runtime', + ...(ownerFilesEnabled ? { ownerFiles: ['src/daemon/handlers/session-runtime-command.ts'] as const } : {}), catalog: { group: 'internal' }, daemon: { route: 'session' }, timeoutPolicy: DEFAULT_TIMEOUT_POLICY, @@ -402,6 +425,7 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'clipboard', + ...(ownerFilesEnabled ? { ownerFiles: ['src/commands/system/index.ts'] as const } : {}), catalog: { group: 'public' }, daemon: { route: 'session', replayScopedAction: true }, dispatch: {}, @@ -415,6 +439,7 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'keyboard', + ...(ownerFilesEnabled ? { ownerFiles: ['src/commands/system/index.ts'] as const } : {}), catalog: { group: 'public' }, daemon: { route: 'session', replayScopedAction: true, androidBlockingDialogGuard: true }, dispatch: {}, @@ -428,6 +453,7 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'install', + ...(ownerFilesEnabled ? { ownerFiles: ['src/commands/management/install.ts'] as const } : {}), catalog: { group: 'public' }, daemon: { route: 'session' }, capability: APP_INSTALL_CAPABILITY, @@ -436,6 +462,7 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'reinstall', + ...(ownerFilesEnabled ? { ownerFiles: ['src/commands/management/install.ts'] as const } : {}), catalog: { group: 'public' }, daemon: { route: 'session' }, capability: APP_INSTALL_CAPABILITY, @@ -444,6 +471,7 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'install_source', + ...(ownerFilesEnabled ? { ownerFiles: ['src/daemon/handlers/install-source.ts'] as const } : {}), catalog: { group: 'internal', key: 'installSource' }, daemon: { route: 'session' }, timeoutPolicy: INSTALL_TIMEOUT_POLICY, @@ -451,6 +479,7 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'release_materialized_paths', + ...(ownerFilesEnabled ? { ownerFiles: ['src/daemon/handlers/install-source.ts'] as const } : {}), catalog: { group: 'internal', key: 'releaseMaterializedPaths' }, daemon: { route: 'session', ...REQUEST_EXECUTION_EXEMPT }, timeoutPolicy: DEFAULT_TIMEOUT_POLICY, @@ -458,6 +487,7 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'push', + ...(ownerFilesEnabled ? { ownerFiles: ['src/commands/management/push.ts'] as const } : {}), catalog: { group: 'public' }, daemon: { route: 'session' }, dispatch: {}, @@ -471,6 +501,7 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'trigger-app-event', + ...(ownerFilesEnabled ? { ownerFiles: ['src/commands/management/push.ts'] as const } : {}), catalog: { group: 'public', key: 'triggerAppEvent' }, daemon: { route: 'session' }, dispatch: {}, @@ -480,6 +511,7 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'open', + ...(ownerFilesEnabled ? { ownerFiles: ['src/commands/management/app.ts'] as const } : {}), catalog: { group: 'public' }, daemon: { route: 'session', allowSessionlessDefaultDevice: allowAnyDeviceSessionless }, dispatch: {}, @@ -489,6 +521,7 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'prepare', + ...(ownerFilesEnabled ? { ownerFiles: ['src/commands/management/prepare.ts'] as const } : {}), catalog: { group: 'public' }, daemon: { route: 'session' }, // Runner warm-up builds are the longest fixed envelope; --timeout overrides. @@ -502,6 +535,7 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'batch', + ...(ownerFilesEnabled ? { ownerFiles: ['src/commands/batch/index.ts'] as const } : {}), catalog: { group: 'public' }, daemon: { route: 'session' }, timeoutPolicy: DEFAULT_TIMEOUT_POLICY, @@ -509,6 +543,7 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'close', + ...(ownerFilesEnabled ? { ownerFiles: ['src/commands/management/app.ts'] as const } : {}), catalog: { group: 'public' }, daemon: { route: 'session', allowInvalidRecording: true }, dispatch: {}, @@ -520,6 +555,7 @@ const RAW_COMMAND_DESCRIPTORS = [ // -- snapshot (route: snapshot) -- { name: 'snapshot', + ...(ownerFilesEnabled ? { ownerFiles: ['src/commands/capture/snapshot.ts'] as const } : {}), catalog: { group: 'public' }, daemon: { route: 'snapshot', replayScopedAction: true }, dispatch: {}, @@ -531,6 +567,7 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'diff', + ...(ownerFilesEnabled ? { ownerFiles: ['src/commands/capture/diff.ts'] as const } : {}), catalog: { group: 'public' }, daemon: { route: 'snapshot', replayScopedAction: true }, capability: ALL_DEVICE_COMMAND_CAPABILITY, @@ -539,6 +576,7 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'wait', + ...(ownerFilesEnabled ? { ownerFiles: ['src/commands/capture/wait.ts'] as const } : {}), catalog: { group: 'public' }, daemon: { route: 'snapshot', replayScopedAction: true }, capability: ALL_DEVICE_COMMAND_CAPABILITY, @@ -552,6 +590,7 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'alert', + ...(ownerFilesEnabled ? { ownerFiles: ['src/commands/capture/alert.ts'] as const } : {}), catalog: { group: 'public' }, daemon: { route: 'snapshot', replayScopedAction: true }, capability: { @@ -564,6 +603,7 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'settings', + ...(ownerFilesEnabled ? { ownerFiles: ['src/commands/capture/settings.ts'] as const } : {}), catalog: { group: 'public' }, daemon: { route: 'snapshot', replayScopedAction: true }, dispatch: {}, @@ -579,6 +619,7 @@ const RAW_COMMAND_DESCRIPTORS = [ // -- specialized routes -- { name: 'react-native', + ...(ownerFilesEnabled ? { ownerFiles: ['src/commands/react-native/index.ts'] as const } : {}), catalog: { group: 'public', key: 'reactNative' }, daemon: { route: 'reactNative', replayScopedAction: true }, capability: { apple: APPLE_SIM_AND_DEVICE, android: ANDROID_ALL, linux: LINUX_NONE }, @@ -587,6 +628,7 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'record', + ...(ownerFilesEnabled ? { ownerFiles: ['src/commands/recording/index.ts'] as const } : {}), catalog: { group: 'public' }, daemon: { route: 'recordTrace', @@ -600,6 +642,7 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'trace', + ...(ownerFilesEnabled ? { ownerFiles: ['src/commands/recording/index.ts'] as const } : {}), catalog: { group: 'public' }, daemon: { route: 'recordTrace' }, timeoutPolicy: DEFAULT_TIMEOUT_POLICY, @@ -607,6 +650,7 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'find', + ...(ownerFilesEnabled ? { ownerFiles: ['src/commands/interaction/index.ts'] as const } : {}), catalog: { group: 'public' }, daemon: { route: 'find', replayScopedAction: true }, capability: ALL_DEVICE_COMMAND_CAPABILITY, @@ -623,6 +667,7 @@ const RAW_COMMAND_DESCRIPTORS = [ // stuck Apple runner work. { name: 'click', + ...(ownerFilesEnabled ? { ownerFiles: ['src/commands/interaction/index.ts'] as const } : {}), catalog: { group: 'public' }, daemon: { route: 'interaction', replayScopedAction: true, androidBlockingDialogGuard: true }, capability: { apple: APPLE_SIM_AND_DEVICE, android: ANDROID_ALL, linux: LINUX_DEVICE }, @@ -633,6 +678,7 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'fill', + ...(ownerFilesEnabled ? { ownerFiles: ['src/commands/interaction/index.ts'] as const } : {}), catalog: { group: 'public' }, daemon: { route: 'interaction', replayScopedAction: true, androidBlockingDialogGuard: true }, dispatch: {}, @@ -644,6 +690,7 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'longpress', + ...(ownerFilesEnabled ? { ownerFiles: ['src/commands/interaction/index.ts'] as const } : {}), catalog: { group: 'public', key: 'longPress' }, daemon: { route: 'interaction', replayScopedAction: true, androidBlockingDialogGuard: true }, dispatch: {}, @@ -654,6 +701,7 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'press', + ...(ownerFilesEnabled ? { ownerFiles: ['src/commands/interaction/index.ts'] as const } : {}), catalog: { group: 'public' }, daemon: { route: 'interaction', replayScopedAction: true, androidBlockingDialogGuard: true }, dispatch: {}, @@ -665,6 +713,7 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'type', + ...(ownerFilesEnabled ? { ownerFiles: ['src/commands/interaction/index.ts'] as const } : {}), catalog: { group: 'public' }, daemon: { route: 'interaction', replayScopedAction: true, androidBlockingDialogGuard: true }, dispatch: {}, @@ -674,6 +723,7 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'get', + ...(ownerFilesEnabled ? { ownerFiles: ['src/commands/interaction/index.ts'] as const } : {}), catalog: { group: 'public' }, daemon: { route: 'interaction', replayScopedAction: true }, capability: ALL_DEVICE_COMMAND_CAPABILITY, @@ -682,6 +732,7 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'read', + ...(ownerFilesEnabled ? { ownerFiles: ['src/daemon/handlers/interaction.ts'] as const } : {}), catalog: { group: 'dispatch-alias' }, dispatch: {}, timeoutPolicy: DEFAULT_TIMEOUT_POLICY, @@ -689,6 +740,7 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'is', + ...(ownerFilesEnabled ? { ownerFiles: ['src/commands/interaction/index.ts'] as const } : {}), catalog: { group: 'public' }, daemon: { route: 'interaction', replayScopedAction: true }, capability: ALL_DEVICE_COMMAND_CAPABILITY, @@ -699,6 +751,7 @@ const RAW_COMMAND_DESCRIPTORS = [ // -- generic (route: generic) -- { name: 'back', + ...(ownerFilesEnabled ? { ownerFiles: ['src/commands/system/index.ts'] as const } : {}), catalog: { group: 'public' }, daemon: { route: 'generic', replayScopedAction: true, androidBlockingDialogGuard: true }, dispatch: {}, @@ -708,6 +761,7 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'gesture', + ...(ownerFilesEnabled ? { ownerFiles: ['src/commands/interaction/index.ts'] as const } : {}), catalog: { group: 'public' }, daemon: { route: 'generic', replayScopedAction: true, androidBlockingDialogGuard: true }, timeoutPolicy: DEFAULT_TIMEOUT_POLICY, @@ -715,6 +769,7 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'home', + ...(ownerFilesEnabled ? { ownerFiles: ['src/commands/system/index.ts'] as const } : {}), catalog: { group: 'public' }, daemon: { route: 'generic', replayScopedAction: true, androidBlockingDialogGuard: true }, dispatch: {}, @@ -728,6 +783,7 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'tv-remote', + ...(ownerFilesEnabled ? { ownerFiles: ['src/commands/system/index.ts'] as const } : {}), catalog: { group: 'public', key: 'tvRemote' }, daemon: { route: 'generic', replayScopedAction: true, androidBlockingDialogGuard: true }, dispatch: {}, @@ -741,6 +797,7 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'rotate', + ...(ownerFilesEnabled ? { ownerFiles: ['src/commands/system/index.ts'] as const } : {}), catalog: { group: 'public' }, daemon: { route: 'generic', replayScopedAction: true, androidBlockingDialogGuard: true }, dispatch: {}, @@ -754,6 +811,7 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'scroll', + ...(ownerFilesEnabled ? { ownerFiles: ['src/commands/interaction/index.ts'] as const } : {}), catalog: { group: 'public' }, daemon: { route: 'generic', replayScopedAction: true, androidBlockingDialogGuard: true }, dispatch: {}, @@ -763,6 +821,7 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'swipe', + ...(ownerFilesEnabled ? { ownerFiles: ['src/commands/interaction/index.ts'] as const } : {}), catalog: { group: 'public' }, daemon: { route: 'generic', replayScopedAction: true, androidBlockingDialogGuard: true }, dispatch: {}, @@ -772,6 +831,7 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'swipe-preset', + ...(ownerFilesEnabled ? { ownerFiles: ['src/core/dispatch.ts'] as const } : {}), catalog: { group: 'dispatch-alias' }, dispatch: {}, timeoutPolicy: DEFAULT_TIMEOUT_POLICY, @@ -779,6 +839,7 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'pinch', + ...(ownerFilesEnabled ? { ownerFiles: ['src/core/dispatch.ts'] as const } : {}), catalog: { group: 'dispatch-alias' }, daemon: { route: 'generic', replayScopedAction: true, androidBlockingDialogGuard: true }, dispatch: {}, @@ -792,6 +853,7 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'focus', + ...(ownerFilesEnabled ? { ownerFiles: ['src/commands/interaction/index.ts'] as const } : {}), catalog: { group: 'public' }, daemon: { route: 'generic', androidBlockingDialogGuard: true }, dispatch: {}, @@ -801,6 +863,7 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'screenshot', + ...(ownerFilesEnabled ? { ownerFiles: ['src/commands/capture/screenshot.ts'] as const } : {}), catalog: { group: 'public' }, daemon: { route: 'generic', replayScopedAction: true }, dispatch: {}, @@ -810,6 +873,7 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'viewport', + ...(ownerFilesEnabled ? { ownerFiles: ['src/commands/management/viewport.ts'] as const } : {}), catalog: { group: 'public' }, daemon: { route: 'generic', replayScopedAction: true }, dispatch: {}, @@ -819,6 +883,7 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'pan', + ...(ownerFilesEnabled ? { ownerFiles: ['src/core/dispatch.ts'] as const } : {}), catalog: { group: 'dispatch-alias' }, daemon: { route: 'generic', androidBlockingDialogGuard: true }, dispatch: {}, @@ -828,6 +893,7 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'fling', + ...(ownerFilesEnabled ? { ownerFiles: ['src/core/dispatch.ts'] as const } : {}), catalog: { group: 'dispatch-alias' }, daemon: { route: 'generic', androidBlockingDialogGuard: true }, dispatch: {}, @@ -837,6 +903,7 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'rotate-gesture', + ...(ownerFilesEnabled ? { ownerFiles: ['src/core/dispatch.ts'] as const } : {}), catalog: { group: 'dispatch-alias' }, daemon: { route: 'generic', androidBlockingDialogGuard: true }, dispatch: {}, @@ -850,6 +917,7 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'transform-gesture', + ...(ownerFilesEnabled ? { ownerFiles: ['src/core/dispatch.ts'] as const } : {}), catalog: { group: 'dispatch-alias' }, daemon: { route: 'generic', androidBlockingDialogGuard: true }, dispatch: {}, @@ -865,6 +933,7 @@ const RAW_COMMAND_DESCRIPTORS = [ // -- capability/batch-only commands (no daemon route) -- { name: 'app-switcher', + ...(ownerFilesEnabled ? { ownerFiles: ['src/commands/system/index.ts'] as const } : {}), catalog: { group: 'public', key: 'appSwitcher' }, dispatch: {}, capability: { @@ -877,6 +946,7 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'install-from-source', + ...(ownerFilesEnabled ? { ownerFiles: ['src/commands/management/install.ts'] as const } : {}), catalog: { group: 'public', key: 'installFromSource' }, capability: APP_INSTALL_CAPABILITY, timeoutPolicy: DEFAULT_TIMEOUT_POLICY, @@ -886,24 +956,28 @@ const RAW_COMMAND_DESCRIPTORS = [ // -- local client-backed CLI/MCP commands (no daemon route/capability) -- { name: 'debug', + ...(ownerFilesEnabled ? { ownerFiles: ['src/commands/debugging/index.ts'] as const } : {}), catalog: { group: 'local-cli' }, timeoutPolicy: DEFAULT_TIMEOUT_POLICY, batchable: false, }, { name: 'metro', + ...(ownerFilesEnabled ? { ownerFiles: ['src/commands/metro/index.ts'] as const } : {}), catalog: { group: 'local-cli' }, timeoutPolicy: DEFAULT_TIMEOUT_POLICY, batchable: false, }, { name: 'session', + ...(ownerFilesEnabled ? { ownerFiles: ['src/commands/management/session.ts'] as const } : {}), catalog: { group: 'local-cli' }, timeoutPolicy: DEFAULT_TIMEOUT_POLICY, batchable: false, }, { name: 'cdp', + ...(ownerFilesEnabled ? { ownerFiles: ['src/cli/commands/agent-cdp.ts'] as const } : {}), catalog: { group: 'local-cli' }, timeoutPolicy: DEFAULT_TIMEOUT_POLICY, batchable: false, @@ -911,6 +985,7 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'auth', + ...(ownerFilesEnabled ? { ownerFiles: ['src/cli/commands/auth.ts'] as const } : {}), catalog: { group: 'local-cli' }, timeoutPolicy: DEFAULT_TIMEOUT_POLICY, batchable: false, @@ -918,6 +993,7 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'connect', + ...(ownerFilesEnabled ? { ownerFiles: ['src/cli/commands/connection.ts'] as const } : {}), catalog: { group: 'local-cli' }, timeoutPolicy: DEFAULT_TIMEOUT_POLICY, batchable: false, @@ -925,6 +1001,7 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'connection', + ...(ownerFilesEnabled ? { ownerFiles: ['src/cli/commands/connection.ts'] as const } : {}), catalog: { group: 'local-cli' }, timeoutPolicy: DEFAULT_TIMEOUT_POLICY, batchable: false, @@ -932,6 +1009,7 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'disconnect', + ...(ownerFilesEnabled ? { ownerFiles: ['src/cli/commands/connection.ts'] as const } : {}), catalog: { group: 'local-cli' }, timeoutPolicy: DEFAULT_TIMEOUT_POLICY, batchable: false, @@ -939,6 +1017,7 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'mcp', + ...(ownerFilesEnabled ? { ownerFiles: ['src/bin.ts'] as const } : {}), catalog: { group: 'local-cli' }, timeoutPolicy: DEFAULT_TIMEOUT_POLICY, batchable: false, @@ -946,6 +1025,7 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'proxy', + ...(ownerFilesEnabled ? { ownerFiles: ['src/cli/commands/proxy.ts'] as const } : {}), catalog: { group: 'local-cli' }, timeoutPolicy: DEFAULT_TIMEOUT_POLICY, batchable: false, @@ -953,6 +1033,7 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'react-devtools', + ...(ownerFilesEnabled ? { ownerFiles: ['src/cli/commands/react-devtools.ts'] as const } : {}), catalog: { group: 'local-cli', key: 'reactDevtools' }, timeoutPolicy: DEFAULT_TIMEOUT_POLICY, batchable: false, @@ -960,6 +1041,7 @@ const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'web', + ...(ownerFilesEnabled ? { ownerFiles: ['src/cli/commands/web.ts'] as const } : {}), catalog: { group: 'local-cli' }, timeoutPolicy: DEFAULT_TIMEOUT_POLICY, batchable: false, @@ -967,6 +1049,7 @@ const RAW_COMMAND_DESCRIPTORS = [ }, ] as const satisfies readonly RawCommandDescriptor[]; + const CLI_CATALOG_GROUPS = new Set(['public', 'local-cli']); const CLI_COMMAND_NAMES = new Set( @@ -983,10 +1066,13 @@ const CLI_COMMAND_NAMES = new Set( * so each entry keeps its literal `name`. That is what makes the {@link Command} * union below a precise set of command-name literals rather than `string`. */ -export const commandDescriptors = RAW_COMMAND_DESCRIPTORS.map((descriptor) => ({ - ...descriptor, - mcpExposed: resolveMcpExposure(descriptor), -})) satisfies readonly CommandDescriptor[]; +export const commandDescriptors = RAW_COMMAND_DESCRIPTORS.map((descriptor) => { + const { ownerFiles: _, ...rest } = descriptor; + return { + ...rest, + mcpExposed: resolveMcpExposure(descriptor), + }; +}) satisfies readonly CommandDescriptor[]; /** The literal union of every registered command name. */ export type Command = (typeof commandDescriptors)[number]['name']; diff --git a/src/global.d.ts b/src/global.d.ts new file mode 100644 index 000000000..67f75a616 --- /dev/null +++ b/src/global.d.ts @@ -0,0 +1,2 @@ +/** Build-time flag: owner-file claims are enabled outside production bundles. */ +declare const __OWNER_FILES__: true; diff --git a/tsdown.config.ts b/tsdown.config.ts index 372a56119..79b671d3c 100644 --- a/tsdown.config.ts +++ b/tsdown.config.ts @@ -71,6 +71,7 @@ export default defineConfig({ tsconfig: 'tsconfig.lib.json', define: { __AGENT_DEVICE_VERSION__: JSON.stringify(packageJson.version), + __OWNER_FILES__: 'false', }, shims: true, hash: false, @@ -84,6 +85,9 @@ export default defineConfig({ }, outExtensions: () => ({ js: '.js', dts: '.d.ts' }), minify: true, + treeshake: { + propertyReadSideEffects: false, + }, dts: { tsgo: { path: getTypeScript7ExePath(), From 206426224aa646cddf98e823eaa142a462f6d783 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Fri, 10 Jul 2026 13:22:39 +0000 Subject: [PATCH 5/9] chore: apply oxfmt formatting Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- src/core/command-descriptor/owner-files.ts | 14 +++++++++++--- src/core/command-descriptor/registry.ts | 17 ++++++++++++----- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/src/core/command-descriptor/owner-files.ts b/src/core/command-descriptor/owner-files.ts index eafce4f81..796acfac1 100644 --- a/src/core/command-descriptor/owner-files.ts +++ b/src/core/command-descriptor/owner-files.ts @@ -17,17 +17,25 @@ import { RAW_COMMAND_DESCRIPTORS, type Command } from './registry.ts'; * import graph never reaches it, so the bundler drops it entirely. */ type OwnerFilesFromDescriptors< - T extends readonly { readonly name: string; readonly ownerFiles?: readonly [string, ...string[]] }[], + T extends readonly { + readonly name: string; + readonly ownerFiles?: readonly [string, ...string[]]; + }[], > = { [K in keyof T & number as T[K]['name']]: NonNullable; }; const buildOwnerFiles = < - const T extends readonly { readonly name: string; readonly ownerFiles?: readonly [string, ...string[]] }[], + const T extends readonly { + readonly name: string; + readonly ownerFiles?: readonly [string, ...string[]]; + }[], >( entries: T, ): OwnerFilesFromDescriptors => - Object.fromEntries(entries.map((entry) => [entry.name, entry.ownerFiles])) as OwnerFilesFromDescriptors; + Object.fromEntries( + entries.map((entry) => [entry.name, entry.ownerFiles]), + ) as OwnerFilesFromDescriptors; export const COMMAND_OWNER_FILES = buildOwnerFiles(RAW_COMMAND_DESCRIPTORS) satisfies Record< Command, diff --git a/src/core/command-descriptor/registry.ts b/src/core/command-descriptor/registry.ts index d488a42f1..4f0d77a77 100644 --- a/src/core/command-descriptor/registry.ts +++ b/src/core/command-descriptor/registry.ts @@ -240,7 +240,9 @@ export const RAW_COMMAND_DESCRIPTORS = [ // -- session (route: session) -- { name: 'session_list', - ...(ownerFilesEnabled ? { ownerFiles: ['src/daemon/handlers/session-inventory.ts'] as const } : {}), + ...(ownerFilesEnabled + ? { ownerFiles: ['src/daemon/handlers/session-inventory.ts'] as const } + : {}), catalog: { group: 'internal', key: 'sessionList' }, daemon: { route: 'session', sessionKind: 'inventory', ...REQUEST_EXECUTION_EXEMPT }, timeoutPolicy: DEFAULT_TIMEOUT_POLICY, @@ -417,7 +419,9 @@ export const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'runtime', - ...(ownerFilesEnabled ? { ownerFiles: ['src/daemon/handlers/session-runtime-command.ts'] as const } : {}), + ...(ownerFilesEnabled + ? { ownerFiles: ['src/daemon/handlers/session-runtime-command.ts'] as const } + : {}), catalog: { group: 'internal' }, daemon: { route: 'session' }, timeoutPolicy: DEFAULT_TIMEOUT_POLICY, @@ -471,7 +475,9 @@ export const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'install_source', - ...(ownerFilesEnabled ? { ownerFiles: ['src/daemon/handlers/install-source.ts'] as const } : {}), + ...(ownerFilesEnabled + ? { ownerFiles: ['src/daemon/handlers/install-source.ts'] as const } + : {}), catalog: { group: 'internal', key: 'installSource' }, daemon: { route: 'session' }, timeoutPolicy: INSTALL_TIMEOUT_POLICY, @@ -479,7 +485,9 @@ export const RAW_COMMAND_DESCRIPTORS = [ }, { name: 'release_materialized_paths', - ...(ownerFilesEnabled ? { ownerFiles: ['src/daemon/handlers/install-source.ts'] as const } : {}), + ...(ownerFilesEnabled + ? { ownerFiles: ['src/daemon/handlers/install-source.ts'] as const } + : {}), catalog: { group: 'internal', key: 'releaseMaterializedPaths' }, daemon: { route: 'session', ...REQUEST_EXECUTION_EXEMPT }, timeoutPolicy: DEFAULT_TIMEOUT_POLICY, @@ -1049,7 +1057,6 @@ export const RAW_COMMAND_DESCRIPTORS = [ }, ] as const satisfies readonly RawCommandDescriptor[]; - const CLI_CATALOG_GROUPS = new Set(['public', 'local-cli']); const CLI_COMMAND_NAMES = new Set( From 7aeac15ca25df1925482caba6724d9ae96f8d0ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Fri, 10 Jul 2026 18:01:03 +0000 Subject: [PATCH 6/9] test(build): guard tooling metadata exclusion Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 1 + package.json | 3 ++- scripts/check-bundle-owner-files.ts | 40 +++++++++++++++++++++++++++++ src/global.d.ts | 2 +- tsdown.config.ts | 3 --- 5 files changed, 44 insertions(+), 5 deletions(-) create mode 100644 scripts/check-bundle-owner-files.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e4b463901..0f6e5ac88 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -150,6 +150,7 @@ jobs: - name: Build and pack CLI run: | pnpm build + pnpm check:bundle-owner-files mkdir -p .tmp/node-compat npm pack --ignore-scripts --pack-destination .tmp/node-compat diff --git a/package.json b/package.json index 3ff854d06..5c2f80033 100644 --- a/package.json +++ b/package.json @@ -121,11 +121,12 @@ "check:layering:baseline": "node --experimental-strip-types scripts/layering/check.ts --update-baseline", "check:production-exports": "fallow dead-code --config fallow-production-exports.json --production --unused-exports --baseline fallow-baselines/production-unused-exports.json --fail-on-issues", "check:production-exports:baseline": "fallow dead-code --config fallow-production-exports.json --production --unused-exports --save-baseline fallow-baselines/production-unused-exports.json --summary", + "check:bundle-owner-files": "node --experimental-strip-types scripts/check-bundle-owner-files.ts", "check:quick": "pnpm lint && pnpm typecheck", "sync:mcp-metadata": "node scripts/sync-mcp-metadata.mjs", "check:mcp-metadata": "node scripts/sync-mcp-metadata.mjs --check", "version": "node scripts/sync-mcp-metadata.mjs && git add server.json", - "check:tooling": "pnpm lint && pnpm typecheck && pnpm check:layering && pnpm check:production-exports && pnpm check:mcp-metadata && pnpm build", + "check:tooling": "pnpm lint && pnpm typecheck && pnpm check:layering && pnpm check:production-exports && pnpm check:mcp-metadata && pnpm build && pnpm check:bundle-owner-files", "check:unit": "pnpm test:unit && pnpm test:smoke", "check": "pnpm check:tooling && pnpm check:fallow && pnpm check:unit", "prepack": "pnpm check:mcp-metadata && pnpm build:all && pnpm package:apple-runner:npm && pnpm package:android-snapshot-helper:npm && pnpm package:android-multitouch-helper:npm && pnpm package:android-ime-helper:npm", diff --git a/scripts/check-bundle-owner-files.ts b/scripts/check-bundle-owner-files.ts new file mode 100644 index 000000000..b2572ecee --- /dev/null +++ b/scripts/check-bundle-owner-files.ts @@ -0,0 +1,40 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { COMMAND_OWNER_FILES } from '../src/core/command-descriptor/owner-files.ts'; +import { getDaemonRouteOwnerFiles } from '../src/daemon/route-owner-files.ts'; + +const repoRoot = path.resolve(import.meta.dirname, '..'); +const distRoot = path.join(repoRoot, 'dist', 'src'); +const ownerPaths = new Set([ + ...Object.values(COMMAND_OWNER_FILES).flat(), + ...Object.values(getDaemonRouteOwnerFiles()), +]); + +const bundleFiles = walkFiles(distRoot).filter((file) => file.endsWith('.js')); +if (bundleFiles.length === 0) { + throw new Error('No dist/src JavaScript files found. Run `pnpm build` first.'); +} + +const leaks = bundleFiles.flatMap((file) => { + const content = fs.readFileSync(file, 'utf8'); + return [...ownerPaths] + .filter((ownerPath) => content.includes(ownerPath)) + .map((ownerPath) => ({ file: path.relative(repoRoot, file), ownerPath })); +}); + +if (leaks.length > 0) { + const details = leaks.map(({ file, ownerPath }) => `- ${ownerPath} in ${file}`).join('\n'); + throw new Error(`Owner-file navigation metadata leaked into production bundles:\n${details}`); +} + +process.stdout.write( + `Verified ${ownerPaths.size} owner-file paths are absent from ${bundleFiles.length} production bundles.\n`, +); + +function walkFiles(root: string): string[] { + if (!fs.existsSync(root)) return []; + return fs.readdirSync(root, { withFileTypes: true }).flatMap((entry) => { + const entryPath = path.join(root, entry.name); + return entry.isDirectory() ? walkFiles(entryPath) : [entryPath]; + }); +} diff --git a/src/global.d.ts b/src/global.d.ts index 67f75a616..54813a725 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -1,2 +1,2 @@ /** Build-time flag: owner-file claims are enabled outside production bundles. */ -declare const __OWNER_FILES__: true; +declare const __OWNER_FILES__: boolean; diff --git a/tsdown.config.ts b/tsdown.config.ts index 79b671d3c..dbec3bd56 100644 --- a/tsdown.config.ts +++ b/tsdown.config.ts @@ -85,9 +85,6 @@ export default defineConfig({ }, outExtensions: () => ({ js: '.js', dts: '.d.ts' }), minify: true, - treeshake: { - propertyReadSideEffects: false, - }, dts: { tsgo: { path: getTypeScript7ExePath(), From 35b74d47463d26a1db00757733c9c999cdadfbf5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Fri, 10 Jul 2026 18:02:06 +0000 Subject: [PATCH 7/9] refactor(command-descriptor): drop global treeshake option and add bundle guard - Remove treeshake.propertyReadSideEffects from tsdown.config.ts; the __OWNER_FILES__ define + conditional spread already keeps owner files out of the bundle, so the global DCE lever is unnecessary and scope-creeping. - Add a comment on the __OWNER_FILES__ global declaration explaining the deliberate type-versus-runtime mismatch. - Add test/output-economy/owner-files-no-leak.test.ts to build dist and assert that no command or daemon-route owner-file path appears in the emitted JS. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- src/global.d.ts | 6 ++- .../owner-files-no-leak.test.ts | 47 +++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 test/output-economy/owner-files-no-leak.test.ts diff --git a/src/global.d.ts b/src/global.d.ts index 54813a725..0c5e15aa9 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -1,2 +1,6 @@ -/** Build-time flag: owner-file claims are enabled outside production bundles. */ +/** + * Build-time flag: owner-file claims are enabled outside production bundles. + * The identifier is undefined in dev/tests and `false` in production builds, + * so the source defaults it on with `typeof __OWNER_FILES__ === 'undefined'`. + */ declare const __OWNER_FILES__: boolean; diff --git a/test/output-economy/owner-files-no-leak.test.ts b/test/output-economy/owner-files-no-leak.test.ts new file mode 100644 index 000000000..b02a84ecd --- /dev/null +++ b/test/output-economy/owner-files-no-leak.test.ts @@ -0,0 +1,47 @@ +import { describe, expect, test } from 'vitest'; +import fs from 'node:fs'; +import path from 'node:path'; +import { runCmdSync } from '../../src/utils/exec.ts'; +import { COMMAND_OWNER_FILES } from '../../src/core/command-descriptor/owner-files.ts'; +import { getDaemonRouteOwnerFiles } from '../../src/daemon/route-owner-files.ts'; + +const repoRoot = path.resolve(import.meta.dirname, '../..'); +const distPath = path.join(repoRoot, 'dist/src'); + +function collectJsFiles(dir: string): string[] { + const entries = fs.readdirSync(dir, { recursive: true, encoding: 'utf8' }); + return entries.filter((name) => name.endsWith('.js')).map((name) => path.join(dir, name)); +} + +describe('owner-file path literals', () => { + test('do not leak into production bundles after a clean build', { timeout: 30_000 }, () => { + fs.rmSync(path.join(repoRoot, 'dist'), { recursive: true, force: true }); + + const build = runCmdSync( + 'node', + [ + '--experimental-strip-types', + 'node_modules/tsdown/dist/run.mjs', + '--config-loader', + 'native', + ], + { cwd: repoRoot }, + ); + expect(build.exitCode, build.stderr).toBe(0); + + const jsFiles = collectJsFiles(distPath); + const bundle = jsFiles.map((file) => fs.readFileSync(file, 'utf8')).join('\n'); + + const ownerPaths = [ + ...Object.values(COMMAND_OWNER_FILES).flat(), + ...Object.values(getDaemonRouteOwnerFiles()), + ]; + + for (const ownerPath of ownerPaths) { + expect( + bundle.includes(ownerPath), + `owner-file path ${ownerPath} leaked into production bundle`, + ).toBe(false); + } + }); +}); From 47efeff70d8060384f01065ec240f5d67a4c160e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Fri, 10 Jul 2026 18:51:15 +0000 Subject: [PATCH 8/9] fix(build): remove owner metadata property reads Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- scripts/check-bundle-owner-files.ts | 11 ++++++----- src/core/command-descriptor/registry.ts | 11 +++++++++-- test/output-economy/owner-files-no-leak.test.ts | 13 +++++++------ 3 files changed, 22 insertions(+), 13 deletions(-) diff --git a/scripts/check-bundle-owner-files.ts b/scripts/check-bundle-owner-files.ts index b2572ecee..09a366cbb 100644 --- a/scripts/check-bundle-owner-files.ts +++ b/scripts/check-bundle-owner-files.ts @@ -9,6 +9,7 @@ const ownerPaths = new Set([ ...Object.values(COMMAND_OWNER_FILES).flat(), ...Object.values(getDaemonRouteOwnerFiles()), ]); +const forbiddenMetadata = new Set(['ownerFiles', ...ownerPaths]); const bundleFiles = walkFiles(distRoot).filter((file) => file.endsWith('.js')); if (bundleFiles.length === 0) { @@ -17,18 +18,18 @@ if (bundleFiles.length === 0) { const leaks = bundleFiles.flatMap((file) => { const content = fs.readFileSync(file, 'utf8'); - return [...ownerPaths] - .filter((ownerPath) => content.includes(ownerPath)) - .map((ownerPath) => ({ file: path.relative(repoRoot, file), ownerPath })); + return [...forbiddenMetadata] + .filter((value) => content.includes(value)) + .map((value) => ({ file: path.relative(repoRoot, file), value })); }); if (leaks.length > 0) { - const details = leaks.map(({ file, ownerPath }) => `- ${ownerPath} in ${file}`).join('\n'); + const details = leaks.map(({ file, value }) => `- ${value} in ${file}`).join('\n'); throw new Error(`Owner-file navigation metadata leaked into production bundles:\n${details}`); } process.stdout.write( - `Verified ${ownerPaths.size} owner-file paths are absent from ${bundleFiles.length} production bundles.\n`, + `Verified the ownerFiles key and ${ownerPaths.size} owner-file paths are absent from ${bundleFiles.length} production bundles.\n`, ); function walkFiles(root: string): string[] { diff --git a/src/core/command-descriptor/registry.ts b/src/core/command-descriptor/registry.ts index 4f0d77a77..85fba2cd7 100644 --- a/src/core/command-descriptor/registry.ts +++ b/src/core/command-descriptor/registry.ts @@ -1074,9 +1074,16 @@ const CLI_COMMAND_NAMES = new Set( * union below a precise set of command-name literals rather than `string`. */ export const commandDescriptors = RAW_COMMAND_DESCRIPTORS.map((descriptor) => { - const { ownerFiles: _, ...rest } = descriptor; + if (!ownerFilesEnabled) { + return { + ...descriptor, + mcpExposed: resolveMcpExposure(descriptor), + }; + } + + const { ownerFiles: _, ...runtimeDescriptor } = descriptor; return { - ...rest, + ...runtimeDescriptor, mcpExposed: resolveMcpExposure(descriptor), }; }) satisfies readonly CommandDescriptor[]; diff --git a/test/output-economy/owner-files-no-leak.test.ts b/test/output-economy/owner-files-no-leak.test.ts index b02a84ecd..38081691a 100644 --- a/test/output-economy/owner-files-no-leak.test.ts +++ b/test/output-economy/owner-files-no-leak.test.ts @@ -13,8 +13,8 @@ function collectJsFiles(dir: string): string[] { return entries.filter((name) => name.endsWith('.js')).map((name) => path.join(dir, name)); } -describe('owner-file path literals', () => { - test('do not leak into production bundles after a clean build', { timeout: 30_000 }, () => { +describe('owner-file metadata', () => { + test('does not leak into production bundles after a clean build', { timeout: 30_000 }, () => { fs.rmSync(path.join(repoRoot, 'dist'), { recursive: true, force: true }); const build = runCmdSync( @@ -32,15 +32,16 @@ describe('owner-file path literals', () => { const jsFiles = collectJsFiles(distPath); const bundle = jsFiles.map((file) => fs.readFileSync(file, 'utf8')).join('\n'); - const ownerPaths = [ + const forbiddenMetadata = [ + 'ownerFiles', ...Object.values(COMMAND_OWNER_FILES).flat(), ...Object.values(getDaemonRouteOwnerFiles()), ]; - for (const ownerPath of ownerPaths) { + for (const value of forbiddenMetadata) { expect( - bundle.includes(ownerPath), - `owner-file path ${ownerPath} leaked into production bundle`, + bundle.includes(value), + `owner-file metadata ${value} leaked into production bundle`, ).toBe(false); } }); From 7444965d26cca375c479577e8877760d56370aa2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Fri, 10 Jul 2026 19:03:01 +0000 Subject: [PATCH 9/9] fix(command-descriptor): enforce owner claim totality Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- src/core/command-descriptor/registry.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/core/command-descriptor/registry.ts b/src/core/command-descriptor/registry.ts index 85fba2cd7..4c74752d5 100644 --- a/src/core/command-descriptor/registry.ts +++ b/src/core/command-descriptor/registry.ts @@ -1057,6 +1057,16 @@ export const RAW_COMMAND_DESCRIPTORS = [ }, ] as const satisfies readonly RawCommandDescriptor[]; +/** + * Compile-time owner-claim totality. `keyof` on a union contains only keys + * shared by every member, so removing `ownerFiles` from any raw descriptor + * makes this resolve to `false` and fail the `AssertTrue` constraint. + */ +type AssertTrue = T; +export type CommandOwnerFileClaimsAreComplete = AssertTrue< + 'ownerFiles' extends keyof (typeof RAW_COMMAND_DESCRIPTORS)[number] ? true : false +>; + const CLI_CATALOG_GROUPS = new Set(['public', 'local-cli']); const CLI_COMMAND_NAMES = new Set(