@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.cjs
CHANGED
|
@@ -500,7 +500,7 @@ const removeSkill = async () => {
|
|
|
500
500
|
};
|
|
501
501
|
//#endregion
|
|
502
502
|
//#region src/commands/add.ts
|
|
503
|
-
const VERSION$5 = "0.1.
|
|
503
|
+
const VERSION$5 = "0.1.40";
|
|
504
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) => {
|
|
505
505
|
console.log(`${picocolors.default.magenta("✿")} ${picocolors.default.bold("React Grab")} ${picocolors.default.gray(VERSION$5)}`);
|
|
506
506
|
console.log();
|
|
@@ -1094,6 +1094,7 @@ const previewCdnTransform = (projectRoot, framework, nextRouterType, targetCdnDo
|
|
|
1094
1094
|
//#endregion
|
|
1095
1095
|
//#region src/utils/constants.ts
|
|
1096
1096
|
const MAX_KEY_HOLD_DURATION_MS = 2e3;
|
|
1097
|
+
const DEFAULT_WATCH_DIR = ".react-grab";
|
|
1097
1098
|
//#endregion
|
|
1098
1099
|
//#region src/utils/format-activation-key.ts
|
|
1099
1100
|
const formatActivationKeyDisplay = (activationKey) => {
|
|
@@ -1111,7 +1112,7 @@ const formatActivationKeyDisplay = (activationKey) => {
|
|
|
1111
1112
|
};
|
|
1112
1113
|
//#endregion
|
|
1113
1114
|
//#region src/commands/configure.ts
|
|
1114
|
-
const VERSION$4 = "0.1.
|
|
1115
|
+
const VERSION$4 = "0.1.40";
|
|
1115
1116
|
const isMac = process.platform === "darwin";
|
|
1116
1117
|
const META_LABEL = isMac ? "Cmd" : "Win";
|
|
1117
1118
|
const ALT_LABEL = isMac ? "Option" : "Alt";
|
|
@@ -1730,7 +1731,7 @@ const installPackagesWithFeedback = async (packages, packageManager, projectRoot
|
|
|
1730
1731
|
};
|
|
1731
1732
|
//#endregion
|
|
1732
1733
|
//#region src/commands/init.ts
|
|
1733
|
-
const VERSION$3 = "0.1.
|
|
1734
|
+
const VERSION$3 = "0.1.40";
|
|
1734
1735
|
const REPORT_URL = "https://react-grab.com/api/report-cli";
|
|
1735
1736
|
const DOCS_URL = "https://github.com/aidenybai/react-grab";
|
|
1736
1737
|
const reportToCli = (type, config, error) => {
|
|
@@ -2074,99 +2075,11 @@ const init = new commander.Command().name("init").alias("setup").description("in
|
|
|
2074
2075
|
}
|
|
2075
2076
|
});
|
|
2076
2077
|
//#endregion
|
|
2077
|
-
//#region src/
|
|
2078
|
-
const
|
|
2079
|
-
const remove = new commander.Command().name("remove").description("uninstall the React Grab skill from your agent").action(async () => {
|
|
2080
|
-
console.log(`${picocolors.default.magenta("✿")} ${picocolors.default.bold("React Grab")} ${picocolors.default.gray(VERSION$2)}`);
|
|
2081
|
-
console.log();
|
|
2082
|
-
try {
|
|
2083
|
-
logger.break();
|
|
2084
|
-
const removedCount = await removeSkill();
|
|
2085
|
-
logger.break();
|
|
2086
|
-
if (removedCount === 0) logger.log("React Grab skill is not installed in any detected agent.");
|
|
2087
|
-
else logger.log(`${highlighter.success("Removed")} the React Grab skill from ${removedCount} agent${removedCount === 1 ? "" : "s"}.`);
|
|
2088
|
-
logger.break();
|
|
2089
|
-
} catch (error) {
|
|
2090
|
-
handleError(error);
|
|
2091
|
-
}
|
|
2092
|
-
});
|
|
2093
|
-
//#endregion
|
|
2094
|
-
//#region src/commands/upgrade.ts
|
|
2095
|
-
const VERSION$1 = "0.1.39";
|
|
2096
|
-
const NPM_REGISTRY_URL = "https://registry.npmjs.org/react-grab/latest";
|
|
2097
|
-
const fetchLatestVersion = async () => {
|
|
2098
|
-
try {
|
|
2099
|
-
return (await (await fetch(NPM_REGISTRY_URL)).json()).version ?? null;
|
|
2100
|
-
} catch {
|
|
2101
|
-
return null;
|
|
2102
|
-
}
|
|
2103
|
-
};
|
|
2104
|
-
const isDevDependency = (projectRoot) => {
|
|
2105
|
-
const packageJsonPath = (0, node_path.join)(projectRoot, "package.json");
|
|
2106
|
-
if (!(0, node_fs.existsSync)(packageJsonPath)) return true;
|
|
2107
|
-
try {
|
|
2108
|
-
const packageJson = JSON.parse((0, node_fs.readFileSync)(packageJsonPath, "utf-8"));
|
|
2109
|
-
if (packageJson.devDependencies?.["react-grab"]) return true;
|
|
2110
|
-
if (packageJson.dependencies?.["react-grab"]) return false;
|
|
2111
|
-
} catch {}
|
|
2112
|
-
return true;
|
|
2113
|
-
};
|
|
2114
|
-
const upgrade = new commander.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) => {
|
|
2115
|
-
console.log(`${picocolors.default.magenta("✿")} ${picocolors.default.bold("React Grab")} ${picocolors.default.gray(VERSION$1)}`);
|
|
2116
|
-
console.log();
|
|
2117
|
-
try {
|
|
2118
|
-
const cwd = (0, node_path.resolve)(opts.cwd);
|
|
2119
|
-
const detectSpinner = spinner("Detecting project.").start();
|
|
2120
|
-
const projectInfo = await detectProject(cwd);
|
|
2121
|
-
if (!projectInfo.hasReactGrab) {
|
|
2122
|
-
detectSpinner.fail("React Grab is not installed.");
|
|
2123
|
-
logger.break();
|
|
2124
|
-
logger.error(`Run ${highlighter.info("npx grab@latest init")} first to install React Grab.`);
|
|
2125
|
-
logger.break();
|
|
2126
|
-
process.exit(1);
|
|
2127
|
-
}
|
|
2128
|
-
detectSpinner.succeed();
|
|
2129
|
-
const versionSpinner = spinner("Checking for updates.").start();
|
|
2130
|
-
const latestVersion = await fetchLatestVersion();
|
|
2131
|
-
if (!latestVersion) {
|
|
2132
|
-
versionSpinner.fail("Could not check for updates.");
|
|
2133
|
-
logger.break();
|
|
2134
|
-
logger.error("Failed to reach the npm registry. Check your network connection.");
|
|
2135
|
-
logger.break();
|
|
2136
|
-
process.exit(1);
|
|
2137
|
-
}
|
|
2138
|
-
const installedVersion = projectInfo.reactGrabVersion;
|
|
2139
|
-
if (installedVersion && installedVersion === latestVersion) {
|
|
2140
|
-
versionSpinner.succeed(`Already on the latest version ${highlighter.info(`v${latestVersion}`)}.`);
|
|
2141
|
-
logger.break();
|
|
2142
|
-
process.exit(0);
|
|
2143
|
-
}
|
|
2144
|
-
const fromLabel = installedVersion ? `v${installedVersion}` : "unknown";
|
|
2145
|
-
versionSpinner.succeed(`Update available: ${highlighter.dim(fromLabel)} → ${highlighter.info(`v${latestVersion}`)}.`);
|
|
2146
|
-
const upgradeSpinner = spinner("Upgrading react-grab.").start();
|
|
2147
|
-
try {
|
|
2148
|
-
await installPackages(["react-grab@latest"], {
|
|
2149
|
-
packageManager: projectInfo.packageManager,
|
|
2150
|
-
cwd: projectInfo.projectRoot,
|
|
2151
|
-
isDev: isDevDependency(projectInfo.projectRoot)
|
|
2152
|
-
});
|
|
2153
|
-
upgradeSpinner.succeed();
|
|
2154
|
-
} catch {
|
|
2155
|
-
upgradeSpinner.fail();
|
|
2156
|
-
logger.break();
|
|
2157
|
-
logger.error("Failed to upgrade. Check your network connection and try again.");
|
|
2158
|
-
logger.break();
|
|
2159
|
-
process.exit(1);
|
|
2160
|
-
}
|
|
2161
|
-
logger.break();
|
|
2162
|
-
logger.log(`${highlighter.success("Success!")} React Grab has been upgraded to ${highlighter.info(`v${latestVersion}`)}.`);
|
|
2163
|
-
logger.break();
|
|
2164
|
-
} catch (error) {
|
|
2165
|
-
handleError(error);
|
|
2166
|
-
}
|
|
2167
|
-
});
|
|
2078
|
+
//#region src/utils/sleep.ts
|
|
2079
|
+
const sleep = (durationMs) => new Promise((resolve) => setTimeout(resolve, durationMs));
|
|
2168
2080
|
//#endregion
|
|
2169
2081
|
//#region src/utils/clipboard.ts
|
|
2082
|
+
const HISTORY_FILE_NAME = "history.jsonl";
|
|
2170
2083
|
const READ_TIMEOUT_MS = 2500;
|
|
2171
2084
|
const MAX_CLIPBOARD_BYTES = 64 * 1024 * 1024;
|
|
2172
2085
|
const ID_RADIX = 36;
|
|
@@ -2177,7 +2090,6 @@ const SIGNATURE_SCAN_CHARS = 32 * 1024;
|
|
|
2177
2090
|
const GRAB_MIME = "application/x-react-grab";
|
|
2178
2091
|
const CHROMIUM_CUSTOM_FORMAT = "chromium/x-web-custom-data";
|
|
2179
2092
|
const GRAB_TEXT_SIGNATURE = /\bin\s+\S+\s+\(at\s+[^\n]{1,400}?:\d+:\d+\)/;
|
|
2180
|
-
const sleep = (durationMs) => new Promise((resolve) => setTimeout(resolve, durationMs));
|
|
2181
2093
|
const shortHash = (text) => (0, node_crypto.createHash)("sha1").update(text).digest("hex").slice(0, HASH_LENGTH);
|
|
2182
2094
|
const alignUp = (value) => value + PICKLE_ALIGN_BYTES - 1 & ~(PICKLE_ALIGN_BYTES - 1);
|
|
2183
2095
|
const parseChromiumPickle = (buffer) => {
|
|
@@ -2409,9 +2321,9 @@ const prepareWorkDir = (dir) => {
|
|
|
2409
2321
|
const gitignore = node_path.default.join(dir, ".gitignore");
|
|
2410
2322
|
if (!node_fs.default.existsSync(gitignore)) node_fs.default.writeFileSync(gitignore, "*\n");
|
|
2411
2323
|
};
|
|
2412
|
-
const
|
|
2413
|
-
const { reader, dir, intervalMs, replayLast, onWarn
|
|
2414
|
-
const logPath = node_path.default.join(dir,
|
|
2324
|
+
const runWatchLoop = async (options) => {
|
|
2325
|
+
const { reader, dir, intervalMs, replayLast, onWarn } = options;
|
|
2326
|
+
const logPath = node_path.default.join(dir, HISTORY_FILE_NAME);
|
|
2415
2327
|
const { read } = reader;
|
|
2416
2328
|
let lastChangeCount = null;
|
|
2417
2329
|
let lastTimestamp = 0;
|
|
@@ -2426,9 +2338,8 @@ const watchForNextGrab = async (options) => {
|
|
|
2426
2338
|
lastTimestamp = JSON.parse(initial.grab).timestamp ?? 0;
|
|
2427
2339
|
} catch {}
|
|
2428
2340
|
}
|
|
2429
|
-
while (
|
|
2341
|
+
while (true) {
|
|
2430
2342
|
await sleep(intervalMs);
|
|
2431
|
-
if (signal?.aborted) return null;
|
|
2432
2343
|
try {
|
|
2433
2344
|
const snapshot = read();
|
|
2434
2345
|
if (!snapshot) continue;
|
|
@@ -2474,7 +2385,6 @@ const watchForNextGrab = async (options) => {
|
|
|
2474
2385
|
lastChangeCount = snapshot.changeCount;
|
|
2475
2386
|
lastTextHash = textHash;
|
|
2476
2387
|
lastTimestamp = nextTimestamp;
|
|
2477
|
-
return captured;
|
|
2478
2388
|
} catch (error) {
|
|
2479
2389
|
const message = String(error?.message ?? error);
|
|
2480
2390
|
if (message !== lastErrorMessage) {
|
|
@@ -2483,51 +2393,315 @@ const watchForNextGrab = async (options) => {
|
|
|
2483
2393
|
}
|
|
2484
2394
|
}
|
|
2485
2395
|
}
|
|
2486
|
-
return null;
|
|
2487
2396
|
};
|
|
2488
2397
|
//#endregion
|
|
2489
|
-
//#region src/
|
|
2490
|
-
const
|
|
2491
|
-
const
|
|
2492
|
-
const
|
|
2493
|
-
|
|
2398
|
+
//#region src/utils/grab-log.ts
|
|
2399
|
+
const CURSOR_FILE_NAME = "cursor.txt";
|
|
2400
|
+
const cursorFilePath = (dir) => node_path.default.join(dir, CURSOR_FILE_NAME);
|
|
2401
|
+
const readCompleteGrabLines = (dir) => {
|
|
2402
|
+
let raw;
|
|
2403
|
+
try {
|
|
2404
|
+
raw = node_fs.default.readFileSync(node_path.default.join(dir, HISTORY_FILE_NAME), "utf8");
|
|
2405
|
+
} catch {
|
|
2406
|
+
return [];
|
|
2407
|
+
}
|
|
2408
|
+
const lastNewline = raw.lastIndexOf("\n");
|
|
2409
|
+
if (lastNewline < 0) return [];
|
|
2410
|
+
return raw.slice(0, lastNewline).split("\n").filter(Boolean);
|
|
2411
|
+
};
|
|
2412
|
+
const readGrabCursor = (dir) => {
|
|
2413
|
+
try {
|
|
2414
|
+
const value = Number.parseInt(node_fs.default.readFileSync(cursorFilePath(dir), "utf8").trim(), 10);
|
|
2415
|
+
return Number.isInteger(value) && value >= 0 ? value : 0;
|
|
2416
|
+
} catch {
|
|
2417
|
+
return 0;
|
|
2418
|
+
}
|
|
2419
|
+
};
|
|
2420
|
+
const consumeGrabs = (dir, options) => {
|
|
2421
|
+
const lines = readCompleteGrabLines(dir);
|
|
2422
|
+
if (options.all) return lines;
|
|
2423
|
+
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);
|
|
2428
|
+
};
|
|
2429
|
+
//#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) => {
|
|
2494
2432
|
const dir = node_path.default.resolve(options.dir);
|
|
2495
|
-
const intervalMs = Number(options.interval);
|
|
2496
|
-
const textOnly = Boolean(options.textOnly);
|
|
2497
2433
|
try {
|
|
2498
2434
|
prepareWorkDir(dir);
|
|
2499
2435
|
} catch (error) {
|
|
2500
|
-
process.stderr.write(`react-grab
|
|
2436
|
+
process.stderr.write(`react-grab read: ${String(error?.message ?? error)}\n`);
|
|
2501
2437
|
process.exit(1);
|
|
2502
2438
|
}
|
|
2439
|
+
unrefStdin();
|
|
2440
|
+
const limitRaw = Number(options.limit);
|
|
2441
|
+
const limit = Number.isInteger(limitRaw) && limitRaw >= 0 ? limitRaw : 50;
|
|
2442
|
+
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);
|
|
2454
|
+
while (Date.now() < deadline) {
|
|
2455
|
+
await sleep(200);
|
|
2456
|
+
if (drain() > 0) break;
|
|
2457
|
+
}
|
|
2458
|
+
process.exit(0);
|
|
2459
|
+
});
|
|
2460
|
+
//#endregion
|
|
2461
|
+
//#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 () => {
|
|
2464
|
+
console.log(`${picocolors.default.magenta("✿")} ${picocolors.default.bold("React Grab")} ${picocolors.default.gray(VERSION$2)}`);
|
|
2465
|
+
console.log();
|
|
2466
|
+
try {
|
|
2467
|
+
logger.break();
|
|
2468
|
+
const removedCount = await removeSkill();
|
|
2469
|
+
logger.break();
|
|
2470
|
+
if (removedCount === 0) logger.log("React Grab skill is not installed in any detected agent.");
|
|
2471
|
+
else logger.log(`${highlighter.success("Removed")} the React Grab skill from ${removedCount} agent${removedCount === 1 ? "" : "s"}.`);
|
|
2472
|
+
logger.break();
|
|
2473
|
+
} catch (error) {
|
|
2474
|
+
handleError(error);
|
|
2475
|
+
}
|
|
2476
|
+
});
|
|
2477
|
+
//#endregion
|
|
2478
|
+
//#region src/commands/upgrade.ts
|
|
2479
|
+
const VERSION$1 = "0.1.40";
|
|
2480
|
+
const NPM_REGISTRY_URL = "https://registry.npmjs.org/react-grab/latest";
|
|
2481
|
+
const fetchLatestVersion = async () => {
|
|
2482
|
+
try {
|
|
2483
|
+
return (await (await fetch(NPM_REGISTRY_URL)).json()).version ?? null;
|
|
2484
|
+
} catch {
|
|
2485
|
+
return null;
|
|
2486
|
+
}
|
|
2487
|
+
};
|
|
2488
|
+
const isDevDependency = (projectRoot) => {
|
|
2489
|
+
const packageJsonPath = (0, node_path.join)(projectRoot, "package.json");
|
|
2490
|
+
if (!(0, node_fs.existsSync)(packageJsonPath)) return true;
|
|
2491
|
+
try {
|
|
2492
|
+
const packageJson = JSON.parse((0, node_fs.readFileSync)(packageJsonPath, "utf-8"));
|
|
2493
|
+
if (packageJson.devDependencies?.["react-grab"]) return true;
|
|
2494
|
+
if (packageJson.dependencies?.["react-grab"]) return false;
|
|
2495
|
+
} catch {}
|
|
2496
|
+
return true;
|
|
2497
|
+
};
|
|
2498
|
+
const upgrade = new commander.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) => {
|
|
2499
|
+
console.log(`${picocolors.default.magenta("✿")} ${picocolors.default.bold("React Grab")} ${picocolors.default.gray(VERSION$1)}`);
|
|
2500
|
+
console.log();
|
|
2501
|
+
try {
|
|
2502
|
+
const cwd = (0, node_path.resolve)(opts.cwd);
|
|
2503
|
+
const detectSpinner = spinner("Detecting project.").start();
|
|
2504
|
+
const projectInfo = await detectProject(cwd);
|
|
2505
|
+
if (!projectInfo.hasReactGrab) {
|
|
2506
|
+
detectSpinner.fail("React Grab is not installed.");
|
|
2507
|
+
logger.break();
|
|
2508
|
+
logger.error(`Run ${highlighter.info("npx grab@latest init")} first to install React Grab.`);
|
|
2509
|
+
logger.break();
|
|
2510
|
+
process.exit(1);
|
|
2511
|
+
}
|
|
2512
|
+
detectSpinner.succeed();
|
|
2513
|
+
const versionSpinner = spinner("Checking for updates.").start();
|
|
2514
|
+
const latestVersion = await fetchLatestVersion();
|
|
2515
|
+
if (!latestVersion) {
|
|
2516
|
+
versionSpinner.fail("Could not check for updates.");
|
|
2517
|
+
logger.break();
|
|
2518
|
+
logger.error("Failed to reach the npm registry. Check your network connection.");
|
|
2519
|
+
logger.break();
|
|
2520
|
+
process.exit(1);
|
|
2521
|
+
}
|
|
2522
|
+
const installedVersion = projectInfo.reactGrabVersion;
|
|
2523
|
+
if (installedVersion && installedVersion === latestVersion) {
|
|
2524
|
+
versionSpinner.succeed(`Already on the latest version ${highlighter.info(`v${latestVersion}`)}.`);
|
|
2525
|
+
logger.break();
|
|
2526
|
+
process.exit(0);
|
|
2527
|
+
}
|
|
2528
|
+
const fromLabel = installedVersion ? `v${installedVersion}` : "unknown";
|
|
2529
|
+
versionSpinner.succeed(`Update available: ${highlighter.dim(fromLabel)} → ${highlighter.info(`v${latestVersion}`)}.`);
|
|
2530
|
+
const upgradeSpinner = spinner("Upgrading react-grab.").start();
|
|
2531
|
+
try {
|
|
2532
|
+
await installPackages(["react-grab@latest"], {
|
|
2533
|
+
packageManager: projectInfo.packageManager,
|
|
2534
|
+
cwd: projectInfo.projectRoot,
|
|
2535
|
+
isDev: isDevDependency(projectInfo.projectRoot)
|
|
2536
|
+
});
|
|
2537
|
+
upgradeSpinner.succeed();
|
|
2538
|
+
} catch {
|
|
2539
|
+
upgradeSpinner.fail();
|
|
2540
|
+
logger.break();
|
|
2541
|
+
logger.error("Failed to upgrade. Check your network connection and try again.");
|
|
2542
|
+
logger.break();
|
|
2543
|
+
process.exit(1);
|
|
2544
|
+
}
|
|
2545
|
+
logger.break();
|
|
2546
|
+
logger.log(`${highlighter.success("Success!")} React Grab has been upgraded to ${highlighter.info(`v${latestVersion}`)}.`);
|
|
2547
|
+
logger.break();
|
|
2548
|
+
} catch (error) {
|
|
2549
|
+
handleError(error);
|
|
2550
|
+
}
|
|
2551
|
+
});
|
|
2552
|
+
//#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
|
+
//#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
|
+
const writeStatus = (message) => {
|
|
2634
|
+
process.stderr.write(`react-grab watch: ${message}\n`);
|
|
2635
|
+
};
|
|
2636
|
+
const runForeground = (dir, intervalMs, textOnly, replayLast) => {
|
|
2637
|
+
if (!claimDaemon(dir)) process.exit(0);
|
|
2638
|
+
process.on("exit", () => releaseDaemon(dir));
|
|
2503
2639
|
const reader = createReader({
|
|
2504
2640
|
textOnly,
|
|
2505
2641
|
readersDir: readersDir(),
|
|
2506
2642
|
workDir: dir
|
|
2507
2643
|
});
|
|
2508
2644
|
if (!reader) {
|
|
2509
|
-
|
|
2645
|
+
writeStatus(NO_READER_MESSAGE);
|
|
2510
2646
|
process.exit(1);
|
|
2511
2647
|
}
|
|
2512
|
-
|
|
2513
|
-
|
|
2648
|
+
writeStatus(`watching clipboard via ${reader.mode}; history → ${node_path.default.join(dir, HISTORY_FILE_NAME)}`);
|
|
2649
|
+
runWatchLoop({
|
|
2514
2650
|
reader,
|
|
2515
2651
|
dir,
|
|
2516
|
-
intervalMs
|
|
2517
|
-
replayLast
|
|
2518
|
-
onWarn:
|
|
2519
|
-
}).then((grab) => {
|
|
2520
|
-
if (!grab) process.exit(0);
|
|
2521
|
-
process.stdout.write(`${JSON.stringify(grab)}\n`);
|
|
2522
|
-
process.exit(0);
|
|
2652
|
+
intervalMs,
|
|
2653
|
+
replayLast,
|
|
2654
|
+
onWarn: writeStatus
|
|
2523
2655
|
}).catch((error) => {
|
|
2524
|
-
|
|
2656
|
+
writeStatus(String(error?.message ?? error));
|
|
2525
2657
|
process.exit(1);
|
|
2526
2658
|
});
|
|
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);
|
|
2527
2701
|
});
|
|
2528
2702
|
//#endregion
|
|
2529
2703
|
//#region src/cli.ts
|
|
2530
|
-
const VERSION = "0.1.
|
|
2704
|
+
const VERSION = "0.1.40";
|
|
2531
2705
|
const VERSION_API_URL = "https://www.react-grab.com/api/version";
|
|
2532
2706
|
process.on("SIGINT", () => process.exit(0));
|
|
2533
2707
|
process.on("SIGTERM", () => process.exit(0));
|
|
@@ -2541,6 +2715,7 @@ program.addCommand(remove);
|
|
|
2541
2715
|
program.addCommand(configure);
|
|
2542
2716
|
program.addCommand(upgrade);
|
|
2543
2717
|
program.addCommand(watch);
|
|
2718
|
+
program.addCommand(read);
|
|
2544
2719
|
const main = async () => {
|
|
2545
2720
|
await program.parseAsync();
|
|
2546
2721
|
};
|