[Init] Initial commit - NetMesh terminal manager
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
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
This commit is contained in:
456
domain/hostConnectScripts.ts
Normal file
456
domain/hostConnectScripts.ts
Normal file
@@ -0,0 +1,456 @@
|
||||
import type { Host, Snippet } from './models';
|
||||
import { isScriptSnippet } from './snippetScript.ts';
|
||||
import {
|
||||
getScriptsLinkedToHost,
|
||||
linkHostToScript,
|
||||
snippetTargetsHostExplicitly,
|
||||
snippetTargetsHostGroup,
|
||||
unlinkHostFromScripts,
|
||||
} from './snippetTargets.ts';
|
||||
import { sortByVaultOrder } from './vaultOrder.ts';
|
||||
|
||||
function isOnConnectScript(snippet: Snippet): boolean {
|
||||
return isScriptSnippet(snippet) && snippet.trigger === 'onConnect' && Boolean(snippet.id);
|
||||
}
|
||||
|
||||
function scriptById(snippets: Snippet[], scriptId: string): Snippet | undefined {
|
||||
return snippets.find((snippet) => snippet.id === scriptId && isScriptSnippet(snippet));
|
||||
}
|
||||
|
||||
function pruneConnectScriptIds(ids: string[], snippets: Snippet[]): string[] {
|
||||
const seen = new Set<string>();
|
||||
const result: string[] = [];
|
||||
for (const id of ids) {
|
||||
if (!id || seen.has(id)) continue;
|
||||
const snippet = scriptById(snippets, id);
|
||||
if (!snippet || !isOnConnectScript(snippet)) continue;
|
||||
seen.add(id);
|
||||
result.push(id);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Draft/edit queue prune: keep any existing script IDs.
|
||||
* Host save promotes non-onConnect scripts via prepareSnippetForHostConnectQueue.
|
||||
*/
|
||||
function pruneEditableConnectScriptIds(ids: string[], snippets: Snippet[]): string[] {
|
||||
const seen = new Set<string>();
|
||||
const result: string[] = [];
|
||||
for (const id of ids) {
|
||||
if (!id || seen.has(id)) continue;
|
||||
const snippet = scriptById(snippets, id);
|
||||
if (!snippet) continue;
|
||||
seen.add(id);
|
||||
result.push(id);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Global onConnect scripts (targetsAllHosts), sorted by vault order. */
|
||||
export function getGlobalConnectScripts(snippets: Snippet[]): Snippet[] {
|
||||
return sortByVaultOrder(
|
||||
snippets.filter(
|
||||
(snippet) => isOnConnectScript(snippet) && Boolean(snippet.targetsAllHosts),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/** Derive initial connectScriptIds from legacy host + snippet bindings. */
|
||||
export function migrateHostConnectScriptIds(host: Host, snippets: Snippet[]): string[] {
|
||||
const ordered: string[] = [];
|
||||
const seen = new Set<string>();
|
||||
|
||||
const push = (scriptId?: string) => {
|
||||
if (!scriptId || seen.has(scriptId)) return;
|
||||
const snippet = scriptById(snippets, scriptId);
|
||||
if (!snippet || !isOnConnectScript(snippet)) return;
|
||||
if (!snippetTargetsHostExplicitly(snippet, host.id) && !snippet.targetsAllHosts) return;
|
||||
seen.add(scriptId);
|
||||
ordered.push(scriptId);
|
||||
};
|
||||
|
||||
push(host.loginScriptId);
|
||||
|
||||
for (const snippet of getScriptsLinkedToHost(snippets, host.id)) {
|
||||
if (snippet.trigger === 'onConnect') {
|
||||
push(snippet.id);
|
||||
}
|
||||
}
|
||||
|
||||
for (const snippet of sortByVaultOrder(snippets)) {
|
||||
if (!isOnConnectScript(snippet)) continue;
|
||||
if (snippet.targetsAllHosts) continue;
|
||||
if (!snippetTargetsHostExplicitly(snippet, host.id)) continue;
|
||||
push(snippet.id);
|
||||
}
|
||||
|
||||
return ordered;
|
||||
}
|
||||
|
||||
/** Effective ordered script IDs for a host (lazy migrate + prune). */
|
||||
export function getHostConnectScriptIds(host: Host, snippets: Snippet[]): string[] {
|
||||
if (host.connectScriptIds !== undefined) {
|
||||
return pruneConnectScriptIds(host.connectScriptIds, snippets);
|
||||
}
|
||||
return migrateHostConnectScriptIds(host, snippets);
|
||||
}
|
||||
|
||||
/**
|
||||
* Host-details draft queue: includes scripts pending promote-to-onConnect on save.
|
||||
* Runtime connect still uses getHostConnectScriptIds (onConnect only).
|
||||
*/
|
||||
export function getEditableHostConnectScriptIds(host: Host, snippets: Snippet[]): string[] {
|
||||
if (host.connectScriptIds !== undefined) {
|
||||
return pruneEditableConnectScriptIds(host.connectScriptIds, snippets);
|
||||
}
|
||||
return migrateHostConnectScriptIds(host, snippets);
|
||||
}
|
||||
|
||||
export function ensureHostConnectScriptIds(host: Host, snippets: Snippet[]): Host {
|
||||
if (host.connectScriptIds !== undefined) {
|
||||
const pruned = pruneEditableConnectScriptIds(host.connectScriptIds, snippets);
|
||||
if (pruned.length === host.connectScriptIds.length
|
||||
&& pruned.every((id, index) => id === host.connectScriptIds![index])) {
|
||||
return host;
|
||||
}
|
||||
return { ...host, connectScriptIds: pruned };
|
||||
}
|
||||
const migrated = migrateHostConnectScriptIds(host, snippets);
|
||||
return migrated.length > 0 ? { ...host, connectScriptIds: migrated } : host;
|
||||
}
|
||||
|
||||
/** True when host references connect scripts that are not present in snippets yet. */
|
||||
export function hasUnresolvedConnectScriptBindings(host: Host, snippets: Snippet[]): boolean {
|
||||
const candidateIds = new Set<string>();
|
||||
if (host.loginScriptId) candidateIds.add(host.loginScriptId);
|
||||
for (const id of host.connectScriptIds ?? []) {
|
||||
if (id) candidateIds.add(id);
|
||||
}
|
||||
for (const id of candidateIds) {
|
||||
if (!snippets.some((snippet) => snippet.id === id)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Group-scoped onConnect scripts inherited dynamically from the host's current group. */
|
||||
export function getGroupConnectScriptsForHost(host: Host, snippets: Snippet[]): Snippet[] {
|
||||
return sortByVaultOrder(
|
||||
snippets.filter(
|
||||
(snippet) => isOnConnectScript(snippet)
|
||||
&& !snippet.targetsAllHosts
|
||||
&& snippetTargetsHostGroup(snippet, host),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/** Resolve full onConnect run list: global, group-inherited, then host queue. */
|
||||
export function resolveConnectScriptsForHost(host: Host, snippets: Snippet[]): Snippet[] {
|
||||
const hostIds = getHostConnectScriptIds(host, snippets);
|
||||
const hostIdSet = new Set(hostIds);
|
||||
const globals = getGlobalConnectScripts(snippets).filter(
|
||||
(snippet) => snippet.id && !hostIdSet.has(snippet.id),
|
||||
);
|
||||
const inheritedIds = new Set(globals.map((snippet) => snippet.id));
|
||||
const groupScripts = getGroupConnectScriptsForHost(host, snippets).filter(
|
||||
(snippet) => snippet.id && !hostIdSet.has(snippet.id) && !inheritedIds.has(snippet.id),
|
||||
);
|
||||
const hostScripts = hostIds
|
||||
.map((id) => scriptById(snippets, id))
|
||||
.filter((snippet): snippet is Snippet => Boolean(snippet));
|
||||
return [...globals, ...groupScripts, ...hostScripts];
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether connecting this host can run automation that depends on the initial
|
||||
* login output. Missing referenced scripts still count: vault hydration or a
|
||||
* later sync may restore them after the terminal has already started.
|
||||
*/
|
||||
export function hasHostConnectAutomation(host: Host, snippets: Snippet[]): boolean {
|
||||
return hasUnresolvedConnectScriptBindings(host, snippets)
|
||||
|| resolveConnectScriptsForHost(host, snippets).length > 0;
|
||||
}
|
||||
|
||||
export function shouldUseFreshSshConnectionForAutomation(options: {
|
||||
host: Host;
|
||||
snippets: Snippet[];
|
||||
vaultInitialized: boolean;
|
||||
hasPendingScript?: boolean;
|
||||
connectAutomationConsumed?: boolean;
|
||||
}): boolean {
|
||||
return options.hasPendingScript === true
|
||||
|| (
|
||||
options.connectAutomationConsumed !== true
|
||||
&& (
|
||||
!options.vaultInitialized
|
||||
|| hasHostConnectAutomation(options.host, options.snippets)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark the current connection's automation decision as final once the vault
|
||||
* has hydrated, even when it hydrated to an empty script list. This prevents a
|
||||
* later sync from starting a newly arrived global script against a connection
|
||||
* whose initial login output may already have been skipped.
|
||||
*/
|
||||
export function shouldMarkConnectAutomationConsumed(options: {
|
||||
allConnectScriptsDone: boolean;
|
||||
vaultInitialized: boolean;
|
||||
hasUnresolvedBindings: boolean;
|
||||
}): boolean {
|
||||
return options.allConnectScriptsDone
|
||||
&& options.vaultInitialized
|
||||
&& !options.hasUnresolvedBindings;
|
||||
}
|
||||
|
||||
export function appendHostConnectScript(host: Host, scriptId: string, snippets: Snippet[]): Host {
|
||||
const snippet = scriptById(snippets, scriptId);
|
||||
if (!snippet) return host;
|
||||
const current = getEditableHostConnectScriptIds(host, snippets);
|
||||
if (current.includes(scriptId)) {
|
||||
return { ...host, connectScriptIds: current };
|
||||
}
|
||||
return { ...host, connectScriptIds: [...current, scriptId] };
|
||||
}
|
||||
|
||||
export function removeHostConnectScript(host: Host, scriptId: string, snippets: Snippet[]): Host {
|
||||
const current = getEditableHostConnectScriptIds(host, snippets);
|
||||
const next = current.filter((id) => id !== scriptId);
|
||||
return { ...host, connectScriptIds: next };
|
||||
}
|
||||
|
||||
export function reorderHostConnectScript(
|
||||
host: Host,
|
||||
draggedScriptId: string,
|
||||
targetScriptId: string,
|
||||
position: 'before' | 'after',
|
||||
snippets: Snippet[],
|
||||
): Host {
|
||||
if (draggedScriptId === targetScriptId) return host;
|
||||
const current = [...getEditableHostConnectScriptIds(host, snippets)];
|
||||
const fromIndex = current.indexOf(draggedScriptId);
|
||||
const targetIndex = current.indexOf(targetScriptId);
|
||||
if (fromIndex === -1 || targetIndex === -1) return host;
|
||||
|
||||
current.splice(fromIndex, 1);
|
||||
let insertIndex = current.indexOf(targetScriptId);
|
||||
if (insertIndex === -1) return host;
|
||||
if (position === 'after') insertIndex += 1;
|
||||
current.splice(insertIndex, 0, draggedScriptId);
|
||||
return { ...host, connectScriptIds: current };
|
||||
}
|
||||
|
||||
export function prepareSnippetForHostConnectQueue(snippet: Snippet, hostId: string): Snippet {
|
||||
if (!isScriptSnippet(snippet)) return snippet;
|
||||
if (snippet.targetsAllHosts) {
|
||||
return snippet.trigger === 'onConnect'
|
||||
? snippet
|
||||
: { ...snippet, trigger: 'onConnect' };
|
||||
}
|
||||
return {
|
||||
...linkHostToScript(snippet, hostId),
|
||||
trigger: 'onConnect',
|
||||
};
|
||||
}
|
||||
|
||||
function connectQueueSnippetNeedsPromote(snippet: Snippet, hostId: string): boolean {
|
||||
if (!isScriptSnippet(snippet)) return false;
|
||||
if (snippet.trigger !== 'onConnect') return true;
|
||||
if (snippet.targetsAllHosts) return false;
|
||||
return !snippetTargetsHostExplicitly(snippet, hostId);
|
||||
}
|
||||
|
||||
function snippetTargetsEqual(left: Snippet, right: Snippet): boolean {
|
||||
if (Boolean(left.targetsAllHosts) !== Boolean(right.targetsAllHosts)) return false;
|
||||
const leftTargets = left.targets ?? [];
|
||||
const rightTargets = right.targets ?? [];
|
||||
if (leftTargets.length !== rightTargets.length) return false;
|
||||
if (!leftTargets.every((id, index) => id === rightTargets[index])) return false;
|
||||
const leftGroups = left.targetGroups ?? [];
|
||||
const rightGroups = right.targetGroups ?? [];
|
||||
if (leftGroups.length !== rightGroups.length) return false;
|
||||
return leftGroups.every((path, index) => path === rightGroups[index]);
|
||||
}
|
||||
|
||||
/**
|
||||
* After promoting a script to onConnect, ensure every remaining target host with an
|
||||
* explicit connectScriptIds queue includes it. Hosts without an explicit queue still
|
||||
* pick the script up via migrateHostConnectScriptIds.
|
||||
*/
|
||||
export function ensureTargetHostsHaveConnectScript(
|
||||
hosts: Host[],
|
||||
snippet: Snippet,
|
||||
snippets: Snippet[],
|
||||
excludeHostId?: string,
|
||||
): Host[] {
|
||||
if (!snippet.id || !isScriptSnippet(snippet) || snippet.trigger !== 'onConnect') return hosts;
|
||||
if (snippet.targetsAllHosts) return hosts;
|
||||
const targetIds = new Set(snippet.targets ?? []);
|
||||
if (targetIds.size === 0) return hosts;
|
||||
|
||||
let changed = false;
|
||||
const nextHosts = hosts.map((host) => {
|
||||
if (host.id === excludeHostId) return host;
|
||||
if (!targetIds.has(host.id)) return host;
|
||||
if (host.connectScriptIds === undefined) return host;
|
||||
const updated = appendHostConnectScript(host, snippet.id!, snippets);
|
||||
if (updated !== host) changed = true;
|
||||
return updated;
|
||||
});
|
||||
return changed ? nextHosts : hosts;
|
||||
}
|
||||
|
||||
export type SyncHostConnectQueueSaveOptions = {
|
||||
/** Snippets snapshot from when the host editor opened (detect concurrent demotion). */
|
||||
baselineSnippets?: Snippet[];
|
||||
/** When provided, promote also syncs other hosts that remain in targets. */
|
||||
hosts?: Host[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Sync script metadata when saving a host connect queue.
|
||||
* Promotes draft/manual queue entries, preserves global onConnect scripts,
|
||||
* drops concurrently demoted entries, and optionally syncs peer host queues.
|
||||
*/
|
||||
export function syncSnippetsForHostConnectQueueSave(
|
||||
snippets: Snippet[],
|
||||
hostId: string,
|
||||
previousQueueIds: string[],
|
||||
nextQueueIds: string[],
|
||||
options: SyncHostConnectQueueSaveOptions = {},
|
||||
): {
|
||||
snippets: Snippet[];
|
||||
hosts: Host[];
|
||||
connectScriptIds: string[];
|
||||
changed: boolean;
|
||||
} {
|
||||
const previousSet = new Set(previousQueueIds);
|
||||
const baseline = options.baselineSnippets ?? snippets;
|
||||
let nextSnippets = snippets;
|
||||
let nextHosts = options.hosts ?? [];
|
||||
let changed = false;
|
||||
const retainedIds: string[] = [];
|
||||
const demotedDropIds = new Set<string>();
|
||||
|
||||
for (const scriptId of nextQueueIds) {
|
||||
const item = nextSnippets.find((entry) => entry.id === scriptId && isScriptSnippet(entry));
|
||||
if (!item) continue;
|
||||
|
||||
if (!connectQueueSnippetNeedsPromote(item, hostId)) {
|
||||
retainedIds.push(scriptId);
|
||||
continue;
|
||||
}
|
||||
|
||||
const newlyAdded = !previousSet.has(scriptId);
|
||||
const baselineItem = baseline.find((entry) => entry.id === scriptId);
|
||||
const baselineTrigger = baselineItem && isScriptSnippet(baselineItem)
|
||||
? baselineItem.trigger
|
||||
: undefined;
|
||||
if (!newlyAdded && baselineTrigger === 'onConnect' && item.trigger !== 'onConnect') {
|
||||
// Concurrent demotion while the editor stayed open: keep demotion, drop stale queue id.
|
||||
demotedDropIds.add(scriptId);
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
!newlyAdded
|
||||
&& baselineItem
|
||||
&& isScriptSnippet(baselineItem)
|
||||
&& (Boolean(baselineItem.targetsAllHosts) || snippetTargetsHostExplicitly(baselineItem, hostId))
|
||||
&& !(Boolean(item.targetsAllHosts) || snippetTargetsHostExplicitly(item, hostId))
|
||||
) {
|
||||
// Concurrent target removal for this host: do not re-link on save.
|
||||
demotedDropIds.add(scriptId);
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
!newlyAdded
|
||||
&& baselineTrigger
|
||||
&& baselineTrigger !== 'onConnect'
|
||||
&& item.trigger !== baselineTrigger
|
||||
) {
|
||||
// Concurrent non-onConnect trigger edit (e.g. manual -> onOutput): do not overwrite.
|
||||
demotedDropIds.add(scriptId);
|
||||
continue;
|
||||
}
|
||||
|
||||
const prepared = prepareSnippetForHostConnectQueue(item, hostId);
|
||||
if (
|
||||
prepared.trigger !== item.trigger
|
||||
|| !snippetTargetsEqual(prepared, item)
|
||||
) {
|
||||
nextSnippets = nextSnippets.map((entry) => (entry.id === scriptId ? prepared : entry));
|
||||
changed = true;
|
||||
if (options.hosts) {
|
||||
const syncedHosts = ensureTargetHostsHaveConnectScript(
|
||||
nextHosts,
|
||||
prepared,
|
||||
nextSnippets,
|
||||
hostId,
|
||||
);
|
||||
if (syncedHosts !== nextHosts) {
|
||||
nextHosts = syncedHosts;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
retainedIds.push(scriptId);
|
||||
}
|
||||
|
||||
for (const scriptId of previousQueueIds) {
|
||||
if (retainedIds.includes(scriptId)) continue;
|
||||
if (demotedDropIds.has(scriptId)) continue;
|
||||
const unlinked = unlinkHostFromScripts(nextSnippets, hostId, scriptId);
|
||||
if (unlinked !== nextSnippets) {
|
||||
nextSnippets = unlinked;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
retainedIds.length !== nextQueueIds.length
|
||||
|| retainedIds.some((id, index) => id !== nextQueueIds[index])
|
||||
) {
|
||||
changed = true;
|
||||
}
|
||||
|
||||
return {
|
||||
snippets: nextSnippets,
|
||||
hosts: nextHosts,
|
||||
connectScriptIds: retainedIds,
|
||||
changed,
|
||||
};
|
||||
}
|
||||
|
||||
export function syncHostsForSnippetTargetChange(
|
||||
hosts: Host[],
|
||||
snippet: Snippet,
|
||||
prevTargetIds: string[] | undefined,
|
||||
snippets: Snippet[],
|
||||
): Host[] {
|
||||
if (!isScriptSnippet(snippet) || snippet.trigger !== 'onConnect' || !snippet.id) {
|
||||
return hosts;
|
||||
}
|
||||
if (snippet.targetsAllHosts) {
|
||||
return hosts.map((host) => removeHostConnectScript(host, snippet.id!, snippets));
|
||||
}
|
||||
|
||||
const prev = new Set(prevTargetIds ?? []);
|
||||
const next = new Set(snippet.targets ?? []);
|
||||
const added = [...next].filter((id) => !prev.has(id));
|
||||
const removed = [...prev].filter((id) => !next.has(id));
|
||||
if (added.length === 0 && removed.length === 0) return hosts;
|
||||
|
||||
return hosts.map((host) => {
|
||||
let updated = host;
|
||||
if (added.includes(host.id)) {
|
||||
updated = appendHostConnectScript(updated, snippet.id!, snippets);
|
||||
}
|
||||
if (removed.includes(host.id)) {
|
||||
updated = removeHostConnectScript(updated, snippet.id!, snippets);
|
||||
}
|
||||
return updated;
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user