import { CommandGroup } from './CommandGroup'; import type { CommandContext, CommandEntry, CommandSpec } from './CommandGroup'; import { NavigationCommands } from '../commands/navigation'; import { TabsMutationCommands } from '../commands/tabs'; import { TabsQueryCommands } from '../commands/tabs-query'; import { GroupsCommands } from '../commands/groups'; import { WindowsCommands } from '../commands/windows'; import { DomCommands } from '../commands/dom'; import { BrowserDataCommands } from '../commands/browser-data'; import { SessionCommands } from '../commands/session'; import { PerfCommands } from '../commands/perf'; import { ExtensionCommands } from '../commands/extension'; import type { CommandArgs, Serializable, DispatchArgs, PageRequest } from '../types'; function isCommandSpec(entry: CommandEntry): entry is CommandSpec { return typeof entry !== "function"; } export function makePagedData(items: Serializable[], page: PageRequest) { const total = items.length; const offset = Math.max(0, Number(page.offset) || 0); const requestedLimit = Math.max(1, Number(page.limit) || 100); const limit = Math.min(requestedLimit, 1000); const end = Math.min(offset + limit, total); return { __browserCliPage: true, items: items.slice(offset, end), offset, limit, total, nextOffset: end < total ? end : null, }; } export class CommandRegistry { private readonly entries = new Map(); constructor(private readonly ctx: CommandContext) {} /** Flattens a group's full-id-keyed commands into the registry map. */ register(group: CommandGroup): void { for (const [command, entry] of Object.entries(group.commands)) { if (this.entries.has(command)) { throw new Error(`Duplicate command registration: ${command}`); } this.entries.set(command, entry); } } private resolve(command: string): CommandEntry { const entry = this.entries.get(command); if (!entry) throw new Error(`Unknown command: ${command}`); return entry; } async dispatch(command: string, args: DispatchArgs, opts: { background?: boolean; page?: PageRequest }): Promise { const entry = this.resolve(command); if (isCommandSpec(entry) && entry.background && opts.background) { // Narrow the dynamic IPC dict to the handler's declared arg type at the // call boundary. return this.ctx.jobs.start(command, args, jobArgs => Promise.resolve(entry.run(jobArgs as CommandArgs))); } const run = isCommandSpec(entry) ? entry.run : entry; let result = await run(args as CommandArgs); if (opts.page && Array.isArray(result)) { result = makePagedData(result, opts.page); } return result; } } /** * Builds the registry and registers every command group. The SessionCommands * instance is returned alongside because index.ts wires its lifecycle methods * (chrome.tabs.onActivated → activateLazyTab) and NativeConnection references it * for the clients.rename_profile reconnect side-effect. */ export function assembleRegistry(ctx: CommandContext): { registry: CommandRegistry; session: SessionCommands } { const registry = new CommandRegistry(ctx); const session = new SessionCommands(ctx); registry.register(new NavigationCommands(ctx)); registry.register(new TabsMutationCommands(ctx)); registry.register(new TabsQueryCommands(ctx)); registry.register(new GroupsCommands(ctx)); registry.register(new WindowsCommands(ctx)); registry.register(new DomCommands(ctx)); registry.register(new BrowserDataCommands(ctx)); registry.register(session); registry.register(new PerfCommands(ctx)); registry.register(new ExtensionCommands(ctx)); return { registry, session }; }