@react-grab/cli 0.1.41 → 0.1.43
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 +286 -196
- package/dist/cli.js +286 -196
- package/dist/cli.js.map +1 -1
- package/package.json +5 -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.43";
|
|
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.43";
|
|
1105
1107
|
const isMac = process.platform === "darwin";
|
|
1106
1108
|
const META_LABEL = isMac ? "Cmd" : "Win";
|
|
1107
1109
|
const ALT_LABEL = isMac ? "Option" : "Alt";
|
|
@@ -1719,11 +1721,18 @@ const installPackagesWithFeedback = async (packages, packageManager, projectRoot
|
|
|
1719
1721
|
}
|
|
1720
1722
|
};
|
|
1721
1723
|
//#endregion
|
|
1724
|
+
//#region src/utils/is-telemetry-enabled.ts
|
|
1725
|
+
const isTelemetryEnabled = () => {
|
|
1726
|
+
const doNotTrack = process.env.DO_NOT_TRACK;
|
|
1727
|
+
return doNotTrack !== "1" && doNotTrack !== "true";
|
|
1728
|
+
};
|
|
1729
|
+
//#endregion
|
|
1722
1730
|
//#region src/commands/init.ts
|
|
1723
|
-
const VERSION$3 = "0.1.
|
|
1731
|
+
const VERSION$3 = "0.1.43";
|
|
1724
1732
|
const REPORT_URL = "https://react-grab.com/api/report-cli";
|
|
1725
1733
|
const DOCS_URL = "https://github.com/aidenybai/react-grab";
|
|
1726
1734
|
const reportToCli = (type, config, error) => {
|
|
1735
|
+
if (!isTelemetryEnabled()) return;
|
|
1727
1736
|
fetch(REPORT_URL, {
|
|
1728
1737
|
method: "POST",
|
|
1729
1738
|
headers: { "Content-Type": "application/json" },
|
|
@@ -2077,6 +2086,8 @@ const sleep = (durationMs) => new Promise((resolve) => setTimeout(resolve, durat
|
|
|
2077
2086
|
//#endregion
|
|
2078
2087
|
//#region src/utils/clipboard.ts
|
|
2079
2088
|
const HISTORY_FILE_NAME = "history.jsonl";
|
|
2089
|
+
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.";
|
|
2090
|
+
const READERS_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
2080
2091
|
const READ_TIMEOUT_MS = 2500;
|
|
2081
2092
|
const MAX_CLIPBOARD_BYTES = 64 * 1024 * 1024;
|
|
2082
2093
|
const ID_RADIX = 36;
|
|
@@ -2170,7 +2181,7 @@ const compileSwiftReader = (readersDir, workDir) => {
|
|
|
2170
2181
|
return binary;
|
|
2171
2182
|
};
|
|
2172
2183
|
const createDarwinReader = (options) => {
|
|
2173
|
-
const binary = options.textOnly ? null : compileSwiftReader(
|
|
2184
|
+
const binary = options.textOnly ? null : compileSwiftReader(READERS_DIR, options.workDir);
|
|
2174
2185
|
if (binary) return {
|
|
2175
2186
|
mode: "darwin-native",
|
|
2176
2187
|
read: () => {
|
|
@@ -2253,7 +2264,7 @@ const detectPowershell = () => hasCommand("pwsh") ? "pwsh" : hasCommand("powersh
|
|
|
2253
2264
|
const createWindowsReader = (options) => {
|
|
2254
2265
|
const shell = detectPowershell();
|
|
2255
2266
|
if (!shell) return null;
|
|
2256
|
-
const scriptPath = path.join(
|
|
2267
|
+
const scriptPath = path.join(READERS_DIR, "read-clipboard.ps1");
|
|
2257
2268
|
if (options.textOnly || !fs.existsSync(scriptPath)) return {
|
|
2258
2269
|
mode: "win-text",
|
|
2259
2270
|
read: () => {
|
|
@@ -2392,108 +2403,291 @@ const runWatchLoop = async (options) => {
|
|
|
2392
2403
|
}
|
|
2393
2404
|
};
|
|
2394
2405
|
//#endregion
|
|
2406
|
+
//#region src/utils/daemon.ts
|
|
2407
|
+
const PID_FILE_NAME = "watch.pid";
|
|
2408
|
+
const pidFilePath = (dir) => path.join(dir, PID_FILE_NAME);
|
|
2409
|
+
const cliEntryPath = () => process.argv[1] ?? fileURLToPath(import.meta.url);
|
|
2410
|
+
const isProcessAlive = (pid) => {
|
|
2411
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
2412
|
+
try {
|
|
2413
|
+
process.kill(pid, 0);
|
|
2414
|
+
return true;
|
|
2415
|
+
} catch (error) {
|
|
2416
|
+
return error.code === "EPERM";
|
|
2417
|
+
}
|
|
2418
|
+
};
|
|
2419
|
+
const readDaemonPid = (dir) => {
|
|
2420
|
+
try {
|
|
2421
|
+
const pid = Number.parseInt(fs.readFileSync(pidFilePath(dir), "utf8").trim(), 10);
|
|
2422
|
+
return Number.isInteger(pid) && pid > 0 ? pid : null;
|
|
2423
|
+
} catch {
|
|
2424
|
+
return null;
|
|
2425
|
+
}
|
|
2426
|
+
};
|
|
2427
|
+
const isDaemonRunning = (dir) => {
|
|
2428
|
+
const pid = readDaemonPid(dir);
|
|
2429
|
+
return pid !== null && isProcessAlive(pid);
|
|
2430
|
+
};
|
|
2431
|
+
const claimDaemon = (dir) => {
|
|
2432
|
+
const file = pidFilePath(dir);
|
|
2433
|
+
for (let attempt = 0; attempt < 50; attempt += 1) try {
|
|
2434
|
+
const handle = fs.openSync(file, "wx");
|
|
2435
|
+
fs.writeFileSync(handle, String(process.pid));
|
|
2436
|
+
fs.closeSync(handle);
|
|
2437
|
+
return readDaemonPid(dir) === process.pid;
|
|
2438
|
+
} catch (error) {
|
|
2439
|
+
if (error.code !== "EEXIST") throw error;
|
|
2440
|
+
if (isDaemonRunning(dir)) return false;
|
|
2441
|
+
try {
|
|
2442
|
+
fs.rmSync(file, { force: true });
|
|
2443
|
+
} catch {}
|
|
2444
|
+
}
|
|
2445
|
+
return false;
|
|
2446
|
+
};
|
|
2447
|
+
const releaseDaemon = (dir) => {
|
|
2448
|
+
if (readDaemonPid(dir) === process.pid) try {
|
|
2449
|
+
fs.rmSync(pidFilePath(dir), { force: true });
|
|
2450
|
+
} catch {}
|
|
2451
|
+
};
|
|
2452
|
+
const stopDaemon = (dir) => {
|
|
2453
|
+
const pid = readDaemonPid(dir);
|
|
2454
|
+
if (pid === null) return null;
|
|
2455
|
+
const wasAlive = isProcessAlive(pid);
|
|
2456
|
+
if (wasAlive) try {
|
|
2457
|
+
process.kill(pid, "SIGTERM");
|
|
2458
|
+
} catch {}
|
|
2459
|
+
if (readDaemonPid(dir) === pid) try {
|
|
2460
|
+
fs.rmSync(pidFilePath(dir), { force: true });
|
|
2461
|
+
} catch {}
|
|
2462
|
+
return wasAlive ? pid : null;
|
|
2463
|
+
};
|
|
2464
|
+
const spawnDaemon = (options) => {
|
|
2465
|
+
const args = [
|
|
2466
|
+
cliEntryPath(),
|
|
2467
|
+
"watch",
|
|
2468
|
+
"--dir",
|
|
2469
|
+
options.dir,
|
|
2470
|
+
"--interval",
|
|
2471
|
+
String(options.intervalMs)
|
|
2472
|
+
];
|
|
2473
|
+
if (options.textOnly) args.push("--text-only");
|
|
2474
|
+
if (options.replayLast) args.push("--replay-last");
|
|
2475
|
+
spawn(process.execPath, args, {
|
|
2476
|
+
detached: true,
|
|
2477
|
+
stdio: "ignore",
|
|
2478
|
+
windowsHide: true
|
|
2479
|
+
}).unref();
|
|
2480
|
+
};
|
|
2481
|
+
const ensureDaemon = (options) => {
|
|
2482
|
+
if (isDaemonRunning(options.dir)) return "already-running";
|
|
2483
|
+
if (!createReader({
|
|
2484
|
+
textOnly: options.textOnly,
|
|
2485
|
+
workDir: options.dir
|
|
2486
|
+
})) return "no-reader";
|
|
2487
|
+
spawnDaemon(options);
|
|
2488
|
+
return "started";
|
|
2489
|
+
};
|
|
2490
|
+
//#endregion
|
|
2395
2491
|
//#region src/utils/grab-log.ts
|
|
2396
2492
|
const CURSOR_FILE_NAME = "cursor.txt";
|
|
2493
|
+
const NEWLINE_BYTE = 10;
|
|
2397
2494
|
const cursorFilePath = (dir) => path.join(dir, CURSOR_FILE_NAME);
|
|
2398
|
-
const
|
|
2399
|
-
|
|
2495
|
+
const historyFilePath = (dir) => path.join(dir, HISTORY_FILE_NAME);
|
|
2496
|
+
const fileSize = (filePath) => {
|
|
2400
2497
|
try {
|
|
2401
|
-
|
|
2498
|
+
return fs.statSync(filePath).size;
|
|
2402
2499
|
} catch {
|
|
2403
|
-
return
|
|
2500
|
+
return 0;
|
|
2404
2501
|
}
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2502
|
+
};
|
|
2503
|
+
const readHistoryRange = (dir, start, length) => {
|
|
2504
|
+
if (length <= 0) return "";
|
|
2505
|
+
const fd = fs.openSync(historyFilePath(dir), "r");
|
|
2506
|
+
try {
|
|
2507
|
+
const buffer = Buffer.allocUnsafe(length);
|
|
2508
|
+
const bytesRead = fs.readSync(fd, buffer, 0, length, start);
|
|
2509
|
+
return buffer.toString("utf8", 0, bytesRead);
|
|
2510
|
+
} finally {
|
|
2511
|
+
fs.closeSync(fd);
|
|
2512
|
+
}
|
|
2513
|
+
};
|
|
2514
|
+
const writeGrabCursor = (dir, offset) => {
|
|
2515
|
+
const target = cursorFilePath(dir);
|
|
2516
|
+
const tempPath = `${target}.${process.pid}.tmp`;
|
|
2517
|
+
fs.writeFileSync(tempPath, JSON.stringify({ offset }));
|
|
2518
|
+
fs.renameSync(tempPath, target);
|
|
2519
|
+
};
|
|
2520
|
+
const byteOffsetAfterLines = (dir, lineCount) => {
|
|
2521
|
+
const filePath = historyFilePath(dir);
|
|
2522
|
+
const size = fileSize(filePath);
|
|
2523
|
+
if (lineCount <= 0 || size === 0) return 0;
|
|
2524
|
+
const fd = fs.openSync(filePath, "r");
|
|
2525
|
+
try {
|
|
2526
|
+
const buffer = Buffer.allocUnsafe(MIGRATION_SCAN_CHUNK_BYTES);
|
|
2527
|
+
let position = 0;
|
|
2528
|
+
let seen = 0;
|
|
2529
|
+
while (position < size) {
|
|
2530
|
+
const bytesRead = fs.readSync(fd, buffer, 0, MIGRATION_SCAN_CHUNK_BYTES, position);
|
|
2531
|
+
if (bytesRead <= 0) break;
|
|
2532
|
+
for (let index = 0; index < bytesRead; index += 1) {
|
|
2533
|
+
if (buffer[index] !== NEWLINE_BYTE) continue;
|
|
2534
|
+
seen += 1;
|
|
2535
|
+
if (seen === lineCount) return position + index + 1;
|
|
2536
|
+
}
|
|
2537
|
+
position += bytesRead;
|
|
2538
|
+
}
|
|
2539
|
+
} finally {
|
|
2540
|
+
fs.closeSync(fd);
|
|
2541
|
+
}
|
|
2542
|
+
return size;
|
|
2408
2543
|
};
|
|
2409
2544
|
const readGrabCursor = (dir) => {
|
|
2545
|
+
let raw;
|
|
2410
2546
|
try {
|
|
2411
|
-
|
|
2412
|
-
return Number.isInteger(value) && value >= 0 ? value : 0;
|
|
2547
|
+
raw = fs.readFileSync(cursorFilePath(dir), "utf8").trim();
|
|
2413
2548
|
} catch {
|
|
2414
2549
|
return 0;
|
|
2415
2550
|
}
|
|
2416
|
-
|
|
2417
|
-
|
|
2551
|
+
if (raw === "") return 0;
|
|
2552
|
+
let parsed;
|
|
2418
2553
|
try {
|
|
2419
|
-
|
|
2420
|
-
return typeof value === "number" ? value : null;
|
|
2554
|
+
parsed = JSON.parse(raw);
|
|
2421
2555
|
} catch {
|
|
2422
|
-
return
|
|
2556
|
+
return 0;
|
|
2423
2557
|
}
|
|
2558
|
+
if (typeof parsed === "number") {
|
|
2559
|
+
if (!Number.isInteger(parsed) || parsed < 0) return 0;
|
|
2560
|
+
const offset = byteOffsetAfterLines(dir, parsed);
|
|
2561
|
+
writeGrabCursor(dir, offset);
|
|
2562
|
+
return offset;
|
|
2563
|
+
}
|
|
2564
|
+
return typeof parsed.offset === "number" && Number.isInteger(parsed.offset) && parsed.offset >= 0 ? parsed.offset : 0;
|
|
2565
|
+
};
|
|
2566
|
+
const readCompleteGrabLines = (dir) => {
|
|
2567
|
+
const size = fileSize(historyFilePath(dir));
|
|
2568
|
+
if (size === 0) return [];
|
|
2569
|
+
const raw = readHistoryRange(dir, 0, size);
|
|
2570
|
+
const lastNewline = raw.lastIndexOf("\n");
|
|
2571
|
+
if (lastNewline < 0) return [];
|
|
2572
|
+
return raw.slice(0, lastNewline).split("\n").filter(Boolean);
|
|
2424
2573
|
};
|
|
2425
2574
|
const consumeGrabs = (dir, options) => {
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
const maxAgeMs = options.maxAgeMs ?? 0;
|
|
2575
|
+
if (options.all) return readCompleteGrabLines(dir);
|
|
2576
|
+
const size = fileSize(historyFilePath(dir));
|
|
2429
2577
|
const cursor = readGrabCursor(dir);
|
|
2430
|
-
const
|
|
2578
|
+
const start = cursor > size ? 0 : cursor;
|
|
2579
|
+
if (start >= size) {
|
|
2580
|
+
if (start !== cursor) writeGrabCursor(dir, start);
|
|
2581
|
+
return [];
|
|
2582
|
+
}
|
|
2583
|
+
const chunk = readHistoryRange(dir, start, Math.min(size - start, MAX_READ_HISTORY_BYTES));
|
|
2584
|
+
const lastNewline = chunk.lastIndexOf("\n");
|
|
2585
|
+
if (lastNewline < 0) return [];
|
|
2431
2586
|
const now = Date.now();
|
|
2432
2587
|
const fresh = [];
|
|
2433
|
-
let
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2588
|
+
let consumedBytes = 0;
|
|
2589
|
+
let lineStart = 0;
|
|
2590
|
+
while (lineStart <= lastNewline && (options.limit <= 0 || fresh.length < options.limit)) {
|
|
2591
|
+
const newlineIndex = chunk.indexOf("\n", lineStart);
|
|
2592
|
+
const line = chunk.slice(lineStart, newlineIndex);
|
|
2593
|
+
consumedBytes += Buffer.byteLength(chunk.slice(lineStart, newlineIndex + 1), "utf8");
|
|
2594
|
+
lineStart = newlineIndex + 1;
|
|
2595
|
+
if (line.length === 0) continue;
|
|
2596
|
+
let parsed;
|
|
2597
|
+
try {
|
|
2598
|
+
parsed = JSON.parse(line);
|
|
2599
|
+
} catch {
|
|
2600
|
+
continue;
|
|
2601
|
+
}
|
|
2602
|
+
if (options.maxAgeMs > 0 && typeof parsed.receivedAt === "number") {
|
|
2603
|
+
if (now - parsed.receivedAt > options.maxAgeMs) continue;
|
|
2440
2604
|
}
|
|
2441
2605
|
fresh.push(line);
|
|
2442
2606
|
}
|
|
2443
|
-
|
|
2607
|
+
const nextCursor = start + consumedBytes;
|
|
2608
|
+
if (nextCursor !== cursor) writeGrabCursor(dir, nextCursor);
|
|
2444
2609
|
return fresh;
|
|
2445
2610
|
};
|
|
2446
2611
|
//#endregion
|
|
2447
|
-
//#region src/
|
|
2612
|
+
//#region src/utils/read-args.ts
|
|
2448
2613
|
const parseWaitMs = (raw) => {
|
|
2449
|
-
if (
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2614
|
+
if (raw === void 0) return 0;
|
|
2615
|
+
const trimmed = raw.trim();
|
|
2616
|
+
if (trimmed === "") return 0;
|
|
2617
|
+
if (/^(inf|infinite|infinity|forever)$/i.test(trimmed)) return Number.POSITIVE_INFINITY;
|
|
2618
|
+
const ms = Number(trimmed);
|
|
2619
|
+
return Number.isFinite(ms) && ms >= 0 ? ms : null;
|
|
2620
|
+
};
|
|
2621
|
+
const parseNonNegativeInt = (raw) => {
|
|
2622
|
+
if (raw === void 0) return null;
|
|
2623
|
+
const trimmed = raw.trim();
|
|
2624
|
+
if (trimmed === "") return null;
|
|
2625
|
+
const value = Number(trimmed);
|
|
2626
|
+
return Number.isInteger(value) && value >= 0 ? value : null;
|
|
2627
|
+
};
|
|
2628
|
+
//#endregion
|
|
2629
|
+
//#region src/commands/pull.ts
|
|
2630
|
+
const fail = (message) => {
|
|
2631
|
+
process.stderr.write(`react-grab pull: ${message}\n`);
|
|
2632
|
+
process.exit(1);
|
|
2453
2633
|
};
|
|
2454
|
-
const
|
|
2455
|
-
|
|
2456
|
-
return Number.isInteger(value) && value >= 0 ? value : fallback;
|
|
2634
|
+
const emitAndExit = (lines) => {
|
|
2635
|
+
process.stdout.write(`${lines.join("\n")}\n`, () => process.exit(0));
|
|
2457
2636
|
};
|
|
2458
|
-
const
|
|
2637
|
+
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
2638
|
const dir = path.resolve(options.dir);
|
|
2460
2639
|
try {
|
|
2461
2640
|
prepareWorkDir(dir);
|
|
2462
2641
|
} catch (error) {
|
|
2463
|
-
|
|
2464
|
-
process.exit(1);
|
|
2642
|
+
fail(String(error?.message ?? error));
|
|
2465
2643
|
}
|
|
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
2644
|
const waitMs = parseWaitMs(options.wait);
|
|
2480
|
-
|
|
2481
|
-
|
|
2645
|
+
if (waitMs === null) fail(`invalid --wait "${options.wait}" (use milliseconds or "infinite")`);
|
|
2646
|
+
const limit = parseNonNegativeInt(options.limit);
|
|
2647
|
+
if (limit === null) fail(`invalid --limit "${options.limit}" (use a non-negative integer)`);
|
|
2648
|
+
const maxAgeMs = parseNonNegativeInt(options.maxAge);
|
|
2649
|
+
if (maxAgeMs === null) fail(`invalid --max-age "${options.maxAge}" (use milliseconds, 0 to disable)`);
|
|
2650
|
+
const all = Boolean(options.all);
|
|
2651
|
+
if (ensureDaemon({
|
|
2652
|
+
dir,
|
|
2653
|
+
intervalMs: 800,
|
|
2654
|
+
textOnly: Boolean(options.textOnly),
|
|
2655
|
+
replayLast: false
|
|
2656
|
+
}) === "no-reader") fail(NO_READER_MESSAGE);
|
|
2657
|
+
unrefStdin();
|
|
2658
|
+
const consume = () => consumeGrabs(dir, {
|
|
2659
|
+
limit,
|
|
2660
|
+
all,
|
|
2661
|
+
maxAgeMs
|
|
2662
|
+
});
|
|
2663
|
+
const first = consume();
|
|
2664
|
+
if (first.length > 0) {
|
|
2665
|
+
emitAndExit(first);
|
|
2666
|
+
return;
|
|
2667
|
+
}
|
|
2668
|
+
const deadline = Date.now() + waitMs;
|
|
2482
2669
|
while (Date.now() < deadline) {
|
|
2483
2670
|
await sleep(200);
|
|
2484
|
-
|
|
2671
|
+
const batch = consume();
|
|
2672
|
+
if (batch.length > 0) {
|
|
2673
|
+
emitAndExit(batch);
|
|
2674
|
+
return;
|
|
2675
|
+
}
|
|
2485
2676
|
}
|
|
2486
2677
|
process.exit(0);
|
|
2487
2678
|
});
|
|
2488
2679
|
//#endregion
|
|
2489
2680
|
//#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 () => {
|
|
2681
|
+
const VERSION$2 = "0.1.43";
|
|
2682
|
+
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
2683
|
console.log(`${pc.magenta("✿")} ${pc.bold("React Grab")} ${pc.gray(VERSION$2)}`);
|
|
2493
2684
|
console.log();
|
|
2494
2685
|
try {
|
|
2495
2686
|
logger.break();
|
|
2496
|
-
const removedCount = await removeSkill(
|
|
2687
|
+
const removedCount = await removeSkill({
|
|
2688
|
+
cwd: resolve(opts.cwd),
|
|
2689
|
+
global: opts.global
|
|
2690
|
+
});
|
|
2497
2691
|
logger.break();
|
|
2498
2692
|
if (removedCount === 0) logger.log("React Grab skill is not installed in any detected agent.");
|
|
2499
2693
|
else logger.log(`${highlighter.success("Removed")} the React Grab skill from ${removedCount} agent${removedCount === 1 ? "" : "s"}.`);
|
|
@@ -2503,8 +2697,16 @@ const remove = new Command().name("remove").description("uninstall the React Gra
|
|
|
2503
2697
|
}
|
|
2504
2698
|
});
|
|
2505
2699
|
//#endregion
|
|
2700
|
+
//#region src/commands/stop.ts
|
|
2701
|
+
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) => {
|
|
2702
|
+
const dir = path.resolve(options.dir);
|
|
2703
|
+
const stoppedPid = stopDaemon(dir);
|
|
2704
|
+
process.stderr.write(stoppedPid ? `react-grab stop: stopped watcher (pid ${stoppedPid})\n` : `react-grab stop: no watcher running for ${dir}\n`);
|
|
2705
|
+
process.exit(0);
|
|
2706
|
+
});
|
|
2707
|
+
//#endregion
|
|
2506
2708
|
//#region src/commands/upgrade.ts
|
|
2507
|
-
const VERSION$1 = "0.1.
|
|
2709
|
+
const VERSION$1 = "0.1.43";
|
|
2508
2710
|
const NPM_REGISTRY_URL = "https://registry.npmjs.org/react-grab/latest";
|
|
2509
2711
|
const fetchLatestVersion = async () => {
|
|
2510
2712
|
try {
|
|
@@ -2578,95 +2780,24 @@ const upgrade = new Command().name("upgrade").alias("update").description("upgra
|
|
|
2578
2780
|
}
|
|
2579
2781
|
});
|
|
2580
2782
|
//#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
2783
|
//#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
2784
|
const writeStatus = (message) => {
|
|
2662
2785
|
process.stderr.write(`react-grab watch: ${message}\n`);
|
|
2663
2786
|
};
|
|
2664
|
-
const
|
|
2787
|
+
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) => {
|
|
2788
|
+
const dir = path.resolve(options.dir);
|
|
2789
|
+
const intervalRaw = Number(options.interval);
|
|
2790
|
+
const intervalMs = Number.isFinite(intervalRaw) && intervalRaw > 0 ? intervalRaw : 800;
|
|
2791
|
+
try {
|
|
2792
|
+
prepareWorkDir(dir);
|
|
2793
|
+
} catch (error) {
|
|
2794
|
+
writeStatus(String(error?.message ?? error));
|
|
2795
|
+
process.exit(1);
|
|
2796
|
+
}
|
|
2665
2797
|
if (!claimDaemon(dir)) process.exit(0);
|
|
2666
2798
|
process.on("exit", () => releaseDaemon(dir));
|
|
2667
2799
|
const reader = createReader({
|
|
2668
|
-
textOnly,
|
|
2669
|
-
readersDir: readersDir(),
|
|
2800
|
+
textOnly: Boolean(options.textOnly),
|
|
2670
2801
|
workDir: dir
|
|
2671
2802
|
});
|
|
2672
2803
|
if (!reader) {
|
|
@@ -2678,63 +2809,21 @@ const runForeground = (dir, intervalMs, textOnly, replayLast) => {
|
|
|
2678
2809
|
reader,
|
|
2679
2810
|
dir,
|
|
2680
2811
|
intervalMs,
|
|
2681
|
-
replayLast,
|
|
2812
|
+
replayLast: Boolean(options.replayLast),
|
|
2682
2813
|
onWarn: writeStatus
|
|
2683
2814
|
}).catch((error) => {
|
|
2684
2815
|
writeStatus(String(error?.message ?? error));
|
|
2685
2816
|
process.exit(1);
|
|
2686
2817
|
});
|
|
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
2818
|
});
|
|
2730
2819
|
//#endregion
|
|
2731
2820
|
//#region src/cli.ts
|
|
2732
|
-
const VERSION = "0.1.
|
|
2821
|
+
const VERSION = "0.1.43";
|
|
2733
2822
|
const VERSION_API_URL = "https://www.react-grab.com/api/version";
|
|
2734
2823
|
process.on("SIGINT", () => process.exit(0));
|
|
2735
2824
|
process.on("SIGTERM", () => process.exit(0));
|
|
2736
2825
|
try {
|
|
2737
|
-
fetch(`${VERSION_API_URL}?source=cli&v=${VERSION}&t=${Date.now()}`).catch(() => {});
|
|
2826
|
+
if (isTelemetryEnabled()) fetch(`${VERSION_API_URL}?source=cli&v=${VERSION}&t=${Date.now()}`).catch(() => {});
|
|
2738
2827
|
} catch {}
|
|
2739
2828
|
const program = new Command().name("grab").description("add React Grab to your project").version(VERSION, "-v, --version", "display the version number");
|
|
2740
2829
|
program.addCommand(init);
|
|
@@ -2742,8 +2831,9 @@ program.addCommand(add$1);
|
|
|
2742
2831
|
program.addCommand(remove);
|
|
2743
2832
|
program.addCommand(configure);
|
|
2744
2833
|
program.addCommand(upgrade);
|
|
2745
|
-
program.addCommand(
|
|
2746
|
-
program.addCommand(
|
|
2834
|
+
program.addCommand(pull);
|
|
2835
|
+
program.addCommand(stop);
|
|
2836
|
+
program.addCommand(watch, { hidden: true });
|
|
2747
2837
|
const main = async () => {
|
|
2748
2838
|
await program.parseAsync();
|
|
2749
2839
|
};
|