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
184 lines
5.9 KiB
TypeScript
184 lines
5.9 KiB
TypeScript
import type { Host, ManagedSource, Snippet } from '../types';
|
|
|
|
export function normalizeGroupTargetPath(value: string): string {
|
|
return value
|
|
.replace(/\\/g, '/')
|
|
.split('/')
|
|
.map((part) => part.trim())
|
|
.filter(Boolean)
|
|
.join('/');
|
|
}
|
|
|
|
export function normalizeGroupTargetPaths(values: Iterable<string>): string[] {
|
|
return Array.from(new Set(
|
|
[...values].map(normalizeGroupTargetPath).filter(Boolean),
|
|
));
|
|
}
|
|
|
|
function replaceGroupPathPrefix(path: string, sourcePath: string, nextPath: string): string {
|
|
if (path === sourcePath) return nextPath;
|
|
if (path.startsWith(`${sourcePath}/`)) return nextPath + path.slice(sourcePath.length);
|
|
return path;
|
|
}
|
|
|
|
export function remapSnippetTargetGroupPaths(
|
|
snippets: Snippet[],
|
|
sourceValue: string,
|
|
nextValue: string,
|
|
): Snippet[] {
|
|
const sourcePath = normalizeGroupTargetPath(sourceValue);
|
|
const nextPath = normalizeGroupTargetPath(nextValue);
|
|
if (!sourcePath || !nextPath || sourcePath === nextPath) return snippets;
|
|
|
|
let changed = false;
|
|
const nextSnippets = snippets.map((snippet) => {
|
|
if (!snippet.targetGroups?.length) return snippet;
|
|
const targetGroups = normalizeGroupTargetPaths(
|
|
snippet.targetGroups.map((path) => replaceGroupPathPrefix(
|
|
normalizeGroupTargetPath(path), sourcePath, nextPath,
|
|
)),
|
|
);
|
|
if (
|
|
targetGroups.length === snippet.targetGroups.length
|
|
&& targetGroups.every((path, index) => path === snippet.targetGroups?.[index])
|
|
) return snippet;
|
|
changed = true;
|
|
return { ...snippet, targetGroups };
|
|
});
|
|
return changed ? nextSnippets : snippets;
|
|
}
|
|
|
|
export function removeSnippetTargetGroupPaths(
|
|
snippets: Snippet[],
|
|
removedValues: Iterable<string>,
|
|
): Snippet[] {
|
|
const removedPaths = [...removedValues]
|
|
.map(normalizeGroupTargetPath)
|
|
.filter(Boolean);
|
|
if (removedPaths.length === 0) return snippets;
|
|
const isRemoved = (path: string) => removedPaths.some(
|
|
(removed) => path === removed || path.startsWith(`${removed}/`),
|
|
);
|
|
|
|
let changed = false;
|
|
const nextSnippets = snippets.map((snippet) => {
|
|
if (snippet.targetGroups === undefined) return snippet;
|
|
const targetGroups = normalizeGroupTargetPaths(snippet.targetGroups).filter(
|
|
(path) => !isRemoved(path),
|
|
);
|
|
if (
|
|
targetGroups.length === snippet.targetGroups.length
|
|
&& targetGroups.every((path, index) => path === snippet.targetGroups?.[index])
|
|
) return snippet;
|
|
changed = true;
|
|
// An absent scope keeps the legacy onOutput "current session" behavior.
|
|
// Preserve an explicit empty list when the last selected group disappears
|
|
// so the script is disabled instead of silently widening to every host.
|
|
return { ...snippet, targetGroups };
|
|
});
|
|
return changed ? nextSnippets : snippets;
|
|
}
|
|
|
|
export function groupDisplayName(groupPath: string): string {
|
|
return groupPath.split('/').filter(Boolean).pop() ?? groupPath;
|
|
}
|
|
|
|
export function computeRenamedGroupPath(renameTargetPath: string, nextName: string): string {
|
|
const segments = renameTargetPath.split('/').filter(Boolean);
|
|
const parent = segments.slice(0, -1).join('/');
|
|
return parent ? `${parent}/${nextName}` : nextName;
|
|
}
|
|
|
|
export function allocateUnnamedGroupPath(
|
|
customGroups: string[],
|
|
parentPath: string | null,
|
|
baseName: string,
|
|
): { name: string; path: string } {
|
|
let name = baseName;
|
|
let counter = 2;
|
|
while (true) {
|
|
const path = parentPath ? `${parentPath}/${name}` : name;
|
|
if (!customGroups.includes(path)) {
|
|
return { name, path };
|
|
}
|
|
name = `${baseName} ${counter}`;
|
|
counter += 1;
|
|
}
|
|
}
|
|
|
|
export function ensureAncestorPathsExpanded(
|
|
groupPath: string,
|
|
ensurePathExpanded: (path: string) => void,
|
|
) {
|
|
const segments = groupPath.split('/').filter(Boolean);
|
|
for (let i = 1; i <= segments.length; i++) {
|
|
ensurePathExpanded(segments.slice(0, i).join('/'));
|
|
}
|
|
}
|
|
|
|
export type GroupPathRenameResult =
|
|
| {
|
|
ok: true;
|
|
nextPath: string;
|
|
updatedGroups: string[];
|
|
updatedHosts: Host[];
|
|
updatedManagedSources: ManagedSource[];
|
|
}
|
|
| { ok: false; error: 'required' | 'invalidChars' | 'duplicatePath' | 'unchanged' };
|
|
|
|
export function applyGroupPathRename(params: {
|
|
renameTargetPath: string;
|
|
nextName: string;
|
|
customGroups: string[];
|
|
hosts: Host[];
|
|
managedSources: ManagedSource[];
|
|
}): GroupPathRenameResult {
|
|
const trimmed = params.nextName.trim();
|
|
if (!trimmed) {
|
|
return { ok: false, error: 'required' };
|
|
}
|
|
if (trimmed.includes('/') || trimmed.includes('\\')) {
|
|
return { ok: false, error: 'invalidChars' };
|
|
}
|
|
|
|
const nextPath = computeRenamedGroupPath(params.renameTargetPath, trimmed);
|
|
if (nextPath === params.renameTargetPath) {
|
|
return { ok: false, error: 'unchanged' };
|
|
}
|
|
if (params.customGroups.includes(nextPath)) {
|
|
return { ok: false, error: 'duplicatePath' };
|
|
}
|
|
|
|
const { renameTargetPath, customGroups, hosts, managedSources } = params;
|
|
const updatedGroups = customGroups.map((groupPath) => {
|
|
if (groupPath === renameTargetPath) return nextPath;
|
|
if (groupPath.startsWith(`${renameTargetPath}/`)) {
|
|
return nextPath + groupPath.slice(renameTargetPath.length);
|
|
}
|
|
return groupPath;
|
|
});
|
|
const updatedHosts = hosts.map((host) => {
|
|
const group = host.group || '';
|
|
if (group === renameTargetPath) return { ...host, group: nextPath };
|
|
if (group.startsWith(`${renameTargetPath}/`)) {
|
|
return { ...host, group: nextPath + group.slice(renameTargetPath.length) };
|
|
}
|
|
return host;
|
|
});
|
|
const updatedManagedSources = managedSources.map((source) => {
|
|
if (source.groupName === renameTargetPath) return { ...source, groupName: nextPath };
|
|
if (source.groupName.startsWith(`${renameTargetPath}/`)) {
|
|
return { ...source, groupName: nextPath + source.groupName.slice(renameTargetPath.length) };
|
|
}
|
|
return source;
|
|
});
|
|
|
|
return {
|
|
ok: true,
|
|
nextPath,
|
|
updatedGroups: Array.from(new Set(updatedGroups)),
|
|
updatedHosts,
|
|
updatedManagedSources,
|
|
};
|
|
}
|