@react-grab/cli 0.1.41 → 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 +278 -195
- package/dist/cli.js +278 -195
- package/dist/cli.js.map +1 -1
- package/package.json +1 -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.42";
|
|
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.42";
|
|
1133
1135
|
const isMac = process.platform === "darwin";
|
|
1134
1136
|
const META_LABEL = isMac ? "Cmd" : "Win";
|
|
1135
1137
|
const ALT_LABEL = isMac ? "Option" : "Alt";
|
|
@@ -1748,7 +1750,7 @@ const installPackagesWithFeedback = async (packages, packageManager, projectRoot
|
|
|
1748
1750
|
};
|
|
1749
1751
|
//#endregion
|
|
1750
1752
|
//#region src/commands/init.ts
|
|
1751
|
-
const VERSION$3 = "0.1.
|
|
1753
|
+
const VERSION$3 = "0.1.42";
|
|
1752
1754
|
const REPORT_URL = "https://react-grab.com/api/report-cli";
|
|
1753
1755
|
const DOCS_URL = "https://github.com/aidenybai/react-grab";
|
|
1754
1756
|
const reportToCli = (type, config, error) => {
|
|
@@ -2105,6 +2107,8 @@ const sleep = (durationMs) => new Promise((resolve) => setTimeout(resolve, durat
|
|
|
2105
2107
|
//#endregion
|
|
2106
2108
|
//#region src/utils/clipboard.ts
|
|
2107
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));
|
|
2108
2112
|
const READ_TIMEOUT_MS = 2500;
|
|
2109
2113
|
const MAX_CLIPBOARD_BYTES = 64 * 1024 * 1024;
|
|
2110
2114
|
const ID_RADIX = 36;
|
|
@@ -2198,7 +2202,7 @@ const compileSwiftReader = (readersDir, workDir) => {
|
|
|
2198
2202
|
return binary;
|
|
2199
2203
|
};
|
|
2200
2204
|
const createDarwinReader = (options) => {
|
|
2201
|
-
const binary = options.textOnly ? null : compileSwiftReader(
|
|
2205
|
+
const binary = options.textOnly ? null : compileSwiftReader(READERS_DIR, options.workDir);
|
|
2202
2206
|
if (binary) return {
|
|
2203
2207
|
mode: "darwin-native",
|
|
2204
2208
|
read: () => {
|
|
@@ -2281,7 +2285,7 @@ const detectPowershell = () => hasCommand("pwsh") ? "pwsh" : hasCommand("powersh
|
|
|
2281
2285
|
const createWindowsReader = (options) => {
|
|
2282
2286
|
const shell = detectPowershell();
|
|
2283
2287
|
if (!shell) return null;
|
|
2284
|
-
const scriptPath = node_path.default.join(
|
|
2288
|
+
const scriptPath = node_path.default.join(READERS_DIR, "read-clipboard.ps1");
|
|
2285
2289
|
if (options.textOnly || !node_fs.default.existsSync(scriptPath)) return {
|
|
2286
2290
|
mode: "win-text",
|
|
2287
2291
|
read: () => {
|
|
@@ -2420,108 +2424,291 @@ const runWatchLoop = async (options) => {
|
|
|
2420
2424
|
}
|
|
2421
2425
|
};
|
|
2422
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
|
|
2423
2512
|
//#region src/utils/grab-log.ts
|
|
2424
2513
|
const CURSOR_FILE_NAME = "cursor.txt";
|
|
2514
|
+
const NEWLINE_BYTE = 10;
|
|
2425
2515
|
const cursorFilePath = (dir) => node_path.default.join(dir, CURSOR_FILE_NAME);
|
|
2426
|
-
const
|
|
2427
|
-
|
|
2516
|
+
const historyFilePath = (dir) => node_path.default.join(dir, HISTORY_FILE_NAME);
|
|
2517
|
+
const fileSize = (filePath) => {
|
|
2428
2518
|
try {
|
|
2429
|
-
|
|
2519
|
+
return node_fs.default.statSync(filePath).size;
|
|
2430
2520
|
} catch {
|
|
2431
|
-
return
|
|
2521
|
+
return 0;
|
|
2432
2522
|
}
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
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;
|
|
2436
2564
|
};
|
|
2437
2565
|
const readGrabCursor = (dir) => {
|
|
2566
|
+
let raw;
|
|
2438
2567
|
try {
|
|
2439
|
-
|
|
2440
|
-
return Number.isInteger(value) && value >= 0 ? value : 0;
|
|
2568
|
+
raw = node_fs.default.readFileSync(cursorFilePath(dir), "utf8").trim();
|
|
2441
2569
|
} catch {
|
|
2442
2570
|
return 0;
|
|
2443
2571
|
}
|
|
2444
|
-
|
|
2445
|
-
|
|
2572
|
+
if (raw === "") return 0;
|
|
2573
|
+
let parsed;
|
|
2446
2574
|
try {
|
|
2447
|
-
|
|
2448
|
-
return typeof value === "number" ? value : null;
|
|
2575
|
+
parsed = JSON.parse(raw);
|
|
2449
2576
|
} catch {
|
|
2450
|
-
return
|
|
2577
|
+
return 0;
|
|
2451
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);
|
|
2452
2594
|
};
|
|
2453
2595
|
const consumeGrabs = (dir, options) => {
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
const maxAgeMs = options.maxAgeMs ?? 0;
|
|
2596
|
+
if (options.all) return readCompleteGrabLines(dir);
|
|
2597
|
+
const size = fileSize(historyFilePath(dir));
|
|
2457
2598
|
const cursor = readGrabCursor(dir);
|
|
2458
|
-
const
|
|
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 [];
|
|
2459
2607
|
const now = Date.now();
|
|
2460
2608
|
const fresh = [];
|
|
2461
|
-
let
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
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;
|
|
2468
2625
|
}
|
|
2469
2626
|
fresh.push(line);
|
|
2470
2627
|
}
|
|
2471
|
-
|
|
2628
|
+
const nextCursor = start + consumedBytes;
|
|
2629
|
+
if (nextCursor !== cursor) writeGrabCursor(dir, nextCursor);
|
|
2472
2630
|
return fresh;
|
|
2473
2631
|
};
|
|
2474
2632
|
//#endregion
|
|
2475
|
-
//#region src/
|
|
2633
|
+
//#region src/utils/read-args.ts
|
|
2476
2634
|
const parseWaitMs = (raw) => {
|
|
2477
|
-
if (
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
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;
|
|
2481
2648
|
};
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
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));
|
|
2485
2657
|
};
|
|
2486
|
-
const
|
|
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) => {
|
|
2487
2659
|
const dir = node_path.default.resolve(options.dir);
|
|
2488
2660
|
try {
|
|
2489
2661
|
prepareWorkDir(dir);
|
|
2490
2662
|
} catch (error) {
|
|
2491
|
-
|
|
2492
|
-
process.exit(1);
|
|
2663
|
+
fail(String(error?.message ?? error));
|
|
2493
2664
|
}
|
|
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
2665
|
const waitMs = parseWaitMs(options.wait);
|
|
2508
|
-
|
|
2509
|
-
|
|
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)`);
|
|
2671
|
+
const all = Boolean(options.all);
|
|
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;
|
|
2510
2690
|
while (Date.now() < deadline) {
|
|
2511
2691
|
await sleep(200);
|
|
2512
|
-
|
|
2692
|
+
const batch = consume();
|
|
2693
|
+
if (batch.length > 0) {
|
|
2694
|
+
emitAndExit(batch);
|
|
2695
|
+
return;
|
|
2696
|
+
}
|
|
2513
2697
|
}
|
|
2514
2698
|
process.exit(0);
|
|
2515
2699
|
});
|
|
2516
2700
|
//#endregion
|
|
2517
2701
|
//#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 () => {
|
|
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) => {
|
|
2520
2704
|
console.log(`${picocolors.default.magenta("✿")} ${picocolors.default.bold("React Grab")} ${picocolors.default.gray(VERSION$2)}`);
|
|
2521
2705
|
console.log();
|
|
2522
2706
|
try {
|
|
2523
2707
|
logger.break();
|
|
2524
|
-
const removedCount = await removeSkill(
|
|
2708
|
+
const removedCount = await removeSkill({
|
|
2709
|
+
cwd: (0, node_path.resolve)(opts.cwd),
|
|
2710
|
+
global: opts.global
|
|
2711
|
+
});
|
|
2525
2712
|
logger.break();
|
|
2526
2713
|
if (removedCount === 0) logger.log("React Grab skill is not installed in any detected agent.");
|
|
2527
2714
|
else logger.log(`${highlighter.success("Removed")} the React Grab skill from ${removedCount} agent${removedCount === 1 ? "" : "s"}.`);
|
|
@@ -2531,8 +2718,16 @@ const remove = new commander.Command().name("remove").description("uninstall the
|
|
|
2531
2718
|
}
|
|
2532
2719
|
});
|
|
2533
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
|
|
2534
2729
|
//#region src/commands/upgrade.ts
|
|
2535
|
-
const VERSION$1 = "0.1.
|
|
2730
|
+
const VERSION$1 = "0.1.42";
|
|
2536
2731
|
const NPM_REGISTRY_URL = "https://registry.npmjs.org/react-grab/latest";
|
|
2537
2732
|
const fetchLatestVersion = async () => {
|
|
2538
2733
|
try {
|
|
@@ -2606,95 +2801,24 @@ const upgrade = new commander.Command().name("upgrade").alias("update").descript
|
|
|
2606
2801
|
}
|
|
2607
2802
|
});
|
|
2608
2803
|
//#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
2804
|
//#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
2805
|
const writeStatus = (message) => {
|
|
2690
2806
|
process.stderr.write(`react-grab watch: ${message}\n`);
|
|
2691
2807
|
};
|
|
2692
|
-
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
|
+
}
|
|
2693
2818
|
if (!claimDaemon(dir)) process.exit(0);
|
|
2694
2819
|
process.on("exit", () => releaseDaemon(dir));
|
|
2695
2820
|
const reader = createReader({
|
|
2696
|
-
textOnly,
|
|
2697
|
-
readersDir: readersDir(),
|
|
2821
|
+
textOnly: Boolean(options.textOnly),
|
|
2698
2822
|
workDir: dir
|
|
2699
2823
|
});
|
|
2700
2824
|
if (!reader) {
|
|
@@ -2706,58 +2830,16 @@ const runForeground = (dir, intervalMs, textOnly, replayLast) => {
|
|
|
2706
2830
|
reader,
|
|
2707
2831
|
dir,
|
|
2708
2832
|
intervalMs,
|
|
2709
|
-
replayLast,
|
|
2833
|
+
replayLast: Boolean(options.replayLast),
|
|
2710
2834
|
onWarn: writeStatus
|
|
2711
2835
|
}).catch((error) => {
|
|
2712
2836
|
writeStatus(String(error?.message ?? error));
|
|
2713
2837
|
process.exit(1);
|
|
2714
2838
|
});
|
|
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
2839
|
});
|
|
2758
2840
|
//#endregion
|
|
2759
2841
|
//#region src/cli.ts
|
|
2760
|
-
const VERSION = "0.1.
|
|
2842
|
+
const VERSION = "0.1.42";
|
|
2761
2843
|
const VERSION_API_URL = "https://www.react-grab.com/api/version";
|
|
2762
2844
|
process.on("SIGINT", () => process.exit(0));
|
|
2763
2845
|
process.on("SIGTERM", () => process.exit(0));
|
|
@@ -2770,8 +2852,9 @@ program.addCommand(add);
|
|
|
2770
2852
|
program.addCommand(remove);
|
|
2771
2853
|
program.addCommand(configure);
|
|
2772
2854
|
program.addCommand(upgrade);
|
|
2773
|
-
program.addCommand(
|
|
2774
|
-
program.addCommand(
|
|
2855
|
+
program.addCommand(pull);
|
|
2856
|
+
program.addCommand(stop);
|
|
2857
|
+
program.addCommand(watch, { hidden: true });
|
|
2775
2858
|
const main = async () => {
|
|
2776
2859
|
await program.parseAsync();
|
|
2777
2860
|
};
|