Files
NetMesh/domain/snippetPackage.ts
zhaolei 3c72efcb7f
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
[Init] Initial commit - NetMesh terminal manager
2026-09-13 18:24:01 +08:00

119 lines
3.9 KiB
TypeScript

export const SNIPPET_PACKAGE_NAME_PATTERN = /^[\w\p{L}\p{N}-]+$/u;
export const SNIPPET_PACKAGE_PATH_CHANGE_EVENT = "netcatty:snippets:package-path-change";
export type SnippetPackagePathChange = {
from: string;
to: string | null;
};
export type SnippetPackageRenameError = "empty" | "invalidChars" | "duplicate";
type PackagedSnippet = { package?: string };
export function isSnippetPackagePathAtOrBelow(path: string, root: string): boolean {
return path === root || path.startsWith(`${root}/`);
}
export function getSnippetPackageAncestors(path: string): string[] {
const normalized = path.trim().replace(/\/+$/g, "");
if (!normalized) return [];
const isAbsolute = normalized.startsWith("/");
const parts = normalized.split("/").filter(Boolean);
return parts.map((_, index) => {
const joined = parts.slice(0, index + 1).join("/");
return isAbsolute ? `/${joined}` : joined;
});
}
/** Persisted packages plus inferred ancestors from packages and snippet.package. */
export function collectSnippetPackageTreePaths(
packages: string[],
snippets: PackagedSnippet[] = [],
): string[] {
const paths = new Set<string>();
const add = (raw: string | undefined) => {
if (!raw) return;
getSnippetPackageAncestors(raw).forEach((ancestor) => paths.add(ancestor));
};
packages.forEach(add);
snippets.forEach((snippet) => add(snippet.package));
return Array.from(paths);
}
export const rewriteSnippetPackagePath = (value: string, from: string, to: string): string => {
if (value === from) return to;
if (value.startsWith(`${from}/`)) return to + value.slice(from.length);
return value;
};
export function applySnippetPackagePathChange(
value: string | undefined,
change: SnippetPackagePathChange,
): string {
const packagePath = value || "";
if (!packagePath || !isSnippetPackagePathAtOrBelow(packagePath, change.from)) {
return packagePath;
}
return change.to === null
? ""
: rewriteSnippetPackagePath(packagePath, change.from, change.to);
}
/** Remove a package and descendants; keep snippets and clear their package path. */
export function deleteSnippetPackage<T extends PackagedSnippet>(
packages: string[],
snippets: T[],
path: string,
): { packages: string[]; snippets: T[] } {
return {
packages: packages.filter((item) => !isSnippetPackagePathAtOrBelow(item, path)),
snippets: snippets.map((snippet) => {
const packagePath = snippet.package || "";
if (!packagePath || !isSnippetPackagePathAtOrBelow(packagePath, path)) return snippet;
return { ...snippet, package: "" };
}),
};
}
export function renameSnippetPackage<T extends PackagedSnippet>(
packages: string[],
snippets: T[],
path: string,
newName: string,
):
| { ok: true; packages: string[]; snippets: T[]; newPath: string }
| { ok: false; error: SnippetPackageRenameError } {
const trimmed = newName.trim();
if (!trimmed) return { ok: false, error: "empty" };
if (!SNIPPET_PACKAGE_NAME_PATTERN.test(trimmed)) return { ok: false, error: "invalidChars" };
const parts = path.split("/");
parts[parts.length - 1] = trimmed;
const newPath = parts.join("/");
if (newPath === path) {
return { ok: true, packages, snippets, newPath };
}
const occupied = collectSnippetPackageTreePaths(packages, snippets);
const duplicate = occupied.some(
(item) => item !== path && item.toLowerCase() === newPath.toLowerCase(),
);
if (duplicate) return { ok: false, error: "duplicate" };
return {
ok: true,
newPath,
packages: Array.from(new Set(
packages.map((item) => rewriteSnippetPackagePath(item, path, newPath)),
)),
snippets: snippets.map((snippet) => {
const packagePath = snippet.package || "";
if (!packagePath) return snippet;
const nextPath = rewriteSnippetPackagePath(packagePath, path, newPath);
return nextPath === packagePath ? snippet : { ...snippet, package: nextPath };
}),
};
}