@react-grab/cli 0.1.41 → 0.1.43-dev.5ceaf57

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.js CHANGED
@@ -465,15 +465,15 @@ const promptSkillInstall = async ({ yes = false, global = false, cwd = process.c
465
465
  for (const record of failed) logger.log(` ${highlighter.error("✗")} ${agentLabel(record.agent)} ${record.error}`);
466
466
  return true;
467
467
  };
468
- const removeSkill = async (cwd = process.cwd()) => {
468
+ const removeSkill = async ({ cwd = process.cwd(), global = false } = {}) => {
469
469
  const agents = await detectAvailableAgents();
470
470
  const removedAgents = [];
471
471
  const dirsToRemove = /* @__PURE__ */ new Set();
472
472
  for (const agent of agents) {
473
- const present = [installedSkillDir(agent, false, cwd), installedSkillDir(agent, true, cwd)].filter((dir) => existsSync(dir));
474
- if (present.length === 0) continue;
473
+ const skillDir = installedSkillDir(agent, global, cwd);
474
+ if (!existsSync(skillDir)) continue;
475
475
  removedAgents.push(agent);
476
- for (const dir of present) dirsToRemove.add(dir);
476
+ dirsToRemove.add(skillDir);
477
477
  }
478
478
  for (const skillDir of dirsToRemove) rmSync(skillDir, {
479
479
  recursive: true,
@@ -484,7 +484,7 @@ const removeSkill = async (cwd = process.cwd()) => {
484
484
  };
485
485
  //#endregion
486
486
  //#region src/commands/add.ts
487
- const VERSION$5 = "0.1.41";
487
+ const VERSION$5 = "0.1.43-dev.5ceaf57";
488
488
  const add$1 = new Command().name("add").alias("install").description("install the React Grab skill for your agent").option("-y, --yes", "skip confirmation prompts", false).option("-c, --cwd <cwd>", "working directory (defaults to current directory)", process.cwd()).option("-g, --global", "install the skill globally instead of in the project", false).action(async (opts) => {
489
489
  console.log(`${pc.magenta("✿")} ${pc.bold("React Grab")} ${pc.gray(VERSION$5)}`);
490
490
  console.log();
@@ -1083,7 +1083,9 @@ const previewCdnTransform = (projectRoot, framework, nextRouterType, targetCdnDo
1083
1083
  //#region src/utils/constants.ts
1084
1084
  const MAX_KEY_HOLD_DURATION_MS = 2e3;
1085
1085
  const DEFAULT_WATCH_DIR = ".react-grab";
1086
- const MAX_GRAB_AGE_MS = 300 * 1e3;
1086
+ const DEFAULT_GRAB_AGE_MS = 300 * 1e3;
1087
+ const MAX_READ_HISTORY_BYTES = 128 * 1024 * 1024;
1088
+ const MIGRATION_SCAN_CHUNK_BYTES = 1024 * 1024;
1087
1089
  //#endregion
1088
1090
  //#region src/utils/format-activation-key.ts
1089
1091
  const formatActivationKeyDisplay = (activationKey) => {
@@ -1101,7 +1103,7 @@ const formatActivationKeyDisplay = (activationKey) => {
1101
1103
  };
1102
1104
  //#endregion
1103
1105
  //#region src/commands/configure.ts
1104
- const VERSION$4 = "0.1.41";
1106
+ const VERSION$4 = "0.1.43-dev.5ceaf57";
1105
1107
  const isMac = process.platform === "darwin";
1106
1108
  const META_LABEL = isMac ? "Cmd" : "Win";
1107
1109
  const ALT_LABEL = isMac ? "Option" : "Alt";
@@ -1719,11 +1721,18 @@ const installPackagesWithFeedback = async (packages, packageManager, projectRoot
1719
1721
  }
1720
1722
  };
1721
1723
  //#endregion
1724
+ //#region src/utils/is-telemetry-enabled.ts
1725
+ const isTelemetryEnabled = () => {
1726
+ const doNotTrack = process.env.DO_NOT_TRACK;
1727
+ return doNotTrack !== "1" && doNotTrack !== "true";
1728
+ };
1729
+ //#endregion
1722
1730
  //#region src/commands/init.ts
1723
- const VERSION$3 = "0.1.41";
1731
+ const VERSION$3 = "0.1.43-dev.5ceaf57";
1724
1732
  const REPORT_URL = "https://react-grab.com/api/report-cli";
1725
1733
  const DOCS_URL = "https://github.com/aidenybai/react-grab";
1726
1734
  const reportToCli = (type, config, error) => {
1735
+ if (!isTelemetryEnabled()) return;
1727
1736
  fetch(REPORT_URL, {
1728
1737
  method: "POST",
1729
1738
  headers: { "Content-Type": "application/json" },
@@ -2077,6 +2086,8 @@ const sleep = (durationMs) => new Promise((resolve) => setTimeout(resolve, durat
2077
2086
  //#endregion
2078
2087
  //#region src/utils/clipboard.ts
2079
2088
  const HISTORY_FILE_NAME = "history.jsonl";
2089
+ 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.";
2090
+ const READERS_DIR = path.dirname(fileURLToPath(import.meta.url));
2080
2091
  const READ_TIMEOUT_MS = 2500;
2081
2092
  const MAX_CLIPBOARD_BYTES = 64 * 1024 * 1024;
2082
2093
  const ID_RADIX = 36;
@@ -2170,7 +2181,7 @@ const compileSwiftReader = (readersDir, workDir) => {
2170
2181
  return binary;
2171
2182
  };
2172
2183
  const createDarwinReader = (options) => {
2173
- const binary = options.textOnly ? null : compileSwiftReader(options.readersDir, options.workDir);
2184
+ const binary = options.textOnly ? null : compileSwiftReader(READERS_DIR, options.workDir);
2174
2185
  if (binary) return {
2175
2186
  mode: "darwin-native",
2176
2187
  read: () => {
@@ -2253,7 +2264,7 @@ const detectPowershell = () => hasCommand("pwsh") ? "pwsh" : hasCommand("powersh
2253
2264
  const createWindowsReader = (options) => {
2254
2265
  const shell = detectPowershell();
2255
2266
  if (!shell) return null;
2256
- const scriptPath = path.join(options.readersDir, "read-clipboard.ps1");
2267
+ const scriptPath = path.join(READERS_DIR, "read-clipboard.ps1");
2257
2268
  if (options.textOnly || !fs.existsSync(scriptPath)) return {
2258
2269
  mode: "win-text",
2259
2270
  read: () => {
@@ -2392,108 +2403,291 @@ const runWatchLoop = async (options) => {
2392
2403
  }
2393
2404
  };
2394
2405
  //#endregion
2406
+ //#region src/utils/daemon.ts
2407
+ const PID_FILE_NAME = "watch.pid";
2408
+ const pidFilePath = (dir) => path.join(dir, PID_FILE_NAME);
2409
+ const cliEntryPath = () => process.argv[1] ?? fileURLToPath(import.meta.url);
2410
+ const isProcessAlive = (pid) => {
2411
+ if (!Number.isInteger(pid) || pid <= 0) return false;
2412
+ try {
2413
+ process.kill(pid, 0);
2414
+ return true;
2415
+ } catch (error) {
2416
+ return error.code === "EPERM";
2417
+ }
2418
+ };
2419
+ const readDaemonPid = (dir) => {
2420
+ try {
2421
+ const pid = Number.parseInt(fs.readFileSync(pidFilePath(dir), "utf8").trim(), 10);
2422
+ return Number.isInteger(pid) && pid > 0 ? pid : null;
2423
+ } catch {
2424
+ return null;
2425
+ }
2426
+ };
2427
+ const isDaemonRunning = (dir) => {
2428
+ const pid = readDaemonPid(dir);
2429
+ return pid !== null && isProcessAlive(pid);
2430
+ };
2431
+ const claimDaemon = (dir) => {
2432
+ const file = pidFilePath(dir);
2433
+ for (let attempt = 0; attempt < 50; attempt += 1) try {
2434
+ const handle = fs.openSync(file, "wx");
2435
+ fs.writeFileSync(handle, String(process.pid));
2436
+ fs.closeSync(handle);
2437
+ return readDaemonPid(dir) === process.pid;
2438
+ } catch (error) {
2439
+ if (error.code !== "EEXIST") throw error;
2440
+ if (isDaemonRunning(dir)) return false;
2441
+ try {
2442
+ fs.rmSync(file, { force: true });
2443
+ } catch {}
2444
+ }
2445
+ return false;
2446
+ };
2447
+ const releaseDaemon = (dir) => {
2448
+ if (readDaemonPid(dir) === process.pid) try {
2449
+ fs.rmSync(pidFilePath(dir), { force: true });
2450
+ } catch {}
2451
+ };
2452
+ const stopDaemon = (dir) => {
2453
+ const pid = readDaemonPid(dir);
2454
+ if (pid === null) return null;
2455
+ const wasAlive = isProcessAlive(pid);
2456
+ if (wasAlive) try {
2457
+ process.kill(pid, "SIGTERM");
2458
+ } catch {}
2459
+ if (readDaemonPid(dir) === pid) try {
2460
+ fs.rmSync(pidFilePath(dir), { force: true });
2461
+ } catch {}
2462
+ return wasAlive ? pid : null;
2463
+ };
2464
+ const spawnDaemon = (options) => {
2465
+ const args = [
2466
+ cliEntryPath(),
2467
+ "watch",
2468
+ "--dir",
2469
+ options.dir,
2470
+ "--interval",
2471
+ String(options.intervalMs)
2472
+ ];
2473
+ if (options.textOnly) args.push("--text-only");
2474
+ if (options.replayLast) args.push("--replay-last");
2475
+ spawn(process.execPath, args, {
2476
+ detached: true,
2477
+ stdio: "ignore",
2478
+ windowsHide: true
2479
+ }).unref();
2480
+ };
2481
+ const ensureDaemon = (options) => {
2482
+ if (isDaemonRunning(options.dir)) return "already-running";
2483
+ if (!createReader({
2484
+ textOnly: options.textOnly,
2485
+ workDir: options.dir
2486
+ })) return "no-reader";
2487
+ spawnDaemon(options);
2488
+ return "started";
2489
+ };
2490
+ //#endregion
2395
2491
  //#region src/utils/grab-log.ts
2396
2492
  const CURSOR_FILE_NAME = "cursor.txt";
2493
+ const NEWLINE_BYTE = 10;
2397
2494
  const cursorFilePath = (dir) => path.join(dir, CURSOR_FILE_NAME);
2398
- const readCompleteGrabLines = (dir) => {
2399
- let raw;
2495
+ const historyFilePath = (dir) => path.join(dir, HISTORY_FILE_NAME);
2496
+ const fileSize = (filePath) => {
2400
2497
  try {
2401
- raw = fs.readFileSync(path.join(dir, HISTORY_FILE_NAME), "utf8");
2498
+ return fs.statSync(filePath).size;
2402
2499
  } catch {
2403
- return [];
2500
+ return 0;
2404
2501
  }
2405
- const lastNewline = raw.lastIndexOf("\n");
2406
- if (lastNewline < 0) return [];
2407
- return raw.slice(0, lastNewline).split("\n").filter(Boolean);
2502
+ };
2503
+ const readHistoryRange = (dir, start, length) => {
2504
+ if (length <= 0) return "";
2505
+ const fd = fs.openSync(historyFilePath(dir), "r");
2506
+ try {
2507
+ const buffer = Buffer.allocUnsafe(length);
2508
+ const bytesRead = fs.readSync(fd, buffer, 0, length, start);
2509
+ return buffer.toString("utf8", 0, bytesRead);
2510
+ } finally {
2511
+ fs.closeSync(fd);
2512
+ }
2513
+ };
2514
+ const writeGrabCursor = (dir, offset) => {
2515
+ const target = cursorFilePath(dir);
2516
+ const tempPath = `${target}.${process.pid}.tmp`;
2517
+ fs.writeFileSync(tempPath, JSON.stringify({ offset }));
2518
+ fs.renameSync(tempPath, target);
2519
+ };
2520
+ const byteOffsetAfterLines = (dir, lineCount) => {
2521
+ const filePath = historyFilePath(dir);
2522
+ const size = fileSize(filePath);
2523
+ if (lineCount <= 0 || size === 0) return 0;
2524
+ const fd = fs.openSync(filePath, "r");
2525
+ try {
2526
+ const buffer = Buffer.allocUnsafe(MIGRATION_SCAN_CHUNK_BYTES);
2527
+ let position = 0;
2528
+ let seen = 0;
2529
+ while (position < size) {
2530
+ const bytesRead = fs.readSync(fd, buffer, 0, MIGRATION_SCAN_CHUNK_BYTES, position);
2531
+ if (bytesRead <= 0) break;
2532
+ for (let index = 0; index < bytesRead; index += 1) {
2533
+ if (buffer[index] !== NEWLINE_BYTE) continue;
2534
+ seen += 1;
2535
+ if (seen === lineCount) return position + index + 1;
2536
+ }
2537
+ position += bytesRead;
2538
+ }
2539
+ } finally {
2540
+ fs.closeSync(fd);
2541
+ }
2542
+ return size;
2408
2543
  };
2409
2544
  const readGrabCursor = (dir) => {
2545
+ let raw;
2410
2546
  try {
2411
- const value = Number.parseInt(fs.readFileSync(cursorFilePath(dir), "utf8").trim(), 10);
2412
- return Number.isInteger(value) && value >= 0 ? value : 0;
2547
+ raw = fs.readFileSync(cursorFilePath(dir), "utf8").trim();
2413
2548
  } catch {
2414
2549
  return 0;
2415
2550
  }
2416
- };
2417
- const grabReceivedAt = (line) => {
2551
+ if (raw === "") return 0;
2552
+ let parsed;
2418
2553
  try {
2419
- const value = JSON.parse(line).receivedAt;
2420
- return typeof value === "number" ? value : null;
2554
+ parsed = JSON.parse(raw);
2421
2555
  } catch {
2422
- return null;
2556
+ return 0;
2423
2557
  }
2558
+ if (typeof parsed === "number") {
2559
+ if (!Number.isInteger(parsed) || parsed < 0) return 0;
2560
+ const offset = byteOffsetAfterLines(dir, parsed);
2561
+ writeGrabCursor(dir, offset);
2562
+ return offset;
2563
+ }
2564
+ return typeof parsed.offset === "number" && Number.isInteger(parsed.offset) && parsed.offset >= 0 ? parsed.offset : 0;
2565
+ };
2566
+ const readCompleteGrabLines = (dir) => {
2567
+ const size = fileSize(historyFilePath(dir));
2568
+ if (size === 0) return [];
2569
+ const raw = readHistoryRange(dir, 0, size);
2570
+ const lastNewline = raw.lastIndexOf("\n");
2571
+ if (lastNewline < 0) return [];
2572
+ return raw.slice(0, lastNewline).split("\n").filter(Boolean);
2424
2573
  };
2425
2574
  const consumeGrabs = (dir, options) => {
2426
- const lines = readCompleteGrabLines(dir);
2427
- if (options.all) return lines;
2428
- const maxAgeMs = options.maxAgeMs ?? 0;
2575
+ if (options.all) return readCompleteGrabLines(dir);
2576
+ const size = fileSize(historyFilePath(dir));
2429
2577
  const cursor = readGrabCursor(dir);
2430
- const total = lines.length;
2578
+ const start = cursor > size ? 0 : cursor;
2579
+ if (start >= size) {
2580
+ if (start !== cursor) writeGrabCursor(dir, start);
2581
+ return [];
2582
+ }
2583
+ const chunk = readHistoryRange(dir, start, Math.min(size - start, MAX_READ_HISTORY_BYTES));
2584
+ const lastNewline = chunk.lastIndexOf("\n");
2585
+ if (lastNewline < 0) return [];
2431
2586
  const now = Date.now();
2432
2587
  const fresh = [];
2433
- let index = Math.min(cursor, total);
2434
- while (index < total && (options.limit <= 0 || fresh.length < options.limit)) {
2435
- const line = lines[index];
2436
- index += 1;
2437
- if (maxAgeMs > 0) {
2438
- const receivedAt = grabReceivedAt(line);
2439
- if (receivedAt !== null && now - receivedAt > maxAgeMs) continue;
2588
+ let consumedBytes = 0;
2589
+ let lineStart = 0;
2590
+ while (lineStart <= lastNewline && (options.limit <= 0 || fresh.length < options.limit)) {
2591
+ const newlineIndex = chunk.indexOf("\n", lineStart);
2592
+ const line = chunk.slice(lineStart, newlineIndex);
2593
+ consumedBytes += Buffer.byteLength(chunk.slice(lineStart, newlineIndex + 1), "utf8");
2594
+ lineStart = newlineIndex + 1;
2595
+ if (line.length === 0) continue;
2596
+ let parsed;
2597
+ try {
2598
+ parsed = JSON.parse(line);
2599
+ } catch {
2600
+ continue;
2601
+ }
2602
+ if (options.maxAgeMs > 0 && typeof parsed.receivedAt === "number") {
2603
+ if (now - parsed.receivedAt > options.maxAgeMs) continue;
2440
2604
  }
2441
2605
  fresh.push(line);
2442
2606
  }
2443
- if (index !== cursor) fs.writeFileSync(cursorFilePath(dir), String(index));
2607
+ const nextCursor = start + consumedBytes;
2608
+ if (nextCursor !== cursor) writeGrabCursor(dir, nextCursor);
2444
2609
  return fresh;
2445
2610
  };
2446
2611
  //#endregion
2447
- //#region src/commands/read.ts
2612
+ //#region src/utils/read-args.ts
2448
2613
  const parseWaitMs = (raw) => {
2449
- if (!raw) return 0;
2450
- if (/^(inf|infinite|forever)$/i.test(raw.trim())) return Number.POSITIVE_INFINITY;
2451
- const ms = Number(raw);
2452
- return Number.isFinite(ms) && ms > 0 ? ms : 0;
2614
+ if (raw === void 0) return 0;
2615
+ const trimmed = raw.trim();
2616
+ if (trimmed === "") return 0;
2617
+ if (/^(inf|infinite|infinity|forever)$/i.test(trimmed)) return Number.POSITIVE_INFINITY;
2618
+ const ms = Number(trimmed);
2619
+ return Number.isFinite(ms) && ms >= 0 ? ms : null;
2620
+ };
2621
+ const parseNonNegativeInt = (raw) => {
2622
+ if (raw === void 0) return null;
2623
+ const trimmed = raw.trim();
2624
+ if (trimmed === "") return null;
2625
+ const value = Number(trimmed);
2626
+ return Number.isInteger(value) && value >= 0 ? value : null;
2627
+ };
2628
+ //#endregion
2629
+ //#region src/commands/pull.ts
2630
+ const fail = (message) => {
2631
+ process.stderr.write(`react-grab pull: ${message}\n`);
2632
+ process.exit(1);
2453
2633
  };
2454
- const parseNonNegativeInt = (raw, fallback) => {
2455
- const value = Number(raw);
2456
- return Number.isInteger(value) && value >= 0 ? value : fallback;
2634
+ const emitAndExit = (lines) => {
2635
+ process.stdout.write(`${lines.join("\n")}\n`, () => process.exit(0));
2457
2636
  };
2458
- const read = new 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) => {
2637
+ const pull = new Command().name("pull").description("start the watcher if needed, then wait for and print the next React Grab grab(s)").option("-d, --dir <dir>", "work dir for history.jsonl + watch.pid", DEFAULT_WATCH_DIR).option("-w, --wait <ms>", "how long to wait for a grab: ms, 'infinite', or 0 for none", "infinite").option("-n, --limit <count>", "max grabs to print per call (0 = no limit)", String(50)).option("--max-age <ms>", "skip grabs captured longer ago than <ms> (0 = never)", String(DEFAULT_GRAB_AGE_MS)).option("--text-only", "watcher uses the plain-text clipboard reader (ignored if already running)").option("--all", "print the whole history without advancing the cursor").action(async (options) => {
2459
2638
  const dir = path.resolve(options.dir);
2460
2639
  try {
2461
2640
  prepareWorkDir(dir);
2462
2641
  } catch (error) {
2463
- process.stderr.write(`react-grab read: ${String(error?.message ?? error)}\n`);
2464
- process.exit(1);
2642
+ fail(String(error?.message ?? error));
2465
2643
  }
2466
- unrefStdin();
2467
- const limit = parseNonNegativeInt(options.limit, 50);
2468
- const maxAgeMs = parseNonNegativeInt(options.maxAge, MAX_GRAB_AGE_MS);
2469
- const all = Boolean(options.all);
2470
- const drain = () => {
2471
- const fresh = consumeGrabs(dir, {
2472
- limit,
2473
- all,
2474
- maxAgeMs
2475
- });
2476
- if (fresh.length > 0) process.stdout.write(`${fresh.join("\n")}\n`);
2477
- return fresh.length;
2478
- };
2479
2644
  const waitMs = parseWaitMs(options.wait);
2480
- const deadline = waitMs === Number.POSITIVE_INFINITY ? Number.POSITIVE_INFINITY : Date.now() + waitMs;
2481
- if (drain() > 0) process.exit(0);
2645
+ if (waitMs === null) fail(`invalid --wait "${options.wait}" (use milliseconds or "infinite")`);
2646
+ const limit = parseNonNegativeInt(options.limit);
2647
+ if (limit === null) fail(`invalid --limit "${options.limit}" (use a non-negative integer)`);
2648
+ const maxAgeMs = parseNonNegativeInt(options.maxAge);
2649
+ if (maxAgeMs === null) fail(`invalid --max-age "${options.maxAge}" (use milliseconds, 0 to disable)`);
2650
+ const all = Boolean(options.all);
2651
+ if (ensureDaemon({
2652
+ dir,
2653
+ intervalMs: 800,
2654
+ textOnly: Boolean(options.textOnly),
2655
+ replayLast: false
2656
+ }) === "no-reader") fail(NO_READER_MESSAGE);
2657
+ unrefStdin();
2658
+ const consume = () => consumeGrabs(dir, {
2659
+ limit,
2660
+ all,
2661
+ maxAgeMs
2662
+ });
2663
+ const first = consume();
2664
+ if (first.length > 0) {
2665
+ emitAndExit(first);
2666
+ return;
2667
+ }
2668
+ const deadline = Date.now() + waitMs;
2482
2669
  while (Date.now() < deadline) {
2483
2670
  await sleep(200);
2484
- if (drain() > 0) break;
2671
+ const batch = consume();
2672
+ if (batch.length > 0) {
2673
+ emitAndExit(batch);
2674
+ return;
2675
+ }
2485
2676
  }
2486
2677
  process.exit(0);
2487
2678
  });
2488
2679
  //#endregion
2489
2680
  //#region src/commands/remove.ts
2490
- const VERSION$2 = "0.1.41";
2491
- const remove = new Command().name("remove").description("uninstall the React Grab skill from your agent").action(async () => {
2681
+ const VERSION$2 = "0.1.43-dev.5ceaf57";
2682
+ const remove = new Command().name("remove").description("uninstall the React Grab skill from your agent").option("-c, --cwd <cwd>", "working directory (defaults to current directory)", process.cwd()).option("-g, --global", "remove the globally-installed skill instead of the project's", false).action(async (opts) => {
2492
2683
  console.log(`${pc.magenta("✿")} ${pc.bold("React Grab")} ${pc.gray(VERSION$2)}`);
2493
2684
  console.log();
2494
2685
  try {
2495
2686
  logger.break();
2496
- const removedCount = await removeSkill();
2687
+ const removedCount = await removeSkill({
2688
+ cwd: resolve(opts.cwd),
2689
+ global: opts.global
2690
+ });
2497
2691
  logger.break();
2498
2692
  if (removedCount === 0) logger.log("React Grab skill is not installed in any detected agent.");
2499
2693
  else logger.log(`${highlighter.success("Removed")} the React Grab skill from ${removedCount} agent${removedCount === 1 ? "" : "s"}.`);
@@ -2503,8 +2697,16 @@ const remove = new Command().name("remove").description("uninstall the React Gra
2503
2697
  }
2504
2698
  });
2505
2699
  //#endregion
2700
+ //#region src/commands/stop.ts
2701
+ const stop = new Command().name("stop").description("stop the React Grab watcher for this dir").option("-d, --dir <dir>", "work dir holding watch.pid", DEFAULT_WATCH_DIR).action((options) => {
2702
+ const dir = path.resolve(options.dir);
2703
+ const stoppedPid = stopDaemon(dir);
2704
+ process.stderr.write(stoppedPid ? `react-grab stop: stopped watcher (pid ${stoppedPid})\n` : `react-grab stop: no watcher running for ${dir}\n`);
2705
+ process.exit(0);
2706
+ });
2707
+ //#endregion
2506
2708
  //#region src/commands/upgrade.ts
2507
- const VERSION$1 = "0.1.41";
2709
+ const VERSION$1 = "0.1.43-dev.5ceaf57";
2508
2710
  const NPM_REGISTRY_URL = "https://registry.npmjs.org/react-grab/latest";
2509
2711
  const fetchLatestVersion = async () => {
2510
2712
  try {
@@ -2578,95 +2780,24 @@ const upgrade = new Command().name("upgrade").alias("update").description("upgra
2578
2780
  }
2579
2781
  });
2580
2782
  //#endregion
2581
- //#region src/utils/daemon.ts
2582
- const PID_FILE_NAME = "watch.pid";
2583
- const pidFilePath = (dir) => path.join(dir, PID_FILE_NAME);
2584
- const cliEntryPath = () => process.argv[1] ?? fileURLToPath(import.meta.url);
2585
- const isProcessAlive = (pid) => {
2586
- if (!Number.isInteger(pid) || pid <= 0) return false;
2587
- try {
2588
- process.kill(pid, 0);
2589
- return true;
2590
- } catch (error) {
2591
- return error.code === "EPERM";
2592
- }
2593
- };
2594
- const readDaemonPid = (dir) => {
2595
- try {
2596
- const pid = Number.parseInt(fs.readFileSync(pidFilePath(dir), "utf8").trim(), 10);
2597
- return Number.isInteger(pid) && pid > 0 ? pid : null;
2598
- } catch {
2599
- return null;
2600
- }
2601
- };
2602
- const isDaemonRunning = (dir) => {
2603
- const pid = readDaemonPid(dir);
2604
- return pid !== null && isProcessAlive(pid);
2605
- };
2606
- const claimDaemon = (dir) => {
2607
- const file = pidFilePath(dir);
2608
- for (let attempt = 0; attempt < 50; attempt += 1) try {
2609
- const handle = fs.openSync(file, "wx");
2610
- fs.writeFileSync(handle, String(process.pid));
2611
- fs.closeSync(handle);
2612
- return readDaemonPid(dir) === process.pid;
2613
- } catch (error) {
2614
- if (error.code !== "EEXIST") throw error;
2615
- if (isDaemonRunning(dir)) return false;
2616
- try {
2617
- fs.rmSync(file, { force: true });
2618
- } catch {}
2619
- }
2620
- return false;
2621
- };
2622
- const releaseDaemon = (dir) => {
2623
- if (readDaemonPid(dir) === process.pid) try {
2624
- fs.rmSync(pidFilePath(dir), { force: true });
2625
- } catch {}
2626
- };
2627
- const stopDaemon = (dir) => {
2628
- const pid = readDaemonPid(dir);
2629
- if (pid === null) return null;
2630
- const wasAlive = isProcessAlive(pid);
2631
- if (wasAlive) try {
2632
- process.kill(pid, "SIGTERM");
2633
- } catch {}
2634
- if (readDaemonPid(dir) === pid) try {
2635
- fs.rmSync(pidFilePath(dir), { force: true });
2636
- } catch {}
2637
- return wasAlive ? pid : null;
2638
- };
2639
- const spawnDaemon = (options) => {
2640
- const args = [
2641
- cliEntryPath(),
2642
- "watch",
2643
- "--foreground",
2644
- "--dir",
2645
- options.dir,
2646
- "--interval",
2647
- String(options.intervalMs)
2648
- ];
2649
- if (options.textOnly) args.push("--text-only");
2650
- if (options.replayLast) args.push("--replay-last");
2651
- spawn(process.execPath, args, {
2652
- detached: true,
2653
- stdio: "ignore",
2654
- windowsHide: true
2655
- }).unref();
2656
- };
2657
- //#endregion
2658
2783
  //#region src/commands/watch.ts
2659
- const readersDir = () => path.dirname(fileURLToPath(import.meta.url));
2660
- 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.";
2661
2784
  const writeStatus = (message) => {
2662
2785
  process.stderr.write(`react-grab watch: ${message}\n`);
2663
2786
  };
2664
- const runForeground = (dir, intervalMs, textOnly, replayLast) => {
2787
+ const watch = new Command().name("watch").description("run the React Grab capture daemon in the foreground (used internally by `pull`)").option("-d, --dir <dir>", "work dir for history.jsonl + watch.pid", DEFAULT_WATCH_DIR).option("-i, --interval <ms>", "clipboard poll interval in ms", String(800)).option("--text-only", "skip the native reader and use the plain-text fallback").option("--replay-last", "also capture the grab already on the clipboard at startup").action((options) => {
2788
+ const dir = path.resolve(options.dir);
2789
+ const intervalRaw = Number(options.interval);
2790
+ const intervalMs = Number.isFinite(intervalRaw) && intervalRaw > 0 ? intervalRaw : 800;
2791
+ try {
2792
+ prepareWorkDir(dir);
2793
+ } catch (error) {
2794
+ writeStatus(String(error?.message ?? error));
2795
+ process.exit(1);
2796
+ }
2665
2797
  if (!claimDaemon(dir)) process.exit(0);
2666
2798
  process.on("exit", () => releaseDaemon(dir));
2667
2799
  const reader = createReader({
2668
- textOnly,
2669
- readersDir: readersDir(),
2800
+ textOnly: Boolean(options.textOnly),
2670
2801
  workDir: dir
2671
2802
  });
2672
2803
  if (!reader) {
@@ -2678,63 +2809,21 @@ const runForeground = (dir, intervalMs, textOnly, replayLast) => {
2678
2809
  reader,
2679
2810
  dir,
2680
2811
  intervalMs,
2681
- replayLast,
2812
+ replayLast: Boolean(options.replayLast),
2682
2813
  onWarn: writeStatus
2683
2814
  }).catch((error) => {
2684
2815
  writeStatus(String(error?.message ?? error));
2685
2816
  process.exit(1);
2686
2817
  });
2687
- };
2688
- const watch = new Command().name("watch").description("start a background daemon that captures React Grab selections to history.jsonl").option("-d, --dir <dir>", "work dir for history.jsonl + watch.pid", DEFAULT_WATCH_DIR).option("-i, --interval <ms>", "clipboard poll interval in ms", String(800)).option("--text-only", "skip the native reader and use the plain-text fallback").option("--replay-last", "also capture the grab already on the clipboard at startup").option("--foreground", "run the capture loop in this process instead of detaching a daemon").option("--stop", "stop the daemon watching this dir").action((options) => {
2689
- const dir = path.resolve(options.dir);
2690
- const intervalRaw = Number(options.interval);
2691
- const intervalMs = Number.isFinite(intervalRaw) && intervalRaw > 0 ? intervalRaw : 800;
2692
- const textOnly = Boolean(options.textOnly);
2693
- const replayLast = Boolean(options.replayLast);
2694
- try {
2695
- prepareWorkDir(dir);
2696
- } catch (error) {
2697
- writeStatus(String(error?.message ?? error));
2698
- process.exit(1);
2699
- }
2700
- if (options.stop) {
2701
- const stoppedPid = stopDaemon(dir);
2702
- writeStatus(stoppedPid ? `stopped daemon (pid ${stoppedPid})` : `no daemon running for ${dir}`);
2703
- process.exit(0);
2704
- }
2705
- if (options.foreground) {
2706
- runForeground(dir, intervalMs, textOnly, replayLast);
2707
- return;
2708
- }
2709
- if (isDaemonRunning(dir)) {
2710
- writeStatus(`already watching ${dir} (pid ${readDaemonPid(dir)})`);
2711
- process.exit(0);
2712
- }
2713
- if (!createReader({
2714
- textOnly,
2715
- readersDir: readersDir(),
2716
- workDir: dir
2717
- })) {
2718
- writeStatus(NO_READER_MESSAGE);
2719
- process.exit(1);
2720
- }
2721
- spawnDaemon({
2722
- dir,
2723
- intervalMs,
2724
- textOnly,
2725
- replayLast
2726
- });
2727
- writeStatus(`started; capturing grabs → ${path.join(dir, HISTORY_FILE_NAME)} (run \`grab read\` to consume, \`grab watch --stop\` to stop)`);
2728
- process.exit(0);
2729
2818
  });
2730
2819
  //#endregion
2731
2820
  //#region src/cli.ts
2732
- const VERSION = "0.1.41";
2821
+ const VERSION = "0.1.43-dev.5ceaf57";
2733
2822
  const VERSION_API_URL = "https://www.react-grab.com/api/version";
2734
2823
  process.on("SIGINT", () => process.exit(0));
2735
2824
  process.on("SIGTERM", () => process.exit(0));
2736
2825
  try {
2737
- fetch(`${VERSION_API_URL}?source=cli&v=${VERSION}&t=${Date.now()}`).catch(() => {});
2826
+ if (isTelemetryEnabled()) fetch(`${VERSION_API_URL}?source=cli&v=${VERSION}&t=${Date.now()}`).catch(() => {});
2738
2827
  } catch {}
2739
2828
  const program = new Command().name("grab").description("add React Grab to your project").version(VERSION, "-v, --version", "display the version number");
2740
2829
  program.addCommand(init);
@@ -2742,8 +2831,9 @@ program.addCommand(add$1);
2742
2831
  program.addCommand(remove);
2743
2832
  program.addCommand(configure);
2744
2833
  program.addCommand(upgrade);
2745
- program.addCommand(watch);
2746
- program.addCommand(read);
2834
+ program.addCommand(pull);
2835
+ program.addCommand(stop);
2836
+ program.addCommand(watch, { hidden: true });
2747
2837
  const main = async () => {
2748
2838
  await program.parseAsync();
2749
2839
  };