@react-grab/cli 0.1.41 → 0.1.43
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 +286 -196
- package/dist/cli.js +286 -196
- package/dist/cli.js.map +1 -1
- package/package.json +5 -1
- package/skills/react-grab/SKILL.md +19 -51
package/dist/cli.cjs
CHANGED
|
@@ -493,15 +493,15 @@ const promptSkillInstall = async ({ yes = false, global = false, cwd = process.c
|
|
|
493
493
|
for (const record of failed) logger.log(` ${highlighter.error("✗")} ${agentLabel(record.agent)} ${record.error}`);
|
|
494
494
|
return true;
|
|
495
495
|
};
|
|
496
|
-
const removeSkill = async (cwd = process.cwd()) => {
|
|
496
|
+
const removeSkill = async ({ cwd = process.cwd(), global = false } = {}) => {
|
|
497
497
|
const agents = await detectAvailableAgents();
|
|
498
498
|
const removedAgents = [];
|
|
499
499
|
const dirsToRemove = /* @__PURE__ */ new Set();
|
|
500
500
|
for (const agent of agents) {
|
|
501
|
-
const
|
|
502
|
-
if (
|
|
501
|
+
const skillDir = installedSkillDir(agent, global, cwd);
|
|
502
|
+
if (!(0, node_fs.existsSync)(skillDir)) continue;
|
|
503
503
|
removedAgents.push(agent);
|
|
504
|
-
|
|
504
|
+
dirsToRemove.add(skillDir);
|
|
505
505
|
}
|
|
506
506
|
for (const skillDir of dirsToRemove) (0, node_fs.rmSync)(skillDir, {
|
|
507
507
|
recursive: true,
|
|
@@ -512,7 +512,7 @@ const removeSkill = async (cwd = process.cwd()) => {
|
|
|
512
512
|
};
|
|
513
513
|
//#endregion
|
|
514
514
|
//#region src/commands/add.ts
|
|
515
|
-
const VERSION$5 = "0.1.
|
|
515
|
+
const VERSION$5 = "0.1.43";
|
|
516
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) => {
|
|
517
517
|
console.log(`${picocolors.default.magenta("✿")} ${picocolors.default.bold("React Grab")} ${picocolors.default.gray(VERSION$5)}`);
|
|
518
518
|
console.log();
|
|
@@ -1111,7 +1111,9 @@ const previewCdnTransform = (projectRoot, framework, nextRouterType, targetCdnDo
|
|
|
1111
1111
|
//#region src/utils/constants.ts
|
|
1112
1112
|
const MAX_KEY_HOLD_DURATION_MS = 2e3;
|
|
1113
1113
|
const DEFAULT_WATCH_DIR = ".react-grab";
|
|
1114
|
-
const
|
|
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;
|
|
1115
1117
|
//#endregion
|
|
1116
1118
|
//#region src/utils/format-activation-key.ts
|
|
1117
1119
|
const formatActivationKeyDisplay = (activationKey) => {
|
|
@@ -1129,7 +1131,7 @@ const formatActivationKeyDisplay = (activationKey) => {
|
|
|
1129
1131
|
};
|
|
1130
1132
|
//#endregion
|
|
1131
1133
|
//#region src/commands/configure.ts
|
|
1132
|
-
const VERSION$4 = "0.1.
|
|
1134
|
+
const VERSION$4 = "0.1.43";
|
|
1133
1135
|
const isMac = process.platform === "darwin";
|
|
1134
1136
|
const META_LABEL = isMac ? "Cmd" : "Win";
|
|
1135
1137
|
const ALT_LABEL = isMac ? "Option" : "Alt";
|
|
@@ -1747,11 +1749,18 @@ const installPackagesWithFeedback = async (packages, packageManager, projectRoot
|
|
|
1747
1749
|
}
|
|
1748
1750
|
};
|
|
1749
1751
|
//#endregion
|
|
1752
|
+
//#region src/utils/is-telemetry-enabled.ts
|
|
1753
|
+
const isTelemetryEnabled = () => {
|
|
1754
|
+
const doNotTrack = process.env.DO_NOT_TRACK;
|
|
1755
|
+
return doNotTrack !== "1" && doNotTrack !== "true";
|
|
1756
|
+
};
|
|
1757
|
+
//#endregion
|
|
1750
1758
|
//#region src/commands/init.ts
|
|
1751
|
-
const VERSION$3 = "0.1.
|
|
1759
|
+
const VERSION$3 = "0.1.43";
|
|
1752
1760
|
const REPORT_URL = "https://react-grab.com/api/report-cli";
|
|
1753
1761
|
const DOCS_URL = "https://github.com/aidenybai/react-grab";
|
|
1754
1762
|
const reportToCli = (type, config, error) => {
|
|
1763
|
+
if (!isTelemetryEnabled()) return;
|
|
1755
1764
|
fetch(REPORT_URL, {
|
|
1756
1765
|
method: "POST",
|
|
1757
1766
|
headers: { "Content-Type": "application/json" },
|
|
@@ -2105,6 +2114,8 @@ const sleep = (durationMs) => new Promise((resolve) => setTimeout(resolve, durat
|
|
|
2105
2114
|
//#endregion
|
|
2106
2115
|
//#region src/utils/clipboard.ts
|
|
2107
2116
|
const HISTORY_FILE_NAME = "history.jsonl";
|
|
2117
|
+
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.";
|
|
2118
|
+
const READERS_DIR = node_path.default.dirname((0, node_url.fileURLToPath)(require("url").pathToFileURL(__filename).href));
|
|
2108
2119
|
const READ_TIMEOUT_MS = 2500;
|
|
2109
2120
|
const MAX_CLIPBOARD_BYTES = 64 * 1024 * 1024;
|
|
2110
2121
|
const ID_RADIX = 36;
|
|
@@ -2198,7 +2209,7 @@ const compileSwiftReader = (readersDir, workDir) => {
|
|
|
2198
2209
|
return binary;
|
|
2199
2210
|
};
|
|
2200
2211
|
const createDarwinReader = (options) => {
|
|
2201
|
-
const binary = options.textOnly ? null : compileSwiftReader(
|
|
2212
|
+
const binary = options.textOnly ? null : compileSwiftReader(READERS_DIR, options.workDir);
|
|
2202
2213
|
if (binary) return {
|
|
2203
2214
|
mode: "darwin-native",
|
|
2204
2215
|
read: () => {
|
|
@@ -2281,7 +2292,7 @@ const detectPowershell = () => hasCommand("pwsh") ? "pwsh" : hasCommand("powersh
|
|
|
2281
2292
|
const createWindowsReader = (options) => {
|
|
2282
2293
|
const shell = detectPowershell();
|
|
2283
2294
|
if (!shell) return null;
|
|
2284
|
-
const scriptPath = node_path.default.join(
|
|
2295
|
+
const scriptPath = node_path.default.join(READERS_DIR, "read-clipboard.ps1");
|
|
2285
2296
|
if (options.textOnly || !node_fs.default.existsSync(scriptPath)) return {
|
|
2286
2297
|
mode: "win-text",
|
|
2287
2298
|
read: () => {
|
|
@@ -2420,108 +2431,291 @@ const runWatchLoop = async (options) => {
|
|
|
2420
2431
|
}
|
|
2421
2432
|
};
|
|
2422
2433
|
//#endregion
|
|
2434
|
+
//#region src/utils/daemon.ts
|
|
2435
|
+
const PID_FILE_NAME = "watch.pid";
|
|
2436
|
+
const pidFilePath = (dir) => node_path.default.join(dir, PID_FILE_NAME);
|
|
2437
|
+
const cliEntryPath = () => process.argv[1] ?? (0, node_url.fileURLToPath)(require("url").pathToFileURL(__filename).href);
|
|
2438
|
+
const isProcessAlive = (pid) => {
|
|
2439
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
2440
|
+
try {
|
|
2441
|
+
process.kill(pid, 0);
|
|
2442
|
+
return true;
|
|
2443
|
+
} catch (error) {
|
|
2444
|
+
return error.code === "EPERM";
|
|
2445
|
+
}
|
|
2446
|
+
};
|
|
2447
|
+
const readDaemonPid = (dir) => {
|
|
2448
|
+
try {
|
|
2449
|
+
const pid = Number.parseInt(node_fs.default.readFileSync(pidFilePath(dir), "utf8").trim(), 10);
|
|
2450
|
+
return Number.isInteger(pid) && pid > 0 ? pid : null;
|
|
2451
|
+
} catch {
|
|
2452
|
+
return null;
|
|
2453
|
+
}
|
|
2454
|
+
};
|
|
2455
|
+
const isDaemonRunning = (dir) => {
|
|
2456
|
+
const pid = readDaemonPid(dir);
|
|
2457
|
+
return pid !== null && isProcessAlive(pid);
|
|
2458
|
+
};
|
|
2459
|
+
const claimDaemon = (dir) => {
|
|
2460
|
+
const file = pidFilePath(dir);
|
|
2461
|
+
for (let attempt = 0; attempt < 50; attempt += 1) try {
|
|
2462
|
+
const handle = node_fs.default.openSync(file, "wx");
|
|
2463
|
+
node_fs.default.writeFileSync(handle, String(process.pid));
|
|
2464
|
+
node_fs.default.closeSync(handle);
|
|
2465
|
+
return readDaemonPid(dir) === process.pid;
|
|
2466
|
+
} catch (error) {
|
|
2467
|
+
if (error.code !== "EEXIST") throw error;
|
|
2468
|
+
if (isDaemonRunning(dir)) return false;
|
|
2469
|
+
try {
|
|
2470
|
+
node_fs.default.rmSync(file, { force: true });
|
|
2471
|
+
} catch {}
|
|
2472
|
+
}
|
|
2473
|
+
return false;
|
|
2474
|
+
};
|
|
2475
|
+
const releaseDaemon = (dir) => {
|
|
2476
|
+
if (readDaemonPid(dir) === process.pid) try {
|
|
2477
|
+
node_fs.default.rmSync(pidFilePath(dir), { force: true });
|
|
2478
|
+
} catch {}
|
|
2479
|
+
};
|
|
2480
|
+
const stopDaemon = (dir) => {
|
|
2481
|
+
const pid = readDaemonPid(dir);
|
|
2482
|
+
if (pid === null) return null;
|
|
2483
|
+
const wasAlive = isProcessAlive(pid);
|
|
2484
|
+
if (wasAlive) try {
|
|
2485
|
+
process.kill(pid, "SIGTERM");
|
|
2486
|
+
} catch {}
|
|
2487
|
+
if (readDaemonPid(dir) === pid) try {
|
|
2488
|
+
node_fs.default.rmSync(pidFilePath(dir), { force: true });
|
|
2489
|
+
} catch {}
|
|
2490
|
+
return wasAlive ? pid : null;
|
|
2491
|
+
};
|
|
2492
|
+
const spawnDaemon = (options) => {
|
|
2493
|
+
const args = [
|
|
2494
|
+
cliEntryPath(),
|
|
2495
|
+
"watch",
|
|
2496
|
+
"--dir",
|
|
2497
|
+
options.dir,
|
|
2498
|
+
"--interval",
|
|
2499
|
+
String(options.intervalMs)
|
|
2500
|
+
];
|
|
2501
|
+
if (options.textOnly) args.push("--text-only");
|
|
2502
|
+
if (options.replayLast) args.push("--replay-last");
|
|
2503
|
+
(0, node_child_process.spawn)(process.execPath, args, {
|
|
2504
|
+
detached: true,
|
|
2505
|
+
stdio: "ignore",
|
|
2506
|
+
windowsHide: true
|
|
2507
|
+
}).unref();
|
|
2508
|
+
};
|
|
2509
|
+
const ensureDaemon = (options) => {
|
|
2510
|
+
if (isDaemonRunning(options.dir)) return "already-running";
|
|
2511
|
+
if (!createReader({
|
|
2512
|
+
textOnly: options.textOnly,
|
|
2513
|
+
workDir: options.dir
|
|
2514
|
+
})) return "no-reader";
|
|
2515
|
+
spawnDaemon(options);
|
|
2516
|
+
return "started";
|
|
2517
|
+
};
|
|
2518
|
+
//#endregion
|
|
2423
2519
|
//#region src/utils/grab-log.ts
|
|
2424
2520
|
const CURSOR_FILE_NAME = "cursor.txt";
|
|
2521
|
+
const NEWLINE_BYTE = 10;
|
|
2425
2522
|
const cursorFilePath = (dir) => node_path.default.join(dir, CURSOR_FILE_NAME);
|
|
2426
|
-
const
|
|
2427
|
-
|
|
2523
|
+
const historyFilePath = (dir) => node_path.default.join(dir, HISTORY_FILE_NAME);
|
|
2524
|
+
const fileSize = (filePath) => {
|
|
2428
2525
|
try {
|
|
2429
|
-
|
|
2526
|
+
return node_fs.default.statSync(filePath).size;
|
|
2430
2527
|
} catch {
|
|
2431
|
-
return
|
|
2528
|
+
return 0;
|
|
2432
2529
|
}
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2530
|
+
};
|
|
2531
|
+
const readHistoryRange = (dir, start, length) => {
|
|
2532
|
+
if (length <= 0) return "";
|
|
2533
|
+
const fd = node_fs.default.openSync(historyFilePath(dir), "r");
|
|
2534
|
+
try {
|
|
2535
|
+
const buffer = Buffer.allocUnsafe(length);
|
|
2536
|
+
const bytesRead = node_fs.default.readSync(fd, buffer, 0, length, start);
|
|
2537
|
+
return buffer.toString("utf8", 0, bytesRead);
|
|
2538
|
+
} finally {
|
|
2539
|
+
node_fs.default.closeSync(fd);
|
|
2540
|
+
}
|
|
2541
|
+
};
|
|
2542
|
+
const writeGrabCursor = (dir, offset) => {
|
|
2543
|
+
const target = cursorFilePath(dir);
|
|
2544
|
+
const tempPath = `${target}.${process.pid}.tmp`;
|
|
2545
|
+
node_fs.default.writeFileSync(tempPath, JSON.stringify({ offset }));
|
|
2546
|
+
node_fs.default.renameSync(tempPath, target);
|
|
2547
|
+
};
|
|
2548
|
+
const byteOffsetAfterLines = (dir, lineCount) => {
|
|
2549
|
+
const filePath = historyFilePath(dir);
|
|
2550
|
+
const size = fileSize(filePath);
|
|
2551
|
+
if (lineCount <= 0 || size === 0) return 0;
|
|
2552
|
+
const fd = node_fs.default.openSync(filePath, "r");
|
|
2553
|
+
try {
|
|
2554
|
+
const buffer = Buffer.allocUnsafe(MIGRATION_SCAN_CHUNK_BYTES);
|
|
2555
|
+
let position = 0;
|
|
2556
|
+
let seen = 0;
|
|
2557
|
+
while (position < size) {
|
|
2558
|
+
const bytesRead = node_fs.default.readSync(fd, buffer, 0, MIGRATION_SCAN_CHUNK_BYTES, position);
|
|
2559
|
+
if (bytesRead <= 0) break;
|
|
2560
|
+
for (let index = 0; index < bytesRead; index += 1) {
|
|
2561
|
+
if (buffer[index] !== NEWLINE_BYTE) continue;
|
|
2562
|
+
seen += 1;
|
|
2563
|
+
if (seen === lineCount) return position + index + 1;
|
|
2564
|
+
}
|
|
2565
|
+
position += bytesRead;
|
|
2566
|
+
}
|
|
2567
|
+
} finally {
|
|
2568
|
+
node_fs.default.closeSync(fd);
|
|
2569
|
+
}
|
|
2570
|
+
return size;
|
|
2436
2571
|
};
|
|
2437
2572
|
const readGrabCursor = (dir) => {
|
|
2573
|
+
let raw;
|
|
2438
2574
|
try {
|
|
2439
|
-
|
|
2440
|
-
return Number.isInteger(value) && value >= 0 ? value : 0;
|
|
2575
|
+
raw = node_fs.default.readFileSync(cursorFilePath(dir), "utf8").trim();
|
|
2441
2576
|
} catch {
|
|
2442
2577
|
return 0;
|
|
2443
2578
|
}
|
|
2444
|
-
|
|
2445
|
-
|
|
2579
|
+
if (raw === "") return 0;
|
|
2580
|
+
let parsed;
|
|
2446
2581
|
try {
|
|
2447
|
-
|
|
2448
|
-
return typeof value === "number" ? value : null;
|
|
2582
|
+
parsed = JSON.parse(raw);
|
|
2449
2583
|
} catch {
|
|
2450
|
-
return
|
|
2584
|
+
return 0;
|
|
2451
2585
|
}
|
|
2586
|
+
if (typeof parsed === "number") {
|
|
2587
|
+
if (!Number.isInteger(parsed) || parsed < 0) return 0;
|
|
2588
|
+
const offset = byteOffsetAfterLines(dir, parsed);
|
|
2589
|
+
writeGrabCursor(dir, offset);
|
|
2590
|
+
return offset;
|
|
2591
|
+
}
|
|
2592
|
+
return typeof parsed.offset === "number" && Number.isInteger(parsed.offset) && parsed.offset >= 0 ? parsed.offset : 0;
|
|
2593
|
+
};
|
|
2594
|
+
const readCompleteGrabLines = (dir) => {
|
|
2595
|
+
const size = fileSize(historyFilePath(dir));
|
|
2596
|
+
if (size === 0) return [];
|
|
2597
|
+
const raw = readHistoryRange(dir, 0, size);
|
|
2598
|
+
const lastNewline = raw.lastIndexOf("\n");
|
|
2599
|
+
if (lastNewline < 0) return [];
|
|
2600
|
+
return raw.slice(0, lastNewline).split("\n").filter(Boolean);
|
|
2452
2601
|
};
|
|
2453
2602
|
const consumeGrabs = (dir, options) => {
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
const maxAgeMs = options.maxAgeMs ?? 0;
|
|
2603
|
+
if (options.all) return readCompleteGrabLines(dir);
|
|
2604
|
+
const size = fileSize(historyFilePath(dir));
|
|
2457
2605
|
const cursor = readGrabCursor(dir);
|
|
2458
|
-
const
|
|
2606
|
+
const start = cursor > size ? 0 : cursor;
|
|
2607
|
+
if (start >= size) {
|
|
2608
|
+
if (start !== cursor) writeGrabCursor(dir, start);
|
|
2609
|
+
return [];
|
|
2610
|
+
}
|
|
2611
|
+
const chunk = readHistoryRange(dir, start, Math.min(size - start, MAX_READ_HISTORY_BYTES));
|
|
2612
|
+
const lastNewline = chunk.lastIndexOf("\n");
|
|
2613
|
+
if (lastNewline < 0) return [];
|
|
2459
2614
|
const now = Date.now();
|
|
2460
2615
|
const fresh = [];
|
|
2461
|
-
let
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2616
|
+
let consumedBytes = 0;
|
|
2617
|
+
let lineStart = 0;
|
|
2618
|
+
while (lineStart <= lastNewline && (options.limit <= 0 || fresh.length < options.limit)) {
|
|
2619
|
+
const newlineIndex = chunk.indexOf("\n", lineStart);
|
|
2620
|
+
const line = chunk.slice(lineStart, newlineIndex);
|
|
2621
|
+
consumedBytes += Buffer.byteLength(chunk.slice(lineStart, newlineIndex + 1), "utf8");
|
|
2622
|
+
lineStart = newlineIndex + 1;
|
|
2623
|
+
if (line.length === 0) continue;
|
|
2624
|
+
let parsed;
|
|
2625
|
+
try {
|
|
2626
|
+
parsed = JSON.parse(line);
|
|
2627
|
+
} catch {
|
|
2628
|
+
continue;
|
|
2629
|
+
}
|
|
2630
|
+
if (options.maxAgeMs > 0 && typeof parsed.receivedAt === "number") {
|
|
2631
|
+
if (now - parsed.receivedAt > options.maxAgeMs) continue;
|
|
2468
2632
|
}
|
|
2469
2633
|
fresh.push(line);
|
|
2470
2634
|
}
|
|
2471
|
-
|
|
2635
|
+
const nextCursor = start + consumedBytes;
|
|
2636
|
+
if (nextCursor !== cursor) writeGrabCursor(dir, nextCursor);
|
|
2472
2637
|
return fresh;
|
|
2473
2638
|
};
|
|
2474
2639
|
//#endregion
|
|
2475
|
-
//#region src/
|
|
2640
|
+
//#region src/utils/read-args.ts
|
|
2476
2641
|
const parseWaitMs = (raw) => {
|
|
2477
|
-
if (
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2642
|
+
if (raw === void 0) return 0;
|
|
2643
|
+
const trimmed = raw.trim();
|
|
2644
|
+
if (trimmed === "") return 0;
|
|
2645
|
+
if (/^(inf|infinite|infinity|forever)$/i.test(trimmed)) return Number.POSITIVE_INFINITY;
|
|
2646
|
+
const ms = Number(trimmed);
|
|
2647
|
+
return Number.isFinite(ms) && ms >= 0 ? ms : null;
|
|
2648
|
+
};
|
|
2649
|
+
const parseNonNegativeInt = (raw) => {
|
|
2650
|
+
if (raw === void 0) return null;
|
|
2651
|
+
const trimmed = raw.trim();
|
|
2652
|
+
if (trimmed === "") return null;
|
|
2653
|
+
const value = Number(trimmed);
|
|
2654
|
+
return Number.isInteger(value) && value >= 0 ? value : null;
|
|
2655
|
+
};
|
|
2656
|
+
//#endregion
|
|
2657
|
+
//#region src/commands/pull.ts
|
|
2658
|
+
const fail = (message) => {
|
|
2659
|
+
process.stderr.write(`react-grab pull: ${message}\n`);
|
|
2660
|
+
process.exit(1);
|
|
2481
2661
|
};
|
|
2482
|
-
const
|
|
2483
|
-
|
|
2484
|
-
return Number.isInteger(value) && value >= 0 ? value : fallback;
|
|
2662
|
+
const emitAndExit = (lines) => {
|
|
2663
|
+
process.stdout.write(`${lines.join("\n")}\n`, () => process.exit(0));
|
|
2485
2664
|
};
|
|
2486
|
-
const
|
|
2665
|
+
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) => {
|
|
2487
2666
|
const dir = node_path.default.resolve(options.dir);
|
|
2488
2667
|
try {
|
|
2489
2668
|
prepareWorkDir(dir);
|
|
2490
2669
|
} catch (error) {
|
|
2491
|
-
|
|
2492
|
-
process.exit(1);
|
|
2670
|
+
fail(String(error?.message ?? error));
|
|
2493
2671
|
}
|
|
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
2672
|
const waitMs = parseWaitMs(options.wait);
|
|
2508
|
-
|
|
2509
|
-
|
|
2673
|
+
if (waitMs === null) fail(`invalid --wait "${options.wait}" (use milliseconds or "infinite")`);
|
|
2674
|
+
const limit = parseNonNegativeInt(options.limit);
|
|
2675
|
+
if (limit === null) fail(`invalid --limit "${options.limit}" (use a non-negative integer)`);
|
|
2676
|
+
const maxAgeMs = parseNonNegativeInt(options.maxAge);
|
|
2677
|
+
if (maxAgeMs === null) fail(`invalid --max-age "${options.maxAge}" (use milliseconds, 0 to disable)`);
|
|
2678
|
+
const all = Boolean(options.all);
|
|
2679
|
+
if (ensureDaemon({
|
|
2680
|
+
dir,
|
|
2681
|
+
intervalMs: 800,
|
|
2682
|
+
textOnly: Boolean(options.textOnly),
|
|
2683
|
+
replayLast: false
|
|
2684
|
+
}) === "no-reader") fail(NO_READER_MESSAGE);
|
|
2685
|
+
unrefStdin();
|
|
2686
|
+
const consume = () => consumeGrabs(dir, {
|
|
2687
|
+
limit,
|
|
2688
|
+
all,
|
|
2689
|
+
maxAgeMs
|
|
2690
|
+
});
|
|
2691
|
+
const first = consume();
|
|
2692
|
+
if (first.length > 0) {
|
|
2693
|
+
emitAndExit(first);
|
|
2694
|
+
return;
|
|
2695
|
+
}
|
|
2696
|
+
const deadline = Date.now() + waitMs;
|
|
2510
2697
|
while (Date.now() < deadline) {
|
|
2511
2698
|
await sleep(200);
|
|
2512
|
-
|
|
2699
|
+
const batch = consume();
|
|
2700
|
+
if (batch.length > 0) {
|
|
2701
|
+
emitAndExit(batch);
|
|
2702
|
+
return;
|
|
2703
|
+
}
|
|
2513
2704
|
}
|
|
2514
2705
|
process.exit(0);
|
|
2515
2706
|
});
|
|
2516
2707
|
//#endregion
|
|
2517
2708
|
//#region src/commands/remove.ts
|
|
2518
|
-
const VERSION$2 = "0.1.
|
|
2519
|
-
const remove = new commander.Command().name("remove").description("uninstall the React Grab skill from your agent").action(async () => {
|
|
2709
|
+
const VERSION$2 = "0.1.43";
|
|
2710
|
+
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) => {
|
|
2520
2711
|
console.log(`${picocolors.default.magenta("✿")} ${picocolors.default.bold("React Grab")} ${picocolors.default.gray(VERSION$2)}`);
|
|
2521
2712
|
console.log();
|
|
2522
2713
|
try {
|
|
2523
2714
|
logger.break();
|
|
2524
|
-
const removedCount = await removeSkill(
|
|
2715
|
+
const removedCount = await removeSkill({
|
|
2716
|
+
cwd: (0, node_path.resolve)(opts.cwd),
|
|
2717
|
+
global: opts.global
|
|
2718
|
+
});
|
|
2525
2719
|
logger.break();
|
|
2526
2720
|
if (removedCount === 0) logger.log("React Grab skill is not installed in any detected agent.");
|
|
2527
2721
|
else logger.log(`${highlighter.success("Removed")} the React Grab skill from ${removedCount} agent${removedCount === 1 ? "" : "s"}.`);
|
|
@@ -2531,8 +2725,16 @@ const remove = new commander.Command().name("remove").description("uninstall the
|
|
|
2531
2725
|
}
|
|
2532
2726
|
});
|
|
2533
2727
|
//#endregion
|
|
2728
|
+
//#region src/commands/stop.ts
|
|
2729
|
+
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) => {
|
|
2730
|
+
const dir = node_path.default.resolve(options.dir);
|
|
2731
|
+
const stoppedPid = stopDaemon(dir);
|
|
2732
|
+
process.stderr.write(stoppedPid ? `react-grab stop: stopped watcher (pid ${stoppedPid})\n` : `react-grab stop: no watcher running for ${dir}\n`);
|
|
2733
|
+
process.exit(0);
|
|
2734
|
+
});
|
|
2735
|
+
//#endregion
|
|
2534
2736
|
//#region src/commands/upgrade.ts
|
|
2535
|
-
const VERSION$1 = "0.1.
|
|
2737
|
+
const VERSION$1 = "0.1.43";
|
|
2536
2738
|
const NPM_REGISTRY_URL = "https://registry.npmjs.org/react-grab/latest";
|
|
2537
2739
|
const fetchLatestVersion = async () => {
|
|
2538
2740
|
try {
|
|
@@ -2606,95 +2808,24 @@ const upgrade = new commander.Command().name("upgrade").alias("update").descript
|
|
|
2606
2808
|
}
|
|
2607
2809
|
});
|
|
2608
2810
|
//#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
2811
|
//#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
2812
|
const writeStatus = (message) => {
|
|
2690
2813
|
process.stderr.write(`react-grab watch: ${message}\n`);
|
|
2691
2814
|
};
|
|
2692
|
-
const
|
|
2815
|
+
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) => {
|
|
2816
|
+
const dir = node_path.default.resolve(options.dir);
|
|
2817
|
+
const intervalRaw = Number(options.interval);
|
|
2818
|
+
const intervalMs = Number.isFinite(intervalRaw) && intervalRaw > 0 ? intervalRaw : 800;
|
|
2819
|
+
try {
|
|
2820
|
+
prepareWorkDir(dir);
|
|
2821
|
+
} catch (error) {
|
|
2822
|
+
writeStatus(String(error?.message ?? error));
|
|
2823
|
+
process.exit(1);
|
|
2824
|
+
}
|
|
2693
2825
|
if (!claimDaemon(dir)) process.exit(0);
|
|
2694
2826
|
process.on("exit", () => releaseDaemon(dir));
|
|
2695
2827
|
const reader = createReader({
|
|
2696
|
-
textOnly,
|
|
2697
|
-
readersDir: readersDir(),
|
|
2828
|
+
textOnly: Boolean(options.textOnly),
|
|
2698
2829
|
workDir: dir
|
|
2699
2830
|
});
|
|
2700
2831
|
if (!reader) {
|
|
@@ -2706,63 +2837,21 @@ const runForeground = (dir, intervalMs, textOnly, replayLast) => {
|
|
|
2706
2837
|
reader,
|
|
2707
2838
|
dir,
|
|
2708
2839
|
intervalMs,
|
|
2709
|
-
replayLast,
|
|
2840
|
+
replayLast: Boolean(options.replayLast),
|
|
2710
2841
|
onWarn: writeStatus
|
|
2711
2842
|
}).catch((error) => {
|
|
2712
2843
|
writeStatus(String(error?.message ?? error));
|
|
2713
2844
|
process.exit(1);
|
|
2714
2845
|
});
|
|
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);
|
|
2757
2846
|
});
|
|
2758
2847
|
//#endregion
|
|
2759
2848
|
//#region src/cli.ts
|
|
2760
|
-
const VERSION = "0.1.
|
|
2849
|
+
const VERSION = "0.1.43";
|
|
2761
2850
|
const VERSION_API_URL = "https://www.react-grab.com/api/version";
|
|
2762
2851
|
process.on("SIGINT", () => process.exit(0));
|
|
2763
2852
|
process.on("SIGTERM", () => process.exit(0));
|
|
2764
2853
|
try {
|
|
2765
|
-
fetch(`${VERSION_API_URL}?source=cli&v=${VERSION}&t=${Date.now()}`).catch(() => {});
|
|
2854
|
+
if (isTelemetryEnabled()) fetch(`${VERSION_API_URL}?source=cli&v=${VERSION}&t=${Date.now()}`).catch(() => {});
|
|
2766
2855
|
} catch {}
|
|
2767
2856
|
const program = new commander.Command().name("grab").description("add React Grab to your project").version(VERSION, "-v, --version", "display the version number");
|
|
2768
2857
|
program.addCommand(init);
|
|
@@ -2770,8 +2859,9 @@ program.addCommand(add);
|
|
|
2770
2859
|
program.addCommand(remove);
|
|
2771
2860
|
program.addCommand(configure);
|
|
2772
2861
|
program.addCommand(upgrade);
|
|
2773
|
-
program.addCommand(
|
|
2774
|
-
program.addCommand(
|
|
2862
|
+
program.addCommand(pull);
|
|
2863
|
+
program.addCommand(stop);
|
|
2864
|
+
program.addCommand(watch, { hidden: true });
|
|
2775
2865
|
const main = async () => {
|
|
2776
2866
|
await program.parseAsync();
|
|
2777
2867
|
};
|