@react-grab/cli 0.1.40 → 0.1.42

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.cjs CHANGED
@@ -22,12 +22,12 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
22
22
  }) : target, mod));
23
23
  //#endregion
24
24
  let commander = require("commander");
25
+ let node_path = require("node:path");
26
+ node_path = __toESM(node_path, 1);
25
27
  let picocolors = require("picocolors");
26
28
  picocolors = __toESM(picocolors, 1);
27
29
  let node_fs = require("node:fs");
28
30
  node_fs = __toESM(node_fs, 1);
29
- let node_path = require("node:path");
30
- node_path = __toESM(node_path, 1);
31
31
  let package_manager_detector_detect = require("package-manager-detector/detect");
32
32
  let ignore = require("ignore");
33
33
  ignore = __toESM(ignore, 1);
@@ -450,8 +450,11 @@ const spinner = (text) => (0, ora.default)({ text });
450
450
  const SKILL_NAME = "react-grab";
451
451
  const SKILL_SOURCE = (0, node_url.fileURLToPath)(new URL("../skills/react-grab", require("url").pathToFileURL(__filename).href));
452
452
  const agentLabel = (agent) => (0, agent_install_skill.getSkillAgentConfig)(agent).displayName;
453
- const installedSkillDir = (agent) => (0, node_path.join)((0, agent_install_skill.isUniversalSkillAgent)(agent) ? (0, agent_install_skill.getCanonicalSkillsDir)(true) : (0, agent_install_skill.getSkillAgentDir)(agent, { global: true }), SKILL_NAME);
454
- const promptSkillInstall = async ({ yes = false } = {}) => {
453
+ const installedSkillDir = (agent, global, cwd) => (0, node_path.join)((0, agent_install_skill.isUniversalSkillAgent)(agent) ? (0, agent_install_skill.getCanonicalSkillsDir)(global, cwd) : (0, agent_install_skill.getSkillAgentDir)(agent, {
454
+ global,
455
+ cwd
456
+ }), SKILL_NAME);
457
+ const promptSkillInstall = async ({ yes = false, global = false, cwd = process.cwd() } = {}) => {
455
458
  const detectedAgents = await detectAvailableAgents();
456
459
  if (detectedAgents.length === 0) {
457
460
  logger.warn("No supported agents detected.");
@@ -462,7 +465,7 @@ const promptSkillInstall = async ({ yes = false } = {}) => {
462
465
  const { agents } = await prompts$1({
463
466
  type: "multiselect",
464
467
  name: "agents",
465
- message: "Install the React Grab skill for:",
468
+ message: `Install the React Grab skill (${global ? "global" : "this project"}) for:`,
466
469
  choices: detectedAgents.map((agent) => ({
467
470
  title: agentLabel(agent),
468
471
  value: agent,
@@ -478,7 +481,8 @@ const promptSkillInstall = async ({ yes = false } = {}) => {
478
481
  const { installed, failed } = await (0, agent_install_skill.add)({
479
482
  source: SKILL_SOURCE,
480
483
  agents: selectedAgents,
481
- global: true,
484
+ global,
485
+ cwd,
482
486
  mode: "copy"
483
487
  });
484
488
  if (installed.length === 0) {
@@ -489,19 +493,27 @@ const promptSkillInstall = async ({ yes = false } = {}) => {
489
493
  for (const record of failed) logger.log(` ${highlighter.error("✗")} ${agentLabel(record.agent)} ${record.error}`);
490
494
  return true;
491
495
  };
492
- const removeSkill = async () => {
493
- const agentsWithSkill = (await detectAvailableAgents()).filter((agent) => (0, node_fs.existsSync)(installedSkillDir(agent)));
494
- for (const skillDir of new Set(agentsWithSkill.map(installedSkillDir))) (0, node_fs.rmSync)(skillDir, {
496
+ const removeSkill = async ({ cwd = process.cwd(), global = false } = {}) => {
497
+ const agents = await detectAvailableAgents();
498
+ const removedAgents = [];
499
+ const dirsToRemove = /* @__PURE__ */ new Set();
500
+ for (const agent of agents) {
501
+ const skillDir = installedSkillDir(agent, global, cwd);
502
+ if (!(0, node_fs.existsSync)(skillDir)) continue;
503
+ removedAgents.push(agent);
504
+ dirsToRemove.add(skillDir);
505
+ }
506
+ for (const skillDir of dirsToRemove) (0, node_fs.rmSync)(skillDir, {
495
507
  recursive: true,
496
508
  force: true
497
509
  });
498
- for (const agent of agentsWithSkill) logger.log(` ${highlighter.success("✓")} ${agentLabel(agent)}`);
499
- return agentsWithSkill.length;
510
+ for (const agent of removedAgents) logger.log(` ${highlighter.success("✓")} ${agentLabel(agent)}`);
511
+ return removedAgents.length;
500
512
  };
501
513
  //#endregion
502
514
  //#region src/commands/add.ts
503
- const VERSION$5 = "0.1.40";
504
- 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()).action(async (opts) => {
515
+ const VERSION$5 = "0.1.42";
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) => {
505
517
  console.log(`${picocolors.default.magenta("✿")} ${picocolors.default.bold("React Grab")} ${picocolors.default.gray(VERSION$5)}`);
506
518
  console.log();
507
519
  try {
@@ -516,7 +528,11 @@ const add = new commander.Command().name("add").alias("install").description("in
516
528
  }
517
529
  preflightSpinner.succeed();
518
530
  logger.break();
519
- if (!await promptSkillInstall({ yes: isNonInteractive }) && isNonInteractive) {
531
+ if (!await promptSkillInstall({
532
+ yes: isNonInteractive,
533
+ global: opts.global,
534
+ cwd: (0, node_path.resolve)(opts.cwd)
535
+ }) && isNonInteractive) {
520
536
  logger.break();
521
537
  process.exit(1);
522
538
  }
@@ -1095,6 +1111,9 @@ const previewCdnTransform = (projectRoot, framework, nextRouterType, targetCdnDo
1095
1111
  //#region src/utils/constants.ts
1096
1112
  const MAX_KEY_HOLD_DURATION_MS = 2e3;
1097
1113
  const DEFAULT_WATCH_DIR = ".react-grab";
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;
1098
1117
  //#endregion
1099
1118
  //#region src/utils/format-activation-key.ts
1100
1119
  const formatActivationKeyDisplay = (activationKey) => {
@@ -1112,7 +1131,7 @@ const formatActivationKeyDisplay = (activationKey) => {
1112
1131
  };
1113
1132
  //#endregion
1114
1133
  //#region src/commands/configure.ts
1115
- const VERSION$4 = "0.1.40";
1134
+ const VERSION$4 = "0.1.42";
1116
1135
  const isMac = process.platform === "darwin";
1117
1136
  const META_LABEL = isMac ? "Cmd" : "Win";
1118
1137
  const ALT_LABEL = isMac ? "Option" : "Alt";
@@ -1731,7 +1750,7 @@ const installPackagesWithFeedback = async (packages, packageManager, projectRoot
1731
1750
  };
1732
1751
  //#endregion
1733
1752
  //#region src/commands/init.ts
1734
- const VERSION$3 = "0.1.40";
1753
+ const VERSION$3 = "0.1.42";
1735
1754
  const REPORT_URL = "https://react-grab.com/api/report-cli";
1736
1755
  const DOCS_URL = "https://github.com/aidenybai/react-grab";
1737
1756
  const reportToCli = (type, config, error) => {
@@ -1798,7 +1817,7 @@ const failWithManualSetup = (failingSpinner, message, { listSupportedFrameworks
1798
1817
  logger.break();
1799
1818
  process.exit(1);
1800
1819
  };
1801
- const init = new commander.Command().name("init").alias("setup").description("initialize React Grab in your project").option("-y, --yes", "skip confirmation prompts", false).option("-f, --force", "force overwrite existing config", false).option("-k, --key <key>", "activation key (e.g., Meta+K, Ctrl+Shift+G, Space)").option("--skip-install", "skip package installation", false).option("--pkg <pkg>", "custom package URL for CLI (e.g., grab)").option("-c, --cwd <cwd>", "working directory (defaults to current directory)", process.cwd()).action(async (opts) => {
1820
+ const init = new commander.Command().name("init").alias("setup").description("initialize React Grab in your project").option("-y, --yes", "skip confirmation prompts", false).option("-f, --force", "force overwrite existing config", false).option("-k, --key <key>", "activation key (e.g., Meta+K, Ctrl+Shift+G, Space)").option("--skip-install", "skip package installation", false).option("--pkg <pkg>", "custom package URL for CLI (e.g., grab)").option("-c, --cwd <cwd>", "working directory (defaults to current directory)", process.cwd()).option("-g, --global", "install the skill globally instead of in the project", false).action(async (opts) => {
1802
1821
  console.log(`${picocolors.default.magenta("✿")} ${picocolors.default.bold("React Grab")} ${picocolors.default.gray(VERSION$3)}`);
1803
1822
  console.log();
1804
1823
  try {
@@ -1956,7 +1975,11 @@ const init = new commander.Command().name("init").alias("setup").description("in
1956
1975
  }
1957
1976
  }
1958
1977
  logger.break();
1959
- await promptSkillInstall({ yes: isNonInteractive });
1978
+ await promptSkillInstall({
1979
+ yes: isNonInteractive,
1980
+ global: opts.global,
1981
+ cwd
1982
+ });
1960
1983
  logger.break();
1961
1984
  process.exit(0);
1962
1985
  }
@@ -2022,7 +2045,11 @@ const init = new commander.Command().name("init").alias("setup").description("in
2022
2045
  let didInstallSkill = false;
2023
2046
  if (!isNonInteractive) {
2024
2047
  logger.break();
2025
- didInstallSkill = await promptSkillInstall({ yes: isNonInteractive });
2048
+ didInstallSkill = await promptSkillInstall({
2049
+ yes: isNonInteractive,
2050
+ global: opts.global,
2051
+ cwd
2052
+ });
2026
2053
  }
2027
2054
  const result = previewTransform(projectInfo.projectRoot, finalFramework, finalNextRouterType, false, opts.force);
2028
2055
  if (!result.success) {
@@ -2080,6 +2107,8 @@ const sleep = (durationMs) => new Promise((resolve) => setTimeout(resolve, durat
2080
2107
  //#endregion
2081
2108
  //#region src/utils/clipboard.ts
2082
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));
2083
2112
  const READ_TIMEOUT_MS = 2500;
2084
2113
  const MAX_CLIPBOARD_BYTES = 64 * 1024 * 1024;
2085
2114
  const ID_RADIX = 36;
@@ -2173,7 +2202,7 @@ const compileSwiftReader = (readersDir, workDir) => {
2173
2202
  return binary;
2174
2203
  };
2175
2204
  const createDarwinReader = (options) => {
2176
- const binary = options.textOnly ? null : compileSwiftReader(options.readersDir, options.workDir);
2205
+ const binary = options.textOnly ? null : compileSwiftReader(READERS_DIR, options.workDir);
2177
2206
  if (binary) return {
2178
2207
  mode: "darwin-native",
2179
2208
  read: () => {
@@ -2256,7 +2285,7 @@ const detectPowershell = () => hasCommand("pwsh") ? "pwsh" : hasCommand("powersh
2256
2285
  const createWindowsReader = (options) => {
2257
2286
  const shell = detectPowershell();
2258
2287
  if (!shell) return null;
2259
- const scriptPath = node_path.default.join(options.readersDir, "read-clipboard.ps1");
2288
+ const scriptPath = node_path.default.join(READERS_DIR, "read-clipboard.ps1");
2260
2289
  if (options.textOnly || !node_fs.default.existsSync(scriptPath)) return {
2261
2290
  mode: "win-text",
2262
2291
  read: () => {
@@ -2395,77 +2424,291 @@ const runWatchLoop = async (options) => {
2395
2424
  }
2396
2425
  };
2397
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
2398
2512
  //#region src/utils/grab-log.ts
2399
2513
  const CURSOR_FILE_NAME = "cursor.txt";
2514
+ const NEWLINE_BYTE = 10;
2400
2515
  const cursorFilePath = (dir) => node_path.default.join(dir, CURSOR_FILE_NAME);
2401
- const readCompleteGrabLines = (dir) => {
2402
- let raw;
2516
+ const historyFilePath = (dir) => node_path.default.join(dir, HISTORY_FILE_NAME);
2517
+ const fileSize = (filePath) => {
2403
2518
  try {
2404
- raw = node_fs.default.readFileSync(node_path.default.join(dir, HISTORY_FILE_NAME), "utf8");
2519
+ return node_fs.default.statSync(filePath).size;
2405
2520
  } catch {
2406
- return [];
2521
+ return 0;
2407
2522
  }
2408
- const lastNewline = raw.lastIndexOf("\n");
2409
- if (lastNewline < 0) return [];
2410
- return raw.slice(0, lastNewline).split("\n").filter(Boolean);
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;
2411
2564
  };
2412
2565
  const readGrabCursor = (dir) => {
2566
+ let raw;
2567
+ try {
2568
+ raw = node_fs.default.readFileSync(cursorFilePath(dir), "utf8").trim();
2569
+ } catch {
2570
+ return 0;
2571
+ }
2572
+ if (raw === "") return 0;
2573
+ let parsed;
2413
2574
  try {
2414
- const value = Number.parseInt(node_fs.default.readFileSync(cursorFilePath(dir), "utf8").trim(), 10);
2415
- return Number.isInteger(value) && value >= 0 ? value : 0;
2575
+ parsed = JSON.parse(raw);
2416
2576
  } catch {
2417
2577
  return 0;
2418
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);
2419
2594
  };
2420
2595
  const consumeGrabs = (dir, options) => {
2421
- const lines = readCompleteGrabLines(dir);
2422
- if (options.all) return lines;
2596
+ if (options.all) return readCompleteGrabLines(dir);
2597
+ const size = fileSize(historyFilePath(dir));
2423
2598
  const cursor = readGrabCursor(dir);
2424
- const start = Math.min(cursor, lines.length);
2425
- const end = options.limit > 0 ? Math.min(start + options.limit, lines.length) : lines.length;
2426
- if (end !== cursor) node_fs.default.writeFileSync(cursorFilePath(dir), String(end));
2427
- return lines.slice(start, end);
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 [];
2607
+ const now = Date.now();
2608
+ const fresh = [];
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;
2625
+ }
2626
+ fresh.push(line);
2627
+ }
2628
+ const nextCursor = start + consumedBytes;
2629
+ if (nextCursor !== cursor) writeGrabCursor(dir, nextCursor);
2630
+ return fresh;
2428
2631
  };
2429
2632
  //#endregion
2430
- //#region src/commands/read.ts
2431
- 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> for at least one 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("--all", "print the entire history without advancing the cursor").action(async (options) => {
2633
+ //#region src/utils/read-args.ts
2634
+ const parseWaitMs = (raw) => {
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;
2648
+ };
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));
2657
+ };
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) => {
2432
2659
  const dir = node_path.default.resolve(options.dir);
2433
2660
  try {
2434
2661
  prepareWorkDir(dir);
2435
2662
  } catch (error) {
2436
- process.stderr.write(`react-grab read: ${String(error?.message ?? error)}\n`);
2437
- process.exit(1);
2438
- }
2439
- unrefStdin();
2440
- const limitRaw = Number(options.limit);
2441
- const limit = Number.isInteger(limitRaw) && limitRaw >= 0 ? limitRaw : 50;
2663
+ fail(String(error?.message ?? error));
2664
+ }
2665
+ const waitMs = parseWaitMs(options.wait);
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)`);
2442
2671
  const all = Boolean(options.all);
2443
- const drain = () => {
2444
- const fresh = consumeGrabs(dir, {
2445
- limit,
2446
- all
2447
- });
2448
- if (fresh.length > 0) process.stdout.write(`${fresh.join("\n")}\n`);
2449
- return fresh.length;
2450
- };
2451
- const waitRaw = options.wait ? Number(options.wait) : 0;
2452
- const deadline = Date.now() + (Number.isFinite(waitRaw) && waitRaw > 0 ? waitRaw : 0);
2453
- if (drain() > 0) process.exit(0);
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;
2454
2690
  while (Date.now() < deadline) {
2455
2691
  await sleep(200);
2456
- if (drain() > 0) break;
2692
+ const batch = consume();
2693
+ if (batch.length > 0) {
2694
+ emitAndExit(batch);
2695
+ return;
2696
+ }
2457
2697
  }
2458
2698
  process.exit(0);
2459
2699
  });
2460
2700
  //#endregion
2461
2701
  //#region src/commands/remove.ts
2462
- const VERSION$2 = "0.1.40";
2463
- 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) => {
2464
2704
  console.log(`${picocolors.default.magenta("✿")} ${picocolors.default.bold("React Grab")} ${picocolors.default.gray(VERSION$2)}`);
2465
2705
  console.log();
2466
2706
  try {
2467
2707
  logger.break();
2468
- const removedCount = await removeSkill();
2708
+ const removedCount = await removeSkill({
2709
+ cwd: (0, node_path.resolve)(opts.cwd),
2710
+ global: opts.global
2711
+ });
2469
2712
  logger.break();
2470
2713
  if (removedCount === 0) logger.log("React Grab skill is not installed in any detected agent.");
2471
2714
  else logger.log(`${highlighter.success("Removed")} the React Grab skill from ${removedCount} agent${removedCount === 1 ? "" : "s"}.`);
@@ -2475,8 +2718,16 @@ const remove = new commander.Command().name("remove").description("uninstall the
2475
2718
  }
2476
2719
  });
2477
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
2478
2729
  //#region src/commands/upgrade.ts
2479
- const VERSION$1 = "0.1.40";
2730
+ const VERSION$1 = "0.1.42";
2480
2731
  const NPM_REGISTRY_URL = "https://registry.npmjs.org/react-grab/latest";
2481
2732
  const fetchLatestVersion = async () => {
2482
2733
  try {
@@ -2550,95 +2801,24 @@ const upgrade = new commander.Command().name("upgrade").alias("update").descript
2550
2801
  }
2551
2802
  });
2552
2803
  //#endregion
2553
- //#region src/utils/daemon.ts
2554
- const PID_FILE_NAME = "watch.pid";
2555
- const pidFilePath = (dir) => node_path.default.join(dir, PID_FILE_NAME);
2556
- const cliEntryPath = () => process.argv[1] ?? (0, node_url.fileURLToPath)(require("url").pathToFileURL(__filename).href);
2557
- const isProcessAlive = (pid) => {
2558
- if (!Number.isInteger(pid) || pid <= 0) return false;
2559
- try {
2560
- process.kill(pid, 0);
2561
- return true;
2562
- } catch (error) {
2563
- return error.code === "EPERM";
2564
- }
2565
- };
2566
- const readDaemonPid = (dir) => {
2567
- try {
2568
- const pid = Number.parseInt(node_fs.default.readFileSync(pidFilePath(dir), "utf8").trim(), 10);
2569
- return Number.isInteger(pid) && pid > 0 ? pid : null;
2570
- } catch {
2571
- return null;
2572
- }
2573
- };
2574
- const isDaemonRunning = (dir) => {
2575
- const pid = readDaemonPid(dir);
2576
- return pid !== null && isProcessAlive(pid);
2577
- };
2578
- const claimDaemon = (dir) => {
2579
- const file = pidFilePath(dir);
2580
- for (let attempt = 0; attempt < 50; attempt += 1) try {
2581
- const handle = node_fs.default.openSync(file, "wx");
2582
- node_fs.default.writeFileSync(handle, String(process.pid));
2583
- node_fs.default.closeSync(handle);
2584
- return readDaemonPid(dir) === process.pid;
2585
- } catch (error) {
2586
- if (error.code !== "EEXIST") throw error;
2587
- if (isDaemonRunning(dir)) return false;
2588
- try {
2589
- node_fs.default.rmSync(file, { force: true });
2590
- } catch {}
2591
- }
2592
- return false;
2593
- };
2594
- const releaseDaemon = (dir) => {
2595
- if (readDaemonPid(dir) === process.pid) try {
2596
- node_fs.default.rmSync(pidFilePath(dir), { force: true });
2597
- } catch {}
2598
- };
2599
- const stopDaemon = (dir) => {
2600
- const pid = readDaemonPid(dir);
2601
- if (pid === null) return null;
2602
- const wasAlive = isProcessAlive(pid);
2603
- if (wasAlive) try {
2604
- process.kill(pid, "SIGTERM");
2605
- } catch {}
2606
- if (readDaemonPid(dir) === pid) try {
2607
- node_fs.default.rmSync(pidFilePath(dir), { force: true });
2608
- } catch {}
2609
- return wasAlive ? pid : null;
2610
- };
2611
- const spawnDaemon = (options) => {
2612
- const args = [
2613
- cliEntryPath(),
2614
- "watch",
2615
- "--foreground",
2616
- "--dir",
2617
- options.dir,
2618
- "--interval",
2619
- String(options.intervalMs)
2620
- ];
2621
- if (options.textOnly) args.push("--text-only");
2622
- if (options.replayLast) args.push("--replay-last");
2623
- (0, node_child_process.spawn)(process.execPath, args, {
2624
- detached: true,
2625
- stdio: "ignore",
2626
- windowsHide: true
2627
- }).unref();
2628
- };
2629
- //#endregion
2630
2804
  //#region src/commands/watch.ts
2631
- const readersDir = () => node_path.default.dirname((0, node_url.fileURLToPath)(require("url").pathToFileURL(__filename).href));
2632
- 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.";
2633
2805
  const writeStatus = (message) => {
2634
2806
  process.stderr.write(`react-grab watch: ${message}\n`);
2635
2807
  };
2636
- const runForeground = (dir, intervalMs, textOnly, replayLast) => {
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
+ }
2637
2818
  if (!claimDaemon(dir)) process.exit(0);
2638
2819
  process.on("exit", () => releaseDaemon(dir));
2639
2820
  const reader = createReader({
2640
- textOnly,
2641
- readersDir: readersDir(),
2821
+ textOnly: Boolean(options.textOnly),
2642
2822
  workDir: dir
2643
2823
  });
2644
2824
  if (!reader) {
@@ -2650,58 +2830,16 @@ const runForeground = (dir, intervalMs, textOnly, replayLast) => {
2650
2830
  reader,
2651
2831
  dir,
2652
2832
  intervalMs,
2653
- replayLast,
2833
+ replayLast: Boolean(options.replayLast),
2654
2834
  onWarn: writeStatus
2655
2835
  }).catch((error) => {
2656
2836
  writeStatus(String(error?.message ?? error));
2657
2837
  process.exit(1);
2658
2838
  });
2659
- };
2660
- 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) => {
2661
- const dir = node_path.default.resolve(options.dir);
2662
- const intervalRaw = Number(options.interval);
2663
- const intervalMs = Number.isFinite(intervalRaw) && intervalRaw > 0 ? intervalRaw : 800;
2664
- const textOnly = Boolean(options.textOnly);
2665
- const replayLast = Boolean(options.replayLast);
2666
- try {
2667
- prepareWorkDir(dir);
2668
- } catch (error) {
2669
- writeStatus(String(error?.message ?? error));
2670
- process.exit(1);
2671
- }
2672
- if (options.stop) {
2673
- const stoppedPid = stopDaemon(dir);
2674
- writeStatus(stoppedPid ? `stopped daemon (pid ${stoppedPid})` : `no daemon running for ${dir}`);
2675
- process.exit(0);
2676
- }
2677
- if (options.foreground) {
2678
- runForeground(dir, intervalMs, textOnly, replayLast);
2679
- return;
2680
- }
2681
- if (isDaemonRunning(dir)) {
2682
- writeStatus(`already watching ${dir} (pid ${readDaemonPid(dir)})`);
2683
- process.exit(0);
2684
- }
2685
- if (!createReader({
2686
- textOnly,
2687
- readersDir: readersDir(),
2688
- workDir: dir
2689
- })) {
2690
- writeStatus(NO_READER_MESSAGE);
2691
- process.exit(1);
2692
- }
2693
- spawnDaemon({
2694
- dir,
2695
- intervalMs,
2696
- textOnly,
2697
- replayLast
2698
- });
2699
- writeStatus(`started; capturing grabs → ${node_path.default.join(dir, HISTORY_FILE_NAME)} (run \`grab read\` to consume, \`grab watch --stop\` to stop)`);
2700
- process.exit(0);
2701
2839
  });
2702
2840
  //#endregion
2703
2841
  //#region src/cli.ts
2704
- const VERSION = "0.1.40";
2842
+ const VERSION = "0.1.42";
2705
2843
  const VERSION_API_URL = "https://www.react-grab.com/api/version";
2706
2844
  process.on("SIGINT", () => process.exit(0));
2707
2845
  process.on("SIGTERM", () => process.exit(0));
@@ -2714,8 +2852,9 @@ program.addCommand(add);
2714
2852
  program.addCommand(remove);
2715
2853
  program.addCommand(configure);
2716
2854
  program.addCommand(upgrade);
2717
- program.addCommand(watch);
2718
- program.addCommand(read);
2855
+ program.addCommand(pull);
2856
+ program.addCommand(stop);
2857
+ program.addCommand(watch, { hidden: true });
2719
2858
  const main = async () => {
2720
2859
  await program.parseAsync();
2721
2860
  };