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
119 lines
3.9 KiB
TypeScript
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 };
|
|
}),
|
|
};
|
|
}
|