@react-grab/cli 0.1.40 → 0.1.42
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 +327 -188
- package/dist/cli.js +326 -187
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
- package/skills/react-grab/SKILL.md +19 -48
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(), global = false } = {}) => {
|
|
497
|
+
const agents = await detectAvailableAgents();
|
|
498
|
+
const removedAgents = [];
|
|
499
|
+
const dirsToRemove = /* @__PURE__ */ new Set();
|
|
500
|
+
for (const agent of agents) {
|
|
501
|
+
const skillDir = installedSkillDir(agent, global, cwd);
|
|
502
|
+
if (!(0, node_fs.existsSync)(skillDir)) continue;
|
|
503
|
+
removedAgents.push(agent);
|
|
504
|
+
dirsToRemove.add(skillDir);
|
|
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.42";
|
|
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
|
}
|
|
@@ -1095,6 +1111,9 @@ const previewCdnTransform = (projectRoot, framework, nextRouterType, targetCdnDo
|
|
|
1095
1111
|
//#region src/utils/constants.ts
|
|
1096
1112
|
const MAX_KEY_HOLD_DURATION_MS = 2e3;
|
|
1097
1113
|
const DEFAULT_WATCH_DIR = ".react-grab";
|
|
1114
|
+
const DEFAULT_GRAB_AGE_MS = 300 * 1e3;
|
|
1115
|
+
const MAX_READ_HISTORY_BYTES = 128 * 1024 * 1024;
|
|
1116
|
+
const MIGRATION_SCAN_CHUNK_BYTES = 1024 * 1024;
|
|
1098
1117
|
//#endregion
|
|
1099
1118
|
//#region src/utils/format-activation-key.ts
|
|
1100
1119
|
const formatActivationKeyDisplay = (activationKey) => {
|
|
@@ -1112,7 +1131,7 @@ const formatActivationKeyDisplay = (activationKey) => {
|
|
|
1112
1131
|
};
|
|
1113
1132
|
//#endregion
|
|
1114
1133
|
//#region src/commands/configure.ts
|
|
1115
|
-
const VERSION$4 = "0.1.
|
|
1134
|
+
const VERSION$4 = "0.1.42";
|
|
1116
1135
|
const isMac = process.platform === "darwin";
|
|
1117
1136
|
const META_LABEL = isMac ? "Cmd" : "Win";
|
|
1118
1137
|
const ALT_LABEL = isMac ? "Option" : "Alt";
|
|
@@ -1731,7 +1750,7 @@ const installPackagesWithFeedback = async (packages, packageManager, projectRoot
|
|
|
1731
1750
|
};
|
|
1732
1751
|
//#endregion
|
|
1733
1752
|
//#region src/commands/init.ts
|
|
1734
|
-
const VERSION$3 = "0.1.
|
|
1753
|
+
const VERSION$3 = "0.1.42";
|
|
1735
1754
|
const REPORT_URL = "https://react-grab.com/api/report-cli";
|
|
1736
1755
|
const DOCS_URL = "https://github.com/aidenybai/react-grab";
|
|
1737
1756
|
const reportToCli = (type, config, error) => {
|
|
@@ -1798,7 +1817,7 @@ const failWithManualSetup = (failingSpinner, message, { listSupportedFrameworks
|
|
|
1798
1817
|
logger.break();
|
|
1799
1818
|
process.exit(1);
|
|
1800
1819
|
};
|
|
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) => {
|
|
1820
|
+
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) => {
|
|
1802
1821
|
console.log(`${picocolors.default.magenta("✿")} ${picocolors.default.bold("React Grab")} ${picocolors.default.gray(VERSION$3)}`);
|
|
1803
1822
|
console.log();
|
|
1804
1823
|
try {
|
|
@@ -1956,7 +1975,11 @@ const init = new commander.Command().name("init").alias("setup").description("in
|
|
|
1956
1975
|
}
|
|
1957
1976
|
}
|
|
1958
1977
|
logger.break();
|
|
1959
|
-
await promptSkillInstall({
|
|
1978
|
+
await promptSkillInstall({
|
|
1979
|
+
yes: isNonInteractive,
|
|
1980
|
+
global: opts.global,
|
|
1981
|
+
cwd
|
|
1982
|
+
});
|
|
1960
1983
|
logger.break();
|
|
1961
1984
|
process.exit(0);
|
|
1962
1985
|
}
|
|
@@ -2022,7 +2045,11 @@ const init = new commander.Command().name("init").alias("setup").description("in
|
|
|
2022
2045
|
let didInstallSkill = false;
|
|
2023
2046
|
if (!isNonInteractive) {
|
|
2024
2047
|
logger.break();
|
|
2025
|
-
didInstallSkill = await promptSkillInstall({
|
|
2048
|
+
didInstallSkill = await promptSkillInstall({
|
|
2049
|
+
yes: isNonInteractive,
|
|
2050
|
+
global: opts.global,
|
|
2051
|
+
cwd
|
|
2052
|
+
});
|
|
2026
2053
|
}
|
|
2027
2054
|
const result = previewTransform(projectInfo.projectRoot, finalFramework, finalNextRouterType, false, opts.force);
|
|
2028
2055
|
if (!result.success) {
|
|
@@ -2080,6 +2107,8 @@ const sleep = (durationMs) => new Promise((resolve) => setTimeout(resolve, durat
|
|
|
2080
2107
|
//#endregion
|
|
2081
2108
|
//#region src/utils/clipboard.ts
|
|
2082
2109
|
const HISTORY_FILE_NAME = "history.jsonl";
|
|
2110
|
+
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.";
|
|
2111
|
+
const READERS_DIR = node_path.default.dirname((0, node_url.fileURLToPath)(require("url").pathToFileURL(__filename).href));
|
|
2083
2112
|
const READ_TIMEOUT_MS = 2500;
|
|
2084
2113
|
const MAX_CLIPBOARD_BYTES = 64 * 1024 * 1024;
|
|
2085
2114
|
const ID_RADIX = 36;
|
|
@@ -2173,7 +2202,7 @@ const compileSwiftReader = (readersDir, workDir) => {
|
|
|
2173
2202
|
return binary;
|
|
2174
2203
|
};
|
|
2175
2204
|
const createDarwinReader = (options) => {
|
|
2176
|
-
const binary = options.textOnly ? null : compileSwiftReader(
|
|
2205
|
+
const binary = options.textOnly ? null : compileSwiftReader(READERS_DIR, options.workDir);
|
|
2177
2206
|
if (binary) return {
|
|
2178
2207
|
mode: "darwin-native",
|
|
2179
2208
|
read: () => {
|
|
@@ -2256,7 +2285,7 @@ const detectPowershell = () => hasCommand("pwsh") ? "pwsh" : hasCommand("powersh
|
|
|
2256
2285
|
const createWindowsReader = (options) => {
|
|
2257
2286
|
const shell = detectPowershell();
|
|
2258
2287
|
if (!shell) return null;
|
|
2259
|
-
const scriptPath = node_path.default.join(
|
|
2288
|
+
const scriptPath = node_path.default.join(READERS_DIR, "read-clipboard.ps1");
|
|
2260
2289
|
if (options.textOnly || !node_fs.default.existsSync(scriptPath)) return {
|
|
2261
2290
|
mode: "win-text",
|
|
2262
2291
|
read: () => {
|
|
@@ -2395,77 +2424,291 @@ const runWatchLoop = async (options) => {
|
|
|
2395
2424
|
}
|
|
2396
2425
|
};
|
|
2397
2426
|
//#endregion
|
|
2427
|
+
//#region src/utils/daemon.ts
|
|
2428
|
+
const PID_FILE_NAME = "watch.pid";
|
|
2429
|
+
const pidFilePath = (dir) => node_path.default.join(dir, PID_FILE_NAME);
|
|
2430
|
+
const cliEntryPath = () => process.argv[1] ?? (0, node_url.fileURLToPath)(require("url").pathToFileURL(__filename).href);
|
|
2431
|
+
const isProcessAlive = (pid) => {
|
|
2432
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
2433
|
+
try {
|
|
2434
|
+
process.kill(pid, 0);
|
|
2435
|
+
return true;
|
|
2436
|
+
} catch (error) {
|
|
2437
|
+
return error.code === "EPERM";
|
|
2438
|
+
}
|
|
2439
|
+
};
|
|
2440
|
+
const readDaemonPid = (dir) => {
|
|
2441
|
+
try {
|
|
2442
|
+
const pid = Number.parseInt(node_fs.default.readFileSync(pidFilePath(dir), "utf8").trim(), 10);
|
|
2443
|
+
return Number.isInteger(pid) && pid > 0 ? pid : null;
|
|
2444
|
+
} catch {
|
|
2445
|
+
return null;
|
|
2446
|
+
}
|
|
2447
|
+
};
|
|
2448
|
+
const isDaemonRunning = (dir) => {
|
|
2449
|
+
const pid = readDaemonPid(dir);
|
|
2450
|
+
return pid !== null && isProcessAlive(pid);
|
|
2451
|
+
};
|
|
2452
|
+
const claimDaemon = (dir) => {
|
|
2453
|
+
const file = pidFilePath(dir);
|
|
2454
|
+
for (let attempt = 0; attempt < 50; attempt += 1) try {
|
|
2455
|
+
const handle = node_fs.default.openSync(file, "wx");
|
|
2456
|
+
node_fs.default.writeFileSync(handle, String(process.pid));
|
|
2457
|
+
node_fs.default.closeSync(handle);
|
|
2458
|
+
return readDaemonPid(dir) === process.pid;
|
|
2459
|
+
} catch (error) {
|
|
2460
|
+
if (error.code !== "EEXIST") throw error;
|
|
2461
|
+
if (isDaemonRunning(dir)) return false;
|
|
2462
|
+
try {
|
|
2463
|
+
node_fs.default.rmSync(file, { force: true });
|
|
2464
|
+
} catch {}
|
|
2465
|
+
}
|
|
2466
|
+
return false;
|
|
2467
|
+
};
|
|
2468
|
+
const releaseDaemon = (dir) => {
|
|
2469
|
+
if (readDaemonPid(dir) === process.pid) try {
|
|
2470
|
+
node_fs.default.rmSync(pidFilePath(dir), { force: true });
|
|
2471
|
+
} catch {}
|
|
2472
|
+
};
|
|
2473
|
+
const stopDaemon = (dir) => {
|
|
2474
|
+
const pid = readDaemonPid(dir);
|
|
2475
|
+
if (pid === null) return null;
|
|
2476
|
+
const wasAlive = isProcessAlive(pid);
|
|
2477
|
+
if (wasAlive) try {
|
|
2478
|
+
process.kill(pid, "SIGTERM");
|
|
2479
|
+
} catch {}
|
|
2480
|
+
if (readDaemonPid(dir) === pid) try {
|
|
2481
|
+
node_fs.default.rmSync(pidFilePath(dir), { force: true });
|
|
2482
|
+
} catch {}
|
|
2483
|
+
return wasAlive ? pid : null;
|
|
2484
|
+
};
|
|
2485
|
+
const spawnDaemon = (options) => {
|
|
2486
|
+
const args = [
|
|
2487
|
+
cliEntryPath(),
|
|
2488
|
+
"watch",
|
|
2489
|
+
"--dir",
|
|
2490
|
+
options.dir,
|
|
2491
|
+
"--interval",
|
|
2492
|
+
String(options.intervalMs)
|
|
2493
|
+
];
|
|
2494
|
+
if (options.textOnly) args.push("--text-only");
|
|
2495
|
+
if (options.replayLast) args.push("--replay-last");
|
|
2496
|
+
(0, node_child_process.spawn)(process.execPath, args, {
|
|
2497
|
+
detached: true,
|
|
2498
|
+
stdio: "ignore",
|
|
2499
|
+
windowsHide: true
|
|
2500
|
+
}).unref();
|
|
2501
|
+
};
|
|
2502
|
+
const ensureDaemon = (options) => {
|
|
2503
|
+
if (isDaemonRunning(options.dir)) return "already-running";
|
|
2504
|
+
if (!createReader({
|
|
2505
|
+
textOnly: options.textOnly,
|
|
2506
|
+
workDir: options.dir
|
|
2507
|
+
})) return "no-reader";
|
|
2508
|
+
spawnDaemon(options);
|
|
2509
|
+
return "started";
|
|
2510
|
+
};
|
|
2511
|
+
//#endregion
|
|
2398
2512
|
//#region src/utils/grab-log.ts
|
|
2399
2513
|
const CURSOR_FILE_NAME = "cursor.txt";
|
|
2514
|
+
const NEWLINE_BYTE = 10;
|
|
2400
2515
|
const cursorFilePath = (dir) => node_path.default.join(dir, CURSOR_FILE_NAME);
|
|
2401
|
-
const
|
|
2402
|
-
|
|
2516
|
+
const historyFilePath = (dir) => node_path.default.join(dir, HISTORY_FILE_NAME);
|
|
2517
|
+
const fileSize = (filePath) => {
|
|
2403
2518
|
try {
|
|
2404
|
-
|
|
2519
|
+
return node_fs.default.statSync(filePath).size;
|
|
2405
2520
|
} catch {
|
|
2406
|
-
return
|
|
2521
|
+
return 0;
|
|
2407
2522
|
}
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2523
|
+
};
|
|
2524
|
+
const readHistoryRange = (dir, start, length) => {
|
|
2525
|
+
if (length <= 0) return "";
|
|
2526
|
+
const fd = node_fs.default.openSync(historyFilePath(dir), "r");
|
|
2527
|
+
try {
|
|
2528
|
+
const buffer = Buffer.allocUnsafe(length);
|
|
2529
|
+
const bytesRead = node_fs.default.readSync(fd, buffer, 0, length, start);
|
|
2530
|
+
return buffer.toString("utf8", 0, bytesRead);
|
|
2531
|
+
} finally {
|
|
2532
|
+
node_fs.default.closeSync(fd);
|
|
2533
|
+
}
|
|
2534
|
+
};
|
|
2535
|
+
const writeGrabCursor = (dir, offset) => {
|
|
2536
|
+
const target = cursorFilePath(dir);
|
|
2537
|
+
const tempPath = `${target}.${process.pid}.tmp`;
|
|
2538
|
+
node_fs.default.writeFileSync(tempPath, JSON.stringify({ offset }));
|
|
2539
|
+
node_fs.default.renameSync(tempPath, target);
|
|
2540
|
+
};
|
|
2541
|
+
const byteOffsetAfterLines = (dir, lineCount) => {
|
|
2542
|
+
const filePath = historyFilePath(dir);
|
|
2543
|
+
const size = fileSize(filePath);
|
|
2544
|
+
if (lineCount <= 0 || size === 0) return 0;
|
|
2545
|
+
const fd = node_fs.default.openSync(filePath, "r");
|
|
2546
|
+
try {
|
|
2547
|
+
const buffer = Buffer.allocUnsafe(MIGRATION_SCAN_CHUNK_BYTES);
|
|
2548
|
+
let position = 0;
|
|
2549
|
+
let seen = 0;
|
|
2550
|
+
while (position < size) {
|
|
2551
|
+
const bytesRead = node_fs.default.readSync(fd, buffer, 0, MIGRATION_SCAN_CHUNK_BYTES, position);
|
|
2552
|
+
if (bytesRead <= 0) break;
|
|
2553
|
+
for (let index = 0; index < bytesRead; index += 1) {
|
|
2554
|
+
if (buffer[index] !== NEWLINE_BYTE) continue;
|
|
2555
|
+
seen += 1;
|
|
2556
|
+
if (seen === lineCount) return position + index + 1;
|
|
2557
|
+
}
|
|
2558
|
+
position += bytesRead;
|
|
2559
|
+
}
|
|
2560
|
+
} finally {
|
|
2561
|
+
node_fs.default.closeSync(fd);
|
|
2562
|
+
}
|
|
2563
|
+
return size;
|
|
2411
2564
|
};
|
|
2412
2565
|
const readGrabCursor = (dir) => {
|
|
2566
|
+
let raw;
|
|
2567
|
+
try {
|
|
2568
|
+
raw = node_fs.default.readFileSync(cursorFilePath(dir), "utf8").trim();
|
|
2569
|
+
} catch {
|
|
2570
|
+
return 0;
|
|
2571
|
+
}
|
|
2572
|
+
if (raw === "") return 0;
|
|
2573
|
+
let parsed;
|
|
2413
2574
|
try {
|
|
2414
|
-
|
|
2415
|
-
return Number.isInteger(value) && value >= 0 ? value : 0;
|
|
2575
|
+
parsed = JSON.parse(raw);
|
|
2416
2576
|
} catch {
|
|
2417
2577
|
return 0;
|
|
2418
2578
|
}
|
|
2579
|
+
if (typeof parsed === "number") {
|
|
2580
|
+
if (!Number.isInteger(parsed) || parsed < 0) return 0;
|
|
2581
|
+
const offset = byteOffsetAfterLines(dir, parsed);
|
|
2582
|
+
writeGrabCursor(dir, offset);
|
|
2583
|
+
return offset;
|
|
2584
|
+
}
|
|
2585
|
+
return typeof parsed.offset === "number" && Number.isInteger(parsed.offset) && parsed.offset >= 0 ? parsed.offset : 0;
|
|
2586
|
+
};
|
|
2587
|
+
const readCompleteGrabLines = (dir) => {
|
|
2588
|
+
const size = fileSize(historyFilePath(dir));
|
|
2589
|
+
if (size === 0) return [];
|
|
2590
|
+
const raw = readHistoryRange(dir, 0, size);
|
|
2591
|
+
const lastNewline = raw.lastIndexOf("\n");
|
|
2592
|
+
if (lastNewline < 0) return [];
|
|
2593
|
+
return raw.slice(0, lastNewline).split("\n").filter(Boolean);
|
|
2419
2594
|
};
|
|
2420
2595
|
const consumeGrabs = (dir, options) => {
|
|
2421
|
-
|
|
2422
|
-
|
|
2596
|
+
if (options.all) return readCompleteGrabLines(dir);
|
|
2597
|
+
const size = fileSize(historyFilePath(dir));
|
|
2423
2598
|
const cursor = readGrabCursor(dir);
|
|
2424
|
-
const start =
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2599
|
+
const start = cursor > size ? 0 : cursor;
|
|
2600
|
+
if (start >= size) {
|
|
2601
|
+
if (start !== cursor) writeGrabCursor(dir, start);
|
|
2602
|
+
return [];
|
|
2603
|
+
}
|
|
2604
|
+
const chunk = readHistoryRange(dir, start, Math.min(size - start, MAX_READ_HISTORY_BYTES));
|
|
2605
|
+
const lastNewline = chunk.lastIndexOf("\n");
|
|
2606
|
+
if (lastNewline < 0) return [];
|
|
2607
|
+
const now = Date.now();
|
|
2608
|
+
const fresh = [];
|
|
2609
|
+
let consumedBytes = 0;
|
|
2610
|
+
let lineStart = 0;
|
|
2611
|
+
while (lineStart <= lastNewline && (options.limit <= 0 || fresh.length < options.limit)) {
|
|
2612
|
+
const newlineIndex = chunk.indexOf("\n", lineStart);
|
|
2613
|
+
const line = chunk.slice(lineStart, newlineIndex);
|
|
2614
|
+
consumedBytes += Buffer.byteLength(chunk.slice(lineStart, newlineIndex + 1), "utf8");
|
|
2615
|
+
lineStart = newlineIndex + 1;
|
|
2616
|
+
if (line.length === 0) continue;
|
|
2617
|
+
let parsed;
|
|
2618
|
+
try {
|
|
2619
|
+
parsed = JSON.parse(line);
|
|
2620
|
+
} catch {
|
|
2621
|
+
continue;
|
|
2622
|
+
}
|
|
2623
|
+
if (options.maxAgeMs > 0 && typeof parsed.receivedAt === "number") {
|
|
2624
|
+
if (now - parsed.receivedAt > options.maxAgeMs) continue;
|
|
2625
|
+
}
|
|
2626
|
+
fresh.push(line);
|
|
2627
|
+
}
|
|
2628
|
+
const nextCursor = start + consumedBytes;
|
|
2629
|
+
if (nextCursor !== cursor) writeGrabCursor(dir, nextCursor);
|
|
2630
|
+
return fresh;
|
|
2428
2631
|
};
|
|
2429
2632
|
//#endregion
|
|
2430
|
-
//#region src/
|
|
2431
|
-
const
|
|
2633
|
+
//#region src/utils/read-args.ts
|
|
2634
|
+
const parseWaitMs = (raw) => {
|
|
2635
|
+
if (raw === void 0) return 0;
|
|
2636
|
+
const trimmed = raw.trim();
|
|
2637
|
+
if (trimmed === "") return 0;
|
|
2638
|
+
if (/^(inf|infinite|infinity|forever)$/i.test(trimmed)) return Number.POSITIVE_INFINITY;
|
|
2639
|
+
const ms = Number(trimmed);
|
|
2640
|
+
return Number.isFinite(ms) && ms >= 0 ? ms : null;
|
|
2641
|
+
};
|
|
2642
|
+
const parseNonNegativeInt = (raw) => {
|
|
2643
|
+
if (raw === void 0) return null;
|
|
2644
|
+
const trimmed = raw.trim();
|
|
2645
|
+
if (trimmed === "") return null;
|
|
2646
|
+
const value = Number(trimmed);
|
|
2647
|
+
return Number.isInteger(value) && value >= 0 ? value : null;
|
|
2648
|
+
};
|
|
2649
|
+
//#endregion
|
|
2650
|
+
//#region src/commands/pull.ts
|
|
2651
|
+
const fail = (message) => {
|
|
2652
|
+
process.stderr.write(`react-grab pull: ${message}\n`);
|
|
2653
|
+
process.exit(1);
|
|
2654
|
+
};
|
|
2655
|
+
const emitAndExit = (lines) => {
|
|
2656
|
+
process.stdout.write(`${lines.join("\n")}\n`, () => process.exit(0));
|
|
2657
|
+
};
|
|
2658
|
+
const pull = new commander.Command().name("pull").description("start the watcher if needed, then wait for and print the next React Grab grab(s)").option("-d, --dir <dir>", "work dir for history.jsonl + watch.pid", DEFAULT_WATCH_DIR).option("-w, --wait <ms>", "how long to wait for a grab: ms, 'infinite', or 0 for none", "infinite").option("-n, --limit <count>", "max grabs to print per call (0 = no limit)", String(50)).option("--max-age <ms>", "skip grabs captured longer ago than <ms> (0 = never)", String(DEFAULT_GRAB_AGE_MS)).option("--text-only", "watcher uses the plain-text clipboard reader (ignored if already running)").option("--all", "print the whole history without advancing the cursor").action(async (options) => {
|
|
2432
2659
|
const dir = node_path.default.resolve(options.dir);
|
|
2433
2660
|
try {
|
|
2434
2661
|
prepareWorkDir(dir);
|
|
2435
2662
|
} catch (error) {
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
const
|
|
2441
|
-
|
|
2663
|
+
fail(String(error?.message ?? error));
|
|
2664
|
+
}
|
|
2665
|
+
const waitMs = parseWaitMs(options.wait);
|
|
2666
|
+
if (waitMs === null) fail(`invalid --wait "${options.wait}" (use milliseconds or "infinite")`);
|
|
2667
|
+
const limit = parseNonNegativeInt(options.limit);
|
|
2668
|
+
if (limit === null) fail(`invalid --limit "${options.limit}" (use a non-negative integer)`);
|
|
2669
|
+
const maxAgeMs = parseNonNegativeInt(options.maxAge);
|
|
2670
|
+
if (maxAgeMs === null) fail(`invalid --max-age "${options.maxAge}" (use milliseconds, 0 to disable)`);
|
|
2442
2671
|
const all = Boolean(options.all);
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2672
|
+
if (ensureDaemon({
|
|
2673
|
+
dir,
|
|
2674
|
+
intervalMs: 800,
|
|
2675
|
+
textOnly: Boolean(options.textOnly),
|
|
2676
|
+
replayLast: false
|
|
2677
|
+
}) === "no-reader") fail(NO_READER_MESSAGE);
|
|
2678
|
+
unrefStdin();
|
|
2679
|
+
const consume = () => consumeGrabs(dir, {
|
|
2680
|
+
limit,
|
|
2681
|
+
all,
|
|
2682
|
+
maxAgeMs
|
|
2683
|
+
});
|
|
2684
|
+
const first = consume();
|
|
2685
|
+
if (first.length > 0) {
|
|
2686
|
+
emitAndExit(first);
|
|
2687
|
+
return;
|
|
2688
|
+
}
|
|
2689
|
+
const deadline = Date.now() + waitMs;
|
|
2454
2690
|
while (Date.now() < deadline) {
|
|
2455
2691
|
await sleep(200);
|
|
2456
|
-
|
|
2692
|
+
const batch = consume();
|
|
2693
|
+
if (batch.length > 0) {
|
|
2694
|
+
emitAndExit(batch);
|
|
2695
|
+
return;
|
|
2696
|
+
}
|
|
2457
2697
|
}
|
|
2458
2698
|
process.exit(0);
|
|
2459
2699
|
});
|
|
2460
2700
|
//#endregion
|
|
2461
2701
|
//#region src/commands/remove.ts
|
|
2462
|
-
const VERSION$2 = "0.1.
|
|
2463
|
-
const remove = new commander.Command().name("remove").description("uninstall the React Grab skill from your agent").action(async () => {
|
|
2702
|
+
const VERSION$2 = "0.1.42";
|
|
2703
|
+
const remove = new commander.Command().name("remove").description("uninstall the React Grab skill from your agent").option("-c, --cwd <cwd>", "working directory (defaults to current directory)", process.cwd()).option("-g, --global", "remove the globally-installed skill instead of the project's", false).action(async (opts) => {
|
|
2464
2704
|
console.log(`${picocolors.default.magenta("✿")} ${picocolors.default.bold("React Grab")} ${picocolors.default.gray(VERSION$2)}`);
|
|
2465
2705
|
console.log();
|
|
2466
2706
|
try {
|
|
2467
2707
|
logger.break();
|
|
2468
|
-
const removedCount = await removeSkill(
|
|
2708
|
+
const removedCount = await removeSkill({
|
|
2709
|
+
cwd: (0, node_path.resolve)(opts.cwd),
|
|
2710
|
+
global: opts.global
|
|
2711
|
+
});
|
|
2469
2712
|
logger.break();
|
|
2470
2713
|
if (removedCount === 0) logger.log("React Grab skill is not installed in any detected agent.");
|
|
2471
2714
|
else logger.log(`${highlighter.success("Removed")} the React Grab skill from ${removedCount} agent${removedCount === 1 ? "" : "s"}.`);
|
|
@@ -2475,8 +2718,16 @@ const remove = new commander.Command().name("remove").description("uninstall the
|
|
|
2475
2718
|
}
|
|
2476
2719
|
});
|
|
2477
2720
|
//#endregion
|
|
2721
|
+
//#region src/commands/stop.ts
|
|
2722
|
+
const stop = new commander.Command().name("stop").description("stop the React Grab watcher for this dir").option("-d, --dir <dir>", "work dir holding watch.pid", DEFAULT_WATCH_DIR).action((options) => {
|
|
2723
|
+
const dir = node_path.default.resolve(options.dir);
|
|
2724
|
+
const stoppedPid = stopDaemon(dir);
|
|
2725
|
+
process.stderr.write(stoppedPid ? `react-grab stop: stopped watcher (pid ${stoppedPid})\n` : `react-grab stop: no watcher running for ${dir}\n`);
|
|
2726
|
+
process.exit(0);
|
|
2727
|
+
});
|
|
2728
|
+
//#endregion
|
|
2478
2729
|
//#region src/commands/upgrade.ts
|
|
2479
|
-
const VERSION$1 = "0.1.
|
|
2730
|
+
const VERSION$1 = "0.1.42";
|
|
2480
2731
|
const NPM_REGISTRY_URL = "https://registry.npmjs.org/react-grab/latest";
|
|
2481
2732
|
const fetchLatestVersion = async () => {
|
|
2482
2733
|
try {
|
|
@@ -2550,95 +2801,24 @@ const upgrade = new commander.Command().name("upgrade").alias("update").descript
|
|
|
2550
2801
|
}
|
|
2551
2802
|
});
|
|
2552
2803
|
//#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
2804
|
//#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
2805
|
const writeStatus = (message) => {
|
|
2634
2806
|
process.stderr.write(`react-grab watch: ${message}\n`);
|
|
2635
2807
|
};
|
|
2636
|
-
const
|
|
2808
|
+
const watch = new commander.Command().name("watch").description("run the React Grab capture daemon in the foreground (used internally by `pull`)").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").action((options) => {
|
|
2809
|
+
const dir = node_path.default.resolve(options.dir);
|
|
2810
|
+
const intervalRaw = Number(options.interval);
|
|
2811
|
+
const intervalMs = Number.isFinite(intervalRaw) && intervalRaw > 0 ? intervalRaw : 800;
|
|
2812
|
+
try {
|
|
2813
|
+
prepareWorkDir(dir);
|
|
2814
|
+
} catch (error) {
|
|
2815
|
+
writeStatus(String(error?.message ?? error));
|
|
2816
|
+
process.exit(1);
|
|
2817
|
+
}
|
|
2637
2818
|
if (!claimDaemon(dir)) process.exit(0);
|
|
2638
2819
|
process.on("exit", () => releaseDaemon(dir));
|
|
2639
2820
|
const reader = createReader({
|
|
2640
|
-
textOnly,
|
|
2641
|
-
readersDir: readersDir(),
|
|
2821
|
+
textOnly: Boolean(options.textOnly),
|
|
2642
2822
|
workDir: dir
|
|
2643
2823
|
});
|
|
2644
2824
|
if (!reader) {
|
|
@@ -2650,58 +2830,16 @@ const runForeground = (dir, intervalMs, textOnly, replayLast) => {
|
|
|
2650
2830
|
reader,
|
|
2651
2831
|
dir,
|
|
2652
2832
|
intervalMs,
|
|
2653
|
-
replayLast,
|
|
2833
|
+
replayLast: Boolean(options.replayLast),
|
|
2654
2834
|
onWarn: writeStatus
|
|
2655
2835
|
}).catch((error) => {
|
|
2656
2836
|
writeStatus(String(error?.message ?? error));
|
|
2657
2837
|
process.exit(1);
|
|
2658
2838
|
});
|
|
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);
|
|
2701
2839
|
});
|
|
2702
2840
|
//#endregion
|
|
2703
2841
|
//#region src/cli.ts
|
|
2704
|
-
const VERSION = "0.1.
|
|
2842
|
+
const VERSION = "0.1.42";
|
|
2705
2843
|
const VERSION_API_URL = "https://www.react-grab.com/api/version";
|
|
2706
2844
|
process.on("SIGINT", () => process.exit(0));
|
|
2707
2845
|
process.on("SIGTERM", () => process.exit(0));
|
|
@@ -2714,8 +2852,9 @@ program.addCommand(add);
|
|
|
2714
2852
|
program.addCommand(remove);
|
|
2715
2853
|
program.addCommand(configure);
|
|
2716
2854
|
program.addCommand(upgrade);
|
|
2717
|
-
program.addCommand(
|
|
2718
|
-
program.addCommand(
|
|
2855
|
+
program.addCommand(pull);
|
|
2856
|
+
program.addCommand(stop);
|
|
2857
|
+
program.addCommand(watch, { hidden: true });
|
|
2719
2858
|
const main = async () => {
|
|
2720
2859
|
await program.parseAsync();
|
|
2721
2860
|
};
|