@react-grab/cli 0.1.38 → 0.1.40
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.cjs +377 -150
- package/dist/cli.js +381 -154
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
- package/skills/react-grab/SKILL.md +57 -16
package/dist/cli.cjs
CHANGED
|
@@ -393,6 +393,45 @@ const handleError = (error) => {
|
|
|
393
393
|
process.exit(1);
|
|
394
394
|
};
|
|
395
395
|
//#endregion
|
|
396
|
+
//#region src/utils/detect-agents.ts
|
|
397
|
+
const PATH_BINARIES = {
|
|
398
|
+
"claude-code": ["claude"],
|
|
399
|
+
codex: ["codex"],
|
|
400
|
+
cursor: ["cursor", "cursor-agent"],
|
|
401
|
+
droid: ["droid"],
|
|
402
|
+
"gemini-cli": ["gemini"],
|
|
403
|
+
"github-copilot": ["copilot"],
|
|
404
|
+
opencode: ["opencode"],
|
|
405
|
+
pi: ["pi", "omegon"]
|
|
406
|
+
};
|
|
407
|
+
const isCommandAvailable = (command) => {
|
|
408
|
+
const pathDirectories = (process.env.PATH ?? "").split(node_path.delimiter).filter(Boolean);
|
|
409
|
+
for (const directory of pathDirectories) {
|
|
410
|
+
const binaryPath = (0, node_path.join)(directory, command);
|
|
411
|
+
try {
|
|
412
|
+
if ((0, node_fs.statSync)(binaryPath).isFile()) {
|
|
413
|
+
(0, node_fs.accessSync)(binaryPath, node_fs.constants.X_OK);
|
|
414
|
+
return true;
|
|
415
|
+
}
|
|
416
|
+
} catch {}
|
|
417
|
+
}
|
|
418
|
+
return false;
|
|
419
|
+
};
|
|
420
|
+
const detectAvailableAgents = async () => {
|
|
421
|
+
const installedAgents = new Set(await (0, agent_install_skill.detectInstalledSkillAgents)());
|
|
422
|
+
return (0, agent_install_skill.getSkillAgentTypes)().filter((agent) => {
|
|
423
|
+
if (agent === "universal") return false;
|
|
424
|
+
if (installedAgents.has(agent)) return true;
|
|
425
|
+
return PATH_BINARIES[agent]?.some(isCommandAvailable) ?? false;
|
|
426
|
+
});
|
|
427
|
+
};
|
|
428
|
+
//#endregion
|
|
429
|
+
//#region src/utils/unref-stdin.ts
|
|
430
|
+
const unrefStdin = () => {
|
|
431
|
+
if (process.stdin.isTTY) return;
|
|
432
|
+
process.stdin.unref?.();
|
|
433
|
+
};
|
|
434
|
+
//#endregion
|
|
396
435
|
//#region src/utils/prompts.ts
|
|
397
436
|
const onCancel = () => {
|
|
398
437
|
logger.break();
|
|
@@ -401,7 +440,7 @@ const onCancel = () => {
|
|
|
401
440
|
process.exit(0);
|
|
402
441
|
};
|
|
403
442
|
const prompts$1 = (questions) => {
|
|
404
|
-
return (0, prompts.default)(questions, { onCancel });
|
|
443
|
+
return (0, prompts.default)(questions, { onCancel }).finally(unrefStdin);
|
|
405
444
|
};
|
|
406
445
|
//#endregion
|
|
407
446
|
//#region src/utils/spinner.ts
|
|
@@ -413,24 +452,32 @@ const SKILL_SOURCE = (0, node_url.fileURLToPath)(new URL("../skills/react-grab",
|
|
|
413
452
|
const agentLabel = (agent) => (0, agent_install_skill.getSkillAgentConfig)(agent).displayName;
|
|
414
453
|
const installedSkillDir = (agent) => (0, node_path.join)((0, agent_install_skill.isUniversalSkillAgent)(agent) ? (0, agent_install_skill.getCanonicalSkillsDir)(true) : (0, agent_install_skill.getSkillAgentDir)(agent, { global: true }), SKILL_NAME);
|
|
415
454
|
const promptSkillInstall = async ({ yes = false } = {}) => {
|
|
416
|
-
const
|
|
417
|
-
if (
|
|
455
|
+
const detectedAgents = await detectAvailableAgents();
|
|
456
|
+
if (detectedAgents.length === 0) {
|
|
418
457
|
logger.warn("No supported agents detected.");
|
|
419
458
|
return false;
|
|
420
459
|
}
|
|
460
|
+
let selectedAgents = detectedAgents;
|
|
421
461
|
if (!yes) {
|
|
422
|
-
const {
|
|
423
|
-
type: "
|
|
424
|
-
name: "
|
|
425
|
-
message:
|
|
426
|
-
|
|
462
|
+
const { agents } = await prompts$1({
|
|
463
|
+
type: "multiselect",
|
|
464
|
+
name: "agents",
|
|
465
|
+
message: "Install the React Grab skill for:",
|
|
466
|
+
choices: detectedAgents.map((agent) => ({
|
|
467
|
+
title: agentLabel(agent),
|
|
468
|
+
value: agent,
|
|
469
|
+
selected: true
|
|
470
|
+
})),
|
|
471
|
+
instructions: false,
|
|
472
|
+
min: 1
|
|
427
473
|
});
|
|
428
|
-
|
|
474
|
+
selectedAgents = agents ?? [];
|
|
475
|
+
if (selectedAgents.length === 0) return false;
|
|
429
476
|
}
|
|
430
477
|
const installSpinner = spinner("Installing React Grab skill.").start();
|
|
431
478
|
const { installed, failed } = await (0, agent_install_skill.add)({
|
|
432
479
|
source: SKILL_SOURCE,
|
|
433
|
-
agents,
|
|
480
|
+
agents: selectedAgents,
|
|
434
481
|
global: true,
|
|
435
482
|
mode: "copy"
|
|
436
483
|
});
|
|
@@ -443,7 +490,7 @@ const promptSkillInstall = async ({ yes = false } = {}) => {
|
|
|
443
490
|
return true;
|
|
444
491
|
};
|
|
445
492
|
const removeSkill = async () => {
|
|
446
|
-
const agentsWithSkill = (await (
|
|
493
|
+
const agentsWithSkill = (await detectAvailableAgents()).filter((agent) => (0, node_fs.existsSync)(installedSkillDir(agent)));
|
|
447
494
|
for (const skillDir of new Set(agentsWithSkill.map(installedSkillDir))) (0, node_fs.rmSync)(skillDir, {
|
|
448
495
|
recursive: true,
|
|
449
496
|
force: true
|
|
@@ -453,7 +500,7 @@ const removeSkill = async () => {
|
|
|
453
500
|
};
|
|
454
501
|
//#endregion
|
|
455
502
|
//#region src/commands/add.ts
|
|
456
|
-
const VERSION$5 = "0.1.
|
|
503
|
+
const VERSION$5 = "0.1.40";
|
|
457
504
|
const add = new commander.Command().name("add").alias("install").description("install the React Grab skill for your agent").option("-y, --yes", "skip confirmation prompts", false).option("-c, --cwd <cwd>", "working directory (defaults to current directory)", process.cwd()).action(async (opts) => {
|
|
458
505
|
console.log(`${picocolors.default.magenta("✿")} ${picocolors.default.bold("React Grab")} ${picocolors.default.gray(VERSION$5)}`);
|
|
459
506
|
console.log();
|
|
@@ -856,6 +903,15 @@ const transformTanStack = (projectRoot, reactGrabAlreadyConfigured, force = fals
|
|
|
856
903
|
newContent
|
|
857
904
|
};
|
|
858
905
|
};
|
|
906
|
+
const hasFrameworkEntryPoint = (projectRoot, framework, nextRouterType) => {
|
|
907
|
+
switch (framework) {
|
|
908
|
+
case "next": return nextRouterType === "app" ? findLayoutFile(projectRoot) !== null : findDocumentFile(projectRoot) !== null;
|
|
909
|
+
case "vite":
|
|
910
|
+
case "webpack": return findEntryFile(projectRoot) !== null;
|
|
911
|
+
case "tanstack": return findTanStackRootFile(projectRoot) !== null;
|
|
912
|
+
default: return false;
|
|
913
|
+
}
|
|
914
|
+
};
|
|
859
915
|
const previewTransform = (projectRoot, framework, nextRouterType, reactGrabAlreadyConfigured = false, force = false) => {
|
|
860
916
|
switch (framework) {
|
|
861
917
|
case "next":
|
|
@@ -1038,6 +1094,7 @@ const previewCdnTransform = (projectRoot, framework, nextRouterType, targetCdnDo
|
|
|
1038
1094
|
//#endregion
|
|
1039
1095
|
//#region src/utils/constants.ts
|
|
1040
1096
|
const MAX_KEY_HOLD_DURATION_MS = 2e3;
|
|
1097
|
+
const DEFAULT_WATCH_DIR = ".react-grab";
|
|
1041
1098
|
//#endregion
|
|
1042
1099
|
//#region src/utils/format-activation-key.ts
|
|
1043
1100
|
const formatActivationKeyDisplay = (activationKey) => {
|
|
@@ -1055,7 +1112,7 @@ const formatActivationKeyDisplay = (activationKey) => {
|
|
|
1055
1112
|
};
|
|
1056
1113
|
//#endregion
|
|
1057
1114
|
//#region src/commands/configure.ts
|
|
1058
|
-
const VERSION$4 = "0.1.
|
|
1115
|
+
const VERSION$4 = "0.1.40";
|
|
1059
1116
|
const isMac = process.platform === "darwin";
|
|
1060
1117
|
const META_LABEL = isMac ? "Cmd" : "Win";
|
|
1061
1118
|
const ALT_LABEL = isMac ? "Option" : "Alt";
|
|
@@ -1674,7 +1731,7 @@ const installPackagesWithFeedback = async (packages, packageManager, projectRoot
|
|
|
1674
1731
|
};
|
|
1675
1732
|
//#endregion
|
|
1676
1733
|
//#region src/commands/init.ts
|
|
1677
|
-
const VERSION$3 = "0.1.
|
|
1734
|
+
const VERSION$3 = "0.1.40";
|
|
1678
1735
|
const REPORT_URL = "https://react-grab.com/api/report-cli";
|
|
1679
1736
|
const DOCS_URL = "https://github.com/aidenybai/react-grab";
|
|
1680
1737
|
const reportToCli = (type, config, error) => {
|
|
@@ -1732,6 +1789,15 @@ const printSubprojects = (searchRoot, sortedProjects) => {
|
|
|
1732
1789
|
logger.log(` ${highlighter.dim("$")} npx grab@latest init -c ${(0, node_path.relative)(searchRoot, sortedProjects[0].path)}`);
|
|
1733
1790
|
logger.break();
|
|
1734
1791
|
};
|
|
1792
|
+
const SUPPORTED_FRAMEWORKS_LINE = "React Grab supports Next.js, Vite, TanStack Start, and Webpack projects.";
|
|
1793
|
+
const failWithManualSetup = (failingSpinner, message, { listSupportedFrameworks = false } = {}) => {
|
|
1794
|
+
failingSpinner.fail(message);
|
|
1795
|
+
logger.break();
|
|
1796
|
+
if (listSupportedFrameworks) logger.log(SUPPORTED_FRAMEWORKS_LINE);
|
|
1797
|
+
logger.log(`Visit ${highlighter.info(DOCS_URL)} for manual setup.`);
|
|
1798
|
+
logger.break();
|
|
1799
|
+
process.exit(1);
|
|
1800
|
+
};
|
|
1735
1801
|
const init = new commander.Command().name("init").alias("setup").description("initialize React Grab in your project").option("-y, --yes", "skip confirmation prompts", false).option("-f, --force", "force overwrite existing config", false).option("-k, --key <key>", "activation key (e.g., Meta+K, Ctrl+Shift+G, Space)").option("--skip-install", "skip package installation", false).option("--pkg <pkg>", "custom package URL for CLI (e.g., grab)").option("-c, --cwd <cwd>", "working directory (defaults to current directory)", process.cwd()).action(async (opts) => {
|
|
1736
1802
|
console.log(`${picocolors.default.magenta("✿")} ${picocolors.default.bold("React Grab")} ${picocolors.default.gray(VERSION$3)}`);
|
|
1737
1803
|
console.log();
|
|
@@ -1905,7 +1971,7 @@ const init = new commander.Command().name("init").alias("setup").description("in
|
|
|
1905
1971
|
logger.break();
|
|
1906
1972
|
process.exit(1);
|
|
1907
1973
|
}
|
|
1908
|
-
if (projectInfo.framework === "unknown") {
|
|
1974
|
+
if (projectInfo.framework === "unknown" || projectInfo.isMonorepo && !hasFrameworkEntryPoint(projectInfo.projectRoot, projectInfo.framework, projectInfo.nextRouterType)) {
|
|
1909
1975
|
let searchRoot = cwd;
|
|
1910
1976
|
let reactProjects = findReactProjects(searchRoot);
|
|
1911
1977
|
if (reactProjects.length === 0 && cwd !== process.cwd()) {
|
|
@@ -1943,23 +2009,10 @@ const init = new commander.Command().name("init").alias("setup").description("in
|
|
|
1943
2009
|
const newProjectInfo = await detectProject(selectedProject);
|
|
1944
2010
|
Object.assign(projectInfo, newProjectInfo);
|
|
1945
2011
|
const newFrameworkSpinner = spinner("Verifying framework.").start();
|
|
1946
|
-
if (newProjectInfo.framework === "unknown") {
|
|
1947
|
-
newFrameworkSpinner.fail("Could not detect a supported framework in this project.");
|
|
1948
|
-
logger.break();
|
|
1949
|
-
logger.log("React Grab supports Next.js, Vite, TanStack Start, and Webpack projects.");
|
|
1950
|
-
logger.log(`Visit ${highlighter.info(DOCS_URL)} for manual setup.`);
|
|
1951
|
-
logger.break();
|
|
1952
|
-
process.exit(1);
|
|
1953
|
-
}
|
|
2012
|
+
if (newProjectInfo.framework === "unknown") failWithManualSetup(newFrameworkSpinner, "Could not detect a supported framework in this project.", { listSupportedFrameworks: true });
|
|
1954
2013
|
newFrameworkSpinner.succeed(`Verifying framework. Found ${highlighter.info(FRAMEWORK_NAMES[newProjectInfo.framework])}.`);
|
|
1955
|
-
} else {
|
|
1956
|
-
|
|
1957
|
-
logger.break();
|
|
1958
|
-
logger.log("React Grab supports Next.js, Vite, TanStack Start, and Webpack projects.");
|
|
1959
|
-
logger.log(`Visit ${highlighter.info(DOCS_URL)} for manual setup.`);
|
|
1960
|
-
logger.break();
|
|
1961
|
-
process.exit(1);
|
|
1962
|
-
}
|
|
2014
|
+
} else if (projectInfo.framework !== "unknown") failWithManualSetup(frameworkSpinner, `Verifying framework. Found ${highlighter.info(FRAMEWORK_NAMES[projectInfo.framework])}, but could not find an entry file.`);
|
|
2015
|
+
else failWithManualSetup(frameworkSpinner, "Could not detect a supported framework.", { listSupportedFrameworks: true });
|
|
1963
2016
|
} else frameworkSpinner.succeed(`Verifying framework. Found ${highlighter.info(FRAMEWORK_NAMES[projectInfo.framework])}.`);
|
|
1964
2017
|
if (projectInfo.framework === "next") spinner("Detecting router type.").start().succeed(`Detecting router type. Found ${highlighter.info(projectInfo.nextRouterType === "app" ? "App Router" : "Pages Router")}.`);
|
|
1965
2018
|
spinner("Detecting package manager.").start().succeed(`Detecting package manager. Found ${highlighter.info(PACKAGE_MANAGER_NAMES[projectInfo.packageManager])}.`);
|
|
@@ -2022,99 +2075,11 @@ const init = new commander.Command().name("init").alias("setup").description("in
|
|
|
2022
2075
|
}
|
|
2023
2076
|
});
|
|
2024
2077
|
//#endregion
|
|
2025
|
-
//#region src/
|
|
2026
|
-
const
|
|
2027
|
-
const remove = new commander.Command().name("remove").description("uninstall the React Grab skill from your agent").action(async () => {
|
|
2028
|
-
console.log(`${picocolors.default.magenta("✿")} ${picocolors.default.bold("React Grab")} ${picocolors.default.gray(VERSION$2)}`);
|
|
2029
|
-
console.log();
|
|
2030
|
-
try {
|
|
2031
|
-
logger.break();
|
|
2032
|
-
const removedCount = await removeSkill();
|
|
2033
|
-
logger.break();
|
|
2034
|
-
if (removedCount === 0) logger.log("React Grab skill is not installed in any detected agent.");
|
|
2035
|
-
else logger.log(`${highlighter.success("Removed")} the React Grab skill from ${removedCount} agent${removedCount === 1 ? "" : "s"}.`);
|
|
2036
|
-
logger.break();
|
|
2037
|
-
} catch (error) {
|
|
2038
|
-
handleError(error);
|
|
2039
|
-
}
|
|
2040
|
-
});
|
|
2041
|
-
//#endregion
|
|
2042
|
-
//#region src/commands/upgrade.ts
|
|
2043
|
-
const VERSION$1 = "0.1.38";
|
|
2044
|
-
const NPM_REGISTRY_URL = "https://registry.npmjs.org/react-grab/latest";
|
|
2045
|
-
const fetchLatestVersion = async () => {
|
|
2046
|
-
try {
|
|
2047
|
-
return (await (await fetch(NPM_REGISTRY_URL)).json()).version ?? null;
|
|
2048
|
-
} catch {
|
|
2049
|
-
return null;
|
|
2050
|
-
}
|
|
2051
|
-
};
|
|
2052
|
-
const isDevDependency = (projectRoot) => {
|
|
2053
|
-
const packageJsonPath = (0, node_path.join)(projectRoot, "package.json");
|
|
2054
|
-
if (!(0, node_fs.existsSync)(packageJsonPath)) return true;
|
|
2055
|
-
try {
|
|
2056
|
-
const packageJson = JSON.parse((0, node_fs.readFileSync)(packageJsonPath, "utf-8"));
|
|
2057
|
-
if (packageJson.devDependencies?.["react-grab"]) return true;
|
|
2058
|
-
if (packageJson.dependencies?.["react-grab"]) return false;
|
|
2059
|
-
} catch {}
|
|
2060
|
-
return true;
|
|
2061
|
-
};
|
|
2062
|
-
const upgrade = new commander.Command().name("upgrade").alias("update").description("upgrade react-grab to the latest version").option("-c, --cwd <cwd>", "working directory (defaults to current directory)", process.cwd()).action(async (opts) => {
|
|
2063
|
-
console.log(`${picocolors.default.magenta("✿")} ${picocolors.default.bold("React Grab")} ${picocolors.default.gray(VERSION$1)}`);
|
|
2064
|
-
console.log();
|
|
2065
|
-
try {
|
|
2066
|
-
const cwd = (0, node_path.resolve)(opts.cwd);
|
|
2067
|
-
const detectSpinner = spinner("Detecting project.").start();
|
|
2068
|
-
const projectInfo = await detectProject(cwd);
|
|
2069
|
-
if (!projectInfo.hasReactGrab) {
|
|
2070
|
-
detectSpinner.fail("React Grab is not installed.");
|
|
2071
|
-
logger.break();
|
|
2072
|
-
logger.error(`Run ${highlighter.info("npx grab@latest init")} first to install React Grab.`);
|
|
2073
|
-
logger.break();
|
|
2074
|
-
process.exit(1);
|
|
2075
|
-
}
|
|
2076
|
-
detectSpinner.succeed();
|
|
2077
|
-
const versionSpinner = spinner("Checking for updates.").start();
|
|
2078
|
-
const latestVersion = await fetchLatestVersion();
|
|
2079
|
-
if (!latestVersion) {
|
|
2080
|
-
versionSpinner.fail("Could not check for updates.");
|
|
2081
|
-
logger.break();
|
|
2082
|
-
logger.error("Failed to reach the npm registry. Check your network connection.");
|
|
2083
|
-
logger.break();
|
|
2084
|
-
process.exit(1);
|
|
2085
|
-
}
|
|
2086
|
-
const installedVersion = projectInfo.reactGrabVersion;
|
|
2087
|
-
if (installedVersion && installedVersion === latestVersion) {
|
|
2088
|
-
versionSpinner.succeed(`Already on the latest version ${highlighter.info(`v${latestVersion}`)}.`);
|
|
2089
|
-
logger.break();
|
|
2090
|
-
process.exit(0);
|
|
2091
|
-
}
|
|
2092
|
-
const fromLabel = installedVersion ? `v${installedVersion}` : "unknown";
|
|
2093
|
-
versionSpinner.succeed(`Update available: ${highlighter.dim(fromLabel)} → ${highlighter.info(`v${latestVersion}`)}.`);
|
|
2094
|
-
const upgradeSpinner = spinner("Upgrading react-grab.").start();
|
|
2095
|
-
try {
|
|
2096
|
-
await installPackages(["react-grab@latest"], {
|
|
2097
|
-
packageManager: projectInfo.packageManager,
|
|
2098
|
-
cwd: projectInfo.projectRoot,
|
|
2099
|
-
isDev: isDevDependency(projectInfo.projectRoot)
|
|
2100
|
-
});
|
|
2101
|
-
upgradeSpinner.succeed();
|
|
2102
|
-
} catch {
|
|
2103
|
-
upgradeSpinner.fail();
|
|
2104
|
-
logger.break();
|
|
2105
|
-
logger.error("Failed to upgrade. Check your network connection and try again.");
|
|
2106
|
-
logger.break();
|
|
2107
|
-
process.exit(1);
|
|
2108
|
-
}
|
|
2109
|
-
logger.break();
|
|
2110
|
-
logger.log(`${highlighter.success("Success!")} React Grab has been upgraded to ${highlighter.info(`v${latestVersion}`)}.`);
|
|
2111
|
-
logger.break();
|
|
2112
|
-
} catch (error) {
|
|
2113
|
-
handleError(error);
|
|
2114
|
-
}
|
|
2115
|
-
});
|
|
2078
|
+
//#region src/utils/sleep.ts
|
|
2079
|
+
const sleep = (durationMs) => new Promise((resolve) => setTimeout(resolve, durationMs));
|
|
2116
2080
|
//#endregion
|
|
2117
2081
|
//#region src/utils/clipboard.ts
|
|
2082
|
+
const HISTORY_FILE_NAME = "history.jsonl";
|
|
2118
2083
|
const READ_TIMEOUT_MS = 2500;
|
|
2119
2084
|
const MAX_CLIPBOARD_BYTES = 64 * 1024 * 1024;
|
|
2120
2085
|
const ID_RADIX = 36;
|
|
@@ -2125,7 +2090,6 @@ const SIGNATURE_SCAN_CHARS = 32 * 1024;
|
|
|
2125
2090
|
const GRAB_MIME = "application/x-react-grab";
|
|
2126
2091
|
const CHROMIUM_CUSTOM_FORMAT = "chromium/x-web-custom-data";
|
|
2127
2092
|
const GRAB_TEXT_SIGNATURE = /\bin\s+\S+\s+\(at\s+[^\n]{1,400}?:\d+:\d+\)/;
|
|
2128
|
-
const sleep = (durationMs) => new Promise((resolve) => setTimeout(resolve, durationMs));
|
|
2129
2093
|
const shortHash = (text) => (0, node_crypto.createHash)("sha1").update(text).digest("hex").slice(0, HASH_LENGTH);
|
|
2130
2094
|
const alignUp = (value) => value + PICKLE_ALIGN_BYTES - 1 & ~(PICKLE_ALIGN_BYTES - 1);
|
|
2131
2095
|
const parseChromiumPickle = (buffer) => {
|
|
@@ -2357,9 +2321,9 @@ const prepareWorkDir = (dir) => {
|
|
|
2357
2321
|
const gitignore = node_path.default.join(dir, ".gitignore");
|
|
2358
2322
|
if (!node_fs.default.existsSync(gitignore)) node_fs.default.writeFileSync(gitignore, "*\n");
|
|
2359
2323
|
};
|
|
2360
|
-
const
|
|
2361
|
-
const { reader, dir, intervalMs, replayLast, onWarn
|
|
2362
|
-
const logPath = node_path.default.join(dir,
|
|
2324
|
+
const runWatchLoop = async (options) => {
|
|
2325
|
+
const { reader, dir, intervalMs, replayLast, onWarn } = options;
|
|
2326
|
+
const logPath = node_path.default.join(dir, HISTORY_FILE_NAME);
|
|
2363
2327
|
const { read } = reader;
|
|
2364
2328
|
let lastChangeCount = null;
|
|
2365
2329
|
let lastTimestamp = 0;
|
|
@@ -2374,9 +2338,8 @@ const watchForNextGrab = async (options) => {
|
|
|
2374
2338
|
lastTimestamp = JSON.parse(initial.grab).timestamp ?? 0;
|
|
2375
2339
|
} catch {}
|
|
2376
2340
|
}
|
|
2377
|
-
while (
|
|
2341
|
+
while (true) {
|
|
2378
2342
|
await sleep(intervalMs);
|
|
2379
|
-
if (signal?.aborted) return null;
|
|
2380
2343
|
try {
|
|
2381
2344
|
const snapshot = read();
|
|
2382
2345
|
if (!snapshot) continue;
|
|
@@ -2422,7 +2385,6 @@ const watchForNextGrab = async (options) => {
|
|
|
2422
2385
|
lastChangeCount = snapshot.changeCount;
|
|
2423
2386
|
lastTextHash = textHash;
|
|
2424
2387
|
lastTimestamp = nextTimestamp;
|
|
2425
|
-
return captured;
|
|
2426
2388
|
} catch (error) {
|
|
2427
2389
|
const message = String(error?.message ?? error);
|
|
2428
2390
|
if (message !== lastErrorMessage) {
|
|
@@ -2431,51 +2393,315 @@ const watchForNextGrab = async (options) => {
|
|
|
2431
2393
|
}
|
|
2432
2394
|
}
|
|
2433
2395
|
}
|
|
2434
|
-
return null;
|
|
2435
2396
|
};
|
|
2436
2397
|
//#endregion
|
|
2437
|
-
//#region src/
|
|
2438
|
-
const
|
|
2439
|
-
const
|
|
2440
|
-
const
|
|
2441
|
-
|
|
2398
|
+
//#region src/utils/grab-log.ts
|
|
2399
|
+
const CURSOR_FILE_NAME = "cursor.txt";
|
|
2400
|
+
const cursorFilePath = (dir) => node_path.default.join(dir, CURSOR_FILE_NAME);
|
|
2401
|
+
const readCompleteGrabLines = (dir) => {
|
|
2402
|
+
let raw;
|
|
2403
|
+
try {
|
|
2404
|
+
raw = node_fs.default.readFileSync(node_path.default.join(dir, HISTORY_FILE_NAME), "utf8");
|
|
2405
|
+
} catch {
|
|
2406
|
+
return [];
|
|
2407
|
+
}
|
|
2408
|
+
const lastNewline = raw.lastIndexOf("\n");
|
|
2409
|
+
if (lastNewline < 0) return [];
|
|
2410
|
+
return raw.slice(0, lastNewline).split("\n").filter(Boolean);
|
|
2411
|
+
};
|
|
2412
|
+
const readGrabCursor = (dir) => {
|
|
2413
|
+
try {
|
|
2414
|
+
const value = Number.parseInt(node_fs.default.readFileSync(cursorFilePath(dir), "utf8").trim(), 10);
|
|
2415
|
+
return Number.isInteger(value) && value >= 0 ? value : 0;
|
|
2416
|
+
} catch {
|
|
2417
|
+
return 0;
|
|
2418
|
+
}
|
|
2419
|
+
};
|
|
2420
|
+
const consumeGrabs = (dir, options) => {
|
|
2421
|
+
const lines = readCompleteGrabLines(dir);
|
|
2422
|
+
if (options.all) return lines;
|
|
2423
|
+
const cursor = readGrabCursor(dir);
|
|
2424
|
+
const start = Math.min(cursor, lines.length);
|
|
2425
|
+
const end = options.limit > 0 ? Math.min(start + options.limit, lines.length) : lines.length;
|
|
2426
|
+
if (end !== cursor) node_fs.default.writeFileSync(cursorFilePath(dir), String(end));
|
|
2427
|
+
return lines.slice(start, end);
|
|
2428
|
+
};
|
|
2429
|
+
//#endregion
|
|
2430
|
+
//#region src/commands/read.ts
|
|
2431
|
+
const read = new commander.Command().name("read").description("print React Grab selections captured since the last read, then advance the cursor").option("-d, --dir <dir>", "work dir holding history.jsonl + cursor.txt", DEFAULT_WATCH_DIR).option("-w, --wait <ms>", "block up to <ms> for at least one new grab (default: no wait)").option("-n, --limit <count>", "max grabs to print per call (0 = no limit); the cursor only advances past what is printed", String(50)).option("--all", "print the entire history without advancing the cursor").action(async (options) => {
|
|
2442
2432
|
const dir = node_path.default.resolve(options.dir);
|
|
2443
|
-
const intervalMs = Number(options.interval);
|
|
2444
|
-
const textOnly = Boolean(options.textOnly);
|
|
2445
2433
|
try {
|
|
2446
2434
|
prepareWorkDir(dir);
|
|
2447
2435
|
} catch (error) {
|
|
2448
|
-
process.stderr.write(`react-grab
|
|
2436
|
+
process.stderr.write(`react-grab read: ${String(error?.message ?? error)}\n`);
|
|
2449
2437
|
process.exit(1);
|
|
2450
2438
|
}
|
|
2439
|
+
unrefStdin();
|
|
2440
|
+
const limitRaw = Number(options.limit);
|
|
2441
|
+
const limit = Number.isInteger(limitRaw) && limitRaw >= 0 ? limitRaw : 50;
|
|
2442
|
+
const all = Boolean(options.all);
|
|
2443
|
+
const drain = () => {
|
|
2444
|
+
const fresh = consumeGrabs(dir, {
|
|
2445
|
+
limit,
|
|
2446
|
+
all
|
|
2447
|
+
});
|
|
2448
|
+
if (fresh.length > 0) process.stdout.write(`${fresh.join("\n")}\n`);
|
|
2449
|
+
return fresh.length;
|
|
2450
|
+
};
|
|
2451
|
+
const waitRaw = options.wait ? Number(options.wait) : 0;
|
|
2452
|
+
const deadline = Date.now() + (Number.isFinite(waitRaw) && waitRaw > 0 ? waitRaw : 0);
|
|
2453
|
+
if (drain() > 0) process.exit(0);
|
|
2454
|
+
while (Date.now() < deadline) {
|
|
2455
|
+
await sleep(200);
|
|
2456
|
+
if (drain() > 0) break;
|
|
2457
|
+
}
|
|
2458
|
+
process.exit(0);
|
|
2459
|
+
});
|
|
2460
|
+
//#endregion
|
|
2461
|
+
//#region src/commands/remove.ts
|
|
2462
|
+
const VERSION$2 = "0.1.40";
|
|
2463
|
+
const remove = new commander.Command().name("remove").description("uninstall the React Grab skill from your agent").action(async () => {
|
|
2464
|
+
console.log(`${picocolors.default.magenta("✿")} ${picocolors.default.bold("React Grab")} ${picocolors.default.gray(VERSION$2)}`);
|
|
2465
|
+
console.log();
|
|
2466
|
+
try {
|
|
2467
|
+
logger.break();
|
|
2468
|
+
const removedCount = await removeSkill();
|
|
2469
|
+
logger.break();
|
|
2470
|
+
if (removedCount === 0) logger.log("React Grab skill is not installed in any detected agent.");
|
|
2471
|
+
else logger.log(`${highlighter.success("Removed")} the React Grab skill from ${removedCount} agent${removedCount === 1 ? "" : "s"}.`);
|
|
2472
|
+
logger.break();
|
|
2473
|
+
} catch (error) {
|
|
2474
|
+
handleError(error);
|
|
2475
|
+
}
|
|
2476
|
+
});
|
|
2477
|
+
//#endregion
|
|
2478
|
+
//#region src/commands/upgrade.ts
|
|
2479
|
+
const VERSION$1 = "0.1.40";
|
|
2480
|
+
const NPM_REGISTRY_URL = "https://registry.npmjs.org/react-grab/latest";
|
|
2481
|
+
const fetchLatestVersion = async () => {
|
|
2482
|
+
try {
|
|
2483
|
+
return (await (await fetch(NPM_REGISTRY_URL)).json()).version ?? null;
|
|
2484
|
+
} catch {
|
|
2485
|
+
return null;
|
|
2486
|
+
}
|
|
2487
|
+
};
|
|
2488
|
+
const isDevDependency = (projectRoot) => {
|
|
2489
|
+
const packageJsonPath = (0, node_path.join)(projectRoot, "package.json");
|
|
2490
|
+
if (!(0, node_fs.existsSync)(packageJsonPath)) return true;
|
|
2491
|
+
try {
|
|
2492
|
+
const packageJson = JSON.parse((0, node_fs.readFileSync)(packageJsonPath, "utf-8"));
|
|
2493
|
+
if (packageJson.devDependencies?.["react-grab"]) return true;
|
|
2494
|
+
if (packageJson.dependencies?.["react-grab"]) return false;
|
|
2495
|
+
} catch {}
|
|
2496
|
+
return true;
|
|
2497
|
+
};
|
|
2498
|
+
const upgrade = new commander.Command().name("upgrade").alias("update").description("upgrade react-grab to the latest version").option("-c, --cwd <cwd>", "working directory (defaults to current directory)", process.cwd()).action(async (opts) => {
|
|
2499
|
+
console.log(`${picocolors.default.magenta("✿")} ${picocolors.default.bold("React Grab")} ${picocolors.default.gray(VERSION$1)}`);
|
|
2500
|
+
console.log();
|
|
2501
|
+
try {
|
|
2502
|
+
const cwd = (0, node_path.resolve)(opts.cwd);
|
|
2503
|
+
const detectSpinner = spinner("Detecting project.").start();
|
|
2504
|
+
const projectInfo = await detectProject(cwd);
|
|
2505
|
+
if (!projectInfo.hasReactGrab) {
|
|
2506
|
+
detectSpinner.fail("React Grab is not installed.");
|
|
2507
|
+
logger.break();
|
|
2508
|
+
logger.error(`Run ${highlighter.info("npx grab@latest init")} first to install React Grab.`);
|
|
2509
|
+
logger.break();
|
|
2510
|
+
process.exit(1);
|
|
2511
|
+
}
|
|
2512
|
+
detectSpinner.succeed();
|
|
2513
|
+
const versionSpinner = spinner("Checking for updates.").start();
|
|
2514
|
+
const latestVersion = await fetchLatestVersion();
|
|
2515
|
+
if (!latestVersion) {
|
|
2516
|
+
versionSpinner.fail("Could not check for updates.");
|
|
2517
|
+
logger.break();
|
|
2518
|
+
logger.error("Failed to reach the npm registry. Check your network connection.");
|
|
2519
|
+
logger.break();
|
|
2520
|
+
process.exit(1);
|
|
2521
|
+
}
|
|
2522
|
+
const installedVersion = projectInfo.reactGrabVersion;
|
|
2523
|
+
if (installedVersion && installedVersion === latestVersion) {
|
|
2524
|
+
versionSpinner.succeed(`Already on the latest version ${highlighter.info(`v${latestVersion}`)}.`);
|
|
2525
|
+
logger.break();
|
|
2526
|
+
process.exit(0);
|
|
2527
|
+
}
|
|
2528
|
+
const fromLabel = installedVersion ? `v${installedVersion}` : "unknown";
|
|
2529
|
+
versionSpinner.succeed(`Update available: ${highlighter.dim(fromLabel)} → ${highlighter.info(`v${latestVersion}`)}.`);
|
|
2530
|
+
const upgradeSpinner = spinner("Upgrading react-grab.").start();
|
|
2531
|
+
try {
|
|
2532
|
+
await installPackages(["react-grab@latest"], {
|
|
2533
|
+
packageManager: projectInfo.packageManager,
|
|
2534
|
+
cwd: projectInfo.projectRoot,
|
|
2535
|
+
isDev: isDevDependency(projectInfo.projectRoot)
|
|
2536
|
+
});
|
|
2537
|
+
upgradeSpinner.succeed();
|
|
2538
|
+
} catch {
|
|
2539
|
+
upgradeSpinner.fail();
|
|
2540
|
+
logger.break();
|
|
2541
|
+
logger.error("Failed to upgrade. Check your network connection and try again.");
|
|
2542
|
+
logger.break();
|
|
2543
|
+
process.exit(1);
|
|
2544
|
+
}
|
|
2545
|
+
logger.break();
|
|
2546
|
+
logger.log(`${highlighter.success("Success!")} React Grab has been upgraded to ${highlighter.info(`v${latestVersion}`)}.`);
|
|
2547
|
+
logger.break();
|
|
2548
|
+
} catch (error) {
|
|
2549
|
+
handleError(error);
|
|
2550
|
+
}
|
|
2551
|
+
});
|
|
2552
|
+
//#endregion
|
|
2553
|
+
//#region src/utils/daemon.ts
|
|
2554
|
+
const PID_FILE_NAME = "watch.pid";
|
|
2555
|
+
const pidFilePath = (dir) => node_path.default.join(dir, PID_FILE_NAME);
|
|
2556
|
+
const cliEntryPath = () => process.argv[1] ?? (0, node_url.fileURLToPath)(require("url").pathToFileURL(__filename).href);
|
|
2557
|
+
const isProcessAlive = (pid) => {
|
|
2558
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
2559
|
+
try {
|
|
2560
|
+
process.kill(pid, 0);
|
|
2561
|
+
return true;
|
|
2562
|
+
} catch (error) {
|
|
2563
|
+
return error.code === "EPERM";
|
|
2564
|
+
}
|
|
2565
|
+
};
|
|
2566
|
+
const readDaemonPid = (dir) => {
|
|
2567
|
+
try {
|
|
2568
|
+
const pid = Number.parseInt(node_fs.default.readFileSync(pidFilePath(dir), "utf8").trim(), 10);
|
|
2569
|
+
return Number.isInteger(pid) && pid > 0 ? pid : null;
|
|
2570
|
+
} catch {
|
|
2571
|
+
return null;
|
|
2572
|
+
}
|
|
2573
|
+
};
|
|
2574
|
+
const isDaemonRunning = (dir) => {
|
|
2575
|
+
const pid = readDaemonPid(dir);
|
|
2576
|
+
return pid !== null && isProcessAlive(pid);
|
|
2577
|
+
};
|
|
2578
|
+
const claimDaemon = (dir) => {
|
|
2579
|
+
const file = pidFilePath(dir);
|
|
2580
|
+
for (let attempt = 0; attempt < 50; attempt += 1) try {
|
|
2581
|
+
const handle = node_fs.default.openSync(file, "wx");
|
|
2582
|
+
node_fs.default.writeFileSync(handle, String(process.pid));
|
|
2583
|
+
node_fs.default.closeSync(handle);
|
|
2584
|
+
return readDaemonPid(dir) === process.pid;
|
|
2585
|
+
} catch (error) {
|
|
2586
|
+
if (error.code !== "EEXIST") throw error;
|
|
2587
|
+
if (isDaemonRunning(dir)) return false;
|
|
2588
|
+
try {
|
|
2589
|
+
node_fs.default.rmSync(file, { force: true });
|
|
2590
|
+
} catch {}
|
|
2591
|
+
}
|
|
2592
|
+
return false;
|
|
2593
|
+
};
|
|
2594
|
+
const releaseDaemon = (dir) => {
|
|
2595
|
+
if (readDaemonPid(dir) === process.pid) try {
|
|
2596
|
+
node_fs.default.rmSync(pidFilePath(dir), { force: true });
|
|
2597
|
+
} catch {}
|
|
2598
|
+
};
|
|
2599
|
+
const stopDaemon = (dir) => {
|
|
2600
|
+
const pid = readDaemonPid(dir);
|
|
2601
|
+
if (pid === null) return null;
|
|
2602
|
+
const wasAlive = isProcessAlive(pid);
|
|
2603
|
+
if (wasAlive) try {
|
|
2604
|
+
process.kill(pid, "SIGTERM");
|
|
2605
|
+
} catch {}
|
|
2606
|
+
if (readDaemonPid(dir) === pid) try {
|
|
2607
|
+
node_fs.default.rmSync(pidFilePath(dir), { force: true });
|
|
2608
|
+
} catch {}
|
|
2609
|
+
return wasAlive ? pid : null;
|
|
2610
|
+
};
|
|
2611
|
+
const spawnDaemon = (options) => {
|
|
2612
|
+
const args = [
|
|
2613
|
+
cliEntryPath(),
|
|
2614
|
+
"watch",
|
|
2615
|
+
"--foreground",
|
|
2616
|
+
"--dir",
|
|
2617
|
+
options.dir,
|
|
2618
|
+
"--interval",
|
|
2619
|
+
String(options.intervalMs)
|
|
2620
|
+
];
|
|
2621
|
+
if (options.textOnly) args.push("--text-only");
|
|
2622
|
+
if (options.replayLast) args.push("--replay-last");
|
|
2623
|
+
(0, node_child_process.spawn)(process.execPath, args, {
|
|
2624
|
+
detached: true,
|
|
2625
|
+
stdio: "ignore",
|
|
2626
|
+
windowsHide: true
|
|
2627
|
+
}).unref();
|
|
2628
|
+
};
|
|
2629
|
+
//#endregion
|
|
2630
|
+
//#region src/commands/watch.ts
|
|
2631
|
+
const readersDir = () => node_path.default.dirname((0, node_url.fileURLToPath)(require("url").pathToFileURL(__filename).href));
|
|
2632
|
+
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.";
|
|
2633
|
+
const writeStatus = (message) => {
|
|
2634
|
+
process.stderr.write(`react-grab watch: ${message}\n`);
|
|
2635
|
+
};
|
|
2636
|
+
const runForeground = (dir, intervalMs, textOnly, replayLast) => {
|
|
2637
|
+
if (!claimDaemon(dir)) process.exit(0);
|
|
2638
|
+
process.on("exit", () => releaseDaemon(dir));
|
|
2451
2639
|
const reader = createReader({
|
|
2452
2640
|
textOnly,
|
|
2453
2641
|
readersDir: readersDir(),
|
|
2454
2642
|
workDir: dir
|
|
2455
2643
|
});
|
|
2456
2644
|
if (!reader) {
|
|
2457
|
-
|
|
2645
|
+
writeStatus(NO_READER_MESSAGE);
|
|
2458
2646
|
process.exit(1);
|
|
2459
2647
|
}
|
|
2460
|
-
|
|
2461
|
-
|
|
2648
|
+
writeStatus(`watching clipboard via ${reader.mode}; history → ${node_path.default.join(dir, HISTORY_FILE_NAME)}`);
|
|
2649
|
+
runWatchLoop({
|
|
2462
2650
|
reader,
|
|
2463
2651
|
dir,
|
|
2464
|
-
intervalMs
|
|
2465
|
-
replayLast
|
|
2466
|
-
onWarn:
|
|
2467
|
-
}).then((grab) => {
|
|
2468
|
-
if (!grab) process.exit(0);
|
|
2469
|
-
process.stdout.write(`${JSON.stringify(grab)}\n`);
|
|
2470
|
-
process.exit(0);
|
|
2652
|
+
intervalMs,
|
|
2653
|
+
replayLast,
|
|
2654
|
+
onWarn: writeStatus
|
|
2471
2655
|
}).catch((error) => {
|
|
2472
|
-
|
|
2656
|
+
writeStatus(String(error?.message ?? error));
|
|
2473
2657
|
process.exit(1);
|
|
2474
2658
|
});
|
|
2659
|
+
};
|
|
2660
|
+
const watch = new commander.Command().name("watch").description("start a background daemon that captures React Grab selections to history.jsonl").option("-d, --dir <dir>", "work dir for history.jsonl + watch.pid", DEFAULT_WATCH_DIR).option("-i, --interval <ms>", "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").option("--foreground", "run the capture loop in this process instead of detaching a daemon").option("--stop", "stop the daemon watching this dir").action((options) => {
|
|
2661
|
+
const dir = node_path.default.resolve(options.dir);
|
|
2662
|
+
const intervalRaw = Number(options.interval);
|
|
2663
|
+
const intervalMs = Number.isFinite(intervalRaw) && intervalRaw > 0 ? intervalRaw : 800;
|
|
2664
|
+
const textOnly = Boolean(options.textOnly);
|
|
2665
|
+
const replayLast = Boolean(options.replayLast);
|
|
2666
|
+
try {
|
|
2667
|
+
prepareWorkDir(dir);
|
|
2668
|
+
} catch (error) {
|
|
2669
|
+
writeStatus(String(error?.message ?? error));
|
|
2670
|
+
process.exit(1);
|
|
2671
|
+
}
|
|
2672
|
+
if (options.stop) {
|
|
2673
|
+
const stoppedPid = stopDaemon(dir);
|
|
2674
|
+
writeStatus(stoppedPid ? `stopped daemon (pid ${stoppedPid})` : `no daemon running for ${dir}`);
|
|
2675
|
+
process.exit(0);
|
|
2676
|
+
}
|
|
2677
|
+
if (options.foreground) {
|
|
2678
|
+
runForeground(dir, intervalMs, textOnly, replayLast);
|
|
2679
|
+
return;
|
|
2680
|
+
}
|
|
2681
|
+
if (isDaemonRunning(dir)) {
|
|
2682
|
+
writeStatus(`already watching ${dir} (pid ${readDaemonPid(dir)})`);
|
|
2683
|
+
process.exit(0);
|
|
2684
|
+
}
|
|
2685
|
+
if (!createReader({
|
|
2686
|
+
textOnly,
|
|
2687
|
+
readersDir: readersDir(),
|
|
2688
|
+
workDir: dir
|
|
2689
|
+
})) {
|
|
2690
|
+
writeStatus(NO_READER_MESSAGE);
|
|
2691
|
+
process.exit(1);
|
|
2692
|
+
}
|
|
2693
|
+
spawnDaemon({
|
|
2694
|
+
dir,
|
|
2695
|
+
intervalMs,
|
|
2696
|
+
textOnly,
|
|
2697
|
+
replayLast
|
|
2698
|
+
});
|
|
2699
|
+
writeStatus(`started; capturing grabs → ${node_path.default.join(dir, HISTORY_FILE_NAME)} (run \`grab read\` to consume, \`grab watch --stop\` to stop)`);
|
|
2700
|
+
process.exit(0);
|
|
2475
2701
|
});
|
|
2476
2702
|
//#endregion
|
|
2477
2703
|
//#region src/cli.ts
|
|
2478
|
-
const VERSION = "0.1.
|
|
2704
|
+
const VERSION = "0.1.40";
|
|
2479
2705
|
const VERSION_API_URL = "https://www.react-grab.com/api/version";
|
|
2480
2706
|
process.on("SIGINT", () => process.exit(0));
|
|
2481
2707
|
process.on("SIGTERM", () => process.exit(0));
|
|
@@ -2489,6 +2715,7 @@ program.addCommand(remove);
|
|
|
2489
2715
|
program.addCommand(configure);
|
|
2490
2716
|
program.addCommand(upgrade);
|
|
2491
2717
|
program.addCommand(watch);
|
|
2718
|
+
program.addCommand(read);
|
|
2492
2719
|
const main = async () => {
|
|
2493
2720
|
await program.parseAsync();
|
|
2494
2721
|
};
|