diff --git a/Notice b/Notice new file mode 100644 index 0000000..74506d0 --- /dev/null +++ b/Notice @@ -0,0 +1 @@ +To test auto code review in PRs diff --git a/windowsPaths.ts b/windowsPaths.ts new file mode 100644 index 0000000..c0774ea --- /dev/null +++ b/windowsPaths.ts @@ -0,0 +1,173 @@ +import memoize from 'lodash-es/memoize.js' +import * as path from 'path' +import * as pathWin32 from 'path/win32' +import { getCwd } from './cwd.js' +import { logForDebugging } from './debug.js' +import { execSync_DEPRECATED } from './execSyncWrapper.js' +import { memoizeWithLRU } from './memoize.js' +import { getPlatform } from './platform.js' + +/** + * Check if a file or directory exists on Windows using the dir command + * @param path - The path to check + * @returns true if the path exists, false otherwise + */ +function checkPathExists(path: string): boolean { + try { + execSync_DEPRECATED(`dir "${path}"`, { stdio: 'pipe' }) + return true + } catch { + return false + } +} + +/** + * Find an executable using where.exe on Windows + * @param executable - The name of the executable to find + * @returns The path to the executable or null if not found + */ +function findExecutable(executable: string): string | null { + // For git, check common installation locations first + if (executable === 'git') { + const defaultLocations = [ + // check 64 bit before 32 bit + 'C:\\Program Files\\Git\\cmd\\git.exe', + 'C:\\Program Files (x86)\\Git\\cmd\\git.exe', + // intentionally don't look for C:\Program Files\Git\mingw64\bin\git.exe + // because that directory is the "raw" tools with no environment setup + ] + + for (const location of defaultLocations) { + if (checkPathExists(location)) { + return location + } + } + } + + // Fall back to where.exe + try { + const result = execSync_DEPRECATED(`where.exe ${executable}`, { + stdio: 'pipe', + encoding: 'utf8', + }).trim() + + // SECURITY: Filter out any results from the current directory + // to prevent executing malicious git.bat/cmd/exe files + const paths = result.split('\r\n').filter(Boolean) + const cwd = getCwd().toLowerCase() + + for (const candidatePath of paths) { + // Normalize and compare paths to ensure we're not in current directory + const normalizedPath = path.resolve(candidatePath).toLowerCase() + const pathDir = path.dirname(normalizedPath).toLowerCase() + + // Skip if the executable is in the current working directory + if (pathDir === cwd || normalizedPath.startsWith(cwd + path.sep)) { + logForDebugging( + `Skipping potentially malicious executable in current directory: ${candidatePath}`, + ) + continue + } + + // Return the first valid path that's not in the current directory + return candidatePath + } + + return null + } catch { + return null + } +} + +/** + * If Windows, set the SHELL environment variable to git-bash path. + * This is used by BashTool and Shell.ts for user shell commands. + * COMSPEC is left unchanged for system process execution. + */ +export function setShellIfWindows(): void { + if (getPlatform() === 'windows') { + const gitBashPath = findGitBashPath() + process.env.SHELL = gitBashPath + logForDebugging(`Using bash path: "${gitBashPath}"`) + } +} + +/** + * Find the path where `bash.exe` included with git-bash exists, exiting the process if not found. + */ +export const findGitBashPath = memoize((): string => { + if (process.env.CLAUDE_CODE_GIT_BASH_PATH) { + if (checkPathExists(process.env.CLAUDE_CODE_GIT_BASH_PATH)) { + return process.env.CLAUDE_CODE_GIT_BASH_PATH + } + // biome-ignore lint/suspicious/noConsole:: intentional console output + console.error( + `Claude Code was unable to find CLAUDE_CODE_GIT_BASH_PATH path "${process.env.CLAUDE_CODE_GIT_BASH_PATH}"`, + ) + // eslint-disable-next-line custom-rules/no-process-exit + process.exit(1) + } + + const gitPath = findExecutable('git') + if (gitPath) { + const bashPath = pathWin32.join(gitPath, '..', '..', 'bin', 'bash.exe') + if (checkPathExists(bashPath)) { + return bashPath + } + } + + // biome-ignore lint/suspicious/noConsole:: intentional console output + console.error( + 'Claude Code on Windows requires git-bash (https://git-scm.com/downloads/win). If installed but not in PATH, set environment variable pointing to your bash.exe, similar to: CLAUDE_CODE_GIT_BASH_PATH=C:\\Program Files\\Git\\bin\\bash.exe', + ) + // eslint-disable-next-line custom-rules/no-process-exit + process.exit(1) +}) + +/** Convert a Windows path to a POSIX path using pure JS. */ +export const windowsPathToPosixPath = memoizeWithLRU( + (windowsPath: string): string => { + // Handle UNC paths: \\server\share -> //server/share + if (windowsPath.startsWith('\\\\')) { + return windowsPath.replace(/\\/g, '/') + } + // Handle drive letter paths: C:\Users\foo -> /c/Users/foo + const match = windowsPath.match(/^([A-Za-z]):[/\\]/) + if (match) { + const driveLetter = match[1]!.toLowerCase() + return '/' + driveLetter + windowsPath.slice(2).replace(/\\/g, '/') + } + // Already POSIX or relative — just flip slashes + return windowsPath.replace(/\\/g, '/') + }, + (p: string) => p, + 500, +) + +/** Convert a POSIX path to a Windows path using pure JS. */ +export const posixPathToWindowsPath = memoizeWithLRU( + (posixPath: string): string => { + // Handle UNC paths: //server/share -> \\server\share + if (posixPath.startsWith('//')) { + return posixPath.replace(/\//g, '\\') + } + // Handle /cygdrive/c/... format + const cygdriveMatch = posixPath.match(/^\/cygdrive\/([A-Za-z])(\/|$)/) + if (cygdriveMatch) { + const driveLetter = cygdriveMatch[1]!.toUpperCase() + const rest = posixPath.slice(('/cygdrive/' + cygdriveMatch[1]).length) + return driveLetter + ':' + (rest || '\\').replace(/\//g, '\\') + } + // Handle /c/... format (MSYS2/Git Bash) + const driveMatch = posixPath.match(/^\/([A-Za-z])(\/|$)/) + if (driveMatch) { + const driveLetter = driveMatch[1]!.toUpperCase() + const rest = posixPath.slice(2) + return driveLetter + ':' + (rest || '\\').replace(/\//g, '\\') + } + // Already Windows or relative — just flip slashes + return posixPath.replace(/\//g, '\\') + }, + (p: string) => p, + 500, +) diff --git a/withResolvers.ts b/withResolvers.ts new file mode 100644 index 0000000..1f9815b --- /dev/null +++ b/withResolvers.ts @@ -0,0 +1,13 @@ +/** + * Polyfill for Promise.withResolvers() (ES2024, Node 22+). + * package.json declares "engines": { "node": ">=18.0.0" } so we can't use the native one. + */ +export function withResolvers(): PromiseWithResolvers { + let resolve!: (value: T | PromiseLike) => void + let reject!: (reason?: unknown) => void + const promise = new Promise((res, rej) => { + resolve = res + reject = rej + }) + return { promise, resolve, reject } +} diff --git a/words.ts b/words.ts new file mode 100644 index 0000000..aeda869 --- /dev/null +++ b/words.ts @@ -0,0 +1,800 @@ +/** + * Random word slug generator for plan IDs + * Inspired by https://github.com/nas5w/random-word-slugs + * with Claude-flavored words + */ +import { randomBytes } from 'crypto' + +// Adjectives for slug generation - whimsical and delightful +const ADJECTIVES = [ + // Classic pleasant adjectives + 'abundant', + 'ancient', + 'bright', + 'calm', + 'cheerful', + 'clever', + 'cozy', + 'curious', + 'dapper', + 'dazzling', + 'deep', + 'delightful', + 'eager', + 'elegant', + 'enchanted', + 'fancy', + 'fluffy', + 'gentle', + 'gleaming', + 'golden', + 'graceful', + 'happy', + 'hidden', + 'humble', + 'jolly', + 'joyful', + 'keen', + 'kind', + 'lively', + 'lovely', + 'lucky', + 'luminous', + 'magical', + 'majestic', + 'mellow', + 'merry', + 'mighty', + 'misty', + 'noble', + 'peaceful', + 'playful', + 'polished', + 'precious', + 'proud', + 'quiet', + 'quirky', + 'radiant', + 'rosy', + 'serene', + 'shiny', + 'silly', + 'sleepy', + 'smooth', + 'snazzy', + 'snug', + 'snuggly', + 'soft', + 'sparkling', + 'spicy', + 'splendid', + 'sprightly', + 'starry', + 'steady', + 'sunny', + 'swift', + 'tender', + 'tidy', + 'toasty', + 'tranquil', + 'twinkly', + 'valiant', + 'vast', + 'velvet', + 'vivid', + 'warm', + 'whimsical', + 'wild', + 'wise', + 'witty', + 'wondrous', + 'zany', + 'zesty', + 'zippy', + // Whimsical / magical + 'breezy', + 'bubbly', + 'buzzing', + 'cheeky', + 'cosmic', + 'cozy', + 'crispy', + 'crystalline', + 'cuddly', + 'drifting', + 'dreamy', + 'effervescent', + 'ethereal', + 'fizzy', + 'flickering', + 'floating', + 'floofy', + 'fluttering', + 'foamy', + 'frolicking', + 'fuzzy', + 'giggly', + 'glimmering', + 'glistening', + 'glittery', + 'glowing', + 'goofy', + 'groovy', + 'harmonic', + 'hazy', + 'humming', + 'iridescent', + 'jaunty', + 'jazzy', + 'jiggly', + 'melodic', + 'moonlit', + 'mossy', + 'nifty', + 'peppy', + 'prancy', + 'purrfect', + 'purring', + 'quizzical', + 'rippling', + 'rustling', + 'shimmering', + 'shimmying', + 'snappy', + 'snoopy', + 'squishy', + 'swirling', + 'ticklish', + 'tingly', + 'twinkling', + 'velvety', + 'wiggly', + 'wobbly', + 'woolly', + 'zazzy', + // Programming concepts + 'abstract', + 'adaptive', + 'agile', + 'async', + 'atomic', + 'binary', + 'cached', + 'compiled', + 'composed', + 'compressed', + 'concurrent', + 'cryptic', + 'curried', + 'declarative', + 'delegated', + 'distributed', + 'dynamic', + 'eager', + 'elegant', + 'encapsulated', + 'enumerated', + 'eventual', + 'expressive', + 'federated', + 'functional', + 'generic', + 'greedy', + 'hashed', + 'idempotent', + 'immutable', + 'imperative', + 'indexed', + 'inherited', + 'iterative', + 'lazy', + 'lexical', + 'linear', + 'linked', + 'logical', + 'memoized', + 'modular', + 'mutable', + 'nested', + 'optimized', + 'parallel', + 'parsed', + 'partitioned', + 'piped', + 'polymorphic', + 'pure', + 'reactive', + 'recursive', + 'refactored', + 'reflective', + 'replicated', + 'resilient', + 'robust', + 'scalable', + 'sequential', + 'serialized', + 'sharded', + 'sorted', + 'staged', + 'stateful', + 'stateless', + 'streamed', + 'structured', + 'synchronous', + 'synthetic', + 'temporal', + 'transient', + 'typed', + 'unified', + 'validated', + 'vectorized', + 'virtual', +] as const + +// Nouns for slug generation - whimsical creatures, nature, and fun objects +const NOUNS = [ + // Nature & cosmic + 'aurora', + 'avalanche', + 'blossom', + 'breeze', + 'brook', + 'bubble', + 'canyon', + 'cascade', + 'cloud', + 'clover', + 'comet', + 'coral', + 'cosmos', + 'creek', + 'crescent', + 'crystal', + 'dawn', + 'dewdrop', + 'dusk', + 'eclipse', + 'ember', + 'feather', + 'fern', + 'firefly', + 'flame', + 'flurry', + 'fog', + 'forest', + 'frost', + 'galaxy', + 'garden', + 'glacier', + 'glade', + 'grove', + 'harbor', + 'horizon', + 'island', + 'lagoon', + 'lake', + 'leaf', + 'lightning', + 'meadow', + 'meteor', + 'mist', + 'moon', + 'moonbeam', + 'mountain', + 'nebula', + 'nova', + 'ocean', + 'orbit', + 'pebble', + 'petal', + 'pine', + 'planet', + 'pond', + 'puddle', + 'quasar', + 'rain', + 'rainbow', + 'reef', + 'ripple', + 'river', + 'shore', + 'sky', + 'snowflake', + 'spark', + 'spring', + 'star', + 'stardust', + 'starlight', + 'storm', + 'stream', + 'summit', + 'sun', + 'sunbeam', + 'sunrise', + 'sunset', + 'thunder', + 'tide', + 'twilight', + 'valley', + 'volcano', + 'waterfall', + 'wave', + 'willow', + 'wind', + // Cute creatures + 'alpaca', + 'axolotl', + 'badger', + 'bear', + 'beaver', + 'bee', + 'bird', + 'bumblebee', + 'bunny', + 'cat', + 'chipmunk', + 'crab', + 'crane', + 'deer', + 'dolphin', + 'dove', + 'dragon', + 'dragonfly', + 'duckling', + 'eagle', + 'elephant', + 'falcon', + 'finch', + 'flamingo', + 'fox', + 'frog', + 'giraffe', + 'goose', + 'hamster', + 'hare', + 'hedgehog', + 'hippo', + 'hummingbird', + 'jellyfish', + 'kitten', + 'koala', + 'ladybug', + 'lark', + 'lemur', + 'llama', + 'lobster', + 'lynx', + 'manatee', + 'meerkat', + 'moth', + 'narwhal', + 'newt', + 'octopus', + 'otter', + 'owl', + 'panda', + 'parrot', + 'peacock', + 'pelican', + 'penguin', + 'phoenix', + 'piglet', + 'platypus', + 'pony', + 'porcupine', + 'puffin', + 'puppy', + 'quail', + 'quokka', + 'rabbit', + 'raccoon', + 'raven', + 'robin', + 'salamander', + 'seahorse', + 'seal', + 'sloth', + 'snail', + 'sparrow', + 'sphinx', + 'squid', + 'squirrel', + 'starfish', + 'swan', + 'tiger', + 'toucan', + 'turtle', + 'unicorn', + 'walrus', + 'whale', + 'wolf', + 'wombat', + 'wren', + 'yeti', + 'zebra', + // Fun objects & concepts + 'acorn', + 'anchor', + 'balloon', + 'beacon', + 'biscuit', + 'blanket', + 'bonbon', + 'book', + 'boot', + 'cake', + 'candle', + 'candy', + 'castle', + 'charm', + 'clock', + 'cocoa', + 'cookie', + 'crayon', + 'crown', + 'cupcake', + 'donut', + 'dream', + 'fairy', + 'fiddle', + 'flask', + 'flute', + 'fountain', + 'gadget', + 'gem', + 'gizmo', + 'globe', + 'goblet', + 'hammock', + 'harp', + 'haven', + 'hearth', + 'honey', + 'journal', + 'kazoo', + 'kettle', + 'key', + 'kite', + 'lantern', + 'lemon', + 'lighthouse', + 'locket', + 'lollipop', + 'mango', + 'map', + 'marble', + 'marshmallow', + 'melody', + 'mitten', + 'mochi', + 'muffin', + 'music', + 'nest', + 'noodle', + 'oasis', + 'origami', + 'pancake', + 'parasol', + 'peach', + 'pearl', + 'pebble', + 'pie', + 'pillow', + 'pinwheel', + 'pixel', + 'pizza', + 'plum', + 'popcorn', + 'pretzel', + 'prism', + 'pudding', + 'pumpkin', + 'puzzle', + 'quiche', + 'quill', + 'quilt', + 'riddle', + 'rocket', + 'rose', + 'scone', + 'scroll', + 'shell', + 'sketch', + 'snowglobe', + 'sonnet', + 'sparkle', + 'spindle', + 'sprout', + 'sundae', + 'swing', + 'taco', + 'teacup', + 'teapot', + 'thimble', + 'toast', + 'token', + 'tome', + 'tower', + 'treasure', + 'treehouse', + 'trinket', + 'truffle', + 'tulip', + 'umbrella', + 'waffle', + 'wand', + 'whisper', + 'whistle', + 'widget', + 'wreath', + 'zephyr', + // Computer scientists + 'abelson', + 'adleman', + 'aho', + 'allen', + 'babbage', + 'bachman', + 'backus', + 'barto', + 'bengio', + 'bentley', + 'blum', + 'boole', + 'brooks', + 'catmull', + 'cerf', + 'cherny', + 'church', + 'clarke', + 'cocke', + 'codd', + 'conway', + 'cook', + 'corbato', + 'cray', + 'curry', + 'dahl', + 'diffie', + 'dijkstra', + 'dongarra', + 'eich', + 'emerson', + 'engelbart', + 'feigenbaum', + 'floyd', + 'gosling', + 'graham', + 'gray', + 'hamming', + 'hanrahan', + 'hartmanis', + 'hejlsberg', + 'hellman', + 'hennessy', + 'hickey', + 'hinton', + 'hoare', + 'hollerith', + 'hopcroft', + 'hopper', + 'iverson', + 'kahan', + 'kahn', + 'karp', + 'kay', + 'kernighan', + 'knuth', + 'kurzweil', + 'lamport', + 'lampson', + 'lecun', + 'lerdorf', + 'liskov', + 'lovelace', + 'matsumoto', + 'mccarthy', + 'metcalfe', + 'micali', + 'milner', + 'minsky', + 'moler', + 'moore', + 'naur', + 'neumann', + 'newell', + 'nygaard', + 'papert', + 'parnas', + 'pascal', + 'patterson', + 'pearl', + 'perlis', + 'pike', + 'pnueli', + 'rabin', + 'reddy', + 'ritchie', + 'rivest', + 'rossum', + 'russell', + 'scott', + 'sedgewick', + 'shamir', + 'shannon', + 'sifakis', + 'simon', + 'stallman', + 'stearns', + 'steele', + 'stonebraker', + 'stroustrup', + 'sutherland', + 'sutton', + 'tarjan', + 'thacker', + 'thompson', + 'torvalds', + 'turing', + 'ullman', + 'valiant', + 'wadler', + 'wall', + 'wigderson', + 'wilkes', + 'wilkinson', + 'wirth', + 'wozniak', + 'yao', +] as const + +// Verbs for the middle word - whimsical action words +const VERBS = [ + 'baking', + 'beaming', + 'booping', + 'bouncing', + 'brewing', + 'bubbling', + 'chasing', + 'churning', + 'coalescing', + 'conjuring', + 'cooking', + 'crafting', + 'crunching', + 'cuddling', + 'dancing', + 'dazzling', + 'discovering', + 'doodling', + 'dreaming', + 'drifting', + 'enchanting', + 'exploring', + 'finding', + 'floating', + 'fluttering', + 'foraging', + 'forging', + 'frolicking', + 'gathering', + 'giggling', + 'gliding', + 'greeting', + 'growing', + 'hatching', + 'herding', + 'honking', + 'hopping', + 'hugging', + 'humming', + 'imagining', + 'inventing', + 'jingling', + 'juggling', + 'jumping', + 'kindling', + 'knitting', + 'launching', + 'leaping', + 'mapping', + 'marinating', + 'meandering', + 'mixing', + 'moseying', + 'munching', + 'napping', + 'nibbling', + 'noodling', + 'orbiting', + 'painting', + 'percolating', + 'petting', + 'plotting', + 'pondering', + 'popping', + 'prancing', + 'purring', + 'puzzling', + 'questing', + 'riding', + 'roaming', + 'rolling', + 'sauteeing', + 'scribbling', + 'seeking', + 'shimmying', + 'singing', + 'skipping', + 'sleeping', + 'snacking', + 'sniffing', + 'snuggling', + 'soaring', + 'sparking', + 'spinning', + 'splashing', + 'sprouting', + 'squishing', + 'stargazing', + 'stirring', + 'strolling', + 'swimming', + 'swinging', + 'tickling', + 'tinkering', + 'toasting', + 'tumbling', + 'twirling', + 'waddling', + 'wandering', + 'watching', + 'weaving', + 'whistling', + 'wibbling', + 'wiggling', + 'wishing', + 'wobbling', + 'wondering', + 'yawning', + 'zooming', +] as const + +/** + * Generate a cryptographically random integer in the range [0, max) + */ +function randomInt(max: number): number { + // Use crypto.randomBytes for better randomness than Math.random + const bytes = randomBytes(4) + const value = bytes.readUInt32BE(0) + return value % max +} + +/** + * Pick a random element from an array + */ +function pickRandom(array: readonly T[]): T { + return array[randomInt(array.length)]! +} + +/** + * Generate a random word slug in the format "adjective-verb-noun" + * Example: "gleaming-brewing-phoenix", "cosmic-pondering-lighthouse" + */ +export function generateWordSlug(): string { + const adjective = pickRandom(ADJECTIVES) + const verb = pickRandom(VERBS) + const noun = pickRandom(NOUNS) + return `${adjective}-${verb}-${noun}` +} + +/** + * Generate a shorter random word slug in the format "adjective-noun" + * Example: "graceful-unicorn", "cosmic-lighthouse" + */ +export function generateShortWordSlug(): string { + const adjective = pickRandom(ADJECTIVES) + const noun = pickRandom(NOUNS) + return `${adjective}-${noun}` +}