108 lines
3.5 KiB
TypeScript
108 lines
3.5 KiB
TypeScript
import { readdirSync } from "node:fs";
|
||
import { join } from "node:path";
|
||
import { existsSync } from "node:fs";
|
||
|
||
export const ADJECTIVES = [
|
||
"amber", "azure", "bold", "brave", "bright",
|
||
"calm", "clear", "cool", "coral", "crisp",
|
||
"deft", "dusky", "eager", "early", "faint",
|
||
"fast", "fleet", "fresh", "gentle", "gilt",
|
||
"glad", "grand", "green", "happy", "hazy",
|
||
"ivory", "jade", "keen", "lemon", "light",
|
||
"lunar", "maple", "merry", "misty", "noble",
|
||
"opal", "pale", "pearl", "plush", "proud",
|
||
"quiet", "rapid", "rosy", "rusty", "sandy",
|
||
"sharp", "sleek", "solar", "swift", "vivid",
|
||
];
|
||
|
||
export const NOUNS = [
|
||
"aspen", "badger", "breeze", "brook", "cedar",
|
||
"cliff", "crane", "creek", "daisy", "delta",
|
||
"dune", "eagle", "ember", "falcon", "fern",
|
||
"finch", "flame", "frost", "grove", "hawk",
|
||
"heron", "iris", "lark", "lotus", "marsh",
|
||
"mesa", "moss", "oak", "olive", "orbit",
|
||
"otter", "panda", "peach", "petal", "pine",
|
||
"plume", "quail", "raven", "reef", "ridge",
|
||
"robin", "sage", "shore", "spark", "stone",
|
||
"thorn", "tiger", "trail", "trout", "wren",
|
||
];
|
||
|
||
/**
|
||
* Convert a string to a URL-friendly slug.
|
||
*
|
||
* - Lowercase
|
||
* - Replace spaces, underscores, and special chars with hyphens
|
||
* - Collapse multiple hyphens
|
||
* - Trim leading/trailing hyphens
|
||
*/
|
||
export function slugify(str: string): string {
|
||
return str
|
||
.toLowerCase()
|
||
.replace(/[^a-z0-9\s-]/g, "") // Remove special characters except spaces and hyphens
|
||
.replace(/\s+/g, "-") // Replace spaces with hyphens
|
||
.replace(/-+/g, "-") // Collapse multiple hyphens
|
||
.replace(/^-|-$/g, ""); // Trim leading/trailing hyphens
|
||
}
|
||
|
||
/**
|
||
* Generate a random, human-friendly worktree directory name.
|
||
*
|
||
* Names follow an `adjective-noun` pattern (e.g., `swirly-monkey`,
|
||
* `quiet-falcon`, `bright-orchid`) drawn from embedded word lists of
|
||
* ~50 adjectives × ~50 nouns, producing ~2,500 unique combinations.
|
||
*
|
||
* **Collision avoidance:** The function checks existing subdirectories
|
||
* under `<rootDir>/.worktrees/`. If the randomly chosen name already
|
||
* exists, a numeric suffix is appended (e.g., `swift-falcon-2`,
|
||
* `swift-falcon-3`) until a unique name is found.
|
||
*
|
||
* @param rootDir - The project root directory (parent of `.worktrees/`)
|
||
* @returns A unique worktree directory name (not a full path)
|
||
*/
|
||
export function generateWorktreeName(rootDir: string): string {
|
||
return generateReservedWorktreeName(rootDir);
|
||
}
|
||
|
||
/**
|
||
* Generate a unique worktree directory name while also avoiding names that
|
||
* have been reserved in-memory but may not exist on disk yet.
|
||
*/
|
||
export function generateReservedWorktreeName(
|
||
rootDir: string,
|
||
reservedNames: Set<string> = new Set(),
|
||
): string {
|
||
const adjective = ADJECTIVES[Math.floor(Math.random() * ADJECTIVES.length)];
|
||
const noun = NOUNS[Math.floor(Math.random() * NOUNS.length)];
|
||
const baseName = `${adjective}-${noun}`;
|
||
|
||
const worktreesDir = join(rootDir, ".worktrees");
|
||
const existing = getExistingWorktreeNames(worktreesDir);
|
||
for (const reserved of reservedNames) {
|
||
existing.add(reserved);
|
||
}
|
||
|
||
if (!existing.has(baseName)) {
|
||
return baseName;
|
||
}
|
||
|
||
// Collision — append numeric suffix
|
||
let suffix = 2;
|
||
while (existing.has(`${baseName}-${suffix}`)) {
|
||
suffix++;
|
||
}
|
||
return `${baseName}-${suffix}`;
|
||
}
|
||
|
||
function getExistingWorktreeNames(worktreesDir: string): Set<string> {
|
||
if (!existsSync(worktreesDir)) {
|
||
return new Set();
|
||
}
|
||
try {
|
||
const entries = readdirSync(worktreesDir, { withFileTypes: true });
|
||
return new Set(entries.filter((e) => e.isDirectory()).map((e) => e.name));
|
||
} catch {
|
||
return new Set();
|
||
}
|
||
}
|