Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
140 lines
4.9 KiB
TypeScript
140 lines
4.9 KiB
TypeScript
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<Snippet, 'targets' | 'targetGroups' | 'targetsAllHosts'>,
|
|
): boolean {
|
|
if (snippet.targetsAllHosts) return true;
|
|
return Boolean(
|
|
(snippet.targets && snippet.targets.length > 0)
|
|
|| (snippet.targetGroups && snippet.targetGroups.length > 0),
|
|
);
|
|
}
|
|
|
|
export type SnippetTargetHost = Pick<Host, 'id' | 'group'>;
|
|
|
|
export function snippetTargetsHostExplicitly(
|
|
snippet: Pick<Snippet, 'targets'>,
|
|
hostId: string,
|
|
): boolean {
|
|
return Boolean(snippet.targets?.includes(hostId));
|
|
}
|
|
|
|
export function snippetTargetsHostGroup(
|
|
snippet: Pick<Snippet, 'targetGroups'>,
|
|
host: Pick<Host, 'group'>,
|
|
): 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<Snippet, 'targetGroups' | 'targetsAllHosts'>,
|
|
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<Snippet, 'targets' | 'targetGroups' | 'targetsAllHosts'>,
|
|
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<Snippet, 'targets' | 'targetGroups' | 'targetsAllHosts'>,
|
|
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<Snippet, 'targets' | 'targetGroups' | 'targetsAllHosts'>,
|
|
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<Snippet, 'trigger' | 'targets' | 'targetGroups' | 'targetsAllHosts'>,
|
|
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
|
|
));
|
|
}
|