@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.js
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Command } from "commander";
|
|
3
3
|
import pc from "picocolors";
|
|
4
|
-
import fs, { accessSync, constants, existsSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
5
|
-
import path, { basename, dirname, join, relative, resolve } from "node:path";
|
|
4
|
+
import fs, { accessSync, constants, existsSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
5
|
+
import path, { basename, delimiter, dirname, join, relative, resolve } from "node:path";
|
|
6
6
|
import { detect } from "package-manager-detector/detect";
|
|
7
7
|
import ignore from "ignore";
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
9
|
-
import { add, detectInstalledSkillAgents, getCanonicalSkillsDir, getSkillAgentConfig, getSkillAgentDir, isUniversalSkillAgent } from "agent-install/skill";
|
|
9
|
+
import { add, detectInstalledSkillAgents, getCanonicalSkillsDir, getSkillAgentConfig, getSkillAgentDir, getSkillAgentTypes, isUniversalSkillAgent } from "agent-install/skill";
|
|
10
10
|
import basePrompts from "prompts";
|
|
11
11
|
import ora from "ora";
|
|
12
12
|
import { x } from "tinyexec";
|
|
13
|
-
import { spawnSync } from "node:child_process";
|
|
13
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
14
14
|
import { createHash } from "node:crypto";
|
|
15
15
|
//#region src/utils/is-non-interactive.ts
|
|
16
16
|
const AGENT_ENVIRONMENT_VARIABLES = [
|
|
@@ -365,6 +365,45 @@ const handleError = (error) => {
|
|
|
365
365
|
process.exit(1);
|
|
366
366
|
};
|
|
367
367
|
//#endregion
|
|
368
|
+
//#region src/utils/detect-agents.ts
|
|
369
|
+
const PATH_BINARIES = {
|
|
370
|
+
"claude-code": ["claude"],
|
|
371
|
+
codex: ["codex"],
|
|
372
|
+
cursor: ["cursor", "cursor-agent"],
|
|
373
|
+
droid: ["droid"],
|
|
374
|
+
"gemini-cli": ["gemini"],
|
|
375
|
+
"github-copilot": ["copilot"],
|
|
376
|
+
opencode: ["opencode"],
|
|
377
|
+
pi: ["pi", "omegon"]
|
|
378
|
+
};
|
|
379
|
+
const isCommandAvailable = (command) => {
|
|
380
|
+
const pathDirectories = (process.env.PATH ?? "").split(delimiter).filter(Boolean);
|
|
381
|
+
for (const directory of pathDirectories) {
|
|
382
|
+
const binaryPath = join(directory, command);
|
|
383
|
+
try {
|
|
384
|
+
if (statSync(binaryPath).isFile()) {
|
|
385
|
+
accessSync(binaryPath, constants.X_OK);
|
|
386
|
+
return true;
|
|
387
|
+
}
|
|
388
|
+
} catch {}
|
|
389
|
+
}
|
|
390
|
+
return false;
|
|
391
|
+
};
|
|
392
|
+
const detectAvailableAgents = async () => {
|
|
393
|
+
const installedAgents = new Set(await detectInstalledSkillAgents());
|
|
394
|
+
return getSkillAgentTypes().filter((agent) => {
|
|
395
|
+
if (agent === "universal") return false;
|
|
396
|
+
if (installedAgents.has(agent)) return true;
|
|
397
|
+
return PATH_BINARIES[agent]?.some(isCommandAvailable) ?? false;
|
|
398
|
+
});
|
|
399
|
+
};
|
|
400
|
+
//#endregion
|
|
401
|
+
//#region src/utils/unref-stdin.ts
|
|
402
|
+
const unrefStdin = () => {
|
|
403
|
+
if (process.stdin.isTTY) return;
|
|
404
|
+
process.stdin.unref?.();
|
|
405
|
+
};
|
|
406
|
+
//#endregion
|
|
368
407
|
//#region src/utils/prompts.ts
|
|
369
408
|
const onCancel = () => {
|
|
370
409
|
logger.break();
|
|
@@ -373,7 +412,7 @@ const onCancel = () => {
|
|
|
373
412
|
process.exit(0);
|
|
374
413
|
};
|
|
375
414
|
const prompts = (questions) => {
|
|
376
|
-
return basePrompts(questions, { onCancel });
|
|
415
|
+
return basePrompts(questions, { onCancel }).finally(unrefStdin);
|
|
377
416
|
};
|
|
378
417
|
//#endregion
|
|
379
418
|
//#region src/utils/spinner.ts
|
|
@@ -385,24 +424,32 @@ const SKILL_SOURCE = fileURLToPath(new URL("../skills/react-grab", import.meta.u
|
|
|
385
424
|
const agentLabel = (agent) => getSkillAgentConfig(agent).displayName;
|
|
386
425
|
const installedSkillDir = (agent) => join(isUniversalSkillAgent(agent) ? getCanonicalSkillsDir(true) : getSkillAgentDir(agent, { global: true }), SKILL_NAME);
|
|
387
426
|
const promptSkillInstall = async ({ yes = false } = {}) => {
|
|
388
|
-
const
|
|
389
|
-
if (
|
|
427
|
+
const detectedAgents = await detectAvailableAgents();
|
|
428
|
+
if (detectedAgents.length === 0) {
|
|
390
429
|
logger.warn("No supported agents detected.");
|
|
391
430
|
return false;
|
|
392
431
|
}
|
|
432
|
+
let selectedAgents = detectedAgents;
|
|
393
433
|
if (!yes) {
|
|
394
|
-
const {
|
|
395
|
-
type: "
|
|
396
|
-
name: "
|
|
397
|
-
message:
|
|
398
|
-
|
|
434
|
+
const { agents } = await prompts({
|
|
435
|
+
type: "multiselect",
|
|
436
|
+
name: "agents",
|
|
437
|
+
message: "Install the React Grab skill for:",
|
|
438
|
+
choices: detectedAgents.map((agent) => ({
|
|
439
|
+
title: agentLabel(agent),
|
|
440
|
+
value: agent,
|
|
441
|
+
selected: true
|
|
442
|
+
})),
|
|
443
|
+
instructions: false,
|
|
444
|
+
min: 1
|
|
399
445
|
});
|
|
400
|
-
|
|
446
|
+
selectedAgents = agents ?? [];
|
|
447
|
+
if (selectedAgents.length === 0) return false;
|
|
401
448
|
}
|
|
402
449
|
const installSpinner = spinner("Installing React Grab skill.").start();
|
|
403
450
|
const { installed, failed } = await add({
|
|
404
451
|
source: SKILL_SOURCE,
|
|
405
|
-
agents,
|
|
452
|
+
agents: selectedAgents,
|
|
406
453
|
global: true,
|
|
407
454
|
mode: "copy"
|
|
408
455
|
});
|
|
@@ -415,7 +462,7 @@ const promptSkillInstall = async ({ yes = false } = {}) => {
|
|
|
415
462
|
return true;
|
|
416
463
|
};
|
|
417
464
|
const removeSkill = async () => {
|
|
418
|
-
const agentsWithSkill = (await
|
|
465
|
+
const agentsWithSkill = (await detectAvailableAgents()).filter((agent) => existsSync(installedSkillDir(agent)));
|
|
419
466
|
for (const skillDir of new Set(agentsWithSkill.map(installedSkillDir))) rmSync(skillDir, {
|
|
420
467
|
recursive: true,
|
|
421
468
|
force: true
|
|
@@ -425,7 +472,7 @@ const removeSkill = async () => {
|
|
|
425
472
|
};
|
|
426
473
|
//#endregion
|
|
427
474
|
//#region src/commands/add.ts
|
|
428
|
-
const VERSION$5 = "0.1.
|
|
475
|
+
const VERSION$5 = "0.1.40";
|
|
429
476
|
const add$1 = 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 <cwd>", "working directory (defaults to current directory)", process.cwd()).action(async (opts) => {
|
|
430
477
|
console.log(`${pc.magenta("✿")} ${pc.bold("React Grab")} ${pc.gray(VERSION$5)}`);
|
|
431
478
|
console.log();
|
|
@@ -828,6 +875,15 @@ const transformTanStack = (projectRoot, reactGrabAlreadyConfigured, force = fals
|
|
|
828
875
|
newContent
|
|
829
876
|
};
|
|
830
877
|
};
|
|
878
|
+
const hasFrameworkEntryPoint = (projectRoot, framework, nextRouterType) => {
|
|
879
|
+
switch (framework) {
|
|
880
|
+
case "next": return nextRouterType === "app" ? findLayoutFile(projectRoot) !== null : findDocumentFile(projectRoot) !== null;
|
|
881
|
+
case "vite":
|
|
882
|
+
case "webpack": return findEntryFile(projectRoot) !== null;
|
|
883
|
+
case "tanstack": return findTanStackRootFile(projectRoot) !== null;
|
|
884
|
+
default: return false;
|
|
885
|
+
}
|
|
886
|
+
};
|
|
831
887
|
const previewTransform = (projectRoot, framework, nextRouterType, reactGrabAlreadyConfigured = false, force = false) => {
|
|
832
888
|
switch (framework) {
|
|
833
889
|
case "next":
|
|
@@ -1010,6 +1066,7 @@ const previewCdnTransform = (projectRoot, framework, nextRouterType, targetCdnDo
|
|
|
1010
1066
|
//#endregion
|
|
1011
1067
|
//#region src/utils/constants.ts
|
|
1012
1068
|
const MAX_KEY_HOLD_DURATION_MS = 2e3;
|
|
1069
|
+
const DEFAULT_WATCH_DIR = ".react-grab";
|
|
1013
1070
|
//#endregion
|
|
1014
1071
|
//#region src/utils/format-activation-key.ts
|
|
1015
1072
|
const formatActivationKeyDisplay = (activationKey) => {
|
|
@@ -1027,7 +1084,7 @@ const formatActivationKeyDisplay = (activationKey) => {
|
|
|
1027
1084
|
};
|
|
1028
1085
|
//#endregion
|
|
1029
1086
|
//#region src/commands/configure.ts
|
|
1030
|
-
const VERSION$4 = "0.1.
|
|
1087
|
+
const VERSION$4 = "0.1.40";
|
|
1031
1088
|
const isMac = process.platform === "darwin";
|
|
1032
1089
|
const META_LABEL = isMac ? "Cmd" : "Win";
|
|
1033
1090
|
const ALT_LABEL = isMac ? "Option" : "Alt";
|
|
@@ -1646,7 +1703,7 @@ const installPackagesWithFeedback = async (packages, packageManager, projectRoot
|
|
|
1646
1703
|
};
|
|
1647
1704
|
//#endregion
|
|
1648
1705
|
//#region src/commands/init.ts
|
|
1649
|
-
const VERSION$3 = "0.1.
|
|
1706
|
+
const VERSION$3 = "0.1.40";
|
|
1650
1707
|
const REPORT_URL = "https://react-grab.com/api/report-cli";
|
|
1651
1708
|
const DOCS_URL = "https://github.com/aidenybai/react-grab";
|
|
1652
1709
|
const reportToCli = (type, config, error) => {
|
|
@@ -1704,6 +1761,15 @@ const printSubprojects = (searchRoot, sortedProjects) => {
|
|
|
1704
1761
|
logger.log(` ${highlighter.dim("$")} npx grab@latest init -c ${relative(searchRoot, sortedProjects[0].path)}`);
|
|
1705
1762
|
logger.break();
|
|
1706
1763
|
};
|
|
1764
|
+
const SUPPORTED_FRAMEWORKS_LINE = "React Grab supports Next.js, Vite, TanStack Start, and Webpack projects.";
|
|
1765
|
+
const failWithManualSetup = (failingSpinner, message, { listSupportedFrameworks = false } = {}) => {
|
|
1766
|
+
failingSpinner.fail(message);
|
|
1767
|
+
logger.break();
|
|
1768
|
+
if (listSupportedFrameworks) logger.log(SUPPORTED_FRAMEWORKS_LINE);
|
|
1769
|
+
logger.log(`Visit ${highlighter.info(DOCS_URL)} for manual setup.`);
|
|
1770
|
+
logger.break();
|
|
1771
|
+
process.exit(1);
|
|
1772
|
+
};
|
|
1707
1773
|
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", "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) => {
|
|
1708
1774
|
console.log(`${pc.magenta("✿")} ${pc.bold("React Grab")} ${pc.gray(VERSION$3)}`);
|
|
1709
1775
|
console.log();
|
|
@@ -1877,7 +1943,7 @@ const init = new Command().name("init").alias("setup").description("initialize R
|
|
|
1877
1943
|
logger.break();
|
|
1878
1944
|
process.exit(1);
|
|
1879
1945
|
}
|
|
1880
|
-
if (projectInfo.framework === "unknown") {
|
|
1946
|
+
if (projectInfo.framework === "unknown" || projectInfo.isMonorepo && !hasFrameworkEntryPoint(projectInfo.projectRoot, projectInfo.framework, projectInfo.nextRouterType)) {
|
|
1881
1947
|
let searchRoot = cwd;
|
|
1882
1948
|
let reactProjects = findReactProjects(searchRoot);
|
|
1883
1949
|
if (reactProjects.length === 0 && cwd !== process.cwd()) {
|
|
@@ -1915,23 +1981,10 @@ const init = new Command().name("init").alias("setup").description("initialize R
|
|
|
1915
1981
|
const newProjectInfo = await detectProject(selectedProject);
|
|
1916
1982
|
Object.assign(projectInfo, newProjectInfo);
|
|
1917
1983
|
const newFrameworkSpinner = spinner("Verifying framework.").start();
|
|
1918
|
-
if (newProjectInfo.framework === "unknown") {
|
|
1919
|
-
newFrameworkSpinner.fail("Could not detect a supported framework in this project.");
|
|
1920
|
-
logger.break();
|
|
1921
|
-
logger.log("React Grab supports Next.js, Vite, TanStack Start, and Webpack projects.");
|
|
1922
|
-
logger.log(`Visit ${highlighter.info(DOCS_URL)} for manual setup.`);
|
|
1923
|
-
logger.break();
|
|
1924
|
-
process.exit(1);
|
|
1925
|
-
}
|
|
1984
|
+
if (newProjectInfo.framework === "unknown") failWithManualSetup(newFrameworkSpinner, "Could not detect a supported framework in this project.", { listSupportedFrameworks: true });
|
|
1926
1985
|
newFrameworkSpinner.succeed(`Verifying framework. Found ${highlighter.info(FRAMEWORK_NAMES[newProjectInfo.framework])}.`);
|
|
1927
|
-
} else {
|
|
1928
|
-
|
|
1929
|
-
logger.break();
|
|
1930
|
-
logger.log("React Grab supports Next.js, Vite, TanStack Start, and Webpack projects.");
|
|
1931
|
-
logger.log(`Visit ${highlighter.info(DOCS_URL)} for manual setup.`);
|
|
1932
|
-
logger.break();
|
|
1933
|
-
process.exit(1);
|
|
1934
|
-
}
|
|
1986
|
+
} else if (projectInfo.framework !== "unknown") failWithManualSetup(frameworkSpinner, `Verifying framework. Found ${highlighter.info(FRAMEWORK_NAMES[projectInfo.framework])}, but could not find an entry file.`);
|
|
1987
|
+
else failWithManualSetup(frameworkSpinner, "Could not detect a supported framework.", { listSupportedFrameworks: true });
|
|
1935
1988
|
} else frameworkSpinner.succeed(`Verifying framework. Found ${highlighter.info(FRAMEWORK_NAMES[projectInfo.framework])}.`);
|
|
1936
1989
|
if (projectInfo.framework === "next") spinner("Detecting router type.").start().succeed(`Detecting router type. Found ${highlighter.info(projectInfo.nextRouterType === "app" ? "App Router" : "Pages Router")}.`);
|
|
1937
1990
|
spinner("Detecting package manager.").start().succeed(`Detecting package manager. Found ${highlighter.info(PACKAGE_MANAGER_NAMES[projectInfo.packageManager])}.`);
|
|
@@ -1994,99 +2047,11 @@ const init = new Command().name("init").alias("setup").description("initialize R
|
|
|
1994
2047
|
}
|
|
1995
2048
|
});
|
|
1996
2049
|
//#endregion
|
|
1997
|
-
//#region src/
|
|
1998
|
-
const
|
|
1999
|
-
const remove = new Command().name("remove").description("uninstall the React Grab skill from your agent").action(async () => {
|
|
2000
|
-
console.log(`${pc.magenta("✿")} ${pc.bold("React Grab")} ${pc.gray(VERSION$2)}`);
|
|
2001
|
-
console.log();
|
|
2002
|
-
try {
|
|
2003
|
-
logger.break();
|
|
2004
|
-
const removedCount = await removeSkill();
|
|
2005
|
-
logger.break();
|
|
2006
|
-
if (removedCount === 0) logger.log("React Grab skill is not installed in any detected agent.");
|
|
2007
|
-
else logger.log(`${highlighter.success("Removed")} the React Grab skill from ${removedCount} agent${removedCount === 1 ? "" : "s"}.`);
|
|
2008
|
-
logger.break();
|
|
2009
|
-
} catch (error) {
|
|
2010
|
-
handleError(error);
|
|
2011
|
-
}
|
|
2012
|
-
});
|
|
2013
|
-
//#endregion
|
|
2014
|
-
//#region src/commands/upgrade.ts
|
|
2015
|
-
const VERSION$1 = "0.1.38";
|
|
2016
|
-
const NPM_REGISTRY_URL = "https://registry.npmjs.org/react-grab/latest";
|
|
2017
|
-
const fetchLatestVersion = async () => {
|
|
2018
|
-
try {
|
|
2019
|
-
return (await (await fetch(NPM_REGISTRY_URL)).json()).version ?? null;
|
|
2020
|
-
} catch {
|
|
2021
|
-
return null;
|
|
2022
|
-
}
|
|
2023
|
-
};
|
|
2024
|
-
const isDevDependency = (projectRoot) => {
|
|
2025
|
-
const packageJsonPath = join(projectRoot, "package.json");
|
|
2026
|
-
if (!existsSync(packageJsonPath)) return true;
|
|
2027
|
-
try {
|
|
2028
|
-
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
2029
|
-
if (packageJson.devDependencies?.["react-grab"]) return true;
|
|
2030
|
-
if (packageJson.dependencies?.["react-grab"]) return false;
|
|
2031
|
-
} catch {}
|
|
2032
|
-
return true;
|
|
2033
|
-
};
|
|
2034
|
-
const upgrade = new 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) => {
|
|
2035
|
-
console.log(`${pc.magenta("✿")} ${pc.bold("React Grab")} ${pc.gray(VERSION$1)}`);
|
|
2036
|
-
console.log();
|
|
2037
|
-
try {
|
|
2038
|
-
const cwd = resolve(opts.cwd);
|
|
2039
|
-
const detectSpinner = spinner("Detecting project.").start();
|
|
2040
|
-
const projectInfo = await detectProject(cwd);
|
|
2041
|
-
if (!projectInfo.hasReactGrab) {
|
|
2042
|
-
detectSpinner.fail("React Grab is not installed.");
|
|
2043
|
-
logger.break();
|
|
2044
|
-
logger.error(`Run ${highlighter.info("npx grab@latest init")} first to install React Grab.`);
|
|
2045
|
-
logger.break();
|
|
2046
|
-
process.exit(1);
|
|
2047
|
-
}
|
|
2048
|
-
detectSpinner.succeed();
|
|
2049
|
-
const versionSpinner = spinner("Checking for updates.").start();
|
|
2050
|
-
const latestVersion = await fetchLatestVersion();
|
|
2051
|
-
if (!latestVersion) {
|
|
2052
|
-
versionSpinner.fail("Could not check for updates.");
|
|
2053
|
-
logger.break();
|
|
2054
|
-
logger.error("Failed to reach the npm registry. Check your network connection.");
|
|
2055
|
-
logger.break();
|
|
2056
|
-
process.exit(1);
|
|
2057
|
-
}
|
|
2058
|
-
const installedVersion = projectInfo.reactGrabVersion;
|
|
2059
|
-
if (installedVersion && installedVersion === latestVersion) {
|
|
2060
|
-
versionSpinner.succeed(`Already on the latest version ${highlighter.info(`v${latestVersion}`)}.`);
|
|
2061
|
-
logger.break();
|
|
2062
|
-
process.exit(0);
|
|
2063
|
-
}
|
|
2064
|
-
const fromLabel = installedVersion ? `v${installedVersion}` : "unknown";
|
|
2065
|
-
versionSpinner.succeed(`Update available: ${highlighter.dim(fromLabel)} → ${highlighter.info(`v${latestVersion}`)}.`);
|
|
2066
|
-
const upgradeSpinner = spinner("Upgrading react-grab.").start();
|
|
2067
|
-
try {
|
|
2068
|
-
await installPackages(["react-grab@latest"], {
|
|
2069
|
-
packageManager: projectInfo.packageManager,
|
|
2070
|
-
cwd: projectInfo.projectRoot,
|
|
2071
|
-
isDev: isDevDependency(projectInfo.projectRoot)
|
|
2072
|
-
});
|
|
2073
|
-
upgradeSpinner.succeed();
|
|
2074
|
-
} catch {
|
|
2075
|
-
upgradeSpinner.fail();
|
|
2076
|
-
logger.break();
|
|
2077
|
-
logger.error("Failed to upgrade. Check your network connection and try again.");
|
|
2078
|
-
logger.break();
|
|
2079
|
-
process.exit(1);
|
|
2080
|
-
}
|
|
2081
|
-
logger.break();
|
|
2082
|
-
logger.log(`${highlighter.success("Success!")} React Grab has been upgraded to ${highlighter.info(`v${latestVersion}`)}.`);
|
|
2083
|
-
logger.break();
|
|
2084
|
-
} catch (error) {
|
|
2085
|
-
handleError(error);
|
|
2086
|
-
}
|
|
2087
|
-
});
|
|
2050
|
+
//#region src/utils/sleep.ts
|
|
2051
|
+
const sleep = (durationMs) => new Promise((resolve) => setTimeout(resolve, durationMs));
|
|
2088
2052
|
//#endregion
|
|
2089
2053
|
//#region src/utils/clipboard.ts
|
|
2054
|
+
const HISTORY_FILE_NAME = "history.jsonl";
|
|
2090
2055
|
const READ_TIMEOUT_MS = 2500;
|
|
2091
2056
|
const MAX_CLIPBOARD_BYTES = 64 * 1024 * 1024;
|
|
2092
2057
|
const ID_RADIX = 36;
|
|
@@ -2097,7 +2062,6 @@ const SIGNATURE_SCAN_CHARS = 32 * 1024;
|
|
|
2097
2062
|
const GRAB_MIME = "application/x-react-grab";
|
|
2098
2063
|
const CHROMIUM_CUSTOM_FORMAT = "chromium/x-web-custom-data";
|
|
2099
2064
|
const GRAB_TEXT_SIGNATURE = /\bin\s+\S+\s+\(at\s+[^\n]{1,400}?:\d+:\d+\)/;
|
|
2100
|
-
const sleep = (durationMs) => new Promise((resolve) => setTimeout(resolve, durationMs));
|
|
2101
2065
|
const shortHash = (text) => createHash("sha1").update(text).digest("hex").slice(0, HASH_LENGTH);
|
|
2102
2066
|
const alignUp = (value) => value + PICKLE_ALIGN_BYTES - 1 & ~(PICKLE_ALIGN_BYTES - 1);
|
|
2103
2067
|
const parseChromiumPickle = (buffer) => {
|
|
@@ -2329,9 +2293,9 @@ const prepareWorkDir = (dir) => {
|
|
|
2329
2293
|
const gitignore = path.join(dir, ".gitignore");
|
|
2330
2294
|
if (!fs.existsSync(gitignore)) fs.writeFileSync(gitignore, "*\n");
|
|
2331
2295
|
};
|
|
2332
|
-
const
|
|
2333
|
-
const { reader, dir, intervalMs, replayLast, onWarn
|
|
2334
|
-
const logPath = path.join(dir,
|
|
2296
|
+
const runWatchLoop = async (options) => {
|
|
2297
|
+
const { reader, dir, intervalMs, replayLast, onWarn } = options;
|
|
2298
|
+
const logPath = path.join(dir, HISTORY_FILE_NAME);
|
|
2335
2299
|
const { read } = reader;
|
|
2336
2300
|
let lastChangeCount = null;
|
|
2337
2301
|
let lastTimestamp = 0;
|
|
@@ -2346,9 +2310,8 @@ const watchForNextGrab = async (options) => {
|
|
|
2346
2310
|
lastTimestamp = JSON.parse(initial.grab).timestamp ?? 0;
|
|
2347
2311
|
} catch {}
|
|
2348
2312
|
}
|
|
2349
|
-
while (
|
|
2313
|
+
while (true) {
|
|
2350
2314
|
await sleep(intervalMs);
|
|
2351
|
-
if (signal?.aborted) return null;
|
|
2352
2315
|
try {
|
|
2353
2316
|
const snapshot = read();
|
|
2354
2317
|
if (!snapshot) continue;
|
|
@@ -2394,7 +2357,6 @@ const watchForNextGrab = async (options) => {
|
|
|
2394
2357
|
lastChangeCount = snapshot.changeCount;
|
|
2395
2358
|
lastTextHash = textHash;
|
|
2396
2359
|
lastTimestamp = nextTimestamp;
|
|
2397
|
-
return captured;
|
|
2398
2360
|
} catch (error) {
|
|
2399
2361
|
const message = String(error?.message ?? error);
|
|
2400
2362
|
if (message !== lastErrorMessage) {
|
|
@@ -2403,51 +2365,315 @@ const watchForNextGrab = async (options) => {
|
|
|
2403
2365
|
}
|
|
2404
2366
|
}
|
|
2405
2367
|
}
|
|
2406
|
-
return null;
|
|
2407
2368
|
};
|
|
2408
2369
|
//#endregion
|
|
2409
|
-
//#region src/
|
|
2410
|
-
const
|
|
2411
|
-
const
|
|
2412
|
-
const
|
|
2413
|
-
|
|
2370
|
+
//#region src/utils/grab-log.ts
|
|
2371
|
+
const CURSOR_FILE_NAME = "cursor.txt";
|
|
2372
|
+
const cursorFilePath = (dir) => path.join(dir, CURSOR_FILE_NAME);
|
|
2373
|
+
const readCompleteGrabLines = (dir) => {
|
|
2374
|
+
let raw;
|
|
2375
|
+
try {
|
|
2376
|
+
raw = fs.readFileSync(path.join(dir, HISTORY_FILE_NAME), "utf8");
|
|
2377
|
+
} catch {
|
|
2378
|
+
return [];
|
|
2379
|
+
}
|
|
2380
|
+
const lastNewline = raw.lastIndexOf("\n");
|
|
2381
|
+
if (lastNewline < 0) return [];
|
|
2382
|
+
return raw.slice(0, lastNewline).split("\n").filter(Boolean);
|
|
2383
|
+
};
|
|
2384
|
+
const readGrabCursor = (dir) => {
|
|
2385
|
+
try {
|
|
2386
|
+
const value = Number.parseInt(fs.readFileSync(cursorFilePath(dir), "utf8").trim(), 10);
|
|
2387
|
+
return Number.isInteger(value) && value >= 0 ? value : 0;
|
|
2388
|
+
} catch {
|
|
2389
|
+
return 0;
|
|
2390
|
+
}
|
|
2391
|
+
};
|
|
2392
|
+
const consumeGrabs = (dir, options) => {
|
|
2393
|
+
const lines = readCompleteGrabLines(dir);
|
|
2394
|
+
if (options.all) return lines;
|
|
2395
|
+
const cursor = readGrabCursor(dir);
|
|
2396
|
+
const start = Math.min(cursor, lines.length);
|
|
2397
|
+
const end = options.limit > 0 ? Math.min(start + options.limit, lines.length) : lines.length;
|
|
2398
|
+
if (end !== cursor) fs.writeFileSync(cursorFilePath(dir), String(end));
|
|
2399
|
+
return lines.slice(start, end);
|
|
2400
|
+
};
|
|
2401
|
+
//#endregion
|
|
2402
|
+
//#region src/commands/read.ts
|
|
2403
|
+
const read = new 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) => {
|
|
2414
2404
|
const dir = path.resolve(options.dir);
|
|
2415
|
-
const intervalMs = Number(options.interval);
|
|
2416
|
-
const textOnly = Boolean(options.textOnly);
|
|
2417
2405
|
try {
|
|
2418
2406
|
prepareWorkDir(dir);
|
|
2419
2407
|
} catch (error) {
|
|
2420
|
-
process.stderr.write(`react-grab
|
|
2408
|
+
process.stderr.write(`react-grab read: ${String(error?.message ?? error)}\n`);
|
|
2421
2409
|
process.exit(1);
|
|
2422
2410
|
}
|
|
2411
|
+
unrefStdin();
|
|
2412
|
+
const limitRaw = Number(options.limit);
|
|
2413
|
+
const limit = Number.isInteger(limitRaw) && limitRaw >= 0 ? limitRaw : 50;
|
|
2414
|
+
const all = Boolean(options.all);
|
|
2415
|
+
const drain = () => {
|
|
2416
|
+
const fresh = consumeGrabs(dir, {
|
|
2417
|
+
limit,
|
|
2418
|
+
all
|
|
2419
|
+
});
|
|
2420
|
+
if (fresh.length > 0) process.stdout.write(`${fresh.join("\n")}\n`);
|
|
2421
|
+
return fresh.length;
|
|
2422
|
+
};
|
|
2423
|
+
const waitRaw = options.wait ? Number(options.wait) : 0;
|
|
2424
|
+
const deadline = Date.now() + (Number.isFinite(waitRaw) && waitRaw > 0 ? waitRaw : 0);
|
|
2425
|
+
if (drain() > 0) process.exit(0);
|
|
2426
|
+
while (Date.now() < deadline) {
|
|
2427
|
+
await sleep(200);
|
|
2428
|
+
if (drain() > 0) break;
|
|
2429
|
+
}
|
|
2430
|
+
process.exit(0);
|
|
2431
|
+
});
|
|
2432
|
+
//#endregion
|
|
2433
|
+
//#region src/commands/remove.ts
|
|
2434
|
+
const VERSION$2 = "0.1.40";
|
|
2435
|
+
const remove = new Command().name("remove").description("uninstall the React Grab skill from your agent").action(async () => {
|
|
2436
|
+
console.log(`${pc.magenta("✿")} ${pc.bold("React Grab")} ${pc.gray(VERSION$2)}`);
|
|
2437
|
+
console.log();
|
|
2438
|
+
try {
|
|
2439
|
+
logger.break();
|
|
2440
|
+
const removedCount = await removeSkill();
|
|
2441
|
+
logger.break();
|
|
2442
|
+
if (removedCount === 0) logger.log("React Grab skill is not installed in any detected agent.");
|
|
2443
|
+
else logger.log(`${highlighter.success("Removed")} the React Grab skill from ${removedCount} agent${removedCount === 1 ? "" : "s"}.`);
|
|
2444
|
+
logger.break();
|
|
2445
|
+
} catch (error) {
|
|
2446
|
+
handleError(error);
|
|
2447
|
+
}
|
|
2448
|
+
});
|
|
2449
|
+
//#endregion
|
|
2450
|
+
//#region src/commands/upgrade.ts
|
|
2451
|
+
const VERSION$1 = "0.1.40";
|
|
2452
|
+
const NPM_REGISTRY_URL = "https://registry.npmjs.org/react-grab/latest";
|
|
2453
|
+
const fetchLatestVersion = async () => {
|
|
2454
|
+
try {
|
|
2455
|
+
return (await (await fetch(NPM_REGISTRY_URL)).json()).version ?? null;
|
|
2456
|
+
} catch {
|
|
2457
|
+
return null;
|
|
2458
|
+
}
|
|
2459
|
+
};
|
|
2460
|
+
const isDevDependency = (projectRoot) => {
|
|
2461
|
+
const packageJsonPath = join(projectRoot, "package.json");
|
|
2462
|
+
if (!existsSync(packageJsonPath)) return true;
|
|
2463
|
+
try {
|
|
2464
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
2465
|
+
if (packageJson.devDependencies?.["react-grab"]) return true;
|
|
2466
|
+
if (packageJson.dependencies?.["react-grab"]) return false;
|
|
2467
|
+
} catch {}
|
|
2468
|
+
return true;
|
|
2469
|
+
};
|
|
2470
|
+
const upgrade = new 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) => {
|
|
2471
|
+
console.log(`${pc.magenta("✿")} ${pc.bold("React Grab")} ${pc.gray(VERSION$1)}`);
|
|
2472
|
+
console.log();
|
|
2473
|
+
try {
|
|
2474
|
+
const cwd = resolve(opts.cwd);
|
|
2475
|
+
const detectSpinner = spinner("Detecting project.").start();
|
|
2476
|
+
const projectInfo = await detectProject(cwd);
|
|
2477
|
+
if (!projectInfo.hasReactGrab) {
|
|
2478
|
+
detectSpinner.fail("React Grab is not installed.");
|
|
2479
|
+
logger.break();
|
|
2480
|
+
logger.error(`Run ${highlighter.info("npx grab@latest init")} first to install React Grab.`);
|
|
2481
|
+
logger.break();
|
|
2482
|
+
process.exit(1);
|
|
2483
|
+
}
|
|
2484
|
+
detectSpinner.succeed();
|
|
2485
|
+
const versionSpinner = spinner("Checking for updates.").start();
|
|
2486
|
+
const latestVersion = await fetchLatestVersion();
|
|
2487
|
+
if (!latestVersion) {
|
|
2488
|
+
versionSpinner.fail("Could not check for updates.");
|
|
2489
|
+
logger.break();
|
|
2490
|
+
logger.error("Failed to reach the npm registry. Check your network connection.");
|
|
2491
|
+
logger.break();
|
|
2492
|
+
process.exit(1);
|
|
2493
|
+
}
|
|
2494
|
+
const installedVersion = projectInfo.reactGrabVersion;
|
|
2495
|
+
if (installedVersion && installedVersion === latestVersion) {
|
|
2496
|
+
versionSpinner.succeed(`Already on the latest version ${highlighter.info(`v${latestVersion}`)}.`);
|
|
2497
|
+
logger.break();
|
|
2498
|
+
process.exit(0);
|
|
2499
|
+
}
|
|
2500
|
+
const fromLabel = installedVersion ? `v${installedVersion}` : "unknown";
|
|
2501
|
+
versionSpinner.succeed(`Update available: ${highlighter.dim(fromLabel)} → ${highlighter.info(`v${latestVersion}`)}.`);
|
|
2502
|
+
const upgradeSpinner = spinner("Upgrading react-grab.").start();
|
|
2503
|
+
try {
|
|
2504
|
+
await installPackages(["react-grab@latest"], {
|
|
2505
|
+
packageManager: projectInfo.packageManager,
|
|
2506
|
+
cwd: projectInfo.projectRoot,
|
|
2507
|
+
isDev: isDevDependency(projectInfo.projectRoot)
|
|
2508
|
+
});
|
|
2509
|
+
upgradeSpinner.succeed();
|
|
2510
|
+
} catch {
|
|
2511
|
+
upgradeSpinner.fail();
|
|
2512
|
+
logger.break();
|
|
2513
|
+
logger.error("Failed to upgrade. Check your network connection and try again.");
|
|
2514
|
+
logger.break();
|
|
2515
|
+
process.exit(1);
|
|
2516
|
+
}
|
|
2517
|
+
logger.break();
|
|
2518
|
+
logger.log(`${highlighter.success("Success!")} React Grab has been upgraded to ${highlighter.info(`v${latestVersion}`)}.`);
|
|
2519
|
+
logger.break();
|
|
2520
|
+
} catch (error) {
|
|
2521
|
+
handleError(error);
|
|
2522
|
+
}
|
|
2523
|
+
});
|
|
2524
|
+
//#endregion
|
|
2525
|
+
//#region src/utils/daemon.ts
|
|
2526
|
+
const PID_FILE_NAME = "watch.pid";
|
|
2527
|
+
const pidFilePath = (dir) => path.join(dir, PID_FILE_NAME);
|
|
2528
|
+
const cliEntryPath = () => process.argv[1] ?? fileURLToPath(import.meta.url);
|
|
2529
|
+
const isProcessAlive = (pid) => {
|
|
2530
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
2531
|
+
try {
|
|
2532
|
+
process.kill(pid, 0);
|
|
2533
|
+
return true;
|
|
2534
|
+
} catch (error) {
|
|
2535
|
+
return error.code === "EPERM";
|
|
2536
|
+
}
|
|
2537
|
+
};
|
|
2538
|
+
const readDaemonPid = (dir) => {
|
|
2539
|
+
try {
|
|
2540
|
+
const pid = Number.parseInt(fs.readFileSync(pidFilePath(dir), "utf8").trim(), 10);
|
|
2541
|
+
return Number.isInteger(pid) && pid > 0 ? pid : null;
|
|
2542
|
+
} catch {
|
|
2543
|
+
return null;
|
|
2544
|
+
}
|
|
2545
|
+
};
|
|
2546
|
+
const isDaemonRunning = (dir) => {
|
|
2547
|
+
const pid = readDaemonPid(dir);
|
|
2548
|
+
return pid !== null && isProcessAlive(pid);
|
|
2549
|
+
};
|
|
2550
|
+
const claimDaemon = (dir) => {
|
|
2551
|
+
const file = pidFilePath(dir);
|
|
2552
|
+
for (let attempt = 0; attempt < 50; attempt += 1) try {
|
|
2553
|
+
const handle = fs.openSync(file, "wx");
|
|
2554
|
+
fs.writeFileSync(handle, String(process.pid));
|
|
2555
|
+
fs.closeSync(handle);
|
|
2556
|
+
return readDaemonPid(dir) === process.pid;
|
|
2557
|
+
} catch (error) {
|
|
2558
|
+
if (error.code !== "EEXIST") throw error;
|
|
2559
|
+
if (isDaemonRunning(dir)) return false;
|
|
2560
|
+
try {
|
|
2561
|
+
fs.rmSync(file, { force: true });
|
|
2562
|
+
} catch {}
|
|
2563
|
+
}
|
|
2564
|
+
return false;
|
|
2565
|
+
};
|
|
2566
|
+
const releaseDaemon = (dir) => {
|
|
2567
|
+
if (readDaemonPid(dir) === process.pid) try {
|
|
2568
|
+
fs.rmSync(pidFilePath(dir), { force: true });
|
|
2569
|
+
} catch {}
|
|
2570
|
+
};
|
|
2571
|
+
const stopDaemon = (dir) => {
|
|
2572
|
+
const pid = readDaemonPid(dir);
|
|
2573
|
+
if (pid === null) return null;
|
|
2574
|
+
const wasAlive = isProcessAlive(pid);
|
|
2575
|
+
if (wasAlive) try {
|
|
2576
|
+
process.kill(pid, "SIGTERM");
|
|
2577
|
+
} catch {}
|
|
2578
|
+
if (readDaemonPid(dir) === pid) try {
|
|
2579
|
+
fs.rmSync(pidFilePath(dir), { force: true });
|
|
2580
|
+
} catch {}
|
|
2581
|
+
return wasAlive ? pid : null;
|
|
2582
|
+
};
|
|
2583
|
+
const spawnDaemon = (options) => {
|
|
2584
|
+
const args = [
|
|
2585
|
+
cliEntryPath(),
|
|
2586
|
+
"watch",
|
|
2587
|
+
"--foreground",
|
|
2588
|
+
"--dir",
|
|
2589
|
+
options.dir,
|
|
2590
|
+
"--interval",
|
|
2591
|
+
String(options.intervalMs)
|
|
2592
|
+
];
|
|
2593
|
+
if (options.textOnly) args.push("--text-only");
|
|
2594
|
+
if (options.replayLast) args.push("--replay-last");
|
|
2595
|
+
spawn(process.execPath, args, {
|
|
2596
|
+
detached: true,
|
|
2597
|
+
stdio: "ignore",
|
|
2598
|
+
windowsHide: true
|
|
2599
|
+
}).unref();
|
|
2600
|
+
};
|
|
2601
|
+
//#endregion
|
|
2602
|
+
//#region src/commands/watch.ts
|
|
2603
|
+
const readersDir = () => path.dirname(fileURLToPath(import.meta.url));
|
|
2604
|
+
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.";
|
|
2605
|
+
const writeStatus = (message) => {
|
|
2606
|
+
process.stderr.write(`react-grab watch: ${message}\n`);
|
|
2607
|
+
};
|
|
2608
|
+
const runForeground = (dir, intervalMs, textOnly, replayLast) => {
|
|
2609
|
+
if (!claimDaemon(dir)) process.exit(0);
|
|
2610
|
+
process.on("exit", () => releaseDaemon(dir));
|
|
2423
2611
|
const reader = createReader({
|
|
2424
2612
|
textOnly,
|
|
2425
2613
|
readersDir: readersDir(),
|
|
2426
2614
|
workDir: dir
|
|
2427
2615
|
});
|
|
2428
2616
|
if (!reader) {
|
|
2429
|
-
|
|
2617
|
+
writeStatus(NO_READER_MESSAGE);
|
|
2430
2618
|
process.exit(1);
|
|
2431
2619
|
}
|
|
2432
|
-
|
|
2433
|
-
|
|
2620
|
+
writeStatus(`watching clipboard via ${reader.mode}; history → ${path.join(dir, HISTORY_FILE_NAME)}`);
|
|
2621
|
+
runWatchLoop({
|
|
2434
2622
|
reader,
|
|
2435
2623
|
dir,
|
|
2436
|
-
intervalMs
|
|
2437
|
-
replayLast
|
|
2438
|
-
onWarn:
|
|
2439
|
-
}).then((grab) => {
|
|
2440
|
-
if (!grab) process.exit(0);
|
|
2441
|
-
process.stdout.write(`${JSON.stringify(grab)}\n`);
|
|
2442
|
-
process.exit(0);
|
|
2624
|
+
intervalMs,
|
|
2625
|
+
replayLast,
|
|
2626
|
+
onWarn: writeStatus
|
|
2443
2627
|
}).catch((error) => {
|
|
2444
|
-
|
|
2628
|
+
writeStatus(String(error?.message ?? error));
|
|
2445
2629
|
process.exit(1);
|
|
2446
2630
|
});
|
|
2631
|
+
};
|
|
2632
|
+
const watch = new 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) => {
|
|
2633
|
+
const dir = path.resolve(options.dir);
|
|
2634
|
+
const intervalRaw = Number(options.interval);
|
|
2635
|
+
const intervalMs = Number.isFinite(intervalRaw) && intervalRaw > 0 ? intervalRaw : 800;
|
|
2636
|
+
const textOnly = Boolean(options.textOnly);
|
|
2637
|
+
const replayLast = Boolean(options.replayLast);
|
|
2638
|
+
try {
|
|
2639
|
+
prepareWorkDir(dir);
|
|
2640
|
+
} catch (error) {
|
|
2641
|
+
writeStatus(String(error?.message ?? error));
|
|
2642
|
+
process.exit(1);
|
|
2643
|
+
}
|
|
2644
|
+
if (options.stop) {
|
|
2645
|
+
const stoppedPid = stopDaemon(dir);
|
|
2646
|
+
writeStatus(stoppedPid ? `stopped daemon (pid ${stoppedPid})` : `no daemon running for ${dir}`);
|
|
2647
|
+
process.exit(0);
|
|
2648
|
+
}
|
|
2649
|
+
if (options.foreground) {
|
|
2650
|
+
runForeground(dir, intervalMs, textOnly, replayLast);
|
|
2651
|
+
return;
|
|
2652
|
+
}
|
|
2653
|
+
if (isDaemonRunning(dir)) {
|
|
2654
|
+
writeStatus(`already watching ${dir} (pid ${readDaemonPid(dir)})`);
|
|
2655
|
+
process.exit(0);
|
|
2656
|
+
}
|
|
2657
|
+
if (!createReader({
|
|
2658
|
+
textOnly,
|
|
2659
|
+
readersDir: readersDir(),
|
|
2660
|
+
workDir: dir
|
|
2661
|
+
})) {
|
|
2662
|
+
writeStatus(NO_READER_MESSAGE);
|
|
2663
|
+
process.exit(1);
|
|
2664
|
+
}
|
|
2665
|
+
spawnDaemon({
|
|
2666
|
+
dir,
|
|
2667
|
+
intervalMs,
|
|
2668
|
+
textOnly,
|
|
2669
|
+
replayLast
|
|
2670
|
+
});
|
|
2671
|
+
writeStatus(`started; capturing grabs → ${path.join(dir, HISTORY_FILE_NAME)} (run \`grab read\` to consume, \`grab watch --stop\` to stop)`);
|
|
2672
|
+
process.exit(0);
|
|
2447
2673
|
});
|
|
2448
2674
|
//#endregion
|
|
2449
2675
|
//#region src/cli.ts
|
|
2450
|
-
const VERSION = "0.1.
|
|
2676
|
+
const VERSION = "0.1.40";
|
|
2451
2677
|
const VERSION_API_URL = "https://www.react-grab.com/api/version";
|
|
2452
2678
|
process.on("SIGINT", () => process.exit(0));
|
|
2453
2679
|
process.on("SIGTERM", () => process.exit(0));
|
|
@@ -2461,6 +2687,7 @@ program.addCommand(remove);
|
|
|
2461
2687
|
program.addCommand(configure);
|
|
2462
2688
|
program.addCommand(upgrade);
|
|
2463
2689
|
program.addCommand(watch);
|
|
2690
|
+
program.addCommand(read);
|
|
2464
2691
|
const main = async () => {
|
|
2465
2692
|
await program.parseAsync();
|
|
2466
2693
|
};
|