@react-grab/cli 0.1.39 → 0.1.41

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
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from "commander";
3
+ import path, { basename, delimiter, dirname, join, relative, resolve } from "node:path";
3
4
  import pc from "picocolors";
4
5
  import fs, { accessSync, constants, existsSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
5
- import path, { basename, delimiter, dirname, join, relative, resolve } from "node:path";
6
6
  import { detect } from "package-manager-detector/detect";
7
7
  import ignore from "ignore";
8
8
  import { fileURLToPath } from "node:url";
@@ -10,7 +10,7 @@ import { add, detectInstalledSkillAgents, getCanonicalSkillsDir, getSkillAgentCo
10
10
  import basePrompts from "prompts";
11
11
  import ora from "ora";
12
12
  import { x } from "tinyexec";
13
- import { spawnSync } from "node:child_process";
13
+ import { spawn, spawnSync } from "node:child_process";
14
14
  import { createHash } from "node:crypto";
15
15
  //#region src/utils/is-non-interactive.ts
16
16
  const AGENT_ENVIRONMENT_VARIABLES = [
@@ -422,8 +422,11 @@ const spinner = (text) => ora({ text });
422
422
  const SKILL_NAME = "react-grab";
423
423
  const SKILL_SOURCE = fileURLToPath(new URL("../skills/react-grab", import.meta.url));
424
424
  const agentLabel = (agent) => getSkillAgentConfig(agent).displayName;
425
- const installedSkillDir = (agent) => join(isUniversalSkillAgent(agent) ? getCanonicalSkillsDir(true) : getSkillAgentDir(agent, { global: true }), SKILL_NAME);
426
- const promptSkillInstall = async ({ yes = false } = {}) => {
425
+ const installedSkillDir = (agent, global, cwd) => join(isUniversalSkillAgent(agent) ? getCanonicalSkillsDir(global, cwd) : getSkillAgentDir(agent, {
426
+ global,
427
+ cwd
428
+ }), SKILL_NAME);
429
+ const promptSkillInstall = async ({ yes = false, global = false, cwd = process.cwd() } = {}) => {
427
430
  const detectedAgents = await detectAvailableAgents();
428
431
  if (detectedAgents.length === 0) {
429
432
  logger.warn("No supported agents detected.");
@@ -434,7 +437,7 @@ const promptSkillInstall = async ({ yes = false } = {}) => {
434
437
  const { agents } = await prompts({
435
438
  type: "multiselect",
436
439
  name: "agents",
437
- message: "Install the React Grab skill for:",
440
+ message: `Install the React Grab skill (${global ? "global" : "this project"}) for:`,
438
441
  choices: detectedAgents.map((agent) => ({
439
442
  title: agentLabel(agent),
440
443
  value: agent,
@@ -450,7 +453,8 @@ const promptSkillInstall = async ({ yes = false } = {}) => {
450
453
  const { installed, failed } = await add({
451
454
  source: SKILL_SOURCE,
452
455
  agents: selectedAgents,
453
- global: true,
456
+ global,
457
+ cwd,
454
458
  mode: "copy"
455
459
  });
456
460
  if (installed.length === 0) {
@@ -461,19 +465,27 @@ const promptSkillInstall = async ({ yes = false } = {}) => {
461
465
  for (const record of failed) logger.log(` ${highlighter.error("✗")} ${agentLabel(record.agent)} ${record.error}`);
462
466
  return true;
463
467
  };
464
- const removeSkill = async () => {
465
- const agentsWithSkill = (await detectAvailableAgents()).filter((agent) => existsSync(installedSkillDir(agent)));
466
- for (const skillDir of new Set(agentsWithSkill.map(installedSkillDir))) rmSync(skillDir, {
468
+ const removeSkill = async (cwd = process.cwd()) => {
469
+ const agents = await detectAvailableAgents();
470
+ const removedAgents = [];
471
+ const dirsToRemove = /* @__PURE__ */ new Set();
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;
475
+ removedAgents.push(agent);
476
+ for (const dir of present) dirsToRemove.add(dir);
477
+ }
478
+ for (const skillDir of dirsToRemove) rmSync(skillDir, {
467
479
  recursive: true,
468
480
  force: true
469
481
  });
470
- for (const agent of agentsWithSkill) logger.log(` ${highlighter.success("✓")} ${agentLabel(agent)}`);
471
- return agentsWithSkill.length;
482
+ for (const agent of removedAgents) logger.log(` ${highlighter.success("✓")} ${agentLabel(agent)}`);
483
+ return removedAgents.length;
472
484
  };
473
485
  //#endregion
474
486
  //#region src/commands/add.ts
475
- const VERSION$5 = "0.1.39";
476
- 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()).action(async (opts) => {
487
+ const VERSION$5 = "0.1.41";
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) => {
477
489
  console.log(`${pc.magenta("✿")} ${pc.bold("React Grab")} ${pc.gray(VERSION$5)}`);
478
490
  console.log();
479
491
  try {
@@ -488,7 +500,11 @@ const add$1 = new Command().name("add").alias("install").description("install th
488
500
  }
489
501
  preflightSpinner.succeed();
490
502
  logger.break();
491
- if (!await promptSkillInstall({ yes: isNonInteractive }) && isNonInteractive) {
503
+ if (!await promptSkillInstall({
504
+ yes: isNonInteractive,
505
+ global: opts.global,
506
+ cwd: resolve(opts.cwd)
507
+ }) && isNonInteractive) {
492
508
  logger.break();
493
509
  process.exit(1);
494
510
  }
@@ -1066,6 +1082,8 @@ const previewCdnTransform = (projectRoot, framework, nextRouterType, targetCdnDo
1066
1082
  //#endregion
1067
1083
  //#region src/utils/constants.ts
1068
1084
  const MAX_KEY_HOLD_DURATION_MS = 2e3;
1085
+ const DEFAULT_WATCH_DIR = ".react-grab";
1086
+ const MAX_GRAB_AGE_MS = 300 * 1e3;
1069
1087
  //#endregion
1070
1088
  //#region src/utils/format-activation-key.ts
1071
1089
  const formatActivationKeyDisplay = (activationKey) => {
@@ -1083,7 +1101,7 @@ const formatActivationKeyDisplay = (activationKey) => {
1083
1101
  };
1084
1102
  //#endregion
1085
1103
  //#region src/commands/configure.ts
1086
- const VERSION$4 = "0.1.39";
1104
+ const VERSION$4 = "0.1.41";
1087
1105
  const isMac = process.platform === "darwin";
1088
1106
  const META_LABEL = isMac ? "Cmd" : "Win";
1089
1107
  const ALT_LABEL = isMac ? "Option" : "Alt";
@@ -1702,7 +1720,7 @@ const installPackagesWithFeedback = async (packages, packageManager, projectRoot
1702
1720
  };
1703
1721
  //#endregion
1704
1722
  //#region src/commands/init.ts
1705
- const VERSION$3 = "0.1.39";
1723
+ const VERSION$3 = "0.1.41";
1706
1724
  const REPORT_URL = "https://react-grab.com/api/report-cli";
1707
1725
  const DOCS_URL = "https://github.com/aidenybai/react-grab";
1708
1726
  const reportToCli = (type, config, error) => {
@@ -1769,7 +1787,7 @@ const failWithManualSetup = (failingSpinner, message, { listSupportedFrameworks
1769
1787
  logger.break();
1770
1788
  process.exit(1);
1771
1789
  };
1772
- const init = new 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) => {
1790
+ const init = new 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) => {
1773
1791
  console.log(`${pc.magenta("✿")} ${pc.bold("React Grab")} ${pc.gray(VERSION$3)}`);
1774
1792
  console.log();
1775
1793
  try {
@@ -1927,7 +1945,11 @@ const init = new Command().name("init").alias("setup").description("initialize R
1927
1945
  }
1928
1946
  }
1929
1947
  logger.break();
1930
- await promptSkillInstall({ yes: isNonInteractive });
1948
+ await promptSkillInstall({
1949
+ yes: isNonInteractive,
1950
+ global: opts.global,
1951
+ cwd
1952
+ });
1931
1953
  logger.break();
1932
1954
  process.exit(0);
1933
1955
  }
@@ -1993,7 +2015,11 @@ const init = new Command().name("init").alias("setup").description("initialize R
1993
2015
  let didInstallSkill = false;
1994
2016
  if (!isNonInteractive) {
1995
2017
  logger.break();
1996
- didInstallSkill = await promptSkillInstall({ yes: isNonInteractive });
2018
+ didInstallSkill = await promptSkillInstall({
2019
+ yes: isNonInteractive,
2020
+ global: opts.global,
2021
+ cwd
2022
+ });
1997
2023
  }
1998
2024
  const result = previewTransform(projectInfo.projectRoot, finalFramework, finalNextRouterType, false, opts.force);
1999
2025
  if (!result.success) {
@@ -2046,99 +2072,11 @@ const init = new Command().name("init").alias("setup").description("initialize R
2046
2072
  }
2047
2073
  });
2048
2074
  //#endregion
2049
- //#region src/commands/remove.ts
2050
- const VERSION$2 = "0.1.39";
2051
- const remove = new Command().name("remove").description("uninstall the React Grab skill from your agent").action(async () => {
2052
- console.log(`${pc.magenta("✿")} ${pc.bold("React Grab")} ${pc.gray(VERSION$2)}`);
2053
- console.log();
2054
- try {
2055
- logger.break();
2056
- const removedCount = await removeSkill();
2057
- logger.break();
2058
- if (removedCount === 0) logger.log("React Grab skill is not installed in any detected agent.");
2059
- else logger.log(`${highlighter.success("Removed")} the React Grab skill from ${removedCount} agent${removedCount === 1 ? "" : "s"}.`);
2060
- logger.break();
2061
- } catch (error) {
2062
- handleError(error);
2063
- }
2064
- });
2065
- //#endregion
2066
- //#region src/commands/upgrade.ts
2067
- const VERSION$1 = "0.1.39";
2068
- const NPM_REGISTRY_URL = "https://registry.npmjs.org/react-grab/latest";
2069
- const fetchLatestVersion = async () => {
2070
- try {
2071
- return (await (await fetch(NPM_REGISTRY_URL)).json()).version ?? null;
2072
- } catch {
2073
- return null;
2074
- }
2075
- };
2076
- const isDevDependency = (projectRoot) => {
2077
- const packageJsonPath = join(projectRoot, "package.json");
2078
- if (!existsSync(packageJsonPath)) return true;
2079
- try {
2080
- const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
2081
- if (packageJson.devDependencies?.["react-grab"]) return true;
2082
- if (packageJson.dependencies?.["react-grab"]) return false;
2083
- } catch {}
2084
- return true;
2085
- };
2086
- const upgrade = new Command().name("upgrade").alias("update").description("upgrade react-grab to the latest version").option("-c, --cwd <cwd>", "working directory (defaults to current directory)", process.cwd()).action(async (opts) => {
2087
- console.log(`${pc.magenta("✿")} ${pc.bold("React Grab")} ${pc.gray(VERSION$1)}`);
2088
- console.log();
2089
- try {
2090
- const cwd = resolve(opts.cwd);
2091
- const detectSpinner = spinner("Detecting project.").start();
2092
- const projectInfo = await detectProject(cwd);
2093
- if (!projectInfo.hasReactGrab) {
2094
- detectSpinner.fail("React Grab is not installed.");
2095
- logger.break();
2096
- logger.error(`Run ${highlighter.info("npx grab@latest init")} first to install React Grab.`);
2097
- logger.break();
2098
- process.exit(1);
2099
- }
2100
- detectSpinner.succeed();
2101
- const versionSpinner = spinner("Checking for updates.").start();
2102
- const latestVersion = await fetchLatestVersion();
2103
- if (!latestVersion) {
2104
- versionSpinner.fail("Could not check for updates.");
2105
- logger.break();
2106
- logger.error("Failed to reach the npm registry. Check your network connection.");
2107
- logger.break();
2108
- process.exit(1);
2109
- }
2110
- const installedVersion = projectInfo.reactGrabVersion;
2111
- if (installedVersion && installedVersion === latestVersion) {
2112
- versionSpinner.succeed(`Already on the latest version ${highlighter.info(`v${latestVersion}`)}.`);
2113
- logger.break();
2114
- process.exit(0);
2115
- }
2116
- const fromLabel = installedVersion ? `v${installedVersion}` : "unknown";
2117
- versionSpinner.succeed(`Update available: ${highlighter.dim(fromLabel)} → ${highlighter.info(`v${latestVersion}`)}.`);
2118
- const upgradeSpinner = spinner("Upgrading react-grab.").start();
2119
- try {
2120
- await installPackages(["react-grab@latest"], {
2121
- packageManager: projectInfo.packageManager,
2122
- cwd: projectInfo.projectRoot,
2123
- isDev: isDevDependency(projectInfo.projectRoot)
2124
- });
2125
- upgradeSpinner.succeed();
2126
- } catch {
2127
- upgradeSpinner.fail();
2128
- logger.break();
2129
- logger.error("Failed to upgrade. Check your network connection and try again.");
2130
- logger.break();
2131
- process.exit(1);
2132
- }
2133
- logger.break();
2134
- logger.log(`${highlighter.success("Success!")} React Grab has been upgraded to ${highlighter.info(`v${latestVersion}`)}.`);
2135
- logger.break();
2136
- } catch (error) {
2137
- handleError(error);
2138
- }
2139
- });
2075
+ //#region src/utils/sleep.ts
2076
+ const sleep = (durationMs) => new Promise((resolve) => setTimeout(resolve, durationMs));
2140
2077
  //#endregion
2141
2078
  //#region src/utils/clipboard.ts
2079
+ const HISTORY_FILE_NAME = "history.jsonl";
2142
2080
  const READ_TIMEOUT_MS = 2500;
2143
2081
  const MAX_CLIPBOARD_BYTES = 64 * 1024 * 1024;
2144
2082
  const ID_RADIX = 36;
@@ -2149,7 +2087,6 @@ const SIGNATURE_SCAN_CHARS = 32 * 1024;
2149
2087
  const GRAB_MIME = "application/x-react-grab";
2150
2088
  const CHROMIUM_CUSTOM_FORMAT = "chromium/x-web-custom-data";
2151
2089
  const GRAB_TEXT_SIGNATURE = /\bin\s+\S+\s+\(at\s+[^\n]{1,400}?:\d+:\d+\)/;
2152
- const sleep = (durationMs) => new Promise((resolve) => setTimeout(resolve, durationMs));
2153
2090
  const shortHash = (text) => createHash("sha1").update(text).digest("hex").slice(0, HASH_LENGTH);
2154
2091
  const alignUp = (value) => value + PICKLE_ALIGN_BYTES - 1 & ~(PICKLE_ALIGN_BYTES - 1);
2155
2092
  const parseChromiumPickle = (buffer) => {
@@ -2381,9 +2318,9 @@ const prepareWorkDir = (dir) => {
2381
2318
  const gitignore = path.join(dir, ".gitignore");
2382
2319
  if (!fs.existsSync(gitignore)) fs.writeFileSync(gitignore, "*\n");
2383
2320
  };
2384
- const watchForNextGrab = async (options) => {
2385
- const { reader, dir, intervalMs, replayLast, onWarn, signal } = options;
2386
- const logPath = path.join(dir, "history.jsonl");
2321
+ const runWatchLoop = async (options) => {
2322
+ const { reader, dir, intervalMs, replayLast, onWarn } = options;
2323
+ const logPath = path.join(dir, HISTORY_FILE_NAME);
2387
2324
  const { read } = reader;
2388
2325
  let lastChangeCount = null;
2389
2326
  let lastTimestamp = 0;
@@ -2398,9 +2335,8 @@ const watchForNextGrab = async (options) => {
2398
2335
  lastTimestamp = JSON.parse(initial.grab).timestamp ?? 0;
2399
2336
  } catch {}
2400
2337
  }
2401
- while (!signal?.aborted) {
2338
+ while (true) {
2402
2339
  await sleep(intervalMs);
2403
- if (signal?.aborted) return null;
2404
2340
  try {
2405
2341
  const snapshot = read();
2406
2342
  if (!snapshot) continue;
@@ -2446,7 +2382,6 @@ const watchForNextGrab = async (options) => {
2446
2382
  lastChangeCount = snapshot.changeCount;
2447
2383
  lastTextHash = textHash;
2448
2384
  lastTimestamp = nextTimestamp;
2449
- return captured;
2450
2385
  } catch (error) {
2451
2386
  const message = String(error?.message ?? error);
2452
2387
  if (message !== lastErrorMessage) {
@@ -2455,51 +2390,346 @@ const watchForNextGrab = async (options) => {
2455
2390
  }
2456
2391
  }
2457
2392
  }
2458
- return null;
2459
2393
  };
2460
2394
  //#endregion
2461
- //#region src/commands/watch.ts
2462
- const DEFAULT_INTERVAL_MS = 800;
2463
- const DEFAULT_DIR = ".react-grab";
2464
- const readersDir = () => path.dirname(fileURLToPath(import.meta.url));
2465
- const watch = new Command().name("watch").description("watch the clipboard for the next React Grab selection, print it, then exit").option("-d, --dir <dir>", "work dir for history.jsonl + cursor.txt", DEFAULT_DIR).option("-i, --interval <ms>", "clipboard poll interval in ms", String(DEFAULT_INTERVAL_MS)).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) => {
2395
+ //#region src/utils/grab-log.ts
2396
+ const CURSOR_FILE_NAME = "cursor.txt";
2397
+ const cursorFilePath = (dir) => path.join(dir, CURSOR_FILE_NAME);
2398
+ const readCompleteGrabLines = (dir) => {
2399
+ let raw;
2400
+ try {
2401
+ raw = fs.readFileSync(path.join(dir, HISTORY_FILE_NAME), "utf8");
2402
+ } catch {
2403
+ return [];
2404
+ }
2405
+ const lastNewline = raw.lastIndexOf("\n");
2406
+ if (lastNewline < 0) return [];
2407
+ return raw.slice(0, lastNewline).split("\n").filter(Boolean);
2408
+ };
2409
+ const readGrabCursor = (dir) => {
2410
+ try {
2411
+ const value = Number.parseInt(fs.readFileSync(cursorFilePath(dir), "utf8").trim(), 10);
2412
+ return Number.isInteger(value) && value >= 0 ? value : 0;
2413
+ } catch {
2414
+ return 0;
2415
+ }
2416
+ };
2417
+ const grabReceivedAt = (line) => {
2418
+ try {
2419
+ const value = JSON.parse(line).receivedAt;
2420
+ return typeof value === "number" ? value : null;
2421
+ } catch {
2422
+ return null;
2423
+ }
2424
+ };
2425
+ const consumeGrabs = (dir, options) => {
2426
+ const lines = readCompleteGrabLines(dir);
2427
+ if (options.all) return lines;
2428
+ const maxAgeMs = options.maxAgeMs ?? 0;
2429
+ const cursor = readGrabCursor(dir);
2430
+ const total = lines.length;
2431
+ const now = Date.now();
2432
+ 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;
2440
+ }
2441
+ fresh.push(line);
2442
+ }
2443
+ if (index !== cursor) fs.writeFileSync(cursorFilePath(dir), String(index));
2444
+ return fresh;
2445
+ };
2446
+ //#endregion
2447
+ //#region src/commands/read.ts
2448
+ 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;
2453
+ };
2454
+ const parseNonNegativeInt = (raw, fallback) => {
2455
+ const value = Number(raw);
2456
+ return Number.isInteger(value) && value >= 0 ? value : fallback;
2457
+ };
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) => {
2466
2459
  const dir = path.resolve(options.dir);
2467
- const intervalMs = Number(options.interval);
2468
- const textOnly = Boolean(options.textOnly);
2469
2460
  try {
2470
2461
  prepareWorkDir(dir);
2471
2462
  } catch (error) {
2472
- process.stderr.write(`react-grab watch: ${String(error?.message ?? error)}\n`);
2463
+ process.stderr.write(`react-grab read: ${String(error?.message ?? error)}\n`);
2473
2464
  process.exit(1);
2474
2465
  }
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
+ 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);
2482
+ while (Date.now() < deadline) {
2483
+ await sleep(200);
2484
+ if (drain() > 0) break;
2485
+ }
2486
+ process.exit(0);
2487
+ });
2488
+ //#endregion
2489
+ //#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 () => {
2492
+ console.log(`${pc.magenta("✿")} ${pc.bold("React Grab")} ${pc.gray(VERSION$2)}`);
2493
+ console.log();
2494
+ try {
2495
+ logger.break();
2496
+ const removedCount = await removeSkill();
2497
+ logger.break();
2498
+ if (removedCount === 0) logger.log("React Grab skill is not installed in any detected agent.");
2499
+ else logger.log(`${highlighter.success("Removed")} the React Grab skill from ${removedCount} agent${removedCount === 1 ? "" : "s"}.`);
2500
+ logger.break();
2501
+ } catch (error) {
2502
+ handleError(error);
2503
+ }
2504
+ });
2505
+ //#endregion
2506
+ //#region src/commands/upgrade.ts
2507
+ const VERSION$1 = "0.1.41";
2508
+ const NPM_REGISTRY_URL = "https://registry.npmjs.org/react-grab/latest";
2509
+ const fetchLatestVersion = async () => {
2510
+ try {
2511
+ return (await (await fetch(NPM_REGISTRY_URL)).json()).version ?? null;
2512
+ } catch {
2513
+ return null;
2514
+ }
2515
+ };
2516
+ const isDevDependency = (projectRoot) => {
2517
+ const packageJsonPath = join(projectRoot, "package.json");
2518
+ if (!existsSync(packageJsonPath)) return true;
2519
+ try {
2520
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
2521
+ if (packageJson.devDependencies?.["react-grab"]) return true;
2522
+ if (packageJson.dependencies?.["react-grab"]) return false;
2523
+ } catch {}
2524
+ return true;
2525
+ };
2526
+ const upgrade = new Command().name("upgrade").alias("update").description("upgrade react-grab to the latest version").option("-c, --cwd <cwd>", "working directory (defaults to current directory)", process.cwd()).action(async (opts) => {
2527
+ console.log(`${pc.magenta("✿")} ${pc.bold("React Grab")} ${pc.gray(VERSION$1)}`);
2528
+ console.log();
2529
+ try {
2530
+ const cwd = resolve(opts.cwd);
2531
+ const detectSpinner = spinner("Detecting project.").start();
2532
+ const projectInfo = await detectProject(cwd);
2533
+ if (!projectInfo.hasReactGrab) {
2534
+ detectSpinner.fail("React Grab is not installed.");
2535
+ logger.break();
2536
+ logger.error(`Run ${highlighter.info("npx grab@latest init")} first to install React Grab.`);
2537
+ logger.break();
2538
+ process.exit(1);
2539
+ }
2540
+ detectSpinner.succeed();
2541
+ const versionSpinner = spinner("Checking for updates.").start();
2542
+ const latestVersion = await fetchLatestVersion();
2543
+ if (!latestVersion) {
2544
+ versionSpinner.fail("Could not check for updates.");
2545
+ logger.break();
2546
+ logger.error("Failed to reach the npm registry. Check your network connection.");
2547
+ logger.break();
2548
+ process.exit(1);
2549
+ }
2550
+ const installedVersion = projectInfo.reactGrabVersion;
2551
+ if (installedVersion && installedVersion === latestVersion) {
2552
+ versionSpinner.succeed(`Already on the latest version ${highlighter.info(`v${latestVersion}`)}.`);
2553
+ logger.break();
2554
+ process.exit(0);
2555
+ }
2556
+ const fromLabel = installedVersion ? `v${installedVersion}` : "unknown";
2557
+ versionSpinner.succeed(`Update available: ${highlighter.dim(fromLabel)} → ${highlighter.info(`v${latestVersion}`)}.`);
2558
+ const upgradeSpinner = spinner("Upgrading react-grab.").start();
2559
+ try {
2560
+ await installPackages(["react-grab@latest"], {
2561
+ packageManager: projectInfo.packageManager,
2562
+ cwd: projectInfo.projectRoot,
2563
+ isDev: isDevDependency(projectInfo.projectRoot)
2564
+ });
2565
+ upgradeSpinner.succeed();
2566
+ } catch {
2567
+ upgradeSpinner.fail();
2568
+ logger.break();
2569
+ logger.error("Failed to upgrade. Check your network connection and try again.");
2570
+ logger.break();
2571
+ process.exit(1);
2572
+ }
2573
+ logger.break();
2574
+ logger.log(`${highlighter.success("Success!")} React Grab has been upgraded to ${highlighter.info(`v${latestVersion}`)}.`);
2575
+ logger.break();
2576
+ } catch (error) {
2577
+ handleError(error);
2578
+ }
2579
+ });
2580
+ //#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
+ //#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
+ const writeStatus = (message) => {
2662
+ process.stderr.write(`react-grab watch: ${message}\n`);
2663
+ };
2664
+ const runForeground = (dir, intervalMs, textOnly, replayLast) => {
2665
+ if (!claimDaemon(dir)) process.exit(0);
2666
+ process.on("exit", () => releaseDaemon(dir));
2475
2667
  const reader = createReader({
2476
2668
  textOnly,
2477
2669
  readersDir: readersDir(),
2478
2670
  workDir: dir
2479
2671
  });
2480
2672
  if (!reader) {
2481
- process.stderr.write("react-grab watch: 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.\n");
2673
+ writeStatus(NO_READER_MESSAGE);
2482
2674
  process.exit(1);
2483
2675
  }
2484
- process.stderr.write(`react-grab watch: watching clipboard via ${reader.mode}; history → ${path.join(dir, "history.jsonl")} (Ctrl+C to stop)\n`);
2485
- watchForNextGrab({
2676
+ writeStatus(`watching clipboard via ${reader.mode}; history → ${path.join(dir, HISTORY_FILE_NAME)}`);
2677
+ runWatchLoop({
2486
2678
  reader,
2487
2679
  dir,
2488
- intervalMs: Number.isFinite(intervalMs) && intervalMs > 0 ? intervalMs : DEFAULT_INTERVAL_MS,
2489
- replayLast: Boolean(options.replayLast),
2490
- onWarn: (message) => process.stderr.write(`react-grab watch: ${message}\n`)
2491
- }).then((grab) => {
2492
- if (!grab) process.exit(0);
2493
- process.stdout.write(`${JSON.stringify(grab)}\n`);
2494
- process.exit(0);
2680
+ intervalMs,
2681
+ replayLast,
2682
+ onWarn: writeStatus
2495
2683
  }).catch((error) => {
2496
- process.stderr.write(`react-grab watch: ${String(error?.message ?? error)}\n`);
2684
+ writeStatus(String(error?.message ?? error));
2497
2685
  process.exit(1);
2498
2686
  });
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);
2499
2729
  });
2500
2730
  //#endregion
2501
2731
  //#region src/cli.ts
2502
- const VERSION = "0.1.39";
2732
+ const VERSION = "0.1.41";
2503
2733
  const VERSION_API_URL = "https://www.react-grab.com/api/version";
2504
2734
  process.on("SIGINT", () => process.exit(0));
2505
2735
  process.on("SIGTERM", () => process.exit(0));
@@ -2513,6 +2743,7 @@ program.addCommand(remove);
2513
2743
  program.addCommand(configure);
2514
2744
  program.addCommand(upgrade);
2515
2745
  program.addCommand(watch);
2746
+ program.addCommand(read);
2516
2747
  const main = async () => {
2517
2748
  await program.parseAsync();
2518
2749
  };