@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 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 present = [installedSkillDir(agent, false, cwd), installedSkillDir(agent, true, cwd)].filter((dir) => (0, node_fs.existsSync)(dir));
502
- if (present.length === 0) continue;
501
+ const skillDir = installedSkillDir(agent, global, cwd);
502
+ if (!(0, node_fs.existsSync)(skillDir)) continue;
503
503
  removedAgents.push(agent);
504
- for (const dir of present) dirsToRemove.add(dir);
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.41";
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 MAX_GRAB_AGE_MS = 300 * 1e3;
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.41";
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.41";
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(options.readersDir, options.workDir);
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(options.readersDir, "read-clipboard.ps1");
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 readCompleteGrabLines = (dir) => {
2427
- let raw;
2523
+ const historyFilePath = (dir) => node_path.default.join(dir, HISTORY_FILE_NAME);
2524
+ const fileSize = (filePath) => {
2428
2525
  try {
2429
- raw = node_fs.default.readFileSync(node_path.default.join(dir, HISTORY_FILE_NAME), "utf8");
2526
+ return node_fs.default.statSync(filePath).size;
2430
2527
  } catch {
2431
- return [];
2528
+ return 0;
2432
2529
  }
2433
- const lastNewline = raw.lastIndexOf("\n");
2434
- if (lastNewline < 0) return [];
2435
- return raw.slice(0, lastNewline).split("\n").filter(Boolean);
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
- const value = Number.parseInt(node_fs.default.readFileSync(cursorFilePath(dir), "utf8").trim(), 10);
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
- const grabReceivedAt = (line) => {
2579
+ if (raw === "") return 0;
2580
+ let parsed;
2446
2581
  try {
2447
- const value = JSON.parse(line).receivedAt;
2448
- return typeof value === "number" ? value : null;
2582
+ parsed = JSON.parse(raw);
2449
2583
  } catch {
2450
- return null;
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
- const lines = readCompleteGrabLines(dir);
2455
- if (options.all) return lines;
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 total = lines.length;
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 index = Math.min(cursor, total);
2462
- while (index < total && (options.limit <= 0 || fresh.length < options.limit)) {
2463
- const line = lines[index];
2464
- index += 1;
2465
- if (maxAgeMs > 0) {
2466
- const receivedAt = grabReceivedAt(line);
2467
- if (receivedAt !== null && now - receivedAt > maxAgeMs) continue;
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
- if (index !== cursor) node_fs.default.writeFileSync(cursorFilePath(dir), String(index));
2635
+ const nextCursor = start + consumedBytes;
2636
+ if (nextCursor !== cursor) writeGrabCursor(dir, nextCursor);
2472
2637
  return fresh;
2473
2638
  };
2474
2639
  //#endregion
2475
- //#region src/commands/read.ts
2640
+ //#region src/utils/read-args.ts
2476
2641
  const parseWaitMs = (raw) => {
2477
- if (!raw) return 0;
2478
- if (/^(inf|infinite|forever)$/i.test(raw.trim())) return Number.POSITIVE_INFINITY;
2479
- const ms = Number(raw);
2480
- return Number.isFinite(ms) && ms > 0 ? ms : 0;
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 parseNonNegativeInt = (raw, fallback) => {
2483
- const value = Number(raw);
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 read = new commander.Command().name("read").description("print React Grab selections captured since the last read, then advance the cursor").option("-d, --dir <dir>", "work dir holding history.jsonl + cursor.txt", DEFAULT_WATCH_DIR).option("-w, --wait <ms>", "block up to <ms> (or 'infinite') for a new grab (default: no wait)").option("-n, --limit <count>", "max grabs to print per call (0 = no limit); the cursor only advances past what is printed", String(50)).option("--max-age <ms>", "skip grabs captured longer ago than <ms> (0 = never evict)", String(MAX_GRAB_AGE_MS)).option("--all", "print the entire history without advancing the cursor").action(async (options) => {
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
- process.stderr.write(`react-grab read: ${String(error?.message ?? error)}\n`);
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
- const deadline = waitMs === Number.POSITIVE_INFINITY ? Number.POSITIVE_INFINITY : Date.now() + waitMs;
2509
- if (drain() > 0) process.exit(0);
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
- if (drain() > 0) break;
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.41";
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.41";
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 runForeground = (dir, intervalMs, textOnly, replayLast) => {
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.41";
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(watch);
2774
- program.addCommand(read);
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
  };