@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.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Command } from "commander";
|
|
3
|
+
import path, { basename, delimiter, dirname, join, relative, resolve } from "node:path";
|
|
3
4
|
import pc from "picocolors";
|
|
4
5
|
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";
|
|
@@ -422,8 +422,11 @@ const spinner = (text) => ora({ text });
|
|
|
422
422
|
const SKILL_NAME = "react-grab";
|
|
423
423
|
const SKILL_SOURCE = fileURLToPath(new URL("../skills/react-grab", import.meta.url));
|
|
424
424
|
const agentLabel = (agent) => getSkillAgentConfig(agent).displayName;
|
|
425
|
-
const installedSkillDir = (agent) => join(isUniversalSkillAgent(agent) ? getCanonicalSkillsDir(
|
|
426
|
-
|
|
425
|
+
const installedSkillDir = (agent, global, cwd) => join(isUniversalSkillAgent(agent) ? getCanonicalSkillsDir(global, cwd) : getSkillAgentDir(agent, {
|
|
426
|
+
global,
|
|
427
|
+
cwd
|
|
428
|
+
}), SKILL_NAME);
|
|
429
|
+
const promptSkillInstall = async ({ yes = false, global = false, cwd = process.cwd() } = {}) => {
|
|
427
430
|
const detectedAgents = await detectAvailableAgents();
|
|
428
431
|
if (detectedAgents.length === 0) {
|
|
429
432
|
logger.warn("No supported agents detected.");
|
|
@@ -434,7 +437,7 @@ const promptSkillInstall = async ({ yes = false } = {}) => {
|
|
|
434
437
|
const { agents } = await prompts({
|
|
435
438
|
type: "multiselect",
|
|
436
439
|
name: "agents",
|
|
437
|
-
message:
|
|
440
|
+
message: `Install the React Grab skill (${global ? "global" : "this project"}) for:`,
|
|
438
441
|
choices: detectedAgents.map((agent) => ({
|
|
439
442
|
title: agentLabel(agent),
|
|
440
443
|
value: agent,
|
|
@@ -450,7 +453,8 @@ const promptSkillInstall = async ({ yes = false } = {}) => {
|
|
|
450
453
|
const { installed, failed } = await add({
|
|
451
454
|
source: SKILL_SOURCE,
|
|
452
455
|
agents: selectedAgents,
|
|
453
|
-
global
|
|
456
|
+
global,
|
|
457
|
+
cwd,
|
|
454
458
|
mode: "copy"
|
|
455
459
|
});
|
|
456
460
|
if (installed.length === 0) {
|
|
@@ -461,19 +465,27 @@ const promptSkillInstall = async ({ yes = false } = {}) => {
|
|
|
461
465
|
for (const record of failed) logger.log(` ${highlighter.error("✗")} ${agentLabel(record.agent)} ${record.error}`);
|
|
462
466
|
return true;
|
|
463
467
|
};
|
|
464
|
-
const removeSkill = async () => {
|
|
465
|
-
const
|
|
466
|
-
|
|
468
|
+
const removeSkill = async ({ cwd = process.cwd(), global = false } = {}) => {
|
|
469
|
+
const agents = await detectAvailableAgents();
|
|
470
|
+
const removedAgents = [];
|
|
471
|
+
const dirsToRemove = /* @__PURE__ */ new Set();
|
|
472
|
+
for (const agent of agents) {
|
|
473
|
+
const skillDir = installedSkillDir(agent, global, cwd);
|
|
474
|
+
if (!existsSync(skillDir)) continue;
|
|
475
|
+
removedAgents.push(agent);
|
|
476
|
+
dirsToRemove.add(skillDir);
|
|
477
|
+
}
|
|
478
|
+
for (const skillDir of dirsToRemove) rmSync(skillDir, {
|
|
467
479
|
recursive: true,
|
|
468
480
|
force: true
|
|
469
481
|
});
|
|
470
|
-
for (const agent of
|
|
471
|
-
return
|
|
482
|
+
for (const agent of removedAgents) logger.log(` ${highlighter.success("✓")} ${agentLabel(agent)}`);
|
|
483
|
+
return removedAgents.length;
|
|
472
484
|
};
|
|
473
485
|
//#endregion
|
|
474
486
|
//#region src/commands/add.ts
|
|
475
|
-
const VERSION$5 = "0.1.
|
|
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) => {
|
|
487
|
+
const VERSION$5 = "0.1.42";
|
|
488
|
+
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()).option("-g, --global", "install the skill globally instead of in the project", false).action(async (opts) => {
|
|
477
489
|
console.log(`${pc.magenta("✿")} ${pc.bold("React Grab")} ${pc.gray(VERSION$5)}`);
|
|
478
490
|
console.log();
|
|
479
491
|
try {
|
|
@@ -488,7 +500,11 @@ const add$1 = new Command().name("add").alias("install").description("install th
|
|
|
488
500
|
}
|
|
489
501
|
preflightSpinner.succeed();
|
|
490
502
|
logger.break();
|
|
491
|
-
if (!await promptSkillInstall({
|
|
503
|
+
if (!await promptSkillInstall({
|
|
504
|
+
yes: isNonInteractive,
|
|
505
|
+
global: opts.global,
|
|
506
|
+
cwd: resolve(opts.cwd)
|
|
507
|
+
}) && isNonInteractive) {
|
|
492
508
|
logger.break();
|
|
493
509
|
process.exit(1);
|
|
494
510
|
}
|
|
@@ -1067,6 +1083,9 @@ const previewCdnTransform = (projectRoot, framework, nextRouterType, targetCdnDo
|
|
|
1067
1083
|
//#region src/utils/constants.ts
|
|
1068
1084
|
const MAX_KEY_HOLD_DURATION_MS = 2e3;
|
|
1069
1085
|
const DEFAULT_WATCH_DIR = ".react-grab";
|
|
1086
|
+
const DEFAULT_GRAB_AGE_MS = 300 * 1e3;
|
|
1087
|
+
const MAX_READ_HISTORY_BYTES = 128 * 1024 * 1024;
|
|
1088
|
+
const MIGRATION_SCAN_CHUNK_BYTES = 1024 * 1024;
|
|
1070
1089
|
//#endregion
|
|
1071
1090
|
//#region src/utils/format-activation-key.ts
|
|
1072
1091
|
const formatActivationKeyDisplay = (activationKey) => {
|
|
@@ -1084,7 +1103,7 @@ const formatActivationKeyDisplay = (activationKey) => {
|
|
|
1084
1103
|
};
|
|
1085
1104
|
//#endregion
|
|
1086
1105
|
//#region src/commands/configure.ts
|
|
1087
|
-
const VERSION$4 = "0.1.
|
|
1106
|
+
const VERSION$4 = "0.1.42";
|
|
1088
1107
|
const isMac = process.platform === "darwin";
|
|
1089
1108
|
const META_LABEL = isMac ? "Cmd" : "Win";
|
|
1090
1109
|
const ALT_LABEL = isMac ? "Option" : "Alt";
|
|
@@ -1703,7 +1722,7 @@ const installPackagesWithFeedback = async (packages, packageManager, projectRoot
|
|
|
1703
1722
|
};
|
|
1704
1723
|
//#endregion
|
|
1705
1724
|
//#region src/commands/init.ts
|
|
1706
|
-
const VERSION$3 = "0.1.
|
|
1725
|
+
const VERSION$3 = "0.1.42";
|
|
1707
1726
|
const REPORT_URL = "https://react-grab.com/api/report-cli";
|
|
1708
1727
|
const DOCS_URL = "https://github.com/aidenybai/react-grab";
|
|
1709
1728
|
const reportToCli = (type, config, error) => {
|
|
@@ -1770,7 +1789,7 @@ const failWithManualSetup = (failingSpinner, message, { listSupportedFrameworks
|
|
|
1770
1789
|
logger.break();
|
|
1771
1790
|
process.exit(1);
|
|
1772
1791
|
};
|
|
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) => {
|
|
1792
|
+
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()).option("-g, --global", "install the skill globally instead of in the project", false).action(async (opts) => {
|
|
1774
1793
|
console.log(`${pc.magenta("✿")} ${pc.bold("React Grab")} ${pc.gray(VERSION$3)}`);
|
|
1775
1794
|
console.log();
|
|
1776
1795
|
try {
|
|
@@ -1928,7 +1947,11 @@ const init = new Command().name("init").alias("setup").description("initialize R
|
|
|
1928
1947
|
}
|
|
1929
1948
|
}
|
|
1930
1949
|
logger.break();
|
|
1931
|
-
await promptSkillInstall({
|
|
1950
|
+
await promptSkillInstall({
|
|
1951
|
+
yes: isNonInteractive,
|
|
1952
|
+
global: opts.global,
|
|
1953
|
+
cwd
|
|
1954
|
+
});
|
|
1932
1955
|
logger.break();
|
|
1933
1956
|
process.exit(0);
|
|
1934
1957
|
}
|
|
@@ -1994,7 +2017,11 @@ const init = new Command().name("init").alias("setup").description("initialize R
|
|
|
1994
2017
|
let didInstallSkill = false;
|
|
1995
2018
|
if (!isNonInteractive) {
|
|
1996
2019
|
logger.break();
|
|
1997
|
-
didInstallSkill = await promptSkillInstall({
|
|
2020
|
+
didInstallSkill = await promptSkillInstall({
|
|
2021
|
+
yes: isNonInteractive,
|
|
2022
|
+
global: opts.global,
|
|
2023
|
+
cwd
|
|
2024
|
+
});
|
|
1998
2025
|
}
|
|
1999
2026
|
const result = previewTransform(projectInfo.projectRoot, finalFramework, finalNextRouterType, false, opts.force);
|
|
2000
2027
|
if (!result.success) {
|
|
@@ -2052,6 +2079,8 @@ const sleep = (durationMs) => new Promise((resolve) => setTimeout(resolve, durat
|
|
|
2052
2079
|
//#endregion
|
|
2053
2080
|
//#region src/utils/clipboard.ts
|
|
2054
2081
|
const HISTORY_FILE_NAME = "history.jsonl";
|
|
2082
|
+
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.";
|
|
2083
|
+
const READERS_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
2055
2084
|
const READ_TIMEOUT_MS = 2500;
|
|
2056
2085
|
const MAX_CLIPBOARD_BYTES = 64 * 1024 * 1024;
|
|
2057
2086
|
const ID_RADIX = 36;
|
|
@@ -2145,7 +2174,7 @@ const compileSwiftReader = (readersDir, workDir) => {
|
|
|
2145
2174
|
return binary;
|
|
2146
2175
|
};
|
|
2147
2176
|
const createDarwinReader = (options) => {
|
|
2148
|
-
const binary = options.textOnly ? null : compileSwiftReader(
|
|
2177
|
+
const binary = options.textOnly ? null : compileSwiftReader(READERS_DIR, options.workDir);
|
|
2149
2178
|
if (binary) return {
|
|
2150
2179
|
mode: "darwin-native",
|
|
2151
2180
|
read: () => {
|
|
@@ -2228,7 +2257,7 @@ const detectPowershell = () => hasCommand("pwsh") ? "pwsh" : hasCommand("powersh
|
|
|
2228
2257
|
const createWindowsReader = (options) => {
|
|
2229
2258
|
const shell = detectPowershell();
|
|
2230
2259
|
if (!shell) return null;
|
|
2231
|
-
const scriptPath = path.join(
|
|
2260
|
+
const scriptPath = path.join(READERS_DIR, "read-clipboard.ps1");
|
|
2232
2261
|
if (options.textOnly || !fs.existsSync(scriptPath)) return {
|
|
2233
2262
|
mode: "win-text",
|
|
2234
2263
|
read: () => {
|
|
@@ -2367,77 +2396,291 @@ const runWatchLoop = async (options) => {
|
|
|
2367
2396
|
}
|
|
2368
2397
|
};
|
|
2369
2398
|
//#endregion
|
|
2399
|
+
//#region src/utils/daemon.ts
|
|
2400
|
+
const PID_FILE_NAME = "watch.pid";
|
|
2401
|
+
const pidFilePath = (dir) => path.join(dir, PID_FILE_NAME);
|
|
2402
|
+
const cliEntryPath = () => process.argv[1] ?? fileURLToPath(import.meta.url);
|
|
2403
|
+
const isProcessAlive = (pid) => {
|
|
2404
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
2405
|
+
try {
|
|
2406
|
+
process.kill(pid, 0);
|
|
2407
|
+
return true;
|
|
2408
|
+
} catch (error) {
|
|
2409
|
+
return error.code === "EPERM";
|
|
2410
|
+
}
|
|
2411
|
+
};
|
|
2412
|
+
const readDaemonPid = (dir) => {
|
|
2413
|
+
try {
|
|
2414
|
+
const pid = Number.parseInt(fs.readFileSync(pidFilePath(dir), "utf8").trim(), 10);
|
|
2415
|
+
return Number.isInteger(pid) && pid > 0 ? pid : null;
|
|
2416
|
+
} catch {
|
|
2417
|
+
return null;
|
|
2418
|
+
}
|
|
2419
|
+
};
|
|
2420
|
+
const isDaemonRunning = (dir) => {
|
|
2421
|
+
const pid = readDaemonPid(dir);
|
|
2422
|
+
return pid !== null && isProcessAlive(pid);
|
|
2423
|
+
};
|
|
2424
|
+
const claimDaemon = (dir) => {
|
|
2425
|
+
const file = pidFilePath(dir);
|
|
2426
|
+
for (let attempt = 0; attempt < 50; attempt += 1) try {
|
|
2427
|
+
const handle = fs.openSync(file, "wx");
|
|
2428
|
+
fs.writeFileSync(handle, String(process.pid));
|
|
2429
|
+
fs.closeSync(handle);
|
|
2430
|
+
return readDaemonPid(dir) === process.pid;
|
|
2431
|
+
} catch (error) {
|
|
2432
|
+
if (error.code !== "EEXIST") throw error;
|
|
2433
|
+
if (isDaemonRunning(dir)) return false;
|
|
2434
|
+
try {
|
|
2435
|
+
fs.rmSync(file, { force: true });
|
|
2436
|
+
} catch {}
|
|
2437
|
+
}
|
|
2438
|
+
return false;
|
|
2439
|
+
};
|
|
2440
|
+
const releaseDaemon = (dir) => {
|
|
2441
|
+
if (readDaemonPid(dir) === process.pid) try {
|
|
2442
|
+
fs.rmSync(pidFilePath(dir), { force: true });
|
|
2443
|
+
} catch {}
|
|
2444
|
+
};
|
|
2445
|
+
const stopDaemon = (dir) => {
|
|
2446
|
+
const pid = readDaemonPid(dir);
|
|
2447
|
+
if (pid === null) return null;
|
|
2448
|
+
const wasAlive = isProcessAlive(pid);
|
|
2449
|
+
if (wasAlive) try {
|
|
2450
|
+
process.kill(pid, "SIGTERM");
|
|
2451
|
+
} catch {}
|
|
2452
|
+
if (readDaemonPid(dir) === pid) try {
|
|
2453
|
+
fs.rmSync(pidFilePath(dir), { force: true });
|
|
2454
|
+
} catch {}
|
|
2455
|
+
return wasAlive ? pid : null;
|
|
2456
|
+
};
|
|
2457
|
+
const spawnDaemon = (options) => {
|
|
2458
|
+
const args = [
|
|
2459
|
+
cliEntryPath(),
|
|
2460
|
+
"watch",
|
|
2461
|
+
"--dir",
|
|
2462
|
+
options.dir,
|
|
2463
|
+
"--interval",
|
|
2464
|
+
String(options.intervalMs)
|
|
2465
|
+
];
|
|
2466
|
+
if (options.textOnly) args.push("--text-only");
|
|
2467
|
+
if (options.replayLast) args.push("--replay-last");
|
|
2468
|
+
spawn(process.execPath, args, {
|
|
2469
|
+
detached: true,
|
|
2470
|
+
stdio: "ignore",
|
|
2471
|
+
windowsHide: true
|
|
2472
|
+
}).unref();
|
|
2473
|
+
};
|
|
2474
|
+
const ensureDaemon = (options) => {
|
|
2475
|
+
if (isDaemonRunning(options.dir)) return "already-running";
|
|
2476
|
+
if (!createReader({
|
|
2477
|
+
textOnly: options.textOnly,
|
|
2478
|
+
workDir: options.dir
|
|
2479
|
+
})) return "no-reader";
|
|
2480
|
+
spawnDaemon(options);
|
|
2481
|
+
return "started";
|
|
2482
|
+
};
|
|
2483
|
+
//#endregion
|
|
2370
2484
|
//#region src/utils/grab-log.ts
|
|
2371
2485
|
const CURSOR_FILE_NAME = "cursor.txt";
|
|
2486
|
+
const NEWLINE_BYTE = 10;
|
|
2372
2487
|
const cursorFilePath = (dir) => path.join(dir, CURSOR_FILE_NAME);
|
|
2373
|
-
const
|
|
2374
|
-
|
|
2488
|
+
const historyFilePath = (dir) => path.join(dir, HISTORY_FILE_NAME);
|
|
2489
|
+
const fileSize = (filePath) => {
|
|
2375
2490
|
try {
|
|
2376
|
-
|
|
2491
|
+
return fs.statSync(filePath).size;
|
|
2377
2492
|
} catch {
|
|
2378
|
-
return
|
|
2493
|
+
return 0;
|
|
2379
2494
|
}
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2495
|
+
};
|
|
2496
|
+
const readHistoryRange = (dir, start, length) => {
|
|
2497
|
+
if (length <= 0) return "";
|
|
2498
|
+
const fd = fs.openSync(historyFilePath(dir), "r");
|
|
2499
|
+
try {
|
|
2500
|
+
const buffer = Buffer.allocUnsafe(length);
|
|
2501
|
+
const bytesRead = fs.readSync(fd, buffer, 0, length, start);
|
|
2502
|
+
return buffer.toString("utf8", 0, bytesRead);
|
|
2503
|
+
} finally {
|
|
2504
|
+
fs.closeSync(fd);
|
|
2505
|
+
}
|
|
2506
|
+
};
|
|
2507
|
+
const writeGrabCursor = (dir, offset) => {
|
|
2508
|
+
const target = cursorFilePath(dir);
|
|
2509
|
+
const tempPath = `${target}.${process.pid}.tmp`;
|
|
2510
|
+
fs.writeFileSync(tempPath, JSON.stringify({ offset }));
|
|
2511
|
+
fs.renameSync(tempPath, target);
|
|
2512
|
+
};
|
|
2513
|
+
const byteOffsetAfterLines = (dir, lineCount) => {
|
|
2514
|
+
const filePath = historyFilePath(dir);
|
|
2515
|
+
const size = fileSize(filePath);
|
|
2516
|
+
if (lineCount <= 0 || size === 0) return 0;
|
|
2517
|
+
const fd = fs.openSync(filePath, "r");
|
|
2518
|
+
try {
|
|
2519
|
+
const buffer = Buffer.allocUnsafe(MIGRATION_SCAN_CHUNK_BYTES);
|
|
2520
|
+
let position = 0;
|
|
2521
|
+
let seen = 0;
|
|
2522
|
+
while (position < size) {
|
|
2523
|
+
const bytesRead = fs.readSync(fd, buffer, 0, MIGRATION_SCAN_CHUNK_BYTES, position);
|
|
2524
|
+
if (bytesRead <= 0) break;
|
|
2525
|
+
for (let index = 0; index < bytesRead; index += 1) {
|
|
2526
|
+
if (buffer[index] !== NEWLINE_BYTE) continue;
|
|
2527
|
+
seen += 1;
|
|
2528
|
+
if (seen === lineCount) return position + index + 1;
|
|
2529
|
+
}
|
|
2530
|
+
position += bytesRead;
|
|
2531
|
+
}
|
|
2532
|
+
} finally {
|
|
2533
|
+
fs.closeSync(fd);
|
|
2534
|
+
}
|
|
2535
|
+
return size;
|
|
2383
2536
|
};
|
|
2384
2537
|
const readGrabCursor = (dir) => {
|
|
2538
|
+
let raw;
|
|
2539
|
+
try {
|
|
2540
|
+
raw = fs.readFileSync(cursorFilePath(dir), "utf8").trim();
|
|
2541
|
+
} catch {
|
|
2542
|
+
return 0;
|
|
2543
|
+
}
|
|
2544
|
+
if (raw === "") return 0;
|
|
2545
|
+
let parsed;
|
|
2385
2546
|
try {
|
|
2386
|
-
|
|
2387
|
-
return Number.isInteger(value) && value >= 0 ? value : 0;
|
|
2547
|
+
parsed = JSON.parse(raw);
|
|
2388
2548
|
} catch {
|
|
2389
2549
|
return 0;
|
|
2390
2550
|
}
|
|
2551
|
+
if (typeof parsed === "number") {
|
|
2552
|
+
if (!Number.isInteger(parsed) || parsed < 0) return 0;
|
|
2553
|
+
const offset = byteOffsetAfterLines(dir, parsed);
|
|
2554
|
+
writeGrabCursor(dir, offset);
|
|
2555
|
+
return offset;
|
|
2556
|
+
}
|
|
2557
|
+
return typeof parsed.offset === "number" && Number.isInteger(parsed.offset) && parsed.offset >= 0 ? parsed.offset : 0;
|
|
2558
|
+
};
|
|
2559
|
+
const readCompleteGrabLines = (dir) => {
|
|
2560
|
+
const size = fileSize(historyFilePath(dir));
|
|
2561
|
+
if (size === 0) return [];
|
|
2562
|
+
const raw = readHistoryRange(dir, 0, size);
|
|
2563
|
+
const lastNewline = raw.lastIndexOf("\n");
|
|
2564
|
+
if (lastNewline < 0) return [];
|
|
2565
|
+
return raw.slice(0, lastNewline).split("\n").filter(Boolean);
|
|
2391
2566
|
};
|
|
2392
2567
|
const consumeGrabs = (dir, options) => {
|
|
2393
|
-
|
|
2394
|
-
|
|
2568
|
+
if (options.all) return readCompleteGrabLines(dir);
|
|
2569
|
+
const size = fileSize(historyFilePath(dir));
|
|
2395
2570
|
const cursor = readGrabCursor(dir);
|
|
2396
|
-
const start =
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2571
|
+
const start = cursor > size ? 0 : cursor;
|
|
2572
|
+
if (start >= size) {
|
|
2573
|
+
if (start !== cursor) writeGrabCursor(dir, start);
|
|
2574
|
+
return [];
|
|
2575
|
+
}
|
|
2576
|
+
const chunk = readHistoryRange(dir, start, Math.min(size - start, MAX_READ_HISTORY_BYTES));
|
|
2577
|
+
const lastNewline = chunk.lastIndexOf("\n");
|
|
2578
|
+
if (lastNewline < 0) return [];
|
|
2579
|
+
const now = Date.now();
|
|
2580
|
+
const fresh = [];
|
|
2581
|
+
let consumedBytes = 0;
|
|
2582
|
+
let lineStart = 0;
|
|
2583
|
+
while (lineStart <= lastNewline && (options.limit <= 0 || fresh.length < options.limit)) {
|
|
2584
|
+
const newlineIndex = chunk.indexOf("\n", lineStart);
|
|
2585
|
+
const line = chunk.slice(lineStart, newlineIndex);
|
|
2586
|
+
consumedBytes += Buffer.byteLength(chunk.slice(lineStart, newlineIndex + 1), "utf8");
|
|
2587
|
+
lineStart = newlineIndex + 1;
|
|
2588
|
+
if (line.length === 0) continue;
|
|
2589
|
+
let parsed;
|
|
2590
|
+
try {
|
|
2591
|
+
parsed = JSON.parse(line);
|
|
2592
|
+
} catch {
|
|
2593
|
+
continue;
|
|
2594
|
+
}
|
|
2595
|
+
if (options.maxAgeMs > 0 && typeof parsed.receivedAt === "number") {
|
|
2596
|
+
if (now - parsed.receivedAt > options.maxAgeMs) continue;
|
|
2597
|
+
}
|
|
2598
|
+
fresh.push(line);
|
|
2599
|
+
}
|
|
2600
|
+
const nextCursor = start + consumedBytes;
|
|
2601
|
+
if (nextCursor !== cursor) writeGrabCursor(dir, nextCursor);
|
|
2602
|
+
return fresh;
|
|
2400
2603
|
};
|
|
2401
2604
|
//#endregion
|
|
2402
|
-
//#region src/
|
|
2403
|
-
const
|
|
2605
|
+
//#region src/utils/read-args.ts
|
|
2606
|
+
const parseWaitMs = (raw) => {
|
|
2607
|
+
if (raw === void 0) return 0;
|
|
2608
|
+
const trimmed = raw.trim();
|
|
2609
|
+
if (trimmed === "") return 0;
|
|
2610
|
+
if (/^(inf|infinite|infinity|forever)$/i.test(trimmed)) return Number.POSITIVE_INFINITY;
|
|
2611
|
+
const ms = Number(trimmed);
|
|
2612
|
+
return Number.isFinite(ms) && ms >= 0 ? ms : null;
|
|
2613
|
+
};
|
|
2614
|
+
const parseNonNegativeInt = (raw) => {
|
|
2615
|
+
if (raw === void 0) return null;
|
|
2616
|
+
const trimmed = raw.trim();
|
|
2617
|
+
if (trimmed === "") return null;
|
|
2618
|
+
const value = Number(trimmed);
|
|
2619
|
+
return Number.isInteger(value) && value >= 0 ? value : null;
|
|
2620
|
+
};
|
|
2621
|
+
//#endregion
|
|
2622
|
+
//#region src/commands/pull.ts
|
|
2623
|
+
const fail = (message) => {
|
|
2624
|
+
process.stderr.write(`react-grab pull: ${message}\n`);
|
|
2625
|
+
process.exit(1);
|
|
2626
|
+
};
|
|
2627
|
+
const emitAndExit = (lines) => {
|
|
2628
|
+
process.stdout.write(`${lines.join("\n")}\n`, () => process.exit(0));
|
|
2629
|
+
};
|
|
2630
|
+
const pull = new Command().name("pull").description("start the watcher if needed, then wait for and print the next React Grab grab(s)").option("-d, --dir <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) => {
|
|
2404
2631
|
const dir = path.resolve(options.dir);
|
|
2405
2632
|
try {
|
|
2406
2633
|
prepareWorkDir(dir);
|
|
2407
2634
|
} catch (error) {
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
const
|
|
2413
|
-
|
|
2635
|
+
fail(String(error?.message ?? error));
|
|
2636
|
+
}
|
|
2637
|
+
const waitMs = parseWaitMs(options.wait);
|
|
2638
|
+
if (waitMs === null) fail(`invalid --wait "${options.wait}" (use milliseconds or "infinite")`);
|
|
2639
|
+
const limit = parseNonNegativeInt(options.limit);
|
|
2640
|
+
if (limit === null) fail(`invalid --limit "${options.limit}" (use a non-negative integer)`);
|
|
2641
|
+
const maxAgeMs = parseNonNegativeInt(options.maxAge);
|
|
2642
|
+
if (maxAgeMs === null) fail(`invalid --max-age "${options.maxAge}" (use milliseconds, 0 to disable)`);
|
|
2414
2643
|
const all = Boolean(options.all);
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2644
|
+
if (ensureDaemon({
|
|
2645
|
+
dir,
|
|
2646
|
+
intervalMs: 800,
|
|
2647
|
+
textOnly: Boolean(options.textOnly),
|
|
2648
|
+
replayLast: false
|
|
2649
|
+
}) === "no-reader") fail(NO_READER_MESSAGE);
|
|
2650
|
+
unrefStdin();
|
|
2651
|
+
const consume = () => consumeGrabs(dir, {
|
|
2652
|
+
limit,
|
|
2653
|
+
all,
|
|
2654
|
+
maxAgeMs
|
|
2655
|
+
});
|
|
2656
|
+
const first = consume();
|
|
2657
|
+
if (first.length > 0) {
|
|
2658
|
+
emitAndExit(first);
|
|
2659
|
+
return;
|
|
2660
|
+
}
|
|
2661
|
+
const deadline = Date.now() + waitMs;
|
|
2426
2662
|
while (Date.now() < deadline) {
|
|
2427
2663
|
await sleep(200);
|
|
2428
|
-
|
|
2664
|
+
const batch = consume();
|
|
2665
|
+
if (batch.length > 0) {
|
|
2666
|
+
emitAndExit(batch);
|
|
2667
|
+
return;
|
|
2668
|
+
}
|
|
2429
2669
|
}
|
|
2430
2670
|
process.exit(0);
|
|
2431
2671
|
});
|
|
2432
2672
|
//#endregion
|
|
2433
2673
|
//#region src/commands/remove.ts
|
|
2434
|
-
const VERSION$2 = "0.1.
|
|
2435
|
-
const remove = new Command().name("remove").description("uninstall the React Grab skill from your agent").action(async () => {
|
|
2674
|
+
const VERSION$2 = "0.1.42";
|
|
2675
|
+
const remove = new 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) => {
|
|
2436
2676
|
console.log(`${pc.magenta("✿")} ${pc.bold("React Grab")} ${pc.gray(VERSION$2)}`);
|
|
2437
2677
|
console.log();
|
|
2438
2678
|
try {
|
|
2439
2679
|
logger.break();
|
|
2440
|
-
const removedCount = await removeSkill(
|
|
2680
|
+
const removedCount = await removeSkill({
|
|
2681
|
+
cwd: resolve(opts.cwd),
|
|
2682
|
+
global: opts.global
|
|
2683
|
+
});
|
|
2441
2684
|
logger.break();
|
|
2442
2685
|
if (removedCount === 0) logger.log("React Grab skill is not installed in any detected agent.");
|
|
2443
2686
|
else logger.log(`${highlighter.success("Removed")} the React Grab skill from ${removedCount} agent${removedCount === 1 ? "" : "s"}.`);
|
|
@@ -2447,8 +2690,16 @@ const remove = new Command().name("remove").description("uninstall the React Gra
|
|
|
2447
2690
|
}
|
|
2448
2691
|
});
|
|
2449
2692
|
//#endregion
|
|
2693
|
+
//#region src/commands/stop.ts
|
|
2694
|
+
const stop = new 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) => {
|
|
2695
|
+
const dir = path.resolve(options.dir);
|
|
2696
|
+
const stoppedPid = stopDaemon(dir);
|
|
2697
|
+
process.stderr.write(stoppedPid ? `react-grab stop: stopped watcher (pid ${stoppedPid})\n` : `react-grab stop: no watcher running for ${dir}\n`);
|
|
2698
|
+
process.exit(0);
|
|
2699
|
+
});
|
|
2700
|
+
//#endregion
|
|
2450
2701
|
//#region src/commands/upgrade.ts
|
|
2451
|
-
const VERSION$1 = "0.1.
|
|
2702
|
+
const VERSION$1 = "0.1.42";
|
|
2452
2703
|
const NPM_REGISTRY_URL = "https://registry.npmjs.org/react-grab/latest";
|
|
2453
2704
|
const fetchLatestVersion = async () => {
|
|
2454
2705
|
try {
|
|
@@ -2522,95 +2773,24 @@ const upgrade = new Command().name("upgrade").alias("update").description("upgra
|
|
|
2522
2773
|
}
|
|
2523
2774
|
});
|
|
2524
2775
|
//#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
2776
|
//#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
2777
|
const writeStatus = (message) => {
|
|
2606
2778
|
process.stderr.write(`react-grab watch: ${message}\n`);
|
|
2607
2779
|
};
|
|
2608
|
-
const
|
|
2780
|
+
const watch = new 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) => {
|
|
2781
|
+
const dir = path.resolve(options.dir);
|
|
2782
|
+
const intervalRaw = Number(options.interval);
|
|
2783
|
+
const intervalMs = Number.isFinite(intervalRaw) && intervalRaw > 0 ? intervalRaw : 800;
|
|
2784
|
+
try {
|
|
2785
|
+
prepareWorkDir(dir);
|
|
2786
|
+
} catch (error) {
|
|
2787
|
+
writeStatus(String(error?.message ?? error));
|
|
2788
|
+
process.exit(1);
|
|
2789
|
+
}
|
|
2609
2790
|
if (!claimDaemon(dir)) process.exit(0);
|
|
2610
2791
|
process.on("exit", () => releaseDaemon(dir));
|
|
2611
2792
|
const reader = createReader({
|
|
2612
|
-
textOnly,
|
|
2613
|
-
readersDir: readersDir(),
|
|
2793
|
+
textOnly: Boolean(options.textOnly),
|
|
2614
2794
|
workDir: dir
|
|
2615
2795
|
});
|
|
2616
2796
|
if (!reader) {
|
|
@@ -2622,58 +2802,16 @@ const runForeground = (dir, intervalMs, textOnly, replayLast) => {
|
|
|
2622
2802
|
reader,
|
|
2623
2803
|
dir,
|
|
2624
2804
|
intervalMs,
|
|
2625
|
-
replayLast,
|
|
2805
|
+
replayLast: Boolean(options.replayLast),
|
|
2626
2806
|
onWarn: writeStatus
|
|
2627
2807
|
}).catch((error) => {
|
|
2628
2808
|
writeStatus(String(error?.message ?? error));
|
|
2629
2809
|
process.exit(1);
|
|
2630
2810
|
});
|
|
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);
|
|
2673
2811
|
});
|
|
2674
2812
|
//#endregion
|
|
2675
2813
|
//#region src/cli.ts
|
|
2676
|
-
const VERSION = "0.1.
|
|
2814
|
+
const VERSION = "0.1.42";
|
|
2677
2815
|
const VERSION_API_URL = "https://www.react-grab.com/api/version";
|
|
2678
2816
|
process.on("SIGINT", () => process.exit(0));
|
|
2679
2817
|
process.on("SIGTERM", () => process.exit(0));
|
|
@@ -2686,8 +2824,9 @@ program.addCommand(add$1);
|
|
|
2686
2824
|
program.addCommand(remove);
|
|
2687
2825
|
program.addCommand(configure);
|
|
2688
2826
|
program.addCommand(upgrade);
|
|
2689
|
-
program.addCommand(
|
|
2690
|
-
program.addCommand(
|
|
2827
|
+
program.addCommand(pull);
|
|
2828
|
+
program.addCommand(stop);
|
|
2829
|
+
program.addCommand(watch, { hidden: true });
|
|
2691
2830
|
const main = async () => {
|
|
2692
2831
|
await program.parseAsync();
|
|
2693
2832
|
};
|