@react-grab/cli 0.1.39 → 0.1.41
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 +369 -138
- package/dist/cli.js +369 -138
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
- package/skills/react-grab/SKILL.md +60 -16
package/dist/cli.cjs
CHANGED
|
@@ -22,12 +22,12 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
22
22
|
}) : target, mod));
|
|
23
23
|
//#endregion
|
|
24
24
|
let commander = require("commander");
|
|
25
|
+
let node_path = require("node:path");
|
|
26
|
+
node_path = __toESM(node_path, 1);
|
|
25
27
|
let picocolors = require("picocolors");
|
|
26
28
|
picocolors = __toESM(picocolors, 1);
|
|
27
29
|
let node_fs = require("node:fs");
|
|
28
30
|
node_fs = __toESM(node_fs, 1);
|
|
29
|
-
let node_path = require("node:path");
|
|
30
|
-
node_path = __toESM(node_path, 1);
|
|
31
31
|
let package_manager_detector_detect = require("package-manager-detector/detect");
|
|
32
32
|
let ignore = require("ignore");
|
|
33
33
|
ignore = __toESM(ignore, 1);
|
|
@@ -450,8 +450,11 @@ const spinner = (text) => (0, ora.default)({ text });
|
|
|
450
450
|
const SKILL_NAME = "react-grab";
|
|
451
451
|
const SKILL_SOURCE = (0, node_url.fileURLToPath)(new URL("../skills/react-grab", require("url").pathToFileURL(__filename).href));
|
|
452
452
|
const agentLabel = (agent) => (0, agent_install_skill.getSkillAgentConfig)(agent).displayName;
|
|
453
|
-
const installedSkillDir = (agent) => (0, node_path.join)((0, agent_install_skill.isUniversalSkillAgent)(agent) ? (0, agent_install_skill.getCanonicalSkillsDir)(
|
|
454
|
-
|
|
453
|
+
const installedSkillDir = (agent, global, cwd) => (0, node_path.join)((0, agent_install_skill.isUniversalSkillAgent)(agent) ? (0, agent_install_skill.getCanonicalSkillsDir)(global, cwd) : (0, agent_install_skill.getSkillAgentDir)(agent, {
|
|
454
|
+
global,
|
|
455
|
+
cwd
|
|
456
|
+
}), SKILL_NAME);
|
|
457
|
+
const promptSkillInstall = async ({ yes = false, global = false, cwd = process.cwd() } = {}) => {
|
|
455
458
|
const detectedAgents = await detectAvailableAgents();
|
|
456
459
|
if (detectedAgents.length === 0) {
|
|
457
460
|
logger.warn("No supported agents detected.");
|
|
@@ -462,7 +465,7 @@ const promptSkillInstall = async ({ yes = false } = {}) => {
|
|
|
462
465
|
const { agents } = await prompts$1({
|
|
463
466
|
type: "multiselect",
|
|
464
467
|
name: "agents",
|
|
465
|
-
message:
|
|
468
|
+
message: `Install the React Grab skill (${global ? "global" : "this project"}) for:`,
|
|
466
469
|
choices: detectedAgents.map((agent) => ({
|
|
467
470
|
title: agentLabel(agent),
|
|
468
471
|
value: agent,
|
|
@@ -478,7 +481,8 @@ const promptSkillInstall = async ({ yes = false } = {}) => {
|
|
|
478
481
|
const { installed, failed } = await (0, agent_install_skill.add)({
|
|
479
482
|
source: SKILL_SOURCE,
|
|
480
483
|
agents: selectedAgents,
|
|
481
|
-
global
|
|
484
|
+
global,
|
|
485
|
+
cwd,
|
|
482
486
|
mode: "copy"
|
|
483
487
|
});
|
|
484
488
|
if (installed.length === 0) {
|
|
@@ -489,19 +493,27 @@ const promptSkillInstall = async ({ yes = false } = {}) => {
|
|
|
489
493
|
for (const record of failed) logger.log(` ${highlighter.error("✗")} ${agentLabel(record.agent)} ${record.error}`);
|
|
490
494
|
return true;
|
|
491
495
|
};
|
|
492
|
-
const removeSkill = async () => {
|
|
493
|
-
const
|
|
494
|
-
|
|
496
|
+
const removeSkill = async (cwd = process.cwd()) => {
|
|
497
|
+
const agents = await detectAvailableAgents();
|
|
498
|
+
const removedAgents = [];
|
|
499
|
+
const dirsToRemove = /* @__PURE__ */ new Set();
|
|
500
|
+
for (const agent of agents) {
|
|
501
|
+
const present = [installedSkillDir(agent, false, cwd), installedSkillDir(agent, true, cwd)].filter((dir) => (0, node_fs.existsSync)(dir));
|
|
502
|
+
if (present.length === 0) continue;
|
|
503
|
+
removedAgents.push(agent);
|
|
504
|
+
for (const dir of present) dirsToRemove.add(dir);
|
|
505
|
+
}
|
|
506
|
+
for (const skillDir of dirsToRemove) (0, node_fs.rmSync)(skillDir, {
|
|
495
507
|
recursive: true,
|
|
496
508
|
force: true
|
|
497
509
|
});
|
|
498
|
-
for (const agent of
|
|
499
|
-
return
|
|
510
|
+
for (const agent of removedAgents) logger.log(` ${highlighter.success("✓")} ${agentLabel(agent)}`);
|
|
511
|
+
return removedAgents.length;
|
|
500
512
|
};
|
|
501
513
|
//#endregion
|
|
502
514
|
//#region src/commands/add.ts
|
|
503
|
-
const VERSION$5 = "0.1.
|
|
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) => {
|
|
515
|
+
const VERSION$5 = "0.1.41";
|
|
516
|
+
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()).option("-g, --global", "install the skill globally instead of in the project", false).action(async (opts) => {
|
|
505
517
|
console.log(`${picocolors.default.magenta("✿")} ${picocolors.default.bold("React Grab")} ${picocolors.default.gray(VERSION$5)}`);
|
|
506
518
|
console.log();
|
|
507
519
|
try {
|
|
@@ -516,7 +528,11 @@ const add = new commander.Command().name("add").alias("install").description("in
|
|
|
516
528
|
}
|
|
517
529
|
preflightSpinner.succeed();
|
|
518
530
|
logger.break();
|
|
519
|
-
if (!await promptSkillInstall({
|
|
531
|
+
if (!await promptSkillInstall({
|
|
532
|
+
yes: isNonInteractive,
|
|
533
|
+
global: opts.global,
|
|
534
|
+
cwd: (0, node_path.resolve)(opts.cwd)
|
|
535
|
+
}) && isNonInteractive) {
|
|
520
536
|
logger.break();
|
|
521
537
|
process.exit(1);
|
|
522
538
|
}
|
|
@@ -1094,6 +1110,8 @@ const previewCdnTransform = (projectRoot, framework, nextRouterType, targetCdnDo
|
|
|
1094
1110
|
//#endregion
|
|
1095
1111
|
//#region src/utils/constants.ts
|
|
1096
1112
|
const MAX_KEY_HOLD_DURATION_MS = 2e3;
|
|
1113
|
+
const DEFAULT_WATCH_DIR = ".react-grab";
|
|
1114
|
+
const MAX_GRAB_AGE_MS = 300 * 1e3;
|
|
1097
1115
|
//#endregion
|
|
1098
1116
|
//#region src/utils/format-activation-key.ts
|
|
1099
1117
|
const formatActivationKeyDisplay = (activationKey) => {
|
|
@@ -1111,7 +1129,7 @@ const formatActivationKeyDisplay = (activationKey) => {
|
|
|
1111
1129
|
};
|
|
1112
1130
|
//#endregion
|
|
1113
1131
|
//#region src/commands/configure.ts
|
|
1114
|
-
const VERSION$4 = "0.1.
|
|
1132
|
+
const VERSION$4 = "0.1.41";
|
|
1115
1133
|
const isMac = process.platform === "darwin";
|
|
1116
1134
|
const META_LABEL = isMac ? "Cmd" : "Win";
|
|
1117
1135
|
const ALT_LABEL = isMac ? "Option" : "Alt";
|
|
@@ -1730,7 +1748,7 @@ const installPackagesWithFeedback = async (packages, packageManager, projectRoot
|
|
|
1730
1748
|
};
|
|
1731
1749
|
//#endregion
|
|
1732
1750
|
//#region src/commands/init.ts
|
|
1733
|
-
const VERSION$3 = "0.1.
|
|
1751
|
+
const VERSION$3 = "0.1.41";
|
|
1734
1752
|
const REPORT_URL = "https://react-grab.com/api/report-cli";
|
|
1735
1753
|
const DOCS_URL = "https://github.com/aidenybai/react-grab";
|
|
1736
1754
|
const reportToCli = (type, config, error) => {
|
|
@@ -1797,7 +1815,7 @@ const failWithManualSetup = (failingSpinner, message, { listSupportedFrameworks
|
|
|
1797
1815
|
logger.break();
|
|
1798
1816
|
process.exit(1);
|
|
1799
1817
|
};
|
|
1800
|
-
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) => {
|
|
1818
|
+
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()).option("-g, --global", "install the skill globally instead of in the project", false).action(async (opts) => {
|
|
1801
1819
|
console.log(`${picocolors.default.magenta("✿")} ${picocolors.default.bold("React Grab")} ${picocolors.default.gray(VERSION$3)}`);
|
|
1802
1820
|
console.log();
|
|
1803
1821
|
try {
|
|
@@ -1955,7 +1973,11 @@ const init = new commander.Command().name("init").alias("setup").description("in
|
|
|
1955
1973
|
}
|
|
1956
1974
|
}
|
|
1957
1975
|
logger.break();
|
|
1958
|
-
await promptSkillInstall({
|
|
1976
|
+
await promptSkillInstall({
|
|
1977
|
+
yes: isNonInteractive,
|
|
1978
|
+
global: opts.global,
|
|
1979
|
+
cwd
|
|
1980
|
+
});
|
|
1959
1981
|
logger.break();
|
|
1960
1982
|
process.exit(0);
|
|
1961
1983
|
}
|
|
@@ -2021,7 +2043,11 @@ const init = new commander.Command().name("init").alias("setup").description("in
|
|
|
2021
2043
|
let didInstallSkill = false;
|
|
2022
2044
|
if (!isNonInteractive) {
|
|
2023
2045
|
logger.break();
|
|
2024
|
-
didInstallSkill = await promptSkillInstall({
|
|
2046
|
+
didInstallSkill = await promptSkillInstall({
|
|
2047
|
+
yes: isNonInteractive,
|
|
2048
|
+
global: opts.global,
|
|
2049
|
+
cwd
|
|
2050
|
+
});
|
|
2025
2051
|
}
|
|
2026
2052
|
const result = previewTransform(projectInfo.projectRoot, finalFramework, finalNextRouterType, false, opts.force);
|
|
2027
2053
|
if (!result.success) {
|
|
@@ -2074,99 +2100,11 @@ const init = new commander.Command().name("init").alias("setup").description("in
|
|
|
2074
2100
|
}
|
|
2075
2101
|
});
|
|
2076
2102
|
//#endregion
|
|
2077
|
-
//#region src/
|
|
2078
|
-
const
|
|
2079
|
-
const remove = new commander.Command().name("remove").description("uninstall the React Grab skill from your agent").action(async () => {
|
|
2080
|
-
console.log(`${picocolors.default.magenta("✿")} ${picocolors.default.bold("React Grab")} ${picocolors.default.gray(VERSION$2)}`);
|
|
2081
|
-
console.log();
|
|
2082
|
-
try {
|
|
2083
|
-
logger.break();
|
|
2084
|
-
const removedCount = await removeSkill();
|
|
2085
|
-
logger.break();
|
|
2086
|
-
if (removedCount === 0) logger.log("React Grab skill is not installed in any detected agent.");
|
|
2087
|
-
else logger.log(`${highlighter.success("Removed")} the React Grab skill from ${removedCount} agent${removedCount === 1 ? "" : "s"}.`);
|
|
2088
|
-
logger.break();
|
|
2089
|
-
} catch (error) {
|
|
2090
|
-
handleError(error);
|
|
2091
|
-
}
|
|
2092
|
-
});
|
|
2093
|
-
//#endregion
|
|
2094
|
-
//#region src/commands/upgrade.ts
|
|
2095
|
-
const VERSION$1 = "0.1.39";
|
|
2096
|
-
const NPM_REGISTRY_URL = "https://registry.npmjs.org/react-grab/latest";
|
|
2097
|
-
const fetchLatestVersion = async () => {
|
|
2098
|
-
try {
|
|
2099
|
-
return (await (await fetch(NPM_REGISTRY_URL)).json()).version ?? null;
|
|
2100
|
-
} catch {
|
|
2101
|
-
return null;
|
|
2102
|
-
}
|
|
2103
|
-
};
|
|
2104
|
-
const isDevDependency = (projectRoot) => {
|
|
2105
|
-
const packageJsonPath = (0, node_path.join)(projectRoot, "package.json");
|
|
2106
|
-
if (!(0, node_fs.existsSync)(packageJsonPath)) return true;
|
|
2107
|
-
try {
|
|
2108
|
-
const packageJson = JSON.parse((0, node_fs.readFileSync)(packageJsonPath, "utf-8"));
|
|
2109
|
-
if (packageJson.devDependencies?.["react-grab"]) return true;
|
|
2110
|
-
if (packageJson.dependencies?.["react-grab"]) return false;
|
|
2111
|
-
} catch {}
|
|
2112
|
-
return true;
|
|
2113
|
-
};
|
|
2114
|
-
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) => {
|
|
2115
|
-
console.log(`${picocolors.default.magenta("✿")} ${picocolors.default.bold("React Grab")} ${picocolors.default.gray(VERSION$1)}`);
|
|
2116
|
-
console.log();
|
|
2117
|
-
try {
|
|
2118
|
-
const cwd = (0, node_path.resolve)(opts.cwd);
|
|
2119
|
-
const detectSpinner = spinner("Detecting project.").start();
|
|
2120
|
-
const projectInfo = await detectProject(cwd);
|
|
2121
|
-
if (!projectInfo.hasReactGrab) {
|
|
2122
|
-
detectSpinner.fail("React Grab is not installed.");
|
|
2123
|
-
logger.break();
|
|
2124
|
-
logger.error(`Run ${highlighter.info("npx grab@latest init")} first to install React Grab.`);
|
|
2125
|
-
logger.break();
|
|
2126
|
-
process.exit(1);
|
|
2127
|
-
}
|
|
2128
|
-
detectSpinner.succeed();
|
|
2129
|
-
const versionSpinner = spinner("Checking for updates.").start();
|
|
2130
|
-
const latestVersion = await fetchLatestVersion();
|
|
2131
|
-
if (!latestVersion) {
|
|
2132
|
-
versionSpinner.fail("Could not check for updates.");
|
|
2133
|
-
logger.break();
|
|
2134
|
-
logger.error("Failed to reach the npm registry. Check your network connection.");
|
|
2135
|
-
logger.break();
|
|
2136
|
-
process.exit(1);
|
|
2137
|
-
}
|
|
2138
|
-
const installedVersion = projectInfo.reactGrabVersion;
|
|
2139
|
-
if (installedVersion && installedVersion === latestVersion) {
|
|
2140
|
-
versionSpinner.succeed(`Already on the latest version ${highlighter.info(`v${latestVersion}`)}.`);
|
|
2141
|
-
logger.break();
|
|
2142
|
-
process.exit(0);
|
|
2143
|
-
}
|
|
2144
|
-
const fromLabel = installedVersion ? `v${installedVersion}` : "unknown";
|
|
2145
|
-
versionSpinner.succeed(`Update available: ${highlighter.dim(fromLabel)} → ${highlighter.info(`v${latestVersion}`)}.`);
|
|
2146
|
-
const upgradeSpinner = spinner("Upgrading react-grab.").start();
|
|
2147
|
-
try {
|
|
2148
|
-
await installPackages(["react-grab@latest"], {
|
|
2149
|
-
packageManager: projectInfo.packageManager,
|
|
2150
|
-
cwd: projectInfo.projectRoot,
|
|
2151
|
-
isDev: isDevDependency(projectInfo.projectRoot)
|
|
2152
|
-
});
|
|
2153
|
-
upgradeSpinner.succeed();
|
|
2154
|
-
} catch {
|
|
2155
|
-
upgradeSpinner.fail();
|
|
2156
|
-
logger.break();
|
|
2157
|
-
logger.error("Failed to upgrade. Check your network connection and try again.");
|
|
2158
|
-
logger.break();
|
|
2159
|
-
process.exit(1);
|
|
2160
|
-
}
|
|
2161
|
-
logger.break();
|
|
2162
|
-
logger.log(`${highlighter.success("Success!")} React Grab has been upgraded to ${highlighter.info(`v${latestVersion}`)}.`);
|
|
2163
|
-
logger.break();
|
|
2164
|
-
} catch (error) {
|
|
2165
|
-
handleError(error);
|
|
2166
|
-
}
|
|
2167
|
-
});
|
|
2103
|
+
//#region src/utils/sleep.ts
|
|
2104
|
+
const sleep = (durationMs) => new Promise((resolve) => setTimeout(resolve, durationMs));
|
|
2168
2105
|
//#endregion
|
|
2169
2106
|
//#region src/utils/clipboard.ts
|
|
2107
|
+
const HISTORY_FILE_NAME = "history.jsonl";
|
|
2170
2108
|
const READ_TIMEOUT_MS = 2500;
|
|
2171
2109
|
const MAX_CLIPBOARD_BYTES = 64 * 1024 * 1024;
|
|
2172
2110
|
const ID_RADIX = 36;
|
|
@@ -2177,7 +2115,6 @@ const SIGNATURE_SCAN_CHARS = 32 * 1024;
|
|
|
2177
2115
|
const GRAB_MIME = "application/x-react-grab";
|
|
2178
2116
|
const CHROMIUM_CUSTOM_FORMAT = "chromium/x-web-custom-data";
|
|
2179
2117
|
const GRAB_TEXT_SIGNATURE = /\bin\s+\S+\s+\(at\s+[^\n]{1,400}?:\d+:\d+\)/;
|
|
2180
|
-
const sleep = (durationMs) => new Promise((resolve) => setTimeout(resolve, durationMs));
|
|
2181
2118
|
const shortHash = (text) => (0, node_crypto.createHash)("sha1").update(text).digest("hex").slice(0, HASH_LENGTH);
|
|
2182
2119
|
const alignUp = (value) => value + PICKLE_ALIGN_BYTES - 1 & ~(PICKLE_ALIGN_BYTES - 1);
|
|
2183
2120
|
const parseChromiumPickle = (buffer) => {
|
|
@@ -2409,9 +2346,9 @@ const prepareWorkDir = (dir) => {
|
|
|
2409
2346
|
const gitignore = node_path.default.join(dir, ".gitignore");
|
|
2410
2347
|
if (!node_fs.default.existsSync(gitignore)) node_fs.default.writeFileSync(gitignore, "*\n");
|
|
2411
2348
|
};
|
|
2412
|
-
const
|
|
2413
|
-
const { reader, dir, intervalMs, replayLast, onWarn
|
|
2414
|
-
const logPath = node_path.default.join(dir,
|
|
2349
|
+
const runWatchLoop = async (options) => {
|
|
2350
|
+
const { reader, dir, intervalMs, replayLast, onWarn } = options;
|
|
2351
|
+
const logPath = node_path.default.join(dir, HISTORY_FILE_NAME);
|
|
2415
2352
|
const { read } = reader;
|
|
2416
2353
|
let lastChangeCount = null;
|
|
2417
2354
|
let lastTimestamp = 0;
|
|
@@ -2426,9 +2363,8 @@ const watchForNextGrab = async (options) => {
|
|
|
2426
2363
|
lastTimestamp = JSON.parse(initial.grab).timestamp ?? 0;
|
|
2427
2364
|
} catch {}
|
|
2428
2365
|
}
|
|
2429
|
-
while (
|
|
2366
|
+
while (true) {
|
|
2430
2367
|
await sleep(intervalMs);
|
|
2431
|
-
if (signal?.aborted) return null;
|
|
2432
2368
|
try {
|
|
2433
2369
|
const snapshot = read();
|
|
2434
2370
|
if (!snapshot) continue;
|
|
@@ -2474,7 +2410,6 @@ const watchForNextGrab = async (options) => {
|
|
|
2474
2410
|
lastChangeCount = snapshot.changeCount;
|
|
2475
2411
|
lastTextHash = textHash;
|
|
2476
2412
|
lastTimestamp = nextTimestamp;
|
|
2477
|
-
return captured;
|
|
2478
2413
|
} catch (error) {
|
|
2479
2414
|
const message = String(error?.message ?? error);
|
|
2480
2415
|
if (message !== lastErrorMessage) {
|
|
@@ -2483,51 +2418,346 @@ const watchForNextGrab = async (options) => {
|
|
|
2483
2418
|
}
|
|
2484
2419
|
}
|
|
2485
2420
|
}
|
|
2486
|
-
return null;
|
|
2487
2421
|
};
|
|
2488
2422
|
//#endregion
|
|
2489
|
-
//#region src/
|
|
2490
|
-
const
|
|
2491
|
-
const
|
|
2492
|
-
const
|
|
2493
|
-
|
|
2423
|
+
//#region src/utils/grab-log.ts
|
|
2424
|
+
const CURSOR_FILE_NAME = "cursor.txt";
|
|
2425
|
+
const cursorFilePath = (dir) => node_path.default.join(dir, CURSOR_FILE_NAME);
|
|
2426
|
+
const readCompleteGrabLines = (dir) => {
|
|
2427
|
+
let raw;
|
|
2428
|
+
try {
|
|
2429
|
+
raw = node_fs.default.readFileSync(node_path.default.join(dir, HISTORY_FILE_NAME), "utf8");
|
|
2430
|
+
} catch {
|
|
2431
|
+
return [];
|
|
2432
|
+
}
|
|
2433
|
+
const lastNewline = raw.lastIndexOf("\n");
|
|
2434
|
+
if (lastNewline < 0) return [];
|
|
2435
|
+
return raw.slice(0, lastNewline).split("\n").filter(Boolean);
|
|
2436
|
+
};
|
|
2437
|
+
const readGrabCursor = (dir) => {
|
|
2438
|
+
try {
|
|
2439
|
+
const value = Number.parseInt(node_fs.default.readFileSync(cursorFilePath(dir), "utf8").trim(), 10);
|
|
2440
|
+
return Number.isInteger(value) && value >= 0 ? value : 0;
|
|
2441
|
+
} catch {
|
|
2442
|
+
return 0;
|
|
2443
|
+
}
|
|
2444
|
+
};
|
|
2445
|
+
const grabReceivedAt = (line) => {
|
|
2446
|
+
try {
|
|
2447
|
+
const value = JSON.parse(line).receivedAt;
|
|
2448
|
+
return typeof value === "number" ? value : null;
|
|
2449
|
+
} catch {
|
|
2450
|
+
return null;
|
|
2451
|
+
}
|
|
2452
|
+
};
|
|
2453
|
+
const consumeGrabs = (dir, options) => {
|
|
2454
|
+
const lines = readCompleteGrabLines(dir);
|
|
2455
|
+
if (options.all) return lines;
|
|
2456
|
+
const maxAgeMs = options.maxAgeMs ?? 0;
|
|
2457
|
+
const cursor = readGrabCursor(dir);
|
|
2458
|
+
const total = lines.length;
|
|
2459
|
+
const now = Date.now();
|
|
2460
|
+
const fresh = [];
|
|
2461
|
+
let index = Math.min(cursor, total);
|
|
2462
|
+
while (index < total && (options.limit <= 0 || fresh.length < options.limit)) {
|
|
2463
|
+
const line = lines[index];
|
|
2464
|
+
index += 1;
|
|
2465
|
+
if (maxAgeMs > 0) {
|
|
2466
|
+
const receivedAt = grabReceivedAt(line);
|
|
2467
|
+
if (receivedAt !== null && now - receivedAt > maxAgeMs) continue;
|
|
2468
|
+
}
|
|
2469
|
+
fresh.push(line);
|
|
2470
|
+
}
|
|
2471
|
+
if (index !== cursor) node_fs.default.writeFileSync(cursorFilePath(dir), String(index));
|
|
2472
|
+
return fresh;
|
|
2473
|
+
};
|
|
2474
|
+
//#endregion
|
|
2475
|
+
//#region src/commands/read.ts
|
|
2476
|
+
const parseWaitMs = (raw) => {
|
|
2477
|
+
if (!raw) return 0;
|
|
2478
|
+
if (/^(inf|infinite|forever)$/i.test(raw.trim())) return Number.POSITIVE_INFINITY;
|
|
2479
|
+
const ms = Number(raw);
|
|
2480
|
+
return Number.isFinite(ms) && ms > 0 ? ms : 0;
|
|
2481
|
+
};
|
|
2482
|
+
const parseNonNegativeInt = (raw, fallback) => {
|
|
2483
|
+
const value = Number(raw);
|
|
2484
|
+
return Number.isInteger(value) && value >= 0 ? value : fallback;
|
|
2485
|
+
};
|
|
2486
|
+
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> (or 'infinite') for a 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("--max-age <ms>", "skip grabs captured longer ago than <ms> (0 = never evict)", String(MAX_GRAB_AGE_MS)).option("--all", "print the entire history without advancing the cursor").action(async (options) => {
|
|
2494
2487
|
const dir = node_path.default.resolve(options.dir);
|
|
2495
|
-
const intervalMs = Number(options.interval);
|
|
2496
|
-
const textOnly = Boolean(options.textOnly);
|
|
2497
2488
|
try {
|
|
2498
2489
|
prepareWorkDir(dir);
|
|
2499
2490
|
} catch (error) {
|
|
2500
|
-
process.stderr.write(`react-grab
|
|
2491
|
+
process.stderr.write(`react-grab read: ${String(error?.message ?? error)}\n`);
|
|
2501
2492
|
process.exit(1);
|
|
2502
2493
|
}
|
|
2494
|
+
unrefStdin();
|
|
2495
|
+
const limit = parseNonNegativeInt(options.limit, 50);
|
|
2496
|
+
const maxAgeMs = parseNonNegativeInt(options.maxAge, MAX_GRAB_AGE_MS);
|
|
2497
|
+
const all = Boolean(options.all);
|
|
2498
|
+
const drain = () => {
|
|
2499
|
+
const fresh = consumeGrabs(dir, {
|
|
2500
|
+
limit,
|
|
2501
|
+
all,
|
|
2502
|
+
maxAgeMs
|
|
2503
|
+
});
|
|
2504
|
+
if (fresh.length > 0) process.stdout.write(`${fresh.join("\n")}\n`);
|
|
2505
|
+
return fresh.length;
|
|
2506
|
+
};
|
|
2507
|
+
const waitMs = parseWaitMs(options.wait);
|
|
2508
|
+
const deadline = waitMs === Number.POSITIVE_INFINITY ? Number.POSITIVE_INFINITY : Date.now() + waitMs;
|
|
2509
|
+
if (drain() > 0) process.exit(0);
|
|
2510
|
+
while (Date.now() < deadline) {
|
|
2511
|
+
await sleep(200);
|
|
2512
|
+
if (drain() > 0) break;
|
|
2513
|
+
}
|
|
2514
|
+
process.exit(0);
|
|
2515
|
+
});
|
|
2516
|
+
//#endregion
|
|
2517
|
+
//#region src/commands/remove.ts
|
|
2518
|
+
const VERSION$2 = "0.1.41";
|
|
2519
|
+
const remove = new commander.Command().name("remove").description("uninstall the React Grab skill from your agent").action(async () => {
|
|
2520
|
+
console.log(`${picocolors.default.magenta("✿")} ${picocolors.default.bold("React Grab")} ${picocolors.default.gray(VERSION$2)}`);
|
|
2521
|
+
console.log();
|
|
2522
|
+
try {
|
|
2523
|
+
logger.break();
|
|
2524
|
+
const removedCount = await removeSkill();
|
|
2525
|
+
logger.break();
|
|
2526
|
+
if (removedCount === 0) logger.log("React Grab skill is not installed in any detected agent.");
|
|
2527
|
+
else logger.log(`${highlighter.success("Removed")} the React Grab skill from ${removedCount} agent${removedCount === 1 ? "" : "s"}.`);
|
|
2528
|
+
logger.break();
|
|
2529
|
+
} catch (error) {
|
|
2530
|
+
handleError(error);
|
|
2531
|
+
}
|
|
2532
|
+
});
|
|
2533
|
+
//#endregion
|
|
2534
|
+
//#region src/commands/upgrade.ts
|
|
2535
|
+
const VERSION$1 = "0.1.41";
|
|
2536
|
+
const NPM_REGISTRY_URL = "https://registry.npmjs.org/react-grab/latest";
|
|
2537
|
+
const fetchLatestVersion = async () => {
|
|
2538
|
+
try {
|
|
2539
|
+
return (await (await fetch(NPM_REGISTRY_URL)).json()).version ?? null;
|
|
2540
|
+
} catch {
|
|
2541
|
+
return null;
|
|
2542
|
+
}
|
|
2543
|
+
};
|
|
2544
|
+
const isDevDependency = (projectRoot) => {
|
|
2545
|
+
const packageJsonPath = (0, node_path.join)(projectRoot, "package.json");
|
|
2546
|
+
if (!(0, node_fs.existsSync)(packageJsonPath)) return true;
|
|
2547
|
+
try {
|
|
2548
|
+
const packageJson = JSON.parse((0, node_fs.readFileSync)(packageJsonPath, "utf-8"));
|
|
2549
|
+
if (packageJson.devDependencies?.["react-grab"]) return true;
|
|
2550
|
+
if (packageJson.dependencies?.["react-grab"]) return false;
|
|
2551
|
+
} catch {}
|
|
2552
|
+
return true;
|
|
2553
|
+
};
|
|
2554
|
+
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) => {
|
|
2555
|
+
console.log(`${picocolors.default.magenta("✿")} ${picocolors.default.bold("React Grab")} ${picocolors.default.gray(VERSION$1)}`);
|
|
2556
|
+
console.log();
|
|
2557
|
+
try {
|
|
2558
|
+
const cwd = (0, node_path.resolve)(opts.cwd);
|
|
2559
|
+
const detectSpinner = spinner("Detecting project.").start();
|
|
2560
|
+
const projectInfo = await detectProject(cwd);
|
|
2561
|
+
if (!projectInfo.hasReactGrab) {
|
|
2562
|
+
detectSpinner.fail("React Grab is not installed.");
|
|
2563
|
+
logger.break();
|
|
2564
|
+
logger.error(`Run ${highlighter.info("npx grab@latest init")} first to install React Grab.`);
|
|
2565
|
+
logger.break();
|
|
2566
|
+
process.exit(1);
|
|
2567
|
+
}
|
|
2568
|
+
detectSpinner.succeed();
|
|
2569
|
+
const versionSpinner = spinner("Checking for updates.").start();
|
|
2570
|
+
const latestVersion = await fetchLatestVersion();
|
|
2571
|
+
if (!latestVersion) {
|
|
2572
|
+
versionSpinner.fail("Could not check for updates.");
|
|
2573
|
+
logger.break();
|
|
2574
|
+
logger.error("Failed to reach the npm registry. Check your network connection.");
|
|
2575
|
+
logger.break();
|
|
2576
|
+
process.exit(1);
|
|
2577
|
+
}
|
|
2578
|
+
const installedVersion = projectInfo.reactGrabVersion;
|
|
2579
|
+
if (installedVersion && installedVersion === latestVersion) {
|
|
2580
|
+
versionSpinner.succeed(`Already on the latest version ${highlighter.info(`v${latestVersion}`)}.`);
|
|
2581
|
+
logger.break();
|
|
2582
|
+
process.exit(0);
|
|
2583
|
+
}
|
|
2584
|
+
const fromLabel = installedVersion ? `v${installedVersion}` : "unknown";
|
|
2585
|
+
versionSpinner.succeed(`Update available: ${highlighter.dim(fromLabel)} → ${highlighter.info(`v${latestVersion}`)}.`);
|
|
2586
|
+
const upgradeSpinner = spinner("Upgrading react-grab.").start();
|
|
2587
|
+
try {
|
|
2588
|
+
await installPackages(["react-grab@latest"], {
|
|
2589
|
+
packageManager: projectInfo.packageManager,
|
|
2590
|
+
cwd: projectInfo.projectRoot,
|
|
2591
|
+
isDev: isDevDependency(projectInfo.projectRoot)
|
|
2592
|
+
});
|
|
2593
|
+
upgradeSpinner.succeed();
|
|
2594
|
+
} catch {
|
|
2595
|
+
upgradeSpinner.fail();
|
|
2596
|
+
logger.break();
|
|
2597
|
+
logger.error("Failed to upgrade. Check your network connection and try again.");
|
|
2598
|
+
logger.break();
|
|
2599
|
+
process.exit(1);
|
|
2600
|
+
}
|
|
2601
|
+
logger.break();
|
|
2602
|
+
logger.log(`${highlighter.success("Success!")} React Grab has been upgraded to ${highlighter.info(`v${latestVersion}`)}.`);
|
|
2603
|
+
logger.break();
|
|
2604
|
+
} catch (error) {
|
|
2605
|
+
handleError(error);
|
|
2606
|
+
}
|
|
2607
|
+
});
|
|
2608
|
+
//#endregion
|
|
2609
|
+
//#region src/utils/daemon.ts
|
|
2610
|
+
const PID_FILE_NAME = "watch.pid";
|
|
2611
|
+
const pidFilePath = (dir) => node_path.default.join(dir, PID_FILE_NAME);
|
|
2612
|
+
const cliEntryPath = () => process.argv[1] ?? (0, node_url.fileURLToPath)(require("url").pathToFileURL(__filename).href);
|
|
2613
|
+
const isProcessAlive = (pid) => {
|
|
2614
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
2615
|
+
try {
|
|
2616
|
+
process.kill(pid, 0);
|
|
2617
|
+
return true;
|
|
2618
|
+
} catch (error) {
|
|
2619
|
+
return error.code === "EPERM";
|
|
2620
|
+
}
|
|
2621
|
+
};
|
|
2622
|
+
const readDaemonPid = (dir) => {
|
|
2623
|
+
try {
|
|
2624
|
+
const pid = Number.parseInt(node_fs.default.readFileSync(pidFilePath(dir), "utf8").trim(), 10);
|
|
2625
|
+
return Number.isInteger(pid) && pid > 0 ? pid : null;
|
|
2626
|
+
} catch {
|
|
2627
|
+
return null;
|
|
2628
|
+
}
|
|
2629
|
+
};
|
|
2630
|
+
const isDaemonRunning = (dir) => {
|
|
2631
|
+
const pid = readDaemonPid(dir);
|
|
2632
|
+
return pid !== null && isProcessAlive(pid);
|
|
2633
|
+
};
|
|
2634
|
+
const claimDaemon = (dir) => {
|
|
2635
|
+
const file = pidFilePath(dir);
|
|
2636
|
+
for (let attempt = 0; attempt < 50; attempt += 1) try {
|
|
2637
|
+
const handle = node_fs.default.openSync(file, "wx");
|
|
2638
|
+
node_fs.default.writeFileSync(handle, String(process.pid));
|
|
2639
|
+
node_fs.default.closeSync(handle);
|
|
2640
|
+
return readDaemonPid(dir) === process.pid;
|
|
2641
|
+
} catch (error) {
|
|
2642
|
+
if (error.code !== "EEXIST") throw error;
|
|
2643
|
+
if (isDaemonRunning(dir)) return false;
|
|
2644
|
+
try {
|
|
2645
|
+
node_fs.default.rmSync(file, { force: true });
|
|
2646
|
+
} catch {}
|
|
2647
|
+
}
|
|
2648
|
+
return false;
|
|
2649
|
+
};
|
|
2650
|
+
const releaseDaemon = (dir) => {
|
|
2651
|
+
if (readDaemonPid(dir) === process.pid) try {
|
|
2652
|
+
node_fs.default.rmSync(pidFilePath(dir), { force: true });
|
|
2653
|
+
} catch {}
|
|
2654
|
+
};
|
|
2655
|
+
const stopDaemon = (dir) => {
|
|
2656
|
+
const pid = readDaemonPid(dir);
|
|
2657
|
+
if (pid === null) return null;
|
|
2658
|
+
const wasAlive = isProcessAlive(pid);
|
|
2659
|
+
if (wasAlive) try {
|
|
2660
|
+
process.kill(pid, "SIGTERM");
|
|
2661
|
+
} catch {}
|
|
2662
|
+
if (readDaemonPid(dir) === pid) try {
|
|
2663
|
+
node_fs.default.rmSync(pidFilePath(dir), { force: true });
|
|
2664
|
+
} catch {}
|
|
2665
|
+
return wasAlive ? pid : null;
|
|
2666
|
+
};
|
|
2667
|
+
const spawnDaemon = (options) => {
|
|
2668
|
+
const args = [
|
|
2669
|
+
cliEntryPath(),
|
|
2670
|
+
"watch",
|
|
2671
|
+
"--foreground",
|
|
2672
|
+
"--dir",
|
|
2673
|
+
options.dir,
|
|
2674
|
+
"--interval",
|
|
2675
|
+
String(options.intervalMs)
|
|
2676
|
+
];
|
|
2677
|
+
if (options.textOnly) args.push("--text-only");
|
|
2678
|
+
if (options.replayLast) args.push("--replay-last");
|
|
2679
|
+
(0, node_child_process.spawn)(process.execPath, args, {
|
|
2680
|
+
detached: true,
|
|
2681
|
+
stdio: "ignore",
|
|
2682
|
+
windowsHide: true
|
|
2683
|
+
}).unref();
|
|
2684
|
+
};
|
|
2685
|
+
//#endregion
|
|
2686
|
+
//#region src/commands/watch.ts
|
|
2687
|
+
const readersDir = () => node_path.default.dirname((0, node_url.fileURLToPath)(require("url").pathToFileURL(__filename).href));
|
|
2688
|
+
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.";
|
|
2689
|
+
const writeStatus = (message) => {
|
|
2690
|
+
process.stderr.write(`react-grab watch: ${message}\n`);
|
|
2691
|
+
};
|
|
2692
|
+
const runForeground = (dir, intervalMs, textOnly, replayLast) => {
|
|
2693
|
+
if (!claimDaemon(dir)) process.exit(0);
|
|
2694
|
+
process.on("exit", () => releaseDaemon(dir));
|
|
2503
2695
|
const reader = createReader({
|
|
2504
2696
|
textOnly,
|
|
2505
2697
|
readersDir: readersDir(),
|
|
2506
2698
|
workDir: dir
|
|
2507
2699
|
});
|
|
2508
2700
|
if (!reader) {
|
|
2509
|
-
|
|
2701
|
+
writeStatus(NO_READER_MESSAGE);
|
|
2510
2702
|
process.exit(1);
|
|
2511
2703
|
}
|
|
2512
|
-
|
|
2513
|
-
|
|
2704
|
+
writeStatus(`watching clipboard via ${reader.mode}; history → ${node_path.default.join(dir, HISTORY_FILE_NAME)}`);
|
|
2705
|
+
runWatchLoop({
|
|
2514
2706
|
reader,
|
|
2515
2707
|
dir,
|
|
2516
|
-
intervalMs
|
|
2517
|
-
replayLast
|
|
2518
|
-
onWarn:
|
|
2519
|
-
}).then((grab) => {
|
|
2520
|
-
if (!grab) process.exit(0);
|
|
2521
|
-
process.stdout.write(`${JSON.stringify(grab)}\n`);
|
|
2522
|
-
process.exit(0);
|
|
2708
|
+
intervalMs,
|
|
2709
|
+
replayLast,
|
|
2710
|
+
onWarn: writeStatus
|
|
2523
2711
|
}).catch((error) => {
|
|
2524
|
-
|
|
2712
|
+
writeStatus(String(error?.message ?? error));
|
|
2525
2713
|
process.exit(1);
|
|
2526
2714
|
});
|
|
2715
|
+
};
|
|
2716
|
+
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) => {
|
|
2717
|
+
const dir = node_path.default.resolve(options.dir);
|
|
2718
|
+
const intervalRaw = Number(options.interval);
|
|
2719
|
+
const intervalMs = Number.isFinite(intervalRaw) && intervalRaw > 0 ? intervalRaw : 800;
|
|
2720
|
+
const textOnly = Boolean(options.textOnly);
|
|
2721
|
+
const replayLast = Boolean(options.replayLast);
|
|
2722
|
+
try {
|
|
2723
|
+
prepareWorkDir(dir);
|
|
2724
|
+
} catch (error) {
|
|
2725
|
+
writeStatus(String(error?.message ?? error));
|
|
2726
|
+
process.exit(1);
|
|
2727
|
+
}
|
|
2728
|
+
if (options.stop) {
|
|
2729
|
+
const stoppedPid = stopDaemon(dir);
|
|
2730
|
+
writeStatus(stoppedPid ? `stopped daemon (pid ${stoppedPid})` : `no daemon running for ${dir}`);
|
|
2731
|
+
process.exit(0);
|
|
2732
|
+
}
|
|
2733
|
+
if (options.foreground) {
|
|
2734
|
+
runForeground(dir, intervalMs, textOnly, replayLast);
|
|
2735
|
+
return;
|
|
2736
|
+
}
|
|
2737
|
+
if (isDaemonRunning(dir)) {
|
|
2738
|
+
writeStatus(`already watching ${dir} (pid ${readDaemonPid(dir)})`);
|
|
2739
|
+
process.exit(0);
|
|
2740
|
+
}
|
|
2741
|
+
if (!createReader({
|
|
2742
|
+
textOnly,
|
|
2743
|
+
readersDir: readersDir(),
|
|
2744
|
+
workDir: dir
|
|
2745
|
+
})) {
|
|
2746
|
+
writeStatus(NO_READER_MESSAGE);
|
|
2747
|
+
process.exit(1);
|
|
2748
|
+
}
|
|
2749
|
+
spawnDaemon({
|
|
2750
|
+
dir,
|
|
2751
|
+
intervalMs,
|
|
2752
|
+
textOnly,
|
|
2753
|
+
replayLast
|
|
2754
|
+
});
|
|
2755
|
+
writeStatus(`started; capturing grabs → ${node_path.default.join(dir, HISTORY_FILE_NAME)} (run \`grab read\` to consume, \`grab watch --stop\` to stop)`);
|
|
2756
|
+
process.exit(0);
|
|
2527
2757
|
});
|
|
2528
2758
|
//#endregion
|
|
2529
2759
|
//#region src/cli.ts
|
|
2530
|
-
const VERSION = "0.1.
|
|
2760
|
+
const VERSION = "0.1.41";
|
|
2531
2761
|
const VERSION_API_URL = "https://www.react-grab.com/api/version";
|
|
2532
2762
|
process.on("SIGINT", () => process.exit(0));
|
|
2533
2763
|
process.on("SIGTERM", () => process.exit(0));
|
|
@@ -2541,6 +2771,7 @@ program.addCommand(remove);
|
|
|
2541
2771
|
program.addCommand(configure);
|
|
2542
2772
|
program.addCommand(upgrade);
|
|
2543
2773
|
program.addCommand(watch);
|
|
2774
|
+
program.addCommand(read);
|
|
2544
2775
|
const main = async () => {
|
|
2545
2776
|
await program.parseAsync();
|
|
2546
2777
|
};
|