[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:
183
domain/hostGroupPathMutations.ts
Normal file
183
domain/hostGroupPathMutations.ts
Normal file
@@ -0,0 +1,183 @@
|
||||
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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user