@react-grab/cli 0.1.39 → 0.1.40
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 +297 -122
- package/dist/cli.js +298 -123
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
- package/skills/react-grab/SKILL.md +57 -16
package/dist/cli.js
CHANGED
|
@@ -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 = [
|
|
@@ -472,7 +472,7 @@ const removeSkill = async () => {
|
|
|
472
472
|
};
|
|
473
473
|
//#endregion
|
|
474
474
|
//#region src/commands/add.ts
|
|
475
|
-
const VERSION$5 = "0.1.
|
|
475
|
+
const VERSION$5 = "0.1.40";
|
|
476
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) => {
|
|
477
477
|
console.log(`${pc.magenta("✿")} ${pc.bold("React Grab")} ${pc.gray(VERSION$5)}`);
|
|
478
478
|
console.log();
|
|
@@ -1066,6 +1066,7 @@ const previewCdnTransform = (projectRoot, framework, nextRouterType, targetCdnDo
|
|
|
1066
1066
|
//#endregion
|
|
1067
1067
|
//#region src/utils/constants.ts
|
|
1068
1068
|
const MAX_KEY_HOLD_DURATION_MS = 2e3;
|
|
1069
|
+
const DEFAULT_WATCH_DIR = ".react-grab";
|
|
1069
1070
|
//#endregion
|
|
1070
1071
|
//#region src/utils/format-activation-key.ts
|
|
1071
1072
|
const formatActivationKeyDisplay = (activationKey) => {
|
|
@@ -1083,7 +1084,7 @@ const formatActivationKeyDisplay = (activationKey) => {
|
|
|
1083
1084
|
};
|
|
1084
1085
|
//#endregion
|
|
1085
1086
|
//#region src/commands/configure.ts
|
|
1086
|
-
const VERSION$4 = "0.1.
|
|
1087
|
+
const VERSION$4 = "0.1.40";
|
|
1087
1088
|
const isMac = process.platform === "darwin";
|
|
1088
1089
|
const META_LABEL = isMac ? "Cmd" : "Win";
|
|
1089
1090
|
const ALT_LABEL = isMac ? "Option" : "Alt";
|
|
@@ -1702,7 +1703,7 @@ const installPackagesWithFeedback = async (packages, packageManager, projectRoot
|
|
|
1702
1703
|
};
|
|
1703
1704
|
//#endregion
|
|
1704
1705
|
//#region src/commands/init.ts
|
|
1705
|
-
const VERSION$3 = "0.1.
|
|
1706
|
+
const VERSION$3 = "0.1.40";
|
|
1706
1707
|
const REPORT_URL = "https://react-grab.com/api/report-cli";
|
|
1707
1708
|
const DOCS_URL = "https://github.com/aidenybai/react-grab";
|
|
1708
1709
|
const reportToCli = (type, config, error) => {
|
|
@@ -2046,99 +2047,11 @@ const init = new Command().name("init").alias("setup").description("initialize R
|
|
|
2046
2047
|
}
|
|
2047
2048
|
});
|
|
2048
2049
|
//#endregion
|
|
2049
|
-
//#region src/
|
|
2050
|
-
const
|
|
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
|
-
});
|
|
2050
|
+
//#region src/utils/sleep.ts
|
|
2051
|
+
const sleep = (durationMs) => new Promise((resolve) => setTimeout(resolve, durationMs));
|
|
2140
2052
|
//#endregion
|
|
2141
2053
|
//#region src/utils/clipboard.ts
|
|
2054
|
+
const HISTORY_FILE_NAME = "history.jsonl";
|
|
2142
2055
|
const READ_TIMEOUT_MS = 2500;
|
|
2143
2056
|
const MAX_CLIPBOARD_BYTES = 64 * 1024 * 1024;
|
|
2144
2057
|
const ID_RADIX = 36;
|
|
@@ -2149,7 +2062,6 @@ const SIGNATURE_SCAN_CHARS = 32 * 1024;
|
|
|
2149
2062
|
const GRAB_MIME = "application/x-react-grab";
|
|
2150
2063
|
const CHROMIUM_CUSTOM_FORMAT = "chromium/x-web-custom-data";
|
|
2151
2064
|
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
2065
|
const shortHash = (text) => createHash("sha1").update(text).digest("hex").slice(0, HASH_LENGTH);
|
|
2154
2066
|
const alignUp = (value) => value + PICKLE_ALIGN_BYTES - 1 & ~(PICKLE_ALIGN_BYTES - 1);
|
|
2155
2067
|
const parseChromiumPickle = (buffer) => {
|
|
@@ -2381,9 +2293,9 @@ const prepareWorkDir = (dir) => {
|
|
|
2381
2293
|
const gitignore = path.join(dir, ".gitignore");
|
|
2382
2294
|
if (!fs.existsSync(gitignore)) fs.writeFileSync(gitignore, "*\n");
|
|
2383
2295
|
};
|
|
2384
|
-
const
|
|
2385
|
-
const { reader, dir, intervalMs, replayLast, onWarn
|
|
2386
|
-
const logPath = path.join(dir,
|
|
2296
|
+
const runWatchLoop = async (options) => {
|
|
2297
|
+
const { reader, dir, intervalMs, replayLast, onWarn } = options;
|
|
2298
|
+
const logPath = path.join(dir, HISTORY_FILE_NAME);
|
|
2387
2299
|
const { read } = reader;
|
|
2388
2300
|
let lastChangeCount = null;
|
|
2389
2301
|
let lastTimestamp = 0;
|
|
@@ -2398,9 +2310,8 @@ const watchForNextGrab = async (options) => {
|
|
|
2398
2310
|
lastTimestamp = JSON.parse(initial.grab).timestamp ?? 0;
|
|
2399
2311
|
} catch {}
|
|
2400
2312
|
}
|
|
2401
|
-
while (
|
|
2313
|
+
while (true) {
|
|
2402
2314
|
await sleep(intervalMs);
|
|
2403
|
-
if (signal?.aborted) return null;
|
|
2404
2315
|
try {
|
|
2405
2316
|
const snapshot = read();
|
|
2406
2317
|
if (!snapshot) continue;
|
|
@@ -2446,7 +2357,6 @@ const watchForNextGrab = async (options) => {
|
|
|
2446
2357
|
lastChangeCount = snapshot.changeCount;
|
|
2447
2358
|
lastTextHash = textHash;
|
|
2448
2359
|
lastTimestamp = nextTimestamp;
|
|
2449
|
-
return captured;
|
|
2450
2360
|
} catch (error) {
|
|
2451
2361
|
const message = String(error?.message ?? error);
|
|
2452
2362
|
if (message !== lastErrorMessage) {
|
|
@@ -2455,51 +2365,315 @@ const watchForNextGrab = async (options) => {
|
|
|
2455
2365
|
}
|
|
2456
2366
|
}
|
|
2457
2367
|
}
|
|
2458
|
-
return null;
|
|
2459
2368
|
};
|
|
2460
2369
|
//#endregion
|
|
2461
|
-
//#region src/
|
|
2462
|
-
const
|
|
2463
|
-
const
|
|
2464
|
-
const
|
|
2465
|
-
|
|
2370
|
+
//#region src/utils/grab-log.ts
|
|
2371
|
+
const CURSOR_FILE_NAME = "cursor.txt";
|
|
2372
|
+
const cursorFilePath = (dir) => path.join(dir, CURSOR_FILE_NAME);
|
|
2373
|
+
const readCompleteGrabLines = (dir) => {
|
|
2374
|
+
let raw;
|
|
2375
|
+
try {
|
|
2376
|
+
raw = fs.readFileSync(path.join(dir, HISTORY_FILE_NAME), "utf8");
|
|
2377
|
+
} catch {
|
|
2378
|
+
return [];
|
|
2379
|
+
}
|
|
2380
|
+
const lastNewline = raw.lastIndexOf("\n");
|
|
2381
|
+
if (lastNewline < 0) return [];
|
|
2382
|
+
return raw.slice(0, lastNewline).split("\n").filter(Boolean);
|
|
2383
|
+
};
|
|
2384
|
+
const readGrabCursor = (dir) => {
|
|
2385
|
+
try {
|
|
2386
|
+
const value = Number.parseInt(fs.readFileSync(cursorFilePath(dir), "utf8").trim(), 10);
|
|
2387
|
+
return Number.isInteger(value) && value >= 0 ? value : 0;
|
|
2388
|
+
} catch {
|
|
2389
|
+
return 0;
|
|
2390
|
+
}
|
|
2391
|
+
};
|
|
2392
|
+
const consumeGrabs = (dir, options) => {
|
|
2393
|
+
const lines = readCompleteGrabLines(dir);
|
|
2394
|
+
if (options.all) return lines;
|
|
2395
|
+
const cursor = readGrabCursor(dir);
|
|
2396
|
+
const start = Math.min(cursor, lines.length);
|
|
2397
|
+
const end = options.limit > 0 ? Math.min(start + options.limit, lines.length) : lines.length;
|
|
2398
|
+
if (end !== cursor) fs.writeFileSync(cursorFilePath(dir), String(end));
|
|
2399
|
+
return lines.slice(start, end);
|
|
2400
|
+
};
|
|
2401
|
+
//#endregion
|
|
2402
|
+
//#region src/commands/read.ts
|
|
2403
|
+
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> 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) => {
|
|
2466
2404
|
const dir = path.resolve(options.dir);
|
|
2467
|
-
const intervalMs = Number(options.interval);
|
|
2468
|
-
const textOnly = Boolean(options.textOnly);
|
|
2469
2405
|
try {
|
|
2470
2406
|
prepareWorkDir(dir);
|
|
2471
2407
|
} catch (error) {
|
|
2472
|
-
process.stderr.write(`react-grab
|
|
2408
|
+
process.stderr.write(`react-grab read: ${String(error?.message ?? error)}\n`);
|
|
2473
2409
|
process.exit(1);
|
|
2474
2410
|
}
|
|
2411
|
+
unrefStdin();
|
|
2412
|
+
const limitRaw = Number(options.limit);
|
|
2413
|
+
const limit = Number.isInteger(limitRaw) && limitRaw >= 0 ? limitRaw : 50;
|
|
2414
|
+
const all = Boolean(options.all);
|
|
2415
|
+
const drain = () => {
|
|
2416
|
+
const fresh = consumeGrabs(dir, {
|
|
2417
|
+
limit,
|
|
2418
|
+
all
|
|
2419
|
+
});
|
|
2420
|
+
if (fresh.length > 0) process.stdout.write(`${fresh.join("\n")}\n`);
|
|
2421
|
+
return fresh.length;
|
|
2422
|
+
};
|
|
2423
|
+
const waitRaw = options.wait ? Number(options.wait) : 0;
|
|
2424
|
+
const deadline = Date.now() + (Number.isFinite(waitRaw) && waitRaw > 0 ? waitRaw : 0);
|
|
2425
|
+
if (drain() > 0) process.exit(0);
|
|
2426
|
+
while (Date.now() < deadline) {
|
|
2427
|
+
await sleep(200);
|
|
2428
|
+
if (drain() > 0) break;
|
|
2429
|
+
}
|
|
2430
|
+
process.exit(0);
|
|
2431
|
+
});
|
|
2432
|
+
//#endregion
|
|
2433
|
+
//#region src/commands/remove.ts
|
|
2434
|
+
const VERSION$2 = "0.1.40";
|
|
2435
|
+
const remove = new Command().name("remove").description("uninstall the React Grab skill from your agent").action(async () => {
|
|
2436
|
+
console.log(`${pc.magenta("✿")} ${pc.bold("React Grab")} ${pc.gray(VERSION$2)}`);
|
|
2437
|
+
console.log();
|
|
2438
|
+
try {
|
|
2439
|
+
logger.break();
|
|
2440
|
+
const removedCount = await removeSkill();
|
|
2441
|
+
logger.break();
|
|
2442
|
+
if (removedCount === 0) logger.log("React Grab skill is not installed in any detected agent.");
|
|
2443
|
+
else logger.log(`${highlighter.success("Removed")} the React Grab skill from ${removedCount} agent${removedCount === 1 ? "" : "s"}.`);
|
|
2444
|
+
logger.break();
|
|
2445
|
+
} catch (error) {
|
|
2446
|
+
handleError(error);
|
|
2447
|
+
}
|
|
2448
|
+
});
|
|
2449
|
+
//#endregion
|
|
2450
|
+
//#region src/commands/upgrade.ts
|
|
2451
|
+
const VERSION$1 = "0.1.40";
|
|
2452
|
+
const NPM_REGISTRY_URL = "https://registry.npmjs.org/react-grab/latest";
|
|
2453
|
+
const fetchLatestVersion = async () => {
|
|
2454
|
+
try {
|
|
2455
|
+
return (await (await fetch(NPM_REGISTRY_URL)).json()).version ?? null;
|
|
2456
|
+
} catch {
|
|
2457
|
+
return null;
|
|
2458
|
+
}
|
|
2459
|
+
};
|
|
2460
|
+
const isDevDependency = (projectRoot) => {
|
|
2461
|
+
const packageJsonPath = join(projectRoot, "package.json");
|
|
2462
|
+
if (!existsSync(packageJsonPath)) return true;
|
|
2463
|
+
try {
|
|
2464
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
2465
|
+
if (packageJson.devDependencies?.["react-grab"]) return true;
|
|
2466
|
+
if (packageJson.dependencies?.["react-grab"]) return false;
|
|
2467
|
+
} catch {}
|
|
2468
|
+
return true;
|
|
2469
|
+
};
|
|
2470
|
+
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) => {
|
|
2471
|
+
console.log(`${pc.magenta("✿")} ${pc.bold("React Grab")} ${pc.gray(VERSION$1)}`);
|
|
2472
|
+
console.log();
|
|
2473
|
+
try {
|
|
2474
|
+
const cwd = resolve(opts.cwd);
|
|
2475
|
+
const detectSpinner = spinner("Detecting project.").start();
|
|
2476
|
+
const projectInfo = await detectProject(cwd);
|
|
2477
|
+
if (!projectInfo.hasReactGrab) {
|
|
2478
|
+
detectSpinner.fail("React Grab is not installed.");
|
|
2479
|
+
logger.break();
|
|
2480
|
+
logger.error(`Run ${highlighter.info("npx grab@latest init")} first to install React Grab.`);
|
|
2481
|
+
logger.break();
|
|
2482
|
+
process.exit(1);
|
|
2483
|
+
}
|
|
2484
|
+
detectSpinner.succeed();
|
|
2485
|
+
const versionSpinner = spinner("Checking for updates.").start();
|
|
2486
|
+
const latestVersion = await fetchLatestVersion();
|
|
2487
|
+
if (!latestVersion) {
|
|
2488
|
+
versionSpinner.fail("Could not check for updates.");
|
|
2489
|
+
logger.break();
|
|
2490
|
+
logger.error("Failed to reach the npm registry. Check your network connection.");
|
|
2491
|
+
logger.break();
|
|
2492
|
+
process.exit(1);
|
|
2493
|
+
}
|
|
2494
|
+
const installedVersion = projectInfo.reactGrabVersion;
|
|
2495
|
+
if (installedVersion && installedVersion === latestVersion) {
|
|
2496
|
+
versionSpinner.succeed(`Already on the latest version ${highlighter.info(`v${latestVersion}`)}.`);
|
|
2497
|
+
logger.break();
|
|
2498
|
+
process.exit(0);
|
|
2499
|
+
}
|
|
2500
|
+
const fromLabel = installedVersion ? `v${installedVersion}` : "unknown";
|
|
2501
|
+
versionSpinner.succeed(`Update available: ${highlighter.dim(fromLabel)} → ${highlighter.info(`v${latestVersion}`)}.`);
|
|
2502
|
+
const upgradeSpinner = spinner("Upgrading react-grab.").start();
|
|
2503
|
+
try {
|
|
2504
|
+
await installPackages(["react-grab@latest"], {
|
|
2505
|
+
packageManager: projectInfo.packageManager,
|
|
2506
|
+
cwd: projectInfo.projectRoot,
|
|
2507
|
+
isDev: isDevDependency(projectInfo.projectRoot)
|
|
2508
|
+
});
|
|
2509
|
+
upgradeSpinner.succeed();
|
|
2510
|
+
} catch {
|
|
2511
|
+
upgradeSpinner.fail();
|
|
2512
|
+
logger.break();
|
|
2513
|
+
logger.error("Failed to upgrade. Check your network connection and try again.");
|
|
2514
|
+
logger.break();
|
|
2515
|
+
process.exit(1);
|
|
2516
|
+
}
|
|
2517
|
+
logger.break();
|
|
2518
|
+
logger.log(`${highlighter.success("Success!")} React Grab has been upgraded to ${highlighter.info(`v${latestVersion}`)}.`);
|
|
2519
|
+
logger.break();
|
|
2520
|
+
} catch (error) {
|
|
2521
|
+
handleError(error);
|
|
2522
|
+
}
|
|
2523
|
+
});
|
|
2524
|
+
//#endregion
|
|
2525
|
+
//#region src/utils/daemon.ts
|
|
2526
|
+
const PID_FILE_NAME = "watch.pid";
|
|
2527
|
+
const pidFilePath = (dir) => path.join(dir, PID_FILE_NAME);
|
|
2528
|
+
const cliEntryPath = () => process.argv[1] ?? fileURLToPath(import.meta.url);
|
|
2529
|
+
const isProcessAlive = (pid) => {
|
|
2530
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
2531
|
+
try {
|
|
2532
|
+
process.kill(pid, 0);
|
|
2533
|
+
return true;
|
|
2534
|
+
} catch (error) {
|
|
2535
|
+
return error.code === "EPERM";
|
|
2536
|
+
}
|
|
2537
|
+
};
|
|
2538
|
+
const readDaemonPid = (dir) => {
|
|
2539
|
+
try {
|
|
2540
|
+
const pid = Number.parseInt(fs.readFileSync(pidFilePath(dir), "utf8").trim(), 10);
|
|
2541
|
+
return Number.isInteger(pid) && pid > 0 ? pid : null;
|
|
2542
|
+
} catch {
|
|
2543
|
+
return null;
|
|
2544
|
+
}
|
|
2545
|
+
};
|
|
2546
|
+
const isDaemonRunning = (dir) => {
|
|
2547
|
+
const pid = readDaemonPid(dir);
|
|
2548
|
+
return pid !== null && isProcessAlive(pid);
|
|
2549
|
+
};
|
|
2550
|
+
const claimDaemon = (dir) => {
|
|
2551
|
+
const file = pidFilePath(dir);
|
|
2552
|
+
for (let attempt = 0; attempt < 50; attempt += 1) try {
|
|
2553
|
+
const handle = fs.openSync(file, "wx");
|
|
2554
|
+
fs.writeFileSync(handle, String(process.pid));
|
|
2555
|
+
fs.closeSync(handle);
|
|
2556
|
+
return readDaemonPid(dir) === process.pid;
|
|
2557
|
+
} catch (error) {
|
|
2558
|
+
if (error.code !== "EEXIST") throw error;
|
|
2559
|
+
if (isDaemonRunning(dir)) return false;
|
|
2560
|
+
try {
|
|
2561
|
+
fs.rmSync(file, { force: true });
|
|
2562
|
+
} catch {}
|
|
2563
|
+
}
|
|
2564
|
+
return false;
|
|
2565
|
+
};
|
|
2566
|
+
const releaseDaemon = (dir) => {
|
|
2567
|
+
if (readDaemonPid(dir) === process.pid) try {
|
|
2568
|
+
fs.rmSync(pidFilePath(dir), { force: true });
|
|
2569
|
+
} catch {}
|
|
2570
|
+
};
|
|
2571
|
+
const stopDaemon = (dir) => {
|
|
2572
|
+
const pid = readDaemonPid(dir);
|
|
2573
|
+
if (pid === null) return null;
|
|
2574
|
+
const wasAlive = isProcessAlive(pid);
|
|
2575
|
+
if (wasAlive) try {
|
|
2576
|
+
process.kill(pid, "SIGTERM");
|
|
2577
|
+
} catch {}
|
|
2578
|
+
if (readDaemonPid(dir) === pid) try {
|
|
2579
|
+
fs.rmSync(pidFilePath(dir), { force: true });
|
|
2580
|
+
} catch {}
|
|
2581
|
+
return wasAlive ? pid : null;
|
|
2582
|
+
};
|
|
2583
|
+
const spawnDaemon = (options) => {
|
|
2584
|
+
const args = [
|
|
2585
|
+
cliEntryPath(),
|
|
2586
|
+
"watch",
|
|
2587
|
+
"--foreground",
|
|
2588
|
+
"--dir",
|
|
2589
|
+
options.dir,
|
|
2590
|
+
"--interval",
|
|
2591
|
+
String(options.intervalMs)
|
|
2592
|
+
];
|
|
2593
|
+
if (options.textOnly) args.push("--text-only");
|
|
2594
|
+
if (options.replayLast) args.push("--replay-last");
|
|
2595
|
+
spawn(process.execPath, args, {
|
|
2596
|
+
detached: true,
|
|
2597
|
+
stdio: "ignore",
|
|
2598
|
+
windowsHide: true
|
|
2599
|
+
}).unref();
|
|
2600
|
+
};
|
|
2601
|
+
//#endregion
|
|
2602
|
+
//#region src/commands/watch.ts
|
|
2603
|
+
const readersDir = () => path.dirname(fileURLToPath(import.meta.url));
|
|
2604
|
+
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.";
|
|
2605
|
+
const writeStatus = (message) => {
|
|
2606
|
+
process.stderr.write(`react-grab watch: ${message}\n`);
|
|
2607
|
+
};
|
|
2608
|
+
const runForeground = (dir, intervalMs, textOnly, replayLast) => {
|
|
2609
|
+
if (!claimDaemon(dir)) process.exit(0);
|
|
2610
|
+
process.on("exit", () => releaseDaemon(dir));
|
|
2475
2611
|
const reader = createReader({
|
|
2476
2612
|
textOnly,
|
|
2477
2613
|
readersDir: readersDir(),
|
|
2478
2614
|
workDir: dir
|
|
2479
2615
|
});
|
|
2480
2616
|
if (!reader) {
|
|
2481
|
-
|
|
2617
|
+
writeStatus(NO_READER_MESSAGE);
|
|
2482
2618
|
process.exit(1);
|
|
2483
2619
|
}
|
|
2484
|
-
|
|
2485
|
-
|
|
2620
|
+
writeStatus(`watching clipboard via ${reader.mode}; history → ${path.join(dir, HISTORY_FILE_NAME)}`);
|
|
2621
|
+
runWatchLoop({
|
|
2486
2622
|
reader,
|
|
2487
2623
|
dir,
|
|
2488
|
-
intervalMs
|
|
2489
|
-
replayLast
|
|
2490
|
-
onWarn:
|
|
2491
|
-
}).then((grab) => {
|
|
2492
|
-
if (!grab) process.exit(0);
|
|
2493
|
-
process.stdout.write(`${JSON.stringify(grab)}\n`);
|
|
2494
|
-
process.exit(0);
|
|
2624
|
+
intervalMs,
|
|
2625
|
+
replayLast,
|
|
2626
|
+
onWarn: writeStatus
|
|
2495
2627
|
}).catch((error) => {
|
|
2496
|
-
|
|
2628
|
+
writeStatus(String(error?.message ?? error));
|
|
2497
2629
|
process.exit(1);
|
|
2498
2630
|
});
|
|
2631
|
+
};
|
|
2632
|
+
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) => {
|
|
2633
|
+
const dir = path.resolve(options.dir);
|
|
2634
|
+
const intervalRaw = Number(options.interval);
|
|
2635
|
+
const intervalMs = Number.isFinite(intervalRaw) && intervalRaw > 0 ? intervalRaw : 800;
|
|
2636
|
+
const textOnly = Boolean(options.textOnly);
|
|
2637
|
+
const replayLast = Boolean(options.replayLast);
|
|
2638
|
+
try {
|
|
2639
|
+
prepareWorkDir(dir);
|
|
2640
|
+
} catch (error) {
|
|
2641
|
+
writeStatus(String(error?.message ?? error));
|
|
2642
|
+
process.exit(1);
|
|
2643
|
+
}
|
|
2644
|
+
if (options.stop) {
|
|
2645
|
+
const stoppedPid = stopDaemon(dir);
|
|
2646
|
+
writeStatus(stoppedPid ? `stopped daemon (pid ${stoppedPid})` : `no daemon running for ${dir}`);
|
|
2647
|
+
process.exit(0);
|
|
2648
|
+
}
|
|
2649
|
+
if (options.foreground) {
|
|
2650
|
+
runForeground(dir, intervalMs, textOnly, replayLast);
|
|
2651
|
+
return;
|
|
2652
|
+
}
|
|
2653
|
+
if (isDaemonRunning(dir)) {
|
|
2654
|
+
writeStatus(`already watching ${dir} (pid ${readDaemonPid(dir)})`);
|
|
2655
|
+
process.exit(0);
|
|
2656
|
+
}
|
|
2657
|
+
if (!createReader({
|
|
2658
|
+
textOnly,
|
|
2659
|
+
readersDir: readersDir(),
|
|
2660
|
+
workDir: dir
|
|
2661
|
+
})) {
|
|
2662
|
+
writeStatus(NO_READER_MESSAGE);
|
|
2663
|
+
process.exit(1);
|
|
2664
|
+
}
|
|
2665
|
+
spawnDaemon({
|
|
2666
|
+
dir,
|
|
2667
|
+
intervalMs,
|
|
2668
|
+
textOnly,
|
|
2669
|
+
replayLast
|
|
2670
|
+
});
|
|
2671
|
+
writeStatus(`started; capturing grabs → ${path.join(dir, HISTORY_FILE_NAME)} (run \`grab read\` to consume, \`grab watch --stop\` to stop)`);
|
|
2672
|
+
process.exit(0);
|
|
2499
2673
|
});
|
|
2500
2674
|
//#endregion
|
|
2501
2675
|
//#region src/cli.ts
|
|
2502
|
-
const VERSION = "0.1.
|
|
2676
|
+
const VERSION = "0.1.40";
|
|
2503
2677
|
const VERSION_API_URL = "https://www.react-grab.com/api/version";
|
|
2504
2678
|
process.on("SIGINT", () => process.exit(0));
|
|
2505
2679
|
process.on("SIGTERM", () => process.exit(0));
|
|
@@ -2513,6 +2687,7 @@ program.addCommand(remove);
|
|
|
2513
2687
|
program.addCommand(configure);
|
|
2514
2688
|
program.addCommand(upgrade);
|
|
2515
2689
|
program.addCommand(watch);
|
|
2690
|
+
program.addCommand(read);
|
|
2516
2691
|
const main = async () => {
|
|
2517
2692
|
await program.parseAsync();
|
|
2518
2693
|
};
|