#!/usr/bin/env node import { a as previewCdnTransform, c as agentLabel, d as detectAvailableAgents, h as detectProject, i as hasFrameworkEntryPoint, l as installSkill, n as installPackages, o as previewOptionsTransform, r as applyTransform, s as previewTransform, t as getPackagesToInstall, u as removeSkill, y as findReactProjects } from "./install-Car5vKPk.js"; import { Command } from "commander"; import path, { join, relative, resolve } from "node:path"; import pc from "picocolors"; import fs, { existsSync, readFileSync } from "node:fs"; import { fileURLToPath } from "node:url"; import basePrompts from "prompts"; import ora from "ora"; import { spawn, spawnSync } from "node:child_process"; import { createHash } from "node:crypto"; //#region src/utils/is-non-interactive.ts const AGENT_ENVIRONMENT_VARIABLES = [ "CI", "CLAUDECODE", "CURSOR_AGENT", "CODEX_CI", "OPENCODE", "AMP_HOME", "AMI" ]; const isEnvironmentVariableSet = (variable) => Boolean(process.env[variable]); const detectNonInteractive = (yesFlag) => yesFlag || AGENT_ENVIRONMENT_VARIABLES.some(isEnvironmentVariableSet) || !process.stdin.isTTY; //#endregion //#region src/utils/highlighter.ts const highlighter = { error: pc.red, warn: pc.yellow, info: pc.cyan, success: pc.green, dim: pc.dim }; //#endregion //#region src/utils/logger.ts const logger = { error(...args) { console.log(highlighter.error(args.join(" "))); }, warn(...args) { console.log(highlighter.warn(args.join(" "))); }, success(...args) { console.log(highlighter.success(args.join(" "))); }, log(...args) { console.log(args.join(" ")); }, break() { console.log(""); } }; //#endregion //#region src/utils/handle-error.ts const handleError = (error) => { logger.break(); logger.error("Something went wrong. Please check the error below for more details."); logger.error("If the problem persists, please open an issue on GitHub."); logger.error(""); if (error instanceof Error) logger.error(error.message); logger.break(); process.exit(1); }; //#endregion //#region src/utils/unref-stdin.ts const unrefStdin = () => { if (process.stdin.isTTY) return; process.stdin.unref?.(); }; //#endregion //#region src/utils/prompts.ts const onCancel = () => { logger.break(); logger.log("Cancelled."); logger.break(); process.exit(0); }; const prompts = (questions) => { return basePrompts(questions, { onCancel }).finally(unrefStdin); }; //#endregion //#region src/utils/spinner.ts const spinner = (text) => ora({ text }); //#endregion //#region src/utils/prompt-skill-install.ts const promptSkillInstall = async ({ yes = false, global = false, cwd = process.cwd() } = {}) => { const detectedAgents = await detectAvailableAgents(); if (detectedAgents.length === 0) { logger.warn("No supported agents detected."); return false; } let selectedAgents = detectedAgents; if (!yes) { const { confirmed } = await prompts({ type: "confirm", name: "confirmed", message: `Install the React Grab skill (${global ? "global" : "this project"})?`, initial: true }); if (!confirmed) return false; const { agents } = await prompts({ type: "multiselect", name: "agents", message: `Install the React Grab skill (${global ? "global" : "this project"}) for:`, choices: detectedAgents.map((agent) => ({ title: agentLabel(agent), value: agent, selected: true })), instructions: false, min: 1 }); selectedAgents = agents ?? []; if (selectedAgents.length === 0) return false; } const installSpinner = spinner("Installing React Grab skill.").start(); const { installed, failed } = await installSkill({ agents: selectedAgents, global, cwd }); if (installed.length === 0) { installSpinner.fail("Failed to install React Grab skill."); return false; } installSpinner.succeed(`Installed React Grab skill for ${installed.map((record) => agentLabel(record.agent)).join(", ")}.`); for (const record of failed) logger.log(` ${highlighter.error("✗")} ${agentLabel(record.agent)} ${record.error}`); return true; }; //#endregion //#region src/commands/add.ts const VERSION$5 = "0.1.50"; const add = new Command().name("add").alias("install").description("install the React Grab skill for your agent").option("-y, --yes", "skip confirmation prompts", false).option("-c, --cwd ", "working directory (defaults to current directory)", process.cwd()).option("-g, --global", "install the skill globally instead of in the project", false).action(async (opts) => { console.log(`${pc.magenta("✿")} ${pc.bold("React Grab")} ${pc.gray(VERSION$5)}`); console.log(); try { const isNonInteractive = detectNonInteractive(opts.yes); const preflightSpinner = spinner("Preflight checks.").start(); if (!(await detectProject(opts.cwd)).hasReactGrab) { preflightSpinner.fail("React Grab is not installed."); logger.break(); logger.error(`Run ${highlighter.info("react-grab init")} first to install React Grab.`); logger.break(); process.exit(1); } preflightSpinner.succeed(); logger.break(); if (!await promptSkillInstall({ yes: isNonInteractive, global: opts.global, cwd: resolve(opts.cwd) }) && isNonInteractive) { logger.break(); process.exit(1); } logger.break(); } catch (error) { handleError(error); } }); //#endregion //#region src/utils/diff.ts const RED = "\x1B[31m"; const GREEN = "\x1B[32m"; const GRAY = "\x1B[90m"; const RESET = "\x1B[0m"; const BOLD = "\x1B[1m"; const generateDiff = (originalContent, newContent) => { const originalLines = originalContent.split("\n"); const newLines = newContent.split("\n"); const diff = []; let originalIndex = 0; let newIndex = 0; while (originalIndex < originalLines.length || newIndex < newLines.length) { const originalLine = originalLines[originalIndex]; const newLine = newLines[newIndex]; if (originalLine === newLine) { diff.push({ type: "unchanged", content: originalLine, lineNumber: newIndex + 1 }); originalIndex++; newIndex++; } else if (originalLine === void 0) { diff.push({ type: "added", content: newLine, lineNumber: newIndex + 1 }); newIndex++; } else if (newLine === void 0) { diff.push({ type: "removed", content: originalLine }); originalIndex++; } else { const originalInNew = newLines.indexOf(originalLine, newIndex); const newInOriginal = originalLines.indexOf(newLine, originalIndex); if (originalInNew !== -1 && (newInOriginal === -1 || originalInNew - newIndex < newInOriginal - originalIndex)) while (newIndex < originalInNew) { diff.push({ type: "added", content: newLines[newIndex], lineNumber: newIndex + 1 }); newIndex++; } else if (newInOriginal !== -1) while (originalIndex < newInOriginal) { diff.push({ type: "removed", content: originalLines[originalIndex] }); originalIndex++; } else { diff.push({ type: "removed", content: originalLine }); diff.push({ type: "added", content: newLine, lineNumber: newIndex + 1 }); originalIndex++; newIndex++; } } } return diff; }; const formatDiff = (diff, contextLines = 3) => { const lines = []; let lastPrintedIndex = -1; let hasChanges = false; const changedIndices = diff.map((line, index) => line.type !== "unchanged" ? index : -1).filter((index) => index !== -1); if (changedIndices.length === 0) return `${GRAY}No changes${RESET}`; for (const changedIndex of changedIndices) { const startContext = Math.max(0, changedIndex - contextLines); const endContext = Math.min(diff.length - 1, changedIndex + contextLines); if (startContext > lastPrintedIndex + 1 && lastPrintedIndex !== -1) lines.push(`${GRAY} ...${RESET}`); for (let lineIndex = Math.max(startContext, lastPrintedIndex + 1); lineIndex <= endContext; lineIndex++) { const diffLine = diff[lineIndex]; if (diffLine.type === "added") { lines.push(`${GREEN}+ ${diffLine.content}${RESET}`); hasChanges = true; } else if (diffLine.type === "removed") { lines.push(`${RED}- ${diffLine.content}${RESET}`); hasChanges = true; } else lines.push(`${GRAY} ${diffLine.content}${RESET}`); lastPrintedIndex = lineIndex; } } return hasChanges ? lines.join("\n") : `${GRAY}No changes${RESET}`; }; const printDiff = (filePath, originalContent, newContent) => { console.log(`\n${BOLD}File: ${filePath}${RESET}`); console.log("─".repeat(60)); const diff = generateDiff(originalContent, newContent); console.log(formatDiff(diff)); console.log("─".repeat(60)); }; //#endregion //#region src/utils/cli-helpers.ts const applyTransformWithFeedback = (result, message) => { const writeSpinner = spinner(message ?? `Applying changes to ${result.filePath}.`).start(); const writeResult = applyTransform(result); if (!writeResult.success) { writeSpinner.fail(); logger.break(); logger.error(writeResult.error || "Failed to write file."); logger.break(); process.exit(1); } writeSpinner.succeed(); }; const installPackagesWithFeedback = async (packages, packageManager, projectRoot) => { if (packages.length === 0) return; const installSpinner = spinner(`Installing ${packages.join(", ")}.`).start(); try { await installPackages(packages, { packageManager, cwd: projectRoot }); installSpinner.succeed(); } catch (error) { installSpinner.fail(); handleError(error); } }; //#endregion //#region src/utils/constants.ts const MAX_KEY_HOLD_DURATION_MS = 2e3; const DEFAULT_WATCH_DIR = ".react-grab"; const DEFAULT_GRAB_AGE_MS = 300 * 1e3; const MAX_READ_HISTORY_BYTES = 128 * 1024 * 1024; const MIGRATION_SCAN_CHUNK_BYTES = 1024 * 1024; //#endregion //#region src/utils/format-activation-key.ts const formatActivationKeyDisplay = (activationKey) => { const defaultLabel = process.platform === "darwin" ? "Option" : "Alt"; if (!activationKey) return `Default (${defaultLabel})`; return activationKey.split("+").map((part) => { const lower = part.toLowerCase(); if (lower === "meta") return process.platform === "darwin" ? "⌘" : "Win"; if (lower === "alt") return process.platform === "darwin" ? "⌥" : "Alt"; if (lower === "ctrl") return "Ctrl"; if (lower === "shift") return "Shift"; if (lower === "space" || lower === " ") return "Space"; return part.toUpperCase(); }).join(" + "); }; //#endregion //#region src/commands/configure.ts const VERSION$4 = "0.1.50"; const isMac = process.platform === "darwin"; const META_LABEL = isMac ? "Cmd" : "Win"; const ALT_LABEL = isMac ? "Option" : "Alt"; const MODIFIER_ALIASES = { cmd: "meta", command: "meta", win: "meta", windows: "meta", meta: "meta", ctrl: "ctrl", control: "ctrl", shift: "shift", alt: "alt", option: "alt", opt: "alt" }; const MODIFIERS = [ "meta", "ctrl", "shift", "alt" ]; const BASE_KEYS = [ { key: " ", aliases: ["space", "spacebar"] }, { key: "Enter", aliases: ["enter", "return"] }, { key: "Escape", aliases: ["escape", "esc"] }, { key: "Tab", aliases: ["tab"] }, { key: "Backspace", aliases: ["backspace", "back"] }, { key: "Delete", aliases: ["delete", "del"] }, { key: "Insert", aliases: ["insert", "ins"] }, { key: "Home", aliases: ["home"] }, { key: "End", aliases: ["end"] }, { key: "PageUp", aliases: ["pageup", "pgup"] }, { key: "PageDown", aliases: [ "pagedown", "pgdn", "pgdown" ] }, { key: "ArrowUp", aliases: ["arrowup", "up"] }, { key: "ArrowDown", aliases: ["arrowdown", "down"] }, { key: "ArrowLeft", aliases: ["arrowleft", "left"] }, { key: "ArrowRight", aliases: ["arrowright", "right"] }, ...Array.from({ length: 12 }, (_, i) => ({ key: `F${i + 1}`, aliases: [`f${i + 1}`] })), ...Array.from({ length: 26 }, (_, i) => { const letter = String.fromCharCode(97 + i); return { key: letter, aliases: [letter] }; }), ...Array.from({ length: 10 }, (_, i) => ({ key: String(i), aliases: [String(i)] })), { key: "`", aliases: [ "backtick", "grave", "`" ] }, { key: "-", aliases: [ "minus", "dash", "-" ] }, { key: "=", aliases: [ "equals", "equal", "=" ] }, { key: "[", aliases: [ "leftbracket", "lbracket", "[" ] }, { key: "]", aliases: [ "rightbracket", "rbracket", "]" ] }, { key: "\\", aliases: ["backslash", "\\"] }, { key: ";", aliases: ["semicolon", ";"] }, { key: "'", aliases: [ "quote", "apostrophe", "'" ] }, { key: ",", aliases: ["comma", ","] }, { key: ".", aliases: [ "period", "dot", "." ] }, { key: "/", aliases: [ "slash", "forwardslash", "/" ] } ]; const formatCombo = (combo) => { const parts = []; if (combo.metaKey) parts.push(META_LABEL); if (combo.ctrlKey) parts.push("Ctrl"); if (combo.shiftKey) parts.push("Shift"); if (combo.altKey) parts.push(ALT_LABEL); const keyDisplay = combo.key === " " ? "Space" : combo.key.length === 1 ? combo.key.toUpperCase() : combo.key; parts.push(keyDisplay); return parts.join("+"); }; const parseInput = (input) => { const parts = input.toLowerCase().replace(/\s+/g, "").split(/[+-]/); const modifiers = /* @__PURE__ */ new Set(); let partial = ""; for (const part of parts) { if (!part) continue; const modifierKey = MODIFIER_ALIASES[part]; if (modifierKey) modifiers.add(modifierKey); else partial = part; } return { modifiers, partial }; }; const POPULAR_KEYS = [ "g", "k", "e", "d", "b", " ", "Escape", "Enter" ]; const generateSuggestions = (input) => { const { modifiers, partial } = parseInput(input); const suggestions = []; if (!partial && modifiers.size === 0 && !input) { for (const mod of MODIFIERS) { const label = mod === "meta" ? META_LABEL : mod === "alt" ? ALT_LABEL : mod.charAt(0).toUpperCase() + mod.slice(1); for (const popularKey of POPULAR_KEYS) { const keyDisplay = popularKey === " " ? "Space" : popularKey.length === 1 ? popularKey.toUpperCase() : popularKey; suggestions.push({ title: `${label}+${keyDisplay}`, value: { key: popularKey, ...mod === "meta" ? { metaKey: true } : {}, ...mod === "ctrl" ? { ctrlKey: true } : {}, ...mod === "shift" ? { shiftKey: true } : {}, ...mod === "alt" ? { altKey: true } : {} } }); } } for (const baseKey of BASE_KEYS) suggestions.push({ title: baseKey.key === " " ? "Space" : baseKey.key.length === 1 ? baseKey.key.toUpperCase() : baseKey.key, value: { key: baseKey.key } }); return suggestions; } const buildCombo = (key, mods, extraMod) => ({ key, ...mods.has("meta") || extraMod === "meta" ? { metaKey: true } : {}, ...mods.has("ctrl") || extraMod === "ctrl" ? { ctrlKey: true } : {}, ...mods.has("shift") || extraMod === "shift" ? { shiftKey: true } : {}, ...mods.has("alt") || extraMod === "alt" ? { altKey: true } : {} }); for (const baseKey of BASE_KEYS) if (partial ? baseKey.aliases.some((alias) => alias.startsWith(partial)) : true) { const combo = buildCombo(baseKey.key, modifiers); suggestions.push({ title: formatCombo(combo), value: combo }); } if (!partial) { const unusedMods = MODIFIERS.filter((m) => !modifiers.has(m)); for (const mod of unusedMods) for (const popularKey of POPULAR_KEYS) { const combo = buildCombo(popularKey, modifiers, mod); suggestions.push({ title: formatCombo(combo), value: combo }); } } return suggestions.slice(0, 30); }; const CONFIG_OPTIONS = [ { id: "activationKey", title: "Shortcut", description: "The shortcut used to activate React Grab (e.g., g, k, space)" }, { id: "activationMode", title: "Activation Mode", description: "Toggle (press to activate/deactivate) or Hold (hold key)" }, { id: "keyHoldDuration", title: "Key Hold Duration", description: "Milliseconds to hold the key before activation (hold mode)" }, { id: "allowActivationInsideInput", title: "Allow Activation Inside Input", description: "Whether to allow activation when focused on input fields" }, { id: "maxContextLines", title: "Max Context Lines", description: "Max source-location lines in copied context (raise for large apps)" } ]; const comboToString = (combo) => { const parts = []; if (combo.metaKey) parts.push("Meta"); if (combo.ctrlKey) parts.push("Ctrl"); if (combo.shiftKey) parts.push("Shift"); if (combo.altKey) parts.push("Alt"); if (combo.key) { const keyDisplay = combo.key === " " ? "Space" : combo.key; parts.push(keyDisplay); } return parts.join("+"); }; const configure = new Command().name("configure").alias("config").description("configure React Grab options").option("-y, --yes", "skip confirmation prompts", false).option("-k, --key ", "shortcut (e.g., Meta+K, Ctrl+Shift+G, Space)").option("-m, --mode ", "activation mode (toggle, hold)").option("--hold-duration ", "key hold duration in milliseconds (for hold mode)").option("--allow-input ", "allow activation inside input fields (true/false)").option("--context-lines ", "max context lines to include").option("--cdn ", "CDN domain (e.g., unpkg.com, custom.react-grab.com)").option("-c, --cwd ", "working directory (defaults to current directory)", process.cwd()).action(async (opts) => { console.log(`${pc.magenta("✿")} ${pc.bold("React Grab")} ${pc.gray(VERSION$4)}`); console.log(); try { const cwd = opts.cwd; const preflightSpinner = spinner("Preflight checks.").start(); const projectInfo = await detectProject(cwd); if (!projectInfo.hasReactGrab) { preflightSpinner.fail("React Grab is not installed."); logger.break(); logger.error(`Run ${highlighter.info("react-grab init")} first to install React Grab.`); logger.break(); process.exit(1); } if (!projectInfo.isReactGrabConfigured) { preflightSpinner.fail("React Grab is installed, but setup is missing."); logger.break(); logger.error(`Run ${highlighter.info("react-grab init")} to add the setup script/import before configuring options.`); logger.break(); process.exit(1); } preflightSpinner.succeed(); if (opts.cdn) { const result = previewCdnTransform(projectInfo.projectRoot, projectInfo.framework, projectInfo.nextRouterType, opts.cdn); if (!result.success) { logger.break(); logger.error(result.message); logger.break(); process.exit(1); } if (result.noChanges) { logger.break(); logger.log("No changes needed."); logger.break(); process.exit(0); } logger.break(); printDiff(result.filePath, result.originalContent, result.newContent); if (!opts.yes) { logger.break(); const { proceed } = await prompts({ type: "confirm", name: "proceed", message: "Apply these changes?", initial: true }); if (!proceed) { logger.break(); logger.log("Changes cancelled."); logger.break(); process.exit(0); } } applyTransformWithFeedback(result); logger.break(); logger.log(`${highlighter.success("Success!")} CDN updated.`); logger.break(); return; } const hasFlags = opts.key || opts.mode || opts.holdDuration || opts.allowInput || opts.contextLines; logger.break(); logger.log(`Configure ${highlighter.info("React Grab")} options:`); logger.break(); const collectedOptions = {}; if (hasFlags) { if (opts.key) { collectedOptions.activationKey = opts.key; logger.log(` Shortcut: ${highlighter.info(formatActivationKeyDisplay(collectedOptions.activationKey))}`); } if (opts.mode) { if (opts.mode !== "toggle" && opts.mode !== "hold") { logger.error(`Invalid mode: ${opts.mode}. Use "toggle" or "hold".`); logger.break(); process.exit(1); } collectedOptions.activationMode = opts.mode; logger.log(` Activation mode: ${highlighter.info(opts.mode)}`); } if (opts.holdDuration) { const duration = parseInt(opts.holdDuration, 10); if (isNaN(duration) || duration < 0 || duration > 2e3) { logger.error(`Invalid hold duration. Must be 0-${MAX_KEY_HOLD_DURATION_MS}ms.`); logger.break(); process.exit(1); } collectedOptions.keyHoldDuration = duration; logger.log(` Key hold duration: ${highlighter.info(`${duration}ms`)}`); } if (opts.allowInput !== void 0) { const allowInput = opts.allowInput === "true" || opts.allowInput === true; collectedOptions.allowActivationInsideInput = allowInput; logger.log(` Allow activation inside input: ${highlighter.info(String(allowInput))}`); } if (opts.contextLines) { const lines = parseInt(opts.contextLines, 10); if (isNaN(lines) || lines < 0 || lines > 50) { logger.error(`Invalid context lines. Must be 0-50.`); logger.break(); process.exit(1); } collectedOptions.maxContextLines = lines; logger.log(` Max context lines: ${highlighter.info(String(lines))}`); } } else { const { selectedOption } = await prompts({ type: "autocomplete", name: "selectedOption", message: "Search for an option to configure:", choices: CONFIG_OPTIONS.map((option) => ({ title: option.title, value: option.id, description: option.description })), suggest: (input, choices) => Promise.resolve(choices.filter((choice) => choice.title.toLowerCase().includes(input.toLowerCase()) || (choice.description?.toLowerCase().includes(input.toLowerCase()) ?? false))) }); if (selectedOption === void 0) { logger.break(); process.exit(1); } if (selectedOption === "activationKey") { const { selectedCombo } = await prompts({ type: "autocomplete", name: "selectedCombo", message: "Type key combination (e.g. ctrl+shift+g):", choices: generateSuggestions(""), suggest: (input) => Promise.resolve(generateSuggestions(input)) }); if (selectedCombo === void 0) { logger.break(); process.exit(1); } collectedOptions.activationKey = comboToString(selectedCombo); logger.log(` Shortcut: ${highlighter.info(formatActivationKeyDisplay(collectedOptions.activationKey))}`); } if (selectedOption === "activationMode") { const { activationMode } = await prompts({ type: "select", name: "activationMode", message: `Select ${highlighter.info("activation mode")}:`, choices: [{ title: "Toggle (press to activate/deactivate)", value: "toggle" }, { title: "Hold (hold key to keep active)", value: "hold" }], initial: 0 }); if (activationMode === void 0) { logger.break(); process.exit(1); } collectedOptions.activationMode = activationMode; } if (selectedOption === "keyHoldDuration") { const { keyHoldDuration } = await prompts({ type: "number", name: "keyHoldDuration", message: `Enter ${highlighter.info("key hold duration")} in milliseconds:`, initial: 150, min: 0, max: 2e3 }); if (keyHoldDuration === void 0) { logger.break(); process.exit(1); } collectedOptions.keyHoldDuration = keyHoldDuration; } if (selectedOption === "allowActivationInsideInput") { const { allowActivationInsideInput } = await prompts({ type: "confirm", name: "allowActivationInsideInput", message: `Allow activation ${highlighter.info("inside input fields")}?`, initial: true }); if (allowActivationInsideInput === void 0) { logger.break(); process.exit(1); } collectedOptions.allowActivationInsideInput = allowActivationInsideInput; } if (selectedOption === "maxContextLines") { const { maxContextLines } = await prompts({ type: "number", name: "maxContextLines", message: `Enter ${highlighter.info("max context lines")} to include:`, initial: 3, min: 0, max: 50 }); if (maxContextLines === void 0) { logger.break(); process.exit(1); } collectedOptions.maxContextLines = maxContextLines; } } const result = previewOptionsTransform(projectInfo.projectRoot, projectInfo.framework, projectInfo.nextRouterType, collectedOptions); if (!result.success) { logger.break(); logger.warn(result.message); logger.break(); const configJson = JSON.stringify(collectedOptions); logger.log(`Add this to your ${highlighter.info("init()")} call or ${highlighter.info("data-options")} attribute:`); logger.break(); console.log(` ${pc.cyan(configJson)}`); logger.break(); process.exit(1); } if (!result.noChanges && result.originalContent && result.newContent) { logger.break(); printDiff(result.filePath, result.originalContent, result.newContent); if (!opts.yes) { logger.break(); const { proceed } = await prompts({ type: "confirm", name: "proceed", message: "Apply these changes?", initial: true }); if (!proceed) { logger.break(); logger.log("Changes cancelled."); logger.break(); process.exit(0); } } applyTransformWithFeedback(result); } else { logger.break(); logger.log("No changes needed."); } logger.break(); logger.log(`${highlighter.success("Success!")} React Grab options have been configured.`); logger.break(); } catch (error) { handleError(error); } }); //#endregion //#region src/utils/is-telemetry-enabled.ts const isTelemetryEnabled = () => { const doNotTrack = process.env.DO_NOT_TRACK; return doNotTrack !== "1" && doNotTrack !== "true"; }; //#endregion //#region src/commands/init.ts const VERSION$3 = "0.1.50"; const REPORT_URL = "https://react-grab.com/api/report-cli"; const DOCS_URL = "https://github.com/aidenybai/react-grab"; const reportToCli = (type, config, error) => { if (!isTelemetryEnabled()) return; fetch(REPORT_URL, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ type, version: VERSION$3, config, error: error ? { message: error.message, stack: error.stack } : void 0, timestamp: (/* @__PURE__ */ new Date()).toISOString() }) }).catch(() => {}); }; const FRAMEWORK_NAMES = { next: "Next.js", vite: "Vite", tanstack: "TanStack Start", webpack: "Webpack", unknown: "Unknown" }; const PACKAGE_MANAGER_NAMES = { npm: "npm", yarn: "Yarn", pnpm: "pnpm", bun: "Bun" }; const UNSUPPORTED_FRAMEWORK_NAMES = { remix: "Remix", astro: "Astro", sveltekit: "SvelteKit", gatsby: "Gatsby" }; const sortProjectsByFramework = (projects) => [...projects].sort((projectA, projectB) => { if (projectA.framework === "unknown" && projectB.framework !== "unknown") return 1; if (projectA.framework !== "unknown" && projectB.framework === "unknown") return -1; return 0; }); const printSubprojects = (searchRoot, sortedProjects) => { logger.break(); logger.log("Found the following projects:"); logger.break(); for (const project of sortedProjects) { const frameworkLabel = project.framework !== "unknown" ? ` ${highlighter.dim(`(${FRAMEWORK_NAMES[project.framework]})`)}` : ""; const relativePath = relative(searchRoot, project.path); logger.log(` ${highlighter.info(project.name)}${frameworkLabel} ${highlighter.dim(relativePath)}`); } logger.break(); logger.log(`Re-run with ${highlighter.info("-c ")} to specify a project:`); logger.break(); logger.log(` ${highlighter.dim("$")} npx grab@latest init -c ${relative(searchRoot, sortedProjects[0].path)}`); logger.break(); }; const SUPPORTED_FRAMEWORKS_LINE = "React Grab supports Next.js, Vite, TanStack Start, and Webpack projects."; const failWithManualSetup = (failingSpinner, message, { listSupportedFrameworks = false } = {}) => { failingSpinner.fail(message); logger.break(); if (listSupportedFrameworks) logger.log(SUPPORTED_FRAMEWORKS_LINE); logger.log(`Visit ${highlighter.info(DOCS_URL)} for manual setup.`); logger.break(); process.exit(1); }; const init = new Command().name("init").alias("setup").description("initialize React Grab in your project").option("-y, --yes", "skip confirmation prompts", false).option("-f, --force", "re-run setup checks even when React Grab is already configured", false).option("-k, --key ", "shortcut (e.g., Meta+K, Ctrl+Shift+G, Space)").option("--skip-install", "skip package installation", false).option("--pkg ", "custom package URL for CLI (e.g., grab)").option("-c, --cwd ", "working directory (defaults to current directory)", process.cwd()).option("-g, --global", "install the skill globally instead of in the project", false).action(async (opts) => { console.log(`${pc.magenta("✿")} ${pc.bold("React Grab")} ${pc.gray(VERSION$3)}`); console.log(); try { const cwd = resolve(opts.cwd); const isNonInteractive = detectNonInteractive(opts.yes); if (!existsSync(cwd)) { logger.break(); logger.error(`Directory does not exist: ${highlighter.info(cwd)}`); logger.break(); process.exit(1); } const preflightSpinner = spinner("Preflight checks.").start(); const projectInfo = await detectProject(cwd); if (projectInfo.isReactGrabConfigured && !opts.force) { preflightSpinner.succeed(); if (isNonInteractive) { logger.break(); logger.warn("React Grab is already installed."); logger.log(`Use ${highlighter.info("--force")} to re-run setup checks, or remove ${highlighter.info("--yes")} for interactive mode.`); logger.break(); process.exit(0); } logger.break(); logger.success("React Grab is already installed."); logger.break(); const { wantCustomizeOptions } = await prompts({ type: "confirm", name: "wantCustomizeOptions", message: `Would you like to customize ${highlighter.info("options")}?`, initial: false }); if (wantCustomizeOptions === void 0) { logger.break(); process.exit(1); } if (wantCustomizeOptions || opts.key) { logger.break(); logger.log(`Configure ${highlighter.info("React Grab")} options:`); logger.break(); const collectedOptions = {}; if (opts.key) { collectedOptions.activationKey = opts.key; logger.log(` Shortcut: ${highlighter.info(formatActivationKeyDisplay(collectedOptions.activationKey))}`); } else { const { wantActivationKey } = await prompts({ type: "confirm", name: "wantActivationKey", message: `Configure ${highlighter.info("shortcut")}?`, initial: false }); if (wantActivationKey === void 0) { logger.break(); process.exit(1); } if (wantActivationKey) { const { key } = await prompts({ type: "text", name: "key", message: "Enter the shortcut (e.g., g, k, space):", initial: "" }); if (key === void 0) { logger.break(); process.exit(1); } collectedOptions.activationKey = key ? key.toLowerCase() : void 0; logger.log(` Shortcut: ${highlighter.info(formatActivationKeyDisplay(collectedOptions.activationKey))}`); } } const { activationMode } = await prompts({ type: "select", name: "activationMode", message: `Select ${highlighter.info("activation mode")}:`, choices: [{ title: "Toggle (press to activate/deactivate)", value: "toggle" }, { title: "Hold (hold key to keep active)", value: "hold" }], initial: 0 }); if (activationMode === void 0) { logger.break(); process.exit(1); } collectedOptions.activationMode = activationMode; if (activationMode === "hold") { const { keyHoldDuration } = await prompts({ type: "number", name: "keyHoldDuration", message: `Enter ${highlighter.info("key hold duration")} in milliseconds:`, initial: 150, min: 0, max: 2e3 }); if (keyHoldDuration === void 0) { logger.break(); process.exit(1); } collectedOptions.keyHoldDuration = keyHoldDuration; } const { allowActivationInsideInput } = await prompts({ type: "confirm", name: "allowActivationInsideInput", message: `Allow activation ${highlighter.info("inside input fields")}?`, initial: true }); if (allowActivationInsideInput === void 0) { logger.break(); process.exit(1); } collectedOptions.allowActivationInsideInput = allowActivationInsideInput; const { maxContextLines } = await prompts({ type: "number", name: "maxContextLines", message: `Enter ${highlighter.info("max context lines")} to include:`, initial: 3, min: 0, max: 50 }); if (maxContextLines === void 0) { logger.break(); process.exit(1); } collectedOptions.maxContextLines = maxContextLines; const optionsResult = previewOptionsTransform(projectInfo.projectRoot, projectInfo.framework, projectInfo.nextRouterType, collectedOptions); if (!optionsResult.success) { logger.break(); logger.error(optionsResult.message); logger.break(); process.exit(1); } if (!optionsResult.noChanges && optionsResult.originalContent && optionsResult.newContent) { logger.break(); printDiff(optionsResult.filePath, optionsResult.originalContent, optionsResult.newContent); logger.break(); const { proceed } = await prompts({ type: "confirm", name: "proceed", message: "Apply these changes?", initial: true }); if (!proceed) { logger.break(); logger.log("Options configuration cancelled."); } else { applyTransformWithFeedback(optionsResult); logger.break(); logger.success("React Grab options have been configured."); } } else { logger.break(); logger.log("No option changes needed."); } } logger.break(); await promptSkillInstall({ yes: isNonInteractive, global: opts.global, cwd }); logger.break(); process.exit(0); } preflightSpinner.succeed(); const frameworkSpinner = spinner("Verifying framework.").start(); if (projectInfo.unsupportedFramework) { const frameworkName = UNSUPPORTED_FRAMEWORK_NAMES[projectInfo.unsupportedFramework]; frameworkSpinner.fail(`Found ${highlighter.info(frameworkName)}.`); logger.break(); logger.log(`${frameworkName} is not yet supported by automatic setup.`); logger.log(`Visit ${highlighter.info(DOCS_URL)} for manual setup.`); logger.break(); process.exit(1); } if (projectInfo.framework === "unknown" || projectInfo.isMonorepo && !hasFrameworkEntryPoint(projectInfo.projectRoot, projectInfo.framework, projectInfo.nextRouterType)) { let searchRoot = cwd; let reactProjects = findReactProjects(searchRoot); if (reactProjects.length === 0 && cwd !== process.cwd()) { searchRoot = process.cwd(); reactProjects = findReactProjects(searchRoot); } if (reactProjects.length > 0) { frameworkSpinner.info(`Verifying framework. Found ${reactProjects.length} project${reactProjects.length === 1 ? "" : "s"}.`); const sortedProjects = sortProjectsByFramework(reactProjects); if (isNonInteractive) { printSubprojects(searchRoot, sortedProjects); process.exit(1); } logger.break(); const { selectedProject } = await prompts({ type: "select", name: "selectedProject", message: "Select a project to install React Grab:", choices: [...sortedProjects.map((project) => { const frameworkLabel = project.framework !== "unknown" ? ` ${highlighter.dim(`(${FRAMEWORK_NAMES[project.framework]})`)}` : ""; return { title: `${project.name}${frameworkLabel}`, value: project.path }; }), { title: "Skip", value: "skip" }] }); if (!selectedProject || selectedProject === "skip") { logger.break(); process.exit(0); } process.chdir(selectedProject); const newProjectInfo = await detectProject(selectedProject); Object.assign(projectInfo, newProjectInfo); const newFrameworkSpinner = spinner("Verifying framework.").start(); if (newProjectInfo.framework === "unknown") failWithManualSetup(newFrameworkSpinner, "Could not detect a supported framework in this project.", { listSupportedFrameworks: true }); newFrameworkSpinner.succeed(`Verifying framework. Found ${highlighter.info(FRAMEWORK_NAMES[newProjectInfo.framework])}.`); } else if (projectInfo.framework !== "unknown") failWithManualSetup(frameworkSpinner, `Verifying framework. Found ${highlighter.info(FRAMEWORK_NAMES[projectInfo.framework])}, but could not find an entry file.`); else failWithManualSetup(frameworkSpinner, "Could not detect a supported framework.", { listSupportedFrameworks: true }); } else frameworkSpinner.succeed(`Verifying framework. Found ${highlighter.info(FRAMEWORK_NAMES[projectInfo.framework])}.`); if (projectInfo.framework === "next") spinner("Detecting router type.").start().succeed(`Detecting router type. Found ${highlighter.info(projectInfo.nextRouterType === "app" ? "App Router" : "Pages Router")}.`); spinner("Detecting package manager.").start().succeed(`Detecting package manager. Found ${highlighter.info(PACKAGE_MANAGER_NAMES[projectInfo.packageManager])}.`); const finalFramework = projectInfo.framework; const finalPackageManager = projectInfo.packageManager; const finalNextRouterType = projectInfo.nextRouterType; let didInstallSkill = false; if (!isNonInteractive) { logger.break(); didInstallSkill = await promptSkillInstall({ yes: isNonInteractive, global: opts.global, cwd }); } const result = previewTransform(projectInfo.projectRoot, finalFramework, finalNextRouterType, projectInfo.isReactGrabConfigured); if (!result.success) { logger.break(); logger.error(result.message); logger.error(`Visit ${highlighter.info(DOCS_URL)} for manual setup.`); logger.break(); process.exit(1); } const hasLayoutChanges = !result.noChanges && result.originalContent && result.newContent; if (hasLayoutChanges) { logger.break(); printDiff(result.filePath, result.originalContent, result.newContent); logger.break(); logger.warn("Auto-detection may not be 100% accurate."); logger.warn("Please verify the changes before committing."); if (!isNonInteractive) { logger.break(); const { proceed } = await prompts({ type: "confirm", name: "proceed", message: "Apply these changes?", initial: true }); if (!proceed) { logger.break(); logger.log("Changes cancelled."); logger.break(); process.exit(0); } } } const shouldInstallReactGrab = !projectInfo.hasReactGrab; if (!opts.skipInstall && shouldInstallReactGrab) await installPackagesWithFeedback(getPackagesToInstall(shouldInstallReactGrab), finalPackageManager, projectInfo.projectRoot); if (hasLayoutChanges) applyTransformWithFeedback(result); logger.break(); if (hasLayoutChanges) logger.log(`${highlighter.success("Success!")} React Grab has been installed.`); else logger.log(`${highlighter.success("Success!")} ${result.message}.`); logger.log("You may now start your development server."); logger.break(); reportToCli("completed", { framework: finalFramework, packageManager: finalPackageManager, router: finalNextRouterType, agent: didInstallSkill ? "skill" : void 0, isMonorepo: projectInfo.isMonorepo }); } catch (error) { handleError(error); reportToCli("error", void 0, error); } }); //#endregion //#region src/utils/sleep.ts const sleep = (durationMs) => new Promise((resolve) => setTimeout(resolve, durationMs)); //#endregion //#region src/utils/clipboard.ts const HISTORY_FILE_NAME = "history.jsonl"; const NO_READER_MESSAGE = "no clipboard reader available. Linux: install xclip or wl-clipboard. macOS: install Xcode CLI tools (swiftc) or rely on pbpaste. Windows: ensure PowerShell is on PATH."; const READERS_DIR = path.dirname(fileURLToPath(import.meta.url)); const READ_TIMEOUT_MS = 2500; const MAX_CLIPBOARD_BYTES = 64 * 1024 * 1024; const ID_RADIX = 36; const HASH_LENGTH = 12; const PICKLE_HEADER_BYTES = 4; const PICKLE_ALIGN_BYTES = 4; const SIGNATURE_SCAN_CHARS = 32 * 1024; const GRAB_MIME = "application/x-react-grab"; const CHROMIUM_CUSTOM_FORMAT = "chromium/x-web-custom-data"; const GRAB_TEXT_SIGNATURE = /\bin\s+\S+\s+\(at\s+[^\n]{1,400}?:\d+:\d+\)/; const shortHash = (text) => createHash("sha1").update(text).digest("hex").slice(0, HASH_LENGTH); const alignUp = (value) => value + PICKLE_ALIGN_BYTES - 1 & ~(PICKLE_ALIGN_BYTES - 1); const parseChromiumPickle = (buffer) => { const formats = {}; if (!buffer || buffer.length < PICKLE_HEADER_BYTES + 4) return formats; let offset = PICKLE_HEADER_BYTES; const pairCount = buffer.readUInt32LE(offset); offset += 4; for (let pairIndex = 0; pairIndex < pairCount; pairIndex += 1) { if (offset + 4 > buffer.length) break; const formatCodeUnits = buffer.readUInt32LE(offset); offset += 4; if (offset + formatCodeUnits * 2 > buffer.length) break; const format = buffer.toString("utf16le", offset, offset + formatCodeUnits * 2); offset = alignUp(offset + formatCodeUnits * 2); if (offset + 4 > buffer.length) break; const dataCodeUnits = buffer.readUInt32LE(offset); offset += 4; if (offset + dataCodeUnits * 2 > buffer.length) break; const value = buffer.toString("utf16le", offset, offset + dataCodeUnits * 2); offset = alignUp(offset + dataCodeUnits * 2); formats[format] = value; } return formats; }; const extractGrab = (raw) => { if (raw.grab) return raw.grab; if (raw.pickleBase64) return parseChromiumPickle(Buffer.from(raw.pickleBase64, "base64"))[GRAB_MIME]; }; const isGrabText = (text) => GRAB_TEXT_SIGNATURE.test(text.length > SIGNATURE_SCAN_CHARS ? text.slice(0, SIGNATURE_SCAN_CHARS) : text); const extractPrompt = (record) => { const comments = (Array.isArray(record.entries) ? record.entries : []).map((entry) => entry?.commentText?.trim?.()).filter(Boolean); if (comments.length > 0) return comments.join("\n"); const lines = (typeof record.content === "string" ? record.content : "").split("\n"); const firstReferenceLine = lines.findIndex((line) => line.startsWith("[")); if (firstReferenceLine <= 0) return void 0; return lines.slice(0, firstReferenceLine).join("\n").trim() || void 0; }; const hasCommand = (name) => { return spawnSync(process.platform === "win32" ? "where" : "which", [name], { stdio: "ignore" }).status === 0; }; const runText = (command, args) => { const output = spawnSync(command, args, { encoding: "utf8", maxBuffer: MAX_CLIPBOARD_BYTES, timeout: READ_TIMEOUT_MS }); return output.status === 0 ? output.stdout : null; }; const runBuffer = (command, args) => { const output = spawnSync(command, args, { maxBuffer: MAX_CLIPBOARD_BYTES, timeout: READ_TIMEOUT_MS }); return output.status === 0 && output.stdout?.length ? output.stdout : null; }; const runJson = (command, args) => { const output = spawnSync(command, args, { encoding: "utf8", maxBuffer: MAX_CLIPBOARD_BYTES, timeout: READ_TIMEOUT_MS }); if (output.status !== 0 || !output.stdout) return null; try { return JSON.parse(output.stdout); } catch { return null; } }; const compileSwiftReader = (readersDir, workDir) => { if (!hasCommand("swiftc")) return null; const source = path.join(readersDir, "read-clipboard.swift"); if (!fs.existsSync(source)) return null; const binary = path.join(workDir, "pbread"); if ((!fs.existsSync(binary) || fs.statSync(source).mtimeMs > fs.statSync(binary).mtimeMs) && spawnSync("swiftc", [ "-O", source, "-o", binary ]).status !== 0) return null; return binary; }; const createDarwinReader = (options) => { const binary = options.textOnly ? null : compileSwiftReader(READERS_DIR, options.workDir); if (binary) return { mode: "darwin-native", read: () => { const raw = runJson(binary, []); if (!raw) return null; return { changeCount: raw.changeCount ?? null, text: raw.text, grab: extractGrab(raw) }; } }; if (!hasCommand("pbpaste")) return null; return { mode: "darwin-text", read: () => { const text = runText("pbpaste", []); return text == null ? null : { changeCount: null, text, grab: void 0 }; } }; }; const detectLinuxTool = () => { if (Boolean(process.env.WAYLAND_DISPLAY) && hasCommand("wl-paste") || hasCommand("wl-paste") && !hasCommand("xclip")) return { name: "wl-paste", readText: () => runText("wl-paste", [ "-n", "-t", "text/plain" ]) ?? runText("wl-paste", ["-n"]), readCustom: () => runBuffer("wl-paste", [ "-n", "-t", CHROMIUM_CUSTOM_FORMAT ]) }; if (hasCommand("xclip")) return { name: "xclip", readText: () => runText("xclip", [ "-selection", "clipboard", "-o" ]), readCustom: () => runBuffer("xclip", [ "-selection", "clipboard", "-t", CHROMIUM_CUSTOM_FORMAT, "-o" ]) }; if (hasCommand("xsel")) return { name: "xsel", readText: () => runText("xsel", ["--clipboard", "--output"]), readCustom: null }; return null; }; const createLinuxReader = (options) => { const tool = detectLinuxTool(); if (!tool) return null; const useCustom = !options.textOnly && Boolean(tool.readCustom); return { mode: useCustom ? `linux-${tool.name}` : `linux-${tool.name}-text`, read: () => { const text = tool.readText(); if (text == null) return null; return { changeCount: null, text, grab: useCustom && tool.readCustom ? parseChromiumPickle(tool.readCustom())[GRAB_MIME] : void 0 }; } }; }; const detectPowershell = () => hasCommand("pwsh") ? "pwsh" : hasCommand("powershell") ? "powershell" : null; const createWindowsReader = (options) => { const shell = detectPowershell(); if (!shell) return null; const scriptPath = path.join(READERS_DIR, "read-clipboard.ps1"); if (options.textOnly || !fs.existsSync(scriptPath)) return { mode: "win-text", read: () => { const text = runText(shell, [ "-NoProfile", "-Command", "Get-Clipboard -Raw" ]); return text == null ? null : { changeCount: null, text, grab: void 0 }; } }; const args = [ "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", scriptPath ]; spawnSync(shell, args, { stdio: "ignore", maxBuffer: MAX_CLIPBOARD_BYTES }); return { mode: "win-native", read: () => { const raw = runJson(shell, args); if (!raw) return null; return { changeCount: raw.changeCount ?? null, text: raw.text ?? void 0, grab: extractGrab(raw) }; } }; }; const createReader = (options) => { if (process.platform === "darwin") return createDarwinReader(options); if (process.platform === "linux") return createLinuxReader(options); if (process.platform === "win32") return createWindowsReader(options); return null; }; const ensureSafeDir = (dir) => { let stats; try { stats = fs.lstatSync(dir); } catch { return; } if (stats.isSymbolicLink()) throw new Error(`Refusing to use ${dir}: it is a symlink.`); if (process.getuid && stats.uid !== process.getuid()) throw new Error(`Refusing to use ${dir}: owned by another user.`); }; const prepareWorkDir = (dir) => { ensureSafeDir(dir); fs.mkdirSync(dir, { recursive: true, mode: 448 }); const gitignore = path.join(dir, ".gitignore"); if (!fs.existsSync(gitignore)) fs.writeFileSync(gitignore, "*\n"); }; const runWatchLoop = async (options) => { const { reader, dir, intervalMs, replayLast, onWarn } = options; const logPath = path.join(dir, HISTORY_FILE_NAME); const { read } = reader; let lastChangeCount = null; let lastTimestamp = 0; let lastTextHash = ""; let lastErrorMessage = ""; let sequence = 0; const initial = read(); if (initial && !replayLast) { lastChangeCount = initial.changeCount; if (initial.text) lastTextHash = shortHash(initial.text); if (initial.grab) try { lastTimestamp = JSON.parse(initial.grab).timestamp ?? 0; } catch {} } while (true) { await sleep(intervalMs); try { const snapshot = read(); if (!snapshot) continue; if (snapshot.changeCount !== null && snapshot.changeCount === lastChangeCount) continue; const textHash = snapshot.text ? shortHash(snapshot.text) : ""; const didTextChange = textHash !== lastTextHash; let record = null; let nextTimestamp = lastTimestamp; if (snapshot.grab) { let parsed = null; try { parsed = JSON.parse(snapshot.grab); } catch {} if (parsed && typeof parsed.timestamp === "number" && parsed.timestamp > lastTimestamp) { nextTimestamp = parsed.timestamp; record = { source: "custom", timestamp: parsed.timestamp, version: typeof parsed.version === "string" ? parsed.version : void 0, content: typeof parsed.content === "string" ? parsed.content : "", entries: Array.isArray(parsed.entries) ? parsed.entries : [] }; } } else if (didTextChange && snapshot.text && isGrabText(snapshot.text)) record = { source: "text", timestamp: Date.now(), content: snapshot.text, entries: [] }; if (!record) { lastChangeCount = snapshot.changeCount; lastTextHash = textHash; continue; } const prompt = extractPrompt(record); if (prompt) record.prompt = prompt; const captured = { id: `${record.timestamp}-${(sequence += 1).toString(ID_RADIX)}`, receivedAt: Date.now(), ...record }; fs.appendFileSync(logPath, `${JSON.stringify(captured)}\n`); lastChangeCount = snapshot.changeCount; lastTextHash = textHash; lastTimestamp = nextTimestamp; } catch (error) { const message = String(error?.message ?? error); if (message !== lastErrorMessage) { lastErrorMessage = message; onWarn?.(message); } } } }; //#endregion //#region src/utils/daemon.ts const PID_FILE_NAME = "watch.pid"; const pidFilePath = (dir) => path.join(dir, PID_FILE_NAME); const cliEntryPath = () => process.argv[1] ?? fileURLToPath(import.meta.url); const isProcessAlive = (pid) => { if (!Number.isInteger(pid) || pid <= 0) return false; try { process.kill(pid, 0); return true; } catch (error) { return error.code === "EPERM"; } }; const readDaemonPid = (dir) => { try { const pid = Number.parseInt(fs.readFileSync(pidFilePath(dir), "utf8").trim(), 10); return Number.isInteger(pid) && pid > 0 ? pid : null; } catch { return null; } }; const isDaemonRunning = (dir) => { const pid = readDaemonPid(dir); return pid !== null && isProcessAlive(pid); }; const claimDaemon = (dir) => { const file = pidFilePath(dir); for (let attempt = 0; attempt < 50; attempt += 1) try { const handle = fs.openSync(file, "wx"); fs.writeFileSync(handle, String(process.pid)); fs.closeSync(handle); return readDaemonPid(dir) === process.pid; } catch (error) { if (error.code !== "EEXIST") throw error; if (isDaemonRunning(dir)) return false; try { fs.rmSync(file, { force: true }); } catch {} } return false; }; const releaseDaemon = (dir) => { if (readDaemonPid(dir) === process.pid) try { fs.rmSync(pidFilePath(dir), { force: true }); } catch {} }; const stopDaemon = (dir) => { const pid = readDaemonPid(dir); if (pid === null) return null; const wasAlive = isProcessAlive(pid); if (wasAlive) try { process.kill(pid, "SIGTERM"); } catch {} if (readDaemonPid(dir) === pid) try { fs.rmSync(pidFilePath(dir), { force: true }); } catch {} return wasAlive ? pid : null; }; const spawnDaemon = (options) => { const args = [ cliEntryPath(), "watch", "--dir", options.dir, "--interval", String(options.intervalMs) ]; if (options.textOnly) args.push("--text-only"); if (options.replayLast) args.push("--replay-last"); spawn(process.execPath, args, { detached: true, stdio: "ignore", windowsHide: true }).unref(); }; const ensureDaemon = (options) => { if (isDaemonRunning(options.dir)) return "already-running"; if (!createReader({ textOnly: options.textOnly, workDir: options.dir })) return "no-reader"; spawnDaemon(options); return "started"; }; //#endregion //#region src/utils/grab-log.ts const CURSOR_FILE_NAME = "cursor.txt"; const NEWLINE_BYTE = 10; const cursorFilePath = (dir) => path.join(dir, CURSOR_FILE_NAME); const historyFilePath = (dir) => path.join(dir, HISTORY_FILE_NAME); const fileSize = (filePath) => { try { return fs.statSync(filePath).size; } catch { return 0; } }; const readHistoryRange = (dir, start, length) => { if (length <= 0) return ""; const fd = fs.openSync(historyFilePath(dir), "r"); try { const buffer = Buffer.allocUnsafe(length); const bytesRead = fs.readSync(fd, buffer, 0, length, start); return buffer.toString("utf8", 0, bytesRead); } finally { fs.closeSync(fd); } }; const writeGrabCursor = (dir, offset) => { const target = cursorFilePath(dir); const tempPath = `${target}.${process.pid}.tmp`; fs.writeFileSync(tempPath, JSON.stringify({ offset })); fs.renameSync(tempPath, target); }; const byteOffsetAfterLines = (dir, lineCount) => { const filePath = historyFilePath(dir); const size = fileSize(filePath); if (lineCount <= 0 || size === 0) return 0; const fd = fs.openSync(filePath, "r"); try { const buffer = Buffer.allocUnsafe(MIGRATION_SCAN_CHUNK_BYTES); let position = 0; let seen = 0; while (position < size) { const bytesRead = fs.readSync(fd, buffer, 0, MIGRATION_SCAN_CHUNK_BYTES, position); if (bytesRead <= 0) break; for (let index = 0; index < bytesRead; index += 1) { if (buffer[index] !== NEWLINE_BYTE) continue; seen += 1; if (seen === lineCount) return position + index + 1; } position += bytesRead; } } finally { fs.closeSync(fd); } return size; }; const readGrabCursor = (dir) => { let raw; try { raw = fs.readFileSync(cursorFilePath(dir), "utf8").trim(); } catch { return 0; } if (raw === "") return 0; let parsed; try { parsed = JSON.parse(raw); } catch { return 0; } if (typeof parsed === "number") { if (!Number.isInteger(parsed) || parsed < 0) return 0; const offset = byteOffsetAfterLines(dir, parsed); writeGrabCursor(dir, offset); return offset; } return typeof parsed.offset === "number" && Number.isInteger(parsed.offset) && parsed.offset >= 0 ? parsed.offset : 0; }; const readCompleteGrabLines = (dir) => { const size = fileSize(historyFilePath(dir)); if (size === 0) return []; const raw = readHistoryRange(dir, 0, size); const lastNewline = raw.lastIndexOf("\n"); if (lastNewline < 0) return []; return raw.slice(0, lastNewline).split("\n").filter(Boolean); }; const consumeGrabs = (dir, options) => { if (options.all) return readCompleteGrabLines(dir); const size = fileSize(historyFilePath(dir)); const cursor = readGrabCursor(dir); const start = cursor > size ? 0 : cursor; if (start >= size) { if (start !== cursor) writeGrabCursor(dir, start); return []; } const chunk = readHistoryRange(dir, start, Math.min(size - start, MAX_READ_HISTORY_BYTES)); const lastNewline = chunk.lastIndexOf("\n"); if (lastNewline < 0) return []; const now = Date.now(); const fresh = []; let consumedBytes = 0; let lineStart = 0; while (lineStart <= lastNewline && (options.limit <= 0 || fresh.length < options.limit)) { const newlineIndex = chunk.indexOf("\n", lineStart); const line = chunk.slice(lineStart, newlineIndex); consumedBytes += Buffer.byteLength(chunk.slice(lineStart, newlineIndex + 1), "utf8"); lineStart = newlineIndex + 1; if (line.length === 0) continue; let parsed; try { parsed = JSON.parse(line); } catch { continue; } if (options.maxAgeMs > 0 && typeof parsed.receivedAt === "number") { if (now - parsed.receivedAt > options.maxAgeMs) continue; } fresh.push(line); } const nextCursor = start + consumedBytes; if (nextCursor !== cursor) writeGrabCursor(dir, nextCursor); return fresh; }; //#endregion //#region src/utils/read-args.ts const parseWaitMs = (raw) => { if (raw === void 0) return 0; const trimmed = raw.trim(); if (trimmed === "") return 0; if (/^(inf|infinite|infinity|forever)$/i.test(trimmed)) return Number.POSITIVE_INFINITY; const ms = Number(trimmed); return Number.isFinite(ms) && ms >= 0 ? ms : null; }; const parseNonNegativeInt = (raw) => { if (raw === void 0) return null; const trimmed = raw.trim(); if (trimmed === "") return null; const value = Number(trimmed); return Number.isInteger(value) && value >= 0 ? value : null; }; //#endregion //#region src/commands/pull.ts const fail = (message) => { process.stderr.write(`react-grab pull: ${message}\n`); process.exit(1); }; const emitAndExit = (lines) => { process.stdout.write(`${lines.join("\n")}\n`, () => process.exit(0)); }; const pull = new Command().name("pull").description("start the watcher if needed, then wait for and print the next React Grab grab(s)").option("-d, --dir ", "work dir for history.jsonl + watch.pid", DEFAULT_WATCH_DIR).option("-w, --wait ", "how long to wait for a grab: ms, 'infinite', or 0 for none", "infinite").option("-n, --limit ", "max grabs to print per call (0 = no limit)", String(50)).option("--max-age ", "skip grabs captured longer ago than (0 = never)", String(DEFAULT_GRAB_AGE_MS)).option("--text-only", "watcher uses the plain-text clipboard reader (ignored if already running)").option("--all", "print the whole history without advancing the cursor").action(async (options) => { const dir = path.resolve(options.dir); try { prepareWorkDir(dir); } catch (error) { fail(String(error?.message ?? error)); } const waitMs = parseWaitMs(options.wait); if (waitMs === null) fail(`invalid --wait "${options.wait}" (use milliseconds or "infinite")`); const limit = parseNonNegativeInt(options.limit); if (limit === null) fail(`invalid --limit "${options.limit}" (use a non-negative integer)`); const maxAgeMs = parseNonNegativeInt(options.maxAge); if (maxAgeMs === null) fail(`invalid --max-age "${options.maxAge}" (use milliseconds, 0 to disable)`); const all = Boolean(options.all); if (ensureDaemon({ dir, intervalMs: 800, textOnly: Boolean(options.textOnly), replayLast: false }) === "no-reader") fail(NO_READER_MESSAGE); unrefStdin(); const consume = () => consumeGrabs(dir, { limit, all, maxAgeMs }); const first = consume(); if (first.length > 0) { emitAndExit(first); return; } const deadline = Date.now() + waitMs; while (Date.now() < deadline) { await sleep(200); const batch = consume(); if (batch.length > 0) { emitAndExit(batch); return; } } process.exit(0); }); //#endregion //#region src/commands/remove.ts const VERSION$2 = "0.1.50"; const remove = new Command().name("remove").description("uninstall the React Grab skill from your agent").option("-c, --cwd ", "working directory (defaults to current directory)", process.cwd()).option("-g, --global", "remove the globally-installed skill instead of the project's", false).action(async (opts) => { console.log(`${pc.magenta("✿")} ${pc.bold("React Grab")} ${pc.gray(VERSION$2)}`); console.log(); try { logger.break(); const removedAgents = await removeSkill({ cwd: resolve(opts.cwd), global: opts.global }); for (const agent of removedAgents) logger.log(` ${highlighter.success("✓")} ${agentLabel(agent)}`); logger.break(); if (removedAgents.length === 0) logger.log("React Grab skill is not installed in any detected agent."); else logger.log(`${highlighter.success("Removed")} the React Grab skill from ${removedAgents.length} agent${removedAgents.length === 1 ? "" : "s"}.`); logger.break(); } catch (error) { handleError(error); } }); //#endregion //#region src/commands/stop.ts const stop = new Command().name("stop").description("stop the React Grab watcher for this dir").option("-d, --dir ", "work dir holding watch.pid", DEFAULT_WATCH_DIR).action((options) => { const dir = path.resolve(options.dir); const stoppedPid = stopDaemon(dir); process.stderr.write(stoppedPid ? `react-grab stop: stopped watcher (pid ${stoppedPid})\n` : `react-grab stop: no watcher running for ${dir}\n`); process.exit(0); }); //#endregion //#region src/commands/upgrade.ts const VERSION$1 = "0.1.50"; const NPM_REGISTRY_URL = "https://registry.npmjs.org/react-grab/latest"; const fetchLatestVersion = async () => { try { return (await (await fetch(NPM_REGISTRY_URL)).json()).version ?? null; } catch { return null; } }; const isDevDependency = (projectRoot) => { const packageJsonPath = join(projectRoot, "package.json"); if (!existsSync(packageJsonPath)) return true; try { const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8")); if (packageJson.devDependencies?.["react-grab"]) return true; if (packageJson.dependencies?.["react-grab"]) return false; } catch {} return true; }; const upgrade = new Command().name("upgrade").alias("update").description("upgrade react-grab to the latest version").option("-c, --cwd ", "working directory (defaults to current directory)", process.cwd()).action(async (opts) => { console.log(`${pc.magenta("✿")} ${pc.bold("React Grab")} ${pc.gray(VERSION$1)}`); console.log(); try { const cwd = resolve(opts.cwd); const detectSpinner = spinner("Detecting project.").start(); const projectInfo = await detectProject(cwd); if (!projectInfo.hasReactGrab) { detectSpinner.fail("React Grab is not installed."); logger.break(); logger.error(`Run ${highlighter.info("npx grab@latest init")} first to install React Grab.`); logger.break(); process.exit(1); } detectSpinner.succeed(); const versionSpinner = spinner("Checking for updates.").start(); const latestVersion = await fetchLatestVersion(); if (!latestVersion) { versionSpinner.fail("Could not check for updates."); logger.break(); logger.error("Failed to reach the npm registry. Check your network connection."); logger.break(); process.exit(1); } const installedVersion = projectInfo.reactGrabVersion; if (installedVersion && installedVersion === latestVersion) { versionSpinner.succeed(`Already on the latest version ${highlighter.info(`v${latestVersion}`)}.`); logger.break(); process.exit(0); } const fromLabel = installedVersion ? `v${installedVersion}` : "unknown"; versionSpinner.succeed(`Update available: ${highlighter.dim(fromLabel)} → ${highlighter.info(`v${latestVersion}`)}.`); const upgradeSpinner = spinner("Upgrading react-grab.").start(); try { await installPackages(["react-grab@latest"], { packageManager: projectInfo.packageManager, cwd: projectInfo.projectRoot, isDev: isDevDependency(projectInfo.projectRoot) }); upgradeSpinner.succeed(); } catch { upgradeSpinner.fail(); logger.break(); logger.error("Failed to upgrade. Check your network connection and try again."); logger.break(); process.exit(1); } logger.break(); logger.log(`${highlighter.success("Success!")} React Grab has been upgraded to ${highlighter.info(`v${latestVersion}`)}.`); logger.break(); } catch (error) { handleError(error); } }); //#endregion //#region src/commands/watch.ts const writeStatus = (message) => { process.stderr.write(`react-grab watch: ${message}\n`); }; const watch = new Command().name("watch").description("run the React Grab capture daemon in the foreground (used internally by `pull`)").option("-d, --dir ", "work dir for history.jsonl + watch.pid", DEFAULT_WATCH_DIR).option("-i, --interval ", "clipboard poll interval in ms", String(800)).option("--text-only", "skip the native reader and use the plain-text fallback").option("--replay-last", "also capture the grab already on the clipboard at startup").action((options) => { const dir = path.resolve(options.dir); const intervalRaw = Number(options.interval); const intervalMs = Number.isFinite(intervalRaw) && intervalRaw > 0 ? intervalRaw : 800; try { prepareWorkDir(dir); } catch (error) { writeStatus(String(error?.message ?? error)); process.exit(1); } if (!claimDaemon(dir)) process.exit(0); process.on("exit", () => releaseDaemon(dir)); const reader = createReader({ textOnly: Boolean(options.textOnly), workDir: dir }); if (!reader) { writeStatus(NO_READER_MESSAGE); process.exit(1); } writeStatus(`watching clipboard via ${reader.mode}; history → ${path.join(dir, HISTORY_FILE_NAME)}`); runWatchLoop({ reader, dir, intervalMs, replayLast: Boolean(options.replayLast), onWarn: writeStatus }).catch((error) => { writeStatus(String(error?.message ?? error)); process.exit(1); }); }); //#endregion //#region src/cli.ts const VERSION = "0.1.50"; const VERSION_API_URL = "https://www.react-grab.com/api/version"; process.on("SIGINT", () => process.exit(0)); process.on("SIGTERM", () => process.exit(0)); try { if (isTelemetryEnabled()) fetch(`${VERSION_API_URL}?source=cli&v=${VERSION}&t=${Date.now()}`).catch(() => {}); } catch {} const program = new Command().name("grab").description("add React Grab to your project").version(VERSION, "-v, --version", "display the version number"); program.addCommand(init); program.addCommand(add); program.addCommand(remove); program.addCommand(configure); program.addCommand(upgrade); program.addCommand(pull); program.addCommand(stop); program.addCommand(watch, { hidden: true }); const main = async () => { await program.parseAsync(); }; main(); //#endregion export {}; //# sourceMappingURL=cli.js.map