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