import type { Host, Snippet } from './models'; import { hostMatchesGroupPath } from './selectHostSelection.ts'; import { normalizeGroupTargetPath } from './hostGroupPathMutations.ts'; import { isScriptSnippet } from './snippetScript.ts'; /** Whether the snippet has a runnable scope (explicit targets or all-hosts flag). */ export function snippetHasRunTargets( snippet: Pick, ): boolean { if (snippet.targetsAllHosts) return true; return Boolean( (snippet.targets && snippet.targets.length > 0) || (snippet.targetGroups && snippet.targetGroups.length > 0), ); } export type SnippetTargetHost = Pick; export function snippetTargetsHostExplicitly( snippet: Pick, hostId: string, ): boolean { return Boolean(snippet.targets?.includes(hostId)); } export function snippetTargetsHostGroup( snippet: Pick, host: Pick, ): boolean { return Boolean(snippet.targetGroups?.some( (groupPath) => hostMatchesGroupPath(host, normalizeGroupTargetPath(groupPath)), )); } /** Keep legacy unscoped onOutput semantics distinct from a deleted/cleared group scope. */ export function resolveSnippetTargetGroupsForSave( snippet: Pick, selectedGroupPaths: string[], ): string[] | undefined { if (snippet.targetsAllHosts) return undefined; if (snippet.targetGroups === undefined && selectedGroupPaths.length === 0) { return undefined; } return selectedGroupPaths; } function normalizeTargetHost(hostOrId?: SnippetTargetHost | string): SnippetTargetHost | undefined { if (typeof hostOrId === 'string') return { id: hostOrId }; return hostOrId; } /** Connectable hosts for manual run / save-and-run. */ export function getRunnableHostsForSnippet( snippet: Pick, hosts: Host[], ): Host[] { const connectable = hosts.filter((host) => host.protocol !== 'serial'); if (snippet.targetsAllHosts) return connectable; const idSet = new Set(snippet.targets ?? []); return connectable.filter( (host) => idSet.has(host.id) || snippetTargetsHostGroup(snippet, host), ); } /** Whether a snippet/script applies to the given host based on targets or all-hosts flag. */ export function snippetAppliesToHost( snippet: Pick, hostOrId?: SnippetTargetHost | string, ): boolean { const host = normalizeTargetHost(hostOrId); if (!host) return false; if (snippet.targetsAllHosts) return true; return snippetTargetsHostExplicitly(snippet, host.id) || snippetTargetsHostGroup(snippet, host); } /** Whether a library/shortcut invocation may run in the current terminal. */ export function snippetCanRunInTerminal( snippet: Pick, host: SnippetTargetHost, ): boolean { if (snippet.targetsAllHosts) return true; const hasScopedTargets = Boolean( snippet.targets?.length || snippet.targetGroups !== undefined, ); return !hasScopedTargets || snippetAppliesToHost(snippet, host); } /** * onOutput triggers listen on the active terminal session. When no explicit targets * are configured, they apply to whichever host the session is connected to. */ export function snippetAppliesToOutputTrigger( snippet: Pick, hostOrId?: SnippetTargetHost | string, ): boolean { if (snippet.trigger !== 'onOutput') return false; const host = normalizeTargetHost(hostOrId); if (!host) return false; if (snippet.targetsAllHosts) return true; if (snippet.targets?.length || snippet.targetGroups !== undefined) { return snippetAppliesToHost(snippet, host); } return true; } /** Scripts explicitly linked to a host via targets (excludes all-hosts scripts). */ export function getScriptsLinkedToHost(snippets: Snippet[], hostId: string): Snippet[] { return snippets.filter( (snippet) => isScriptSnippet(snippet) && !snippet.targetsAllHosts && Boolean(snippet.targets?.includes(hostId)), ); } export function linkHostToScript(snippet: Snippet, hostId: string): Snippet { const targets = snippet.targets ?? []; if (targets.includes(hostId)) { return { ...snippet, targetsAllHosts: undefined }; } return { ...snippet, targets: [...targets, hostId], targetsAllHosts: undefined, }; } export function unlinkHostFromScript(snippet: Snippet, hostId: string): Snippet { const targets = snippet.targets ?? []; if (!targets.includes(hostId)) return snippet; const nextTargets = targets.filter((id) => id !== hostId); return { ...snippet, targets: nextTargets.length > 0 ? nextTargets : undefined }; } export function unlinkHostFromScripts(snippets: Snippet[], hostId: string, scriptId: string): Snippet[] { return snippets.map((snippet) => ( snippet.id === scriptId ? unlinkHostFromScript(snippet, hostId) : snippet )); }