@react-grab/cli 0.1.36 → 0.1.38
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 +493 -336
- package/dist/cli.js +495 -334
- package/dist/cli.js.map +1 -1
- package/dist/read-clipboard.ps1 +111 -0
- package/dist/read-clipboard.swift +35 -0
- package/package.json +7 -4
- package/skills/react-grab/SKILL.md +49 -0
package/dist/cli.cjs
CHANGED
|
@@ -31,19 +31,15 @@ node_path = __toESM(node_path, 1);
|
|
|
31
31
|
let package_manager_detector_detect = require("package-manager-detector/detect");
|
|
32
32
|
let ignore = require("ignore");
|
|
33
33
|
ignore = __toESM(ignore, 1);
|
|
34
|
-
let
|
|
35
|
-
|
|
36
|
-
let node_process = require("node:process");
|
|
37
|
-
node_process = __toESM(node_process, 1);
|
|
38
|
-
let jsonc_parser = require("jsonc-parser");
|
|
39
|
-
jsonc_parser = __toESM(jsonc_parser, 1);
|
|
40
|
-
let smol_toml = require("smol-toml");
|
|
41
|
-
smol_toml = __toESM(smol_toml, 1);
|
|
34
|
+
let node_url = require("node:url");
|
|
35
|
+
let agent_install_skill = require("agent-install/skill");
|
|
42
36
|
let prompts = require("prompts");
|
|
43
37
|
prompts = __toESM(prompts, 1);
|
|
44
38
|
let ora = require("ora");
|
|
45
39
|
ora = __toESM(ora, 1);
|
|
46
40
|
let tinyexec = require("tinyexec");
|
|
41
|
+
let node_child_process = require("node:child_process");
|
|
42
|
+
let node_crypto = require("node:crypto");
|
|
47
43
|
//#region src/utils/is-non-interactive.ts
|
|
48
44
|
const AGENT_ENVIRONMENT_VARIABLES = [
|
|
49
45
|
"CI",
|
|
@@ -72,24 +68,61 @@ const detectPackageManager = async (projectRoot) => {
|
|
|
72
68
|
}
|
|
73
69
|
return "npm";
|
|
74
70
|
};
|
|
75
|
-
const
|
|
71
|
+
const CONFIG_EXTENSIONS = [
|
|
72
|
+
"ts",
|
|
73
|
+
"mts",
|
|
74
|
+
"cts",
|
|
75
|
+
"js",
|
|
76
|
+
"mjs",
|
|
77
|
+
"cjs"
|
|
78
|
+
];
|
|
79
|
+
const hasConfigFile = (projectRoot, configBaseName) => CONFIG_EXTENSIONS.some((extension) => (0, node_fs.existsSync)((0, node_path.join)(projectRoot, `${configBaseName}.${extension}`)));
|
|
80
|
+
const readMergedDependencies = (projectRoot) => {
|
|
76
81
|
const packageJsonPath = (0, node_path.join)(projectRoot, "package.json");
|
|
77
|
-
if (!(0, node_fs.existsSync)(packageJsonPath)) return
|
|
82
|
+
if (!(0, node_fs.existsSync)(packageJsonPath)) return null;
|
|
78
83
|
try {
|
|
79
84
|
const packageJson = JSON.parse((0, node_fs.readFileSync)(packageJsonPath, "utf-8"));
|
|
80
|
-
|
|
85
|
+
return {
|
|
81
86
|
...packageJson.dependencies,
|
|
82
87
|
...packageJson.devDependencies
|
|
83
88
|
};
|
|
84
|
-
if (allDependencies["next"]) return "next";
|
|
85
|
-
if (allDependencies["@tanstack/react-start"]) return "tanstack";
|
|
86
|
-
if (allDependencies["vite"]) return "vite";
|
|
87
|
-
if (allDependencies["webpack"]) return "webpack";
|
|
88
|
-
return "unknown";
|
|
89
89
|
} catch {
|
|
90
|
-
return
|
|
90
|
+
return null;
|
|
91
91
|
}
|
|
92
92
|
};
|
|
93
|
+
const detectFrameworkFromDependencies = (dependencies) => {
|
|
94
|
+
if (!dependencies) return "unknown";
|
|
95
|
+
if (dependencies["next"]) return "next";
|
|
96
|
+
if (dependencies["@tanstack/react-start"]) return "tanstack";
|
|
97
|
+
if (dependencies["vite"]) return "vite";
|
|
98
|
+
if (dependencies["webpack"]) return "webpack";
|
|
99
|
+
return "unknown";
|
|
100
|
+
};
|
|
101
|
+
const detectFrameworkFromConfigFiles = (projectRoot) => {
|
|
102
|
+
if (hasConfigFile(projectRoot, "next.config")) return "next";
|
|
103
|
+
if (hasConfigFile(projectRoot, "app.config")) return "tanstack";
|
|
104
|
+
if (hasConfigFile(projectRoot, "vite.config")) return "vite";
|
|
105
|
+
if (hasConfigFile(projectRoot, "webpack.config")) return "webpack";
|
|
106
|
+
return "unknown";
|
|
107
|
+
};
|
|
108
|
+
const findEnclosingMonorepoRoot = (projectRoot) => {
|
|
109
|
+
let currentDirectory = (0, node_path.dirname)(projectRoot);
|
|
110
|
+
while (currentDirectory !== (0, node_path.dirname)(currentDirectory)) {
|
|
111
|
+
if (detectMonorepo(currentDirectory)) return currentDirectory;
|
|
112
|
+
currentDirectory = (0, node_path.dirname)(currentDirectory);
|
|
113
|
+
}
|
|
114
|
+
return null;
|
|
115
|
+
};
|
|
116
|
+
const detectFramework = (projectRoot) => {
|
|
117
|
+
const localFramework = detectFrameworkFromDependencies(readMergedDependencies(projectRoot));
|
|
118
|
+
if (localFramework !== "unknown") return localFramework;
|
|
119
|
+
return detectFrameworkFromConfigFiles(projectRoot);
|
|
120
|
+
};
|
|
121
|
+
const detectFrameworkFromMonorepoRoot = (projectRoot) => {
|
|
122
|
+
const monorepoRoot = findEnclosingMonorepoRoot(projectRoot);
|
|
123
|
+
if (!monorepoRoot) return "unknown";
|
|
124
|
+
return detectFrameworkFromDependencies(readMergedDependencies(monorepoRoot));
|
|
125
|
+
};
|
|
93
126
|
const detectNextRouterType = (projectRoot) => {
|
|
94
127
|
const hasAppDir = (0, node_fs.existsSync)((0, node_path.join)(projectRoot, "app"));
|
|
95
128
|
const hasSrcAppDir = (0, node_fs.existsSync)((0, node_path.join)(projectRoot, "src", "app"));
|
|
@@ -162,18 +195,9 @@ const expandWorkspacePattern = (projectRoot, pattern) => {
|
|
|
162
195
|
return results;
|
|
163
196
|
};
|
|
164
197
|
const hasReactDependency = (projectPath) => {
|
|
165
|
-
const
|
|
166
|
-
if (!
|
|
167
|
-
|
|
168
|
-
const packageJson = JSON.parse((0, node_fs.readFileSync)(packageJsonPath, "utf-8"));
|
|
169
|
-
const allDeps = {
|
|
170
|
-
...packageJson.dependencies,
|
|
171
|
-
...packageJson.devDependencies
|
|
172
|
-
};
|
|
173
|
-
return Boolean(allDeps["react"] || allDeps["react-dom"]);
|
|
174
|
-
} catch {
|
|
175
|
-
return false;
|
|
176
|
-
}
|
|
198
|
+
const dependencies = readMergedDependencies(projectPath);
|
|
199
|
+
if (!dependencies) return false;
|
|
200
|
+
return Boolean(dependencies["react"] || dependencies["react-dom"]);
|
|
177
201
|
};
|
|
178
202
|
const buildReactProject = (projectPath) => {
|
|
179
203
|
const framework = detectFramework(projectPath);
|
|
@@ -275,14 +299,7 @@ const hasReactGrabInFile = (filePath) => {
|
|
|
275
299
|
}
|
|
276
300
|
};
|
|
277
301
|
const detectReactGrab = (projectRoot) => {
|
|
278
|
-
|
|
279
|
-
if ((0, node_fs.existsSync)(packageJsonPath)) try {
|
|
280
|
-
const packageJson = JSON.parse((0, node_fs.readFileSync)(packageJsonPath, "utf-8"));
|
|
281
|
-
if ({
|
|
282
|
-
...packageJson.dependencies,
|
|
283
|
-
...packageJson.devDependencies
|
|
284
|
-
}["react-grab"]) return true;
|
|
285
|
-
} catch {}
|
|
302
|
+
if (readMergedDependencies(projectRoot)?.["react-grab"]) return true;
|
|
286
303
|
return [
|
|
287
304
|
(0, node_path.join)(projectRoot, "app", "layout.tsx"),
|
|
288
305
|
(0, node_path.join)(projectRoot, "app", "layout.jsx"),
|
|
@@ -307,22 +324,13 @@ const detectReactGrab = (projectRoot) => {
|
|
|
307
324
|
].some(hasReactGrabInFile);
|
|
308
325
|
};
|
|
309
326
|
const detectUnsupportedFramework = (projectRoot) => {
|
|
310
|
-
const
|
|
311
|
-
if (!
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
};
|
|
318
|
-
if (allDependencies["@remix-run/react"] || allDependencies["remix"]) return "remix";
|
|
319
|
-
if (allDependencies["astro"]) return "astro";
|
|
320
|
-
if (allDependencies["@sveltejs/kit"]) return "sveltekit";
|
|
321
|
-
if (allDependencies["gatsby"]) return "gatsby";
|
|
322
|
-
return null;
|
|
323
|
-
} catch {
|
|
324
|
-
return null;
|
|
325
|
-
}
|
|
327
|
+
const dependencies = readMergedDependencies(projectRoot);
|
|
328
|
+
if (!dependencies) return null;
|
|
329
|
+
if (dependencies["@remix-run/react"] || dependencies["remix"]) return "remix";
|
|
330
|
+
if (dependencies["astro"]) return "astro";
|
|
331
|
+
if (dependencies["@sveltejs/kit"]) return "sveltekit";
|
|
332
|
+
if (dependencies["gatsby"]) return "gatsby";
|
|
333
|
+
return null;
|
|
326
334
|
};
|
|
327
335
|
const detectReactGrabVersion = (projectRoot) => {
|
|
328
336
|
const installedPackageJsonPath = (0, node_path.join)(projectRoot, "node_modules", "react-grab", "package.json");
|
|
@@ -332,7 +340,8 @@ const detectReactGrabVersion = (projectRoot) => {
|
|
|
332
340
|
return null;
|
|
333
341
|
};
|
|
334
342
|
const detectProject = async (projectRoot = process.cwd()) => {
|
|
335
|
-
const
|
|
343
|
+
const localFramework = detectFramework(projectRoot);
|
|
344
|
+
const framework = localFramework === "unknown" ? detectFrameworkFromMonorepoRoot(projectRoot) : localFramework;
|
|
336
345
|
return {
|
|
337
346
|
packageManager: await detectPackageManager(projectRoot),
|
|
338
347
|
framework,
|
|
@@ -398,204 +407,60 @@ const prompts$1 = (questions) => {
|
|
|
398
407
|
//#region src/utils/spinner.ts
|
|
399
408
|
const spinner = (text) => (0, ora.default)({ text });
|
|
400
409
|
//#endregion
|
|
401
|
-
//#region src/utils/install-
|
|
402
|
-
const
|
|
403
|
-
const
|
|
404
|
-
const
|
|
405
|
-
const
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
if (
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
if (
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
const jsonPath = node_path.default.join(configDir, "opencode.json");
|
|
419
|
-
if (node_fs.default.existsSync(jsoncPath)) return jsoncPath;
|
|
420
|
-
if (node_fs.default.existsSync(jsonPath)) return jsonPath;
|
|
421
|
-
return jsoncPath;
|
|
422
|
-
};
|
|
423
|
-
const getClients = () => {
|
|
424
|
-
const homeDir = node_os.default.homedir();
|
|
425
|
-
const baseDir = getBaseDir();
|
|
426
|
-
const stdioConfig = {
|
|
427
|
-
command: "npx",
|
|
428
|
-
args: [
|
|
429
|
-
"-y",
|
|
430
|
-
PACKAGE_NAME,
|
|
431
|
-
"--stdio"
|
|
432
|
-
]
|
|
433
|
-
};
|
|
434
|
-
return [
|
|
435
|
-
{
|
|
436
|
-
name: "Claude Code",
|
|
437
|
-
configPath: node_path.default.join(homeDir, ".claude.json"),
|
|
438
|
-
configKey: "mcpServers",
|
|
439
|
-
format: "json",
|
|
440
|
-
serverConfig: stdioConfig
|
|
441
|
-
},
|
|
442
|
-
{
|
|
443
|
-
name: "Codex",
|
|
444
|
-
configPath: node_path.default.join(node_process.default.env.CODEX_HOME || node_path.default.join(homeDir, ".codex"), "config.toml"),
|
|
445
|
-
configKey: "mcp_servers",
|
|
446
|
-
format: "toml",
|
|
447
|
-
serverConfig: stdioConfig
|
|
448
|
-
},
|
|
449
|
-
{
|
|
450
|
-
name: "Cursor",
|
|
451
|
-
configPath: node_path.default.join(homeDir, ".cursor", "mcp.json"),
|
|
452
|
-
configKey: "mcpServers",
|
|
453
|
-
format: "json",
|
|
454
|
-
serverConfig: stdioConfig
|
|
455
|
-
},
|
|
456
|
-
{
|
|
457
|
-
name: "OpenCode",
|
|
458
|
-
configPath: getOpenCodeConfigPath(),
|
|
459
|
-
configKey: "mcp",
|
|
460
|
-
format: "json",
|
|
461
|
-
serverConfig: {
|
|
462
|
-
type: "local",
|
|
463
|
-
command: [
|
|
464
|
-
"npx",
|
|
465
|
-
"-y",
|
|
466
|
-
PACKAGE_NAME,
|
|
467
|
-
"--stdio"
|
|
468
|
-
]
|
|
469
|
-
}
|
|
470
|
-
},
|
|
471
|
-
{
|
|
472
|
-
name: "VS Code",
|
|
473
|
-
configPath: node_path.default.join(baseDir, "Code", "User", "mcp.json"),
|
|
474
|
-
configKey: "servers",
|
|
475
|
-
format: "json",
|
|
476
|
-
serverConfig: {
|
|
477
|
-
type: "stdio",
|
|
478
|
-
...stdioConfig
|
|
479
|
-
}
|
|
480
|
-
},
|
|
481
|
-
{
|
|
482
|
-
name: "Amp",
|
|
483
|
-
configPath: node_path.default.join(homeDir, ".config", "amp", "settings.json"),
|
|
484
|
-
configKey: "amp.mcpServers",
|
|
485
|
-
format: "json",
|
|
486
|
-
serverConfig: stdioConfig
|
|
487
|
-
},
|
|
488
|
-
{
|
|
489
|
-
name: "Droid",
|
|
490
|
-
configPath: node_path.default.join(homeDir, ".factory", "mcp.json"),
|
|
491
|
-
configKey: "mcpServers",
|
|
492
|
-
format: "json",
|
|
493
|
-
serverConfig: {
|
|
494
|
-
type: "stdio",
|
|
495
|
-
...stdioConfig
|
|
496
|
-
}
|
|
497
|
-
},
|
|
498
|
-
{
|
|
499
|
-
name: "Windsurf",
|
|
500
|
-
configPath: node_path.default.join(homeDir, ".codeium", "windsurf", "mcp_config.json"),
|
|
501
|
-
configKey: "mcpServers",
|
|
502
|
-
format: "json",
|
|
503
|
-
serverConfig: stdioConfig
|
|
504
|
-
},
|
|
505
|
-
{
|
|
506
|
-
name: "Zed",
|
|
507
|
-
configPath: getZedConfigPath(),
|
|
508
|
-
configKey: "context_servers",
|
|
509
|
-
format: "json",
|
|
510
|
-
serverConfig: {
|
|
511
|
-
source: "custom",
|
|
512
|
-
...stdioConfig,
|
|
513
|
-
env: {}
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
];
|
|
517
|
-
};
|
|
518
|
-
const ensureDirectory = (filePath) => {
|
|
519
|
-
const directory = node_path.default.dirname(filePath);
|
|
520
|
-
if (!node_fs.default.existsSync(directory)) node_fs.default.mkdirSync(directory, { recursive: true });
|
|
521
|
-
};
|
|
522
|
-
const JSONC_FORMAT_OPTIONS = {
|
|
523
|
-
tabSize: 2,
|
|
524
|
-
insertSpaces: true
|
|
525
|
-
};
|
|
526
|
-
const upsertIntoJsonc = (filePath, content, configKey, serverName, serverConfig) => {
|
|
527
|
-
const edits = jsonc_parser.modify(content, [configKey, serverName], serverConfig, { formattingOptions: JSONC_FORMAT_OPTIONS });
|
|
528
|
-
node_fs.default.writeFileSync(filePath, jsonc_parser.applyEdits(content, edits));
|
|
529
|
-
};
|
|
530
|
-
const installJsonClient = (client) => {
|
|
531
|
-
ensureDirectory(client.configPath);
|
|
532
|
-
const content = node_fs.default.existsSync(client.configPath) ? node_fs.default.readFileSync(client.configPath, "utf8") : "{}";
|
|
533
|
-
upsertIntoJsonc(client.configPath, content, client.configKey, SERVER_NAME, client.serverConfig);
|
|
534
|
-
};
|
|
535
|
-
const installTomlClient = (client) => {
|
|
536
|
-
ensureDirectory(client.configPath);
|
|
537
|
-
const existingConfig = node_fs.default.existsSync(client.configPath) ? smol_toml.parse(node_fs.default.readFileSync(client.configPath, "utf8")) : {};
|
|
538
|
-
const serverSection = existingConfig[client.configKey] ?? {};
|
|
539
|
-
serverSection[SERVER_NAME] = client.serverConfig;
|
|
540
|
-
existingConfig[client.configKey] = serverSection;
|
|
541
|
-
node_fs.default.writeFileSync(client.configPath, smol_toml.stringify(existingConfig));
|
|
542
|
-
};
|
|
543
|
-
const getMcpClientNames = () => getClients().map((client) => client.name);
|
|
544
|
-
const installMcpServers = (selectedClients) => {
|
|
545
|
-
const allClients = getClients();
|
|
546
|
-
const clients = selectedClients ? allClients.filter((client) => selectedClients.includes(client.name)) : allClients;
|
|
547
|
-
const results = [];
|
|
548
|
-
const installSpinner = spinner("Installing MCP server.").start();
|
|
549
|
-
for (const client of clients) try {
|
|
550
|
-
if (client.format === "toml") installTomlClient(client);
|
|
551
|
-
else installJsonClient(client);
|
|
552
|
-
results.push({
|
|
553
|
-
client: client.name,
|
|
554
|
-
configPath: client.configPath,
|
|
555
|
-
success: true
|
|
556
|
-
});
|
|
557
|
-
} catch (error) {
|
|
558
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
559
|
-
results.push({
|
|
560
|
-
client: client.name,
|
|
561
|
-
configPath: client.configPath,
|
|
562
|
-
success: false,
|
|
563
|
-
error: message
|
|
410
|
+
//#region src/utils/install-skill.ts
|
|
411
|
+
const SKILL_NAME = "react-grab";
|
|
412
|
+
const SKILL_SOURCE = (0, node_url.fileURLToPath)(new URL("../skills/react-grab", require("url").pathToFileURL(__filename).href));
|
|
413
|
+
const agentLabel = (agent) => (0, agent_install_skill.getSkillAgentConfig)(agent).displayName;
|
|
414
|
+
const installedSkillDir = (agent) => (0, node_path.join)((0, agent_install_skill.isUniversalSkillAgent)(agent) ? (0, agent_install_skill.getCanonicalSkillsDir)(true) : (0, agent_install_skill.getSkillAgentDir)(agent, { global: true }), SKILL_NAME);
|
|
415
|
+
const promptSkillInstall = async ({ yes = false } = {}) => {
|
|
416
|
+
const agents = await (0, agent_install_skill.detectInstalledSkillAgents)();
|
|
417
|
+
if (agents.length === 0) {
|
|
418
|
+
logger.warn("No supported agents detected.");
|
|
419
|
+
return false;
|
|
420
|
+
}
|
|
421
|
+
if (!yes) {
|
|
422
|
+
const { confirmed } = await prompts$1({
|
|
423
|
+
type: "confirm",
|
|
424
|
+
name: "confirmed",
|
|
425
|
+
message: `Install the React Grab skill for ${highlighter.info(agents.map(agentLabel).join(", "))}?`,
|
|
426
|
+
initial: true
|
|
564
427
|
});
|
|
428
|
+
if (!confirmed) return false;
|
|
565
429
|
}
|
|
566
|
-
const
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
430
|
+
const installSpinner = spinner("Installing React Grab skill.").start();
|
|
431
|
+
const { installed, failed } = await (0, agent_install_skill.add)({
|
|
432
|
+
source: SKILL_SOURCE,
|
|
433
|
+
agents,
|
|
434
|
+
global: true,
|
|
435
|
+
mode: "copy"
|
|
436
|
+
});
|
|
437
|
+
if (installed.length === 0) {
|
|
438
|
+
installSpinner.fail("Failed to install React Grab skill.");
|
|
439
|
+
return false;
|
|
440
|
+
}
|
|
441
|
+
installSpinner.succeed(`Installed React Grab skill for ${installed.map((record) => agentLabel(record.agent)).join(", ")}.`);
|
|
442
|
+
for (const record of failed) logger.log(` ${highlighter.error("✗")} ${agentLabel(record.agent)} ${record.error}`);
|
|
443
|
+
return true;
|
|
572
444
|
};
|
|
573
|
-
const
|
|
574
|
-
const
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
choices: getMcpClientNames().map((name) => ({
|
|
579
|
-
title: name,
|
|
580
|
-
value: name,
|
|
581
|
-
selected: true
|
|
582
|
-
}))
|
|
445
|
+
const removeSkill = async () => {
|
|
446
|
+
const agentsWithSkill = (await (0, agent_install_skill.detectInstalledSkillAgents)()).filter((agent) => (0, node_fs.existsSync)(installedSkillDir(agent)));
|
|
447
|
+
for (const skillDir of new Set(agentsWithSkill.map(installedSkillDir))) (0, node_fs.rmSync)(skillDir, {
|
|
448
|
+
recursive: true,
|
|
449
|
+
force: true
|
|
583
450
|
});
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
return installMcpServers(selectedAgents).some((result) => result.success);
|
|
451
|
+
for (const agent of agentsWithSkill) logger.log(` ${highlighter.success("✓")} ${agentLabel(agent)}`);
|
|
452
|
+
return agentsWithSkill.length;
|
|
587
453
|
};
|
|
588
454
|
//#endregion
|
|
589
455
|
//#region src/commands/add.ts
|
|
590
|
-
const VERSION$5 = "0.1.
|
|
591
|
-
const add = new commander.Command().name("add").alias("install").description("
|
|
456
|
+
const VERSION$5 = "0.1.38";
|
|
457
|
+
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) => {
|
|
592
458
|
console.log(`${picocolors.default.magenta("✿")} ${picocolors.default.bold("React Grab")} ${picocolors.default.gray(VERSION$5)}`);
|
|
593
459
|
console.log();
|
|
594
460
|
try {
|
|
595
|
-
const cwd = opts.cwd;
|
|
596
461
|
const isNonInteractive = detectNonInteractive(opts.yes);
|
|
597
462
|
const preflightSpinner = spinner("Preflight checks.").start();
|
|
598
|
-
if (!(await detectProject(cwd)).hasReactGrab) {
|
|
463
|
+
if (!(await detectProject(opts.cwd)).hasReactGrab) {
|
|
599
464
|
preflightSpinner.fail("React Grab is not installed.");
|
|
600
465
|
logger.break();
|
|
601
466
|
logger.error(`Run ${highlighter.info("react-grab init")} first to install React Grab.`);
|
|
@@ -603,39 +468,12 @@ const add = new commander.Command().name("add").alias("install").description("co
|
|
|
603
468
|
process.exit(1);
|
|
604
469
|
}
|
|
605
470
|
preflightSpinner.succeed();
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
logger.warn(`Legacy agent packages are deprecated. Use ${highlighter.info("mcp")} instead.`);
|
|
609
|
-
logger.log(`Run ${highlighter.info("grab add mcp")} to install the MCP server.`);
|
|
471
|
+
logger.break();
|
|
472
|
+
if (!await promptSkillInstall({ yes: isNonInteractive }) && isNonInteractive) {
|
|
610
473
|
logger.break();
|
|
611
474
|
process.exit(1);
|
|
612
475
|
}
|
|
613
|
-
|
|
614
|
-
if (isNonInteractive) {
|
|
615
|
-
if (!installMcpServers().some((result) => result.success)) {
|
|
616
|
-
logger.break();
|
|
617
|
-
logger.error("Failed to install MCP server.");
|
|
618
|
-
logger.break();
|
|
619
|
-
process.exit(1);
|
|
620
|
-
}
|
|
621
|
-
} else if (!await promptMcpInstall()) {
|
|
622
|
-
logger.break();
|
|
623
|
-
process.exit(0);
|
|
624
|
-
}
|
|
625
|
-
logger.break();
|
|
626
|
-
logger.log(`${highlighter.success("Success!")} MCP server has been configured.`);
|
|
627
|
-
logger.log("Restart your agents to activate.");
|
|
628
|
-
logger.break();
|
|
629
|
-
} else {
|
|
630
|
-
if (!await promptMcpInstall()) {
|
|
631
|
-
logger.break();
|
|
632
|
-
process.exit(0);
|
|
633
|
-
}
|
|
634
|
-
logger.break();
|
|
635
|
-
logger.log(`${highlighter.success("Success!")} MCP server has been configured.`);
|
|
636
|
-
logger.log("Restart your agents to activate.");
|
|
637
|
-
logger.break();
|
|
638
|
-
}
|
|
476
|
+
logger.break();
|
|
639
477
|
} catch (error) {
|
|
640
478
|
handleError(error);
|
|
641
479
|
}
|
|
@@ -1217,7 +1055,7 @@ const formatActivationKeyDisplay = (activationKey) => {
|
|
|
1217
1055
|
};
|
|
1218
1056
|
//#endregion
|
|
1219
1057
|
//#region src/commands/configure.ts
|
|
1220
|
-
const VERSION$4 = "0.1.
|
|
1058
|
+
const VERSION$4 = "0.1.38";
|
|
1221
1059
|
const isMac = process.platform === "darwin";
|
|
1222
1060
|
const META_LABEL = isMac ? "Cmd" : "Win";
|
|
1223
1061
|
const ALT_LABEL = isMac ? "Option" : "Alt";
|
|
@@ -1836,7 +1674,7 @@ const installPackagesWithFeedback = async (packages, packageManager, projectRoot
|
|
|
1836
1674
|
};
|
|
1837
1675
|
//#endregion
|
|
1838
1676
|
//#region src/commands/init.ts
|
|
1839
|
-
const VERSION$3 = "0.1.
|
|
1677
|
+
const VERSION$3 = "0.1.38";
|
|
1840
1678
|
const REPORT_URL = "https://react-grab.com/api/report-cli";
|
|
1841
1679
|
const DOCS_URL = "https://github.com/aidenybai/react-grab";
|
|
1842
1680
|
const reportToCli = (type, config, error) => {
|
|
@@ -2052,25 +1890,7 @@ const init = new commander.Command().name("init").alias("setup").description("in
|
|
|
2052
1890
|
}
|
|
2053
1891
|
}
|
|
2054
1892
|
logger.break();
|
|
2055
|
-
|
|
2056
|
-
type: "confirm",
|
|
2057
|
-
name: "wantAddMcp",
|
|
2058
|
-
message: `Would you like to ${highlighter.info("connect it to your agent via MCP")}?`,
|
|
2059
|
-
initial: false
|
|
2060
|
-
});
|
|
2061
|
-
if (wantAddMcp === void 0) {
|
|
2062
|
-
logger.break();
|
|
2063
|
-
process.exit(1);
|
|
2064
|
-
}
|
|
2065
|
-
if (wantAddMcp) {
|
|
2066
|
-
if (!await promptMcpInstall()) {
|
|
2067
|
-
logger.break();
|
|
2068
|
-
process.exit(0);
|
|
2069
|
-
}
|
|
2070
|
-
logger.break();
|
|
2071
|
-
logger.success("MCP server has been configured.");
|
|
2072
|
-
logger.log("Restart your agents to activate.");
|
|
2073
|
-
}
|
|
1893
|
+
await promptSkillInstall({ yes: isNonInteractive });
|
|
2074
1894
|
logger.break();
|
|
2075
1895
|
process.exit(0);
|
|
2076
1896
|
}
|
|
@@ -2122,7 +1942,16 @@ const init = new commander.Command().name("init").alias("setup").description("in
|
|
|
2122
1942
|
process.chdir(selectedProject);
|
|
2123
1943
|
const newProjectInfo = await detectProject(selectedProject);
|
|
2124
1944
|
Object.assign(projectInfo, newProjectInfo);
|
|
2125
|
-
spinner("Verifying framework.").start()
|
|
1945
|
+
const newFrameworkSpinner = spinner("Verifying framework.").start();
|
|
1946
|
+
if (newProjectInfo.framework === "unknown") {
|
|
1947
|
+
newFrameworkSpinner.fail("Could not detect a supported framework in this project.");
|
|
1948
|
+
logger.break();
|
|
1949
|
+
logger.log("React Grab supports Next.js, Vite, TanStack Start, and Webpack projects.");
|
|
1950
|
+
logger.log(`Visit ${highlighter.info(DOCS_URL)} for manual setup.`);
|
|
1951
|
+
logger.break();
|
|
1952
|
+
process.exit(1);
|
|
1953
|
+
}
|
|
1954
|
+
newFrameworkSpinner.succeed(`Verifying framework. Found ${highlighter.info(FRAMEWORK_NAMES[newProjectInfo.framework])}.`);
|
|
2126
1955
|
} else {
|
|
2127
1956
|
frameworkSpinner.fail("Could not detect a supported framework.");
|
|
2128
1957
|
logger.break();
|
|
@@ -2137,30 +1966,10 @@ const init = new commander.Command().name("init").alias("setup").description("in
|
|
|
2137
1966
|
const finalFramework = projectInfo.framework;
|
|
2138
1967
|
const finalPackageManager = projectInfo.packageManager;
|
|
2139
1968
|
const finalNextRouterType = projectInfo.nextRouterType;
|
|
2140
|
-
let
|
|
1969
|
+
let didInstallSkill = false;
|
|
2141
1970
|
if (!isNonInteractive) {
|
|
2142
1971
|
logger.break();
|
|
2143
|
-
|
|
2144
|
-
type: "confirm",
|
|
2145
|
-
name: "wantAddMcp",
|
|
2146
|
-
message: `Would you like to ${highlighter.info("connect it to your agent via MCP")}?`,
|
|
2147
|
-
initial: false
|
|
2148
|
-
});
|
|
2149
|
-
if (wantAddMcp === void 0) {
|
|
2150
|
-
logger.break();
|
|
2151
|
-
process.exit(1);
|
|
2152
|
-
}
|
|
2153
|
-
if (wantAddMcp) {
|
|
2154
|
-
didInstallMcp = Boolean(await promptMcpInstall());
|
|
2155
|
-
if (!didInstallMcp) {
|
|
2156
|
-
logger.break();
|
|
2157
|
-
process.exit(0);
|
|
2158
|
-
}
|
|
2159
|
-
logger.break();
|
|
2160
|
-
logger.success("MCP server has been configured.");
|
|
2161
|
-
logger.log("Continuing with React Grab installation...");
|
|
2162
|
-
logger.break();
|
|
2163
|
-
}
|
|
1972
|
+
didInstallSkill = await promptSkillInstall({ yes: isNonInteractive });
|
|
2164
1973
|
}
|
|
2165
1974
|
const result = previewTransform(projectInfo.projectRoot, finalFramework, finalNextRouterType, false, opts.force);
|
|
2166
1975
|
if (!result.success) {
|
|
@@ -2204,7 +2013,7 @@ const init = new commander.Command().name("init").alias("setup").description("in
|
|
|
2204
2013
|
framework: finalFramework,
|
|
2205
2014
|
packageManager: finalPackageManager,
|
|
2206
2015
|
router: finalNextRouterType,
|
|
2207
|
-
agent:
|
|
2016
|
+
agent: didInstallSkill ? "skill" : void 0,
|
|
2208
2017
|
isMonorepo: projectInfo.isMonorepo
|
|
2209
2018
|
});
|
|
2210
2019
|
} catch (error) {
|
|
@@ -2214,29 +2023,16 @@ const init = new commander.Command().name("init").alias("setup").description("in
|
|
|
2214
2023
|
});
|
|
2215
2024
|
//#endregion
|
|
2216
2025
|
//#region src/commands/remove.ts
|
|
2217
|
-
const VERSION$2 = "0.1.
|
|
2218
|
-
const remove = new commander.Command().name("remove").description("
|
|
2026
|
+
const VERSION$2 = "0.1.38";
|
|
2027
|
+
const remove = new commander.Command().name("remove").description("uninstall the React Grab skill from your agent").action(async () => {
|
|
2219
2028
|
console.log(`${picocolors.default.magenta("✿")} ${picocolors.default.bold("React Grab")} ${picocolors.default.gray(VERSION$2)}`);
|
|
2220
2029
|
console.log();
|
|
2221
2030
|
try {
|
|
2222
|
-
const cwd = opts.cwd;
|
|
2223
|
-
const preflightSpinner = spinner("Preflight checks.").start();
|
|
2224
|
-
if (!(await detectProject(cwd)).hasReactGrab) {
|
|
2225
|
-
preflightSpinner.fail("React Grab is not installed.");
|
|
2226
|
-
logger.break();
|
|
2227
|
-
logger.error(`Run ${highlighter.info("react-grab init")} first to install React Grab.`);
|
|
2228
|
-
logger.break();
|
|
2229
|
-
process.exit(1);
|
|
2230
|
-
}
|
|
2231
|
-
preflightSpinner.succeed();
|
|
2232
|
-
if (agentArg && agentArg !== "mcp") {
|
|
2233
|
-
logger.break();
|
|
2234
|
-
logger.warn(`Legacy agent packages are deprecated. Uninstall ${highlighter.info(`@react-grab/${agentArg}`)} manually with your package manager.`);
|
|
2235
|
-
logger.break();
|
|
2236
|
-
process.exit(0);
|
|
2237
|
-
}
|
|
2238
2031
|
logger.break();
|
|
2239
|
-
|
|
2032
|
+
const removedCount = await removeSkill();
|
|
2033
|
+
logger.break();
|
|
2034
|
+
if (removedCount === 0) logger.log("React Grab skill is not installed in any detected agent.");
|
|
2035
|
+
else logger.log(`${highlighter.success("Removed")} the React Grab skill from ${removedCount} agent${removedCount === 1 ? "" : "s"}.`);
|
|
2240
2036
|
logger.break();
|
|
2241
2037
|
} catch (error) {
|
|
2242
2038
|
handleError(error);
|
|
@@ -2244,7 +2040,7 @@ const remove = new commander.Command().name("remove").description("disconnect Re
|
|
|
2244
2040
|
});
|
|
2245
2041
|
//#endregion
|
|
2246
2042
|
//#region src/commands/upgrade.ts
|
|
2247
|
-
const VERSION$1 = "0.1.
|
|
2043
|
+
const VERSION$1 = "0.1.38";
|
|
2248
2044
|
const NPM_REGISTRY_URL = "https://registry.npmjs.org/react-grab/latest";
|
|
2249
2045
|
const fetchLatestVersion = async () => {
|
|
2250
2046
|
try {
|
|
@@ -2318,8 +2114,368 @@ const upgrade = new commander.Command().name("upgrade").alias("update").descript
|
|
|
2318
2114
|
}
|
|
2319
2115
|
});
|
|
2320
2116
|
//#endregion
|
|
2117
|
+
//#region src/utils/clipboard.ts
|
|
2118
|
+
const READ_TIMEOUT_MS = 2500;
|
|
2119
|
+
const MAX_CLIPBOARD_BYTES = 64 * 1024 * 1024;
|
|
2120
|
+
const ID_RADIX = 36;
|
|
2121
|
+
const HASH_LENGTH = 12;
|
|
2122
|
+
const PICKLE_HEADER_BYTES = 4;
|
|
2123
|
+
const PICKLE_ALIGN_BYTES = 4;
|
|
2124
|
+
const SIGNATURE_SCAN_CHARS = 32 * 1024;
|
|
2125
|
+
const GRAB_MIME = "application/x-react-grab";
|
|
2126
|
+
const CHROMIUM_CUSTOM_FORMAT = "chromium/x-web-custom-data";
|
|
2127
|
+
const GRAB_TEXT_SIGNATURE = /\bin\s+\S+\s+\(at\s+[^\n]{1,400}?:\d+:\d+\)/;
|
|
2128
|
+
const sleep = (durationMs) => new Promise((resolve) => setTimeout(resolve, durationMs));
|
|
2129
|
+
const shortHash = (text) => (0, node_crypto.createHash)("sha1").update(text).digest("hex").slice(0, HASH_LENGTH);
|
|
2130
|
+
const alignUp = (value) => value + PICKLE_ALIGN_BYTES - 1 & ~(PICKLE_ALIGN_BYTES - 1);
|
|
2131
|
+
const parseChromiumPickle = (buffer) => {
|
|
2132
|
+
const formats = {};
|
|
2133
|
+
if (!buffer || buffer.length < PICKLE_HEADER_BYTES + 4) return formats;
|
|
2134
|
+
let offset = PICKLE_HEADER_BYTES;
|
|
2135
|
+
const pairCount = buffer.readUInt32LE(offset);
|
|
2136
|
+
offset += 4;
|
|
2137
|
+
for (let pairIndex = 0; pairIndex < pairCount; pairIndex += 1) {
|
|
2138
|
+
if (offset + 4 > buffer.length) break;
|
|
2139
|
+
const formatCodeUnits = buffer.readUInt32LE(offset);
|
|
2140
|
+
offset += 4;
|
|
2141
|
+
if (offset + formatCodeUnits * 2 > buffer.length) break;
|
|
2142
|
+
const format = buffer.toString("utf16le", offset, offset + formatCodeUnits * 2);
|
|
2143
|
+
offset = alignUp(offset + formatCodeUnits * 2);
|
|
2144
|
+
if (offset + 4 > buffer.length) break;
|
|
2145
|
+
const dataCodeUnits = buffer.readUInt32LE(offset);
|
|
2146
|
+
offset += 4;
|
|
2147
|
+
if (offset + dataCodeUnits * 2 > buffer.length) break;
|
|
2148
|
+
const value = buffer.toString("utf16le", offset, offset + dataCodeUnits * 2);
|
|
2149
|
+
offset = alignUp(offset + dataCodeUnits * 2);
|
|
2150
|
+
formats[format] = value;
|
|
2151
|
+
}
|
|
2152
|
+
return formats;
|
|
2153
|
+
};
|
|
2154
|
+
const extractGrab = (raw) => {
|
|
2155
|
+
if (raw.grab) return raw.grab;
|
|
2156
|
+
if (raw.pickleBase64) return parseChromiumPickle(Buffer.from(raw.pickleBase64, "base64"))[GRAB_MIME];
|
|
2157
|
+
};
|
|
2158
|
+
const isGrabText = (text) => GRAB_TEXT_SIGNATURE.test(text.length > SIGNATURE_SCAN_CHARS ? text.slice(0, SIGNATURE_SCAN_CHARS) : text);
|
|
2159
|
+
const extractPrompt = (record) => {
|
|
2160
|
+
const comments = (Array.isArray(record.entries) ? record.entries : []).map((entry) => entry?.commentText?.trim?.()).filter(Boolean);
|
|
2161
|
+
if (comments.length > 0) return comments.join("\n");
|
|
2162
|
+
const lines = (typeof record.content === "string" ? record.content : "").split("\n");
|
|
2163
|
+
const firstReferenceLine = lines.findIndex((line) => line.startsWith("["));
|
|
2164
|
+
if (firstReferenceLine <= 0) return void 0;
|
|
2165
|
+
return lines.slice(0, firstReferenceLine).join("\n").trim() || void 0;
|
|
2166
|
+
};
|
|
2167
|
+
const hasCommand = (name) => {
|
|
2168
|
+
return (0, node_child_process.spawnSync)(process.platform === "win32" ? "where" : "which", [name], { stdio: "ignore" }).status === 0;
|
|
2169
|
+
};
|
|
2170
|
+
const runText = (command, args) => {
|
|
2171
|
+
const output = (0, node_child_process.spawnSync)(command, args, {
|
|
2172
|
+
encoding: "utf8",
|
|
2173
|
+
maxBuffer: MAX_CLIPBOARD_BYTES,
|
|
2174
|
+
timeout: READ_TIMEOUT_MS
|
|
2175
|
+
});
|
|
2176
|
+
return output.status === 0 ? output.stdout : null;
|
|
2177
|
+
};
|
|
2178
|
+
const runBuffer = (command, args) => {
|
|
2179
|
+
const output = (0, node_child_process.spawnSync)(command, args, {
|
|
2180
|
+
maxBuffer: MAX_CLIPBOARD_BYTES,
|
|
2181
|
+
timeout: READ_TIMEOUT_MS
|
|
2182
|
+
});
|
|
2183
|
+
return output.status === 0 && output.stdout?.length ? output.stdout : null;
|
|
2184
|
+
};
|
|
2185
|
+
const runJson = (command, args) => {
|
|
2186
|
+
const output = (0, node_child_process.spawnSync)(command, args, {
|
|
2187
|
+
encoding: "utf8",
|
|
2188
|
+
maxBuffer: MAX_CLIPBOARD_BYTES,
|
|
2189
|
+
timeout: READ_TIMEOUT_MS
|
|
2190
|
+
});
|
|
2191
|
+
if (output.status !== 0 || !output.stdout) return null;
|
|
2192
|
+
try {
|
|
2193
|
+
return JSON.parse(output.stdout);
|
|
2194
|
+
} catch {
|
|
2195
|
+
return null;
|
|
2196
|
+
}
|
|
2197
|
+
};
|
|
2198
|
+
const compileSwiftReader = (readersDir, workDir) => {
|
|
2199
|
+
if (!hasCommand("swiftc")) return null;
|
|
2200
|
+
const source = node_path.default.join(readersDir, "read-clipboard.swift");
|
|
2201
|
+
if (!node_fs.default.existsSync(source)) return null;
|
|
2202
|
+
const binary = node_path.default.join(workDir, "pbread");
|
|
2203
|
+
if ((!node_fs.default.existsSync(binary) || node_fs.default.statSync(source).mtimeMs > node_fs.default.statSync(binary).mtimeMs) && (0, node_child_process.spawnSync)("swiftc", [
|
|
2204
|
+
"-O",
|
|
2205
|
+
source,
|
|
2206
|
+
"-o",
|
|
2207
|
+
binary
|
|
2208
|
+
]).status !== 0) return null;
|
|
2209
|
+
return binary;
|
|
2210
|
+
};
|
|
2211
|
+
const createDarwinReader = (options) => {
|
|
2212
|
+
const binary = options.textOnly ? null : compileSwiftReader(options.readersDir, options.workDir);
|
|
2213
|
+
if (binary) return {
|
|
2214
|
+
mode: "darwin-native",
|
|
2215
|
+
read: () => {
|
|
2216
|
+
const raw = runJson(binary, []);
|
|
2217
|
+
if (!raw) return null;
|
|
2218
|
+
return {
|
|
2219
|
+
changeCount: raw.changeCount ?? null,
|
|
2220
|
+
text: raw.text,
|
|
2221
|
+
grab: extractGrab(raw)
|
|
2222
|
+
};
|
|
2223
|
+
}
|
|
2224
|
+
};
|
|
2225
|
+
if (!hasCommand("pbpaste")) return null;
|
|
2226
|
+
return {
|
|
2227
|
+
mode: "darwin-text",
|
|
2228
|
+
read: () => {
|
|
2229
|
+
const text = runText("pbpaste", []);
|
|
2230
|
+
return text == null ? null : {
|
|
2231
|
+
changeCount: null,
|
|
2232
|
+
text,
|
|
2233
|
+
grab: void 0
|
|
2234
|
+
};
|
|
2235
|
+
}
|
|
2236
|
+
};
|
|
2237
|
+
};
|
|
2238
|
+
const detectLinuxTool = () => {
|
|
2239
|
+
if (Boolean(process.env.WAYLAND_DISPLAY) && hasCommand("wl-paste") || hasCommand("wl-paste") && !hasCommand("xclip")) return {
|
|
2240
|
+
name: "wl-paste",
|
|
2241
|
+
readText: () => runText("wl-paste", [
|
|
2242
|
+
"-n",
|
|
2243
|
+
"-t",
|
|
2244
|
+
"text/plain"
|
|
2245
|
+
]) ?? runText("wl-paste", ["-n"]),
|
|
2246
|
+
readCustom: () => runBuffer("wl-paste", [
|
|
2247
|
+
"-n",
|
|
2248
|
+
"-t",
|
|
2249
|
+
CHROMIUM_CUSTOM_FORMAT
|
|
2250
|
+
])
|
|
2251
|
+
};
|
|
2252
|
+
if (hasCommand("xclip")) return {
|
|
2253
|
+
name: "xclip",
|
|
2254
|
+
readText: () => runText("xclip", [
|
|
2255
|
+
"-selection",
|
|
2256
|
+
"clipboard",
|
|
2257
|
+
"-o"
|
|
2258
|
+
]),
|
|
2259
|
+
readCustom: () => runBuffer("xclip", [
|
|
2260
|
+
"-selection",
|
|
2261
|
+
"clipboard",
|
|
2262
|
+
"-t",
|
|
2263
|
+
CHROMIUM_CUSTOM_FORMAT,
|
|
2264
|
+
"-o"
|
|
2265
|
+
])
|
|
2266
|
+
};
|
|
2267
|
+
if (hasCommand("xsel")) return {
|
|
2268
|
+
name: "xsel",
|
|
2269
|
+
readText: () => runText("xsel", ["--clipboard", "--output"]),
|
|
2270
|
+
readCustom: null
|
|
2271
|
+
};
|
|
2272
|
+
return null;
|
|
2273
|
+
};
|
|
2274
|
+
const createLinuxReader = (options) => {
|
|
2275
|
+
const tool = detectLinuxTool();
|
|
2276
|
+
if (!tool) return null;
|
|
2277
|
+
const useCustom = !options.textOnly && Boolean(tool.readCustom);
|
|
2278
|
+
return {
|
|
2279
|
+
mode: useCustom ? `linux-${tool.name}` : `linux-${tool.name}-text`,
|
|
2280
|
+
read: () => {
|
|
2281
|
+
const text = tool.readText();
|
|
2282
|
+
if (text == null) return null;
|
|
2283
|
+
return {
|
|
2284
|
+
changeCount: null,
|
|
2285
|
+
text,
|
|
2286
|
+
grab: useCustom && tool.readCustom ? parseChromiumPickle(tool.readCustom())[GRAB_MIME] : void 0
|
|
2287
|
+
};
|
|
2288
|
+
}
|
|
2289
|
+
};
|
|
2290
|
+
};
|
|
2291
|
+
const detectPowershell = () => hasCommand("pwsh") ? "pwsh" : hasCommand("powershell") ? "powershell" : null;
|
|
2292
|
+
const createWindowsReader = (options) => {
|
|
2293
|
+
const shell = detectPowershell();
|
|
2294
|
+
if (!shell) return null;
|
|
2295
|
+
const scriptPath = node_path.default.join(options.readersDir, "read-clipboard.ps1");
|
|
2296
|
+
if (options.textOnly || !node_fs.default.existsSync(scriptPath)) return {
|
|
2297
|
+
mode: "win-text",
|
|
2298
|
+
read: () => {
|
|
2299
|
+
const text = runText(shell, [
|
|
2300
|
+
"-NoProfile",
|
|
2301
|
+
"-Command",
|
|
2302
|
+
"Get-Clipboard -Raw"
|
|
2303
|
+
]);
|
|
2304
|
+
return text == null ? null : {
|
|
2305
|
+
changeCount: null,
|
|
2306
|
+
text,
|
|
2307
|
+
grab: void 0
|
|
2308
|
+
};
|
|
2309
|
+
}
|
|
2310
|
+
};
|
|
2311
|
+
const args = [
|
|
2312
|
+
"-NoProfile",
|
|
2313
|
+
"-ExecutionPolicy",
|
|
2314
|
+
"Bypass",
|
|
2315
|
+
"-File",
|
|
2316
|
+
scriptPath
|
|
2317
|
+
];
|
|
2318
|
+
(0, node_child_process.spawnSync)(shell, args, {
|
|
2319
|
+
stdio: "ignore",
|
|
2320
|
+
maxBuffer: MAX_CLIPBOARD_BYTES
|
|
2321
|
+
});
|
|
2322
|
+
return {
|
|
2323
|
+
mode: "win-native",
|
|
2324
|
+
read: () => {
|
|
2325
|
+
const raw = runJson(shell, args);
|
|
2326
|
+
if (!raw) return null;
|
|
2327
|
+
return {
|
|
2328
|
+
changeCount: raw.changeCount ?? null,
|
|
2329
|
+
text: raw.text ?? void 0,
|
|
2330
|
+
grab: extractGrab(raw)
|
|
2331
|
+
};
|
|
2332
|
+
}
|
|
2333
|
+
};
|
|
2334
|
+
};
|
|
2335
|
+
const createReader = (options) => {
|
|
2336
|
+
if (process.platform === "darwin") return createDarwinReader(options);
|
|
2337
|
+
if (process.platform === "linux") return createLinuxReader(options);
|
|
2338
|
+
if (process.platform === "win32") return createWindowsReader(options);
|
|
2339
|
+
return null;
|
|
2340
|
+
};
|
|
2341
|
+
const ensureSafeDir = (dir) => {
|
|
2342
|
+
let stats;
|
|
2343
|
+
try {
|
|
2344
|
+
stats = node_fs.default.lstatSync(dir);
|
|
2345
|
+
} catch {
|
|
2346
|
+
return;
|
|
2347
|
+
}
|
|
2348
|
+
if (stats.isSymbolicLink()) throw new Error(`Refusing to use ${dir}: it is a symlink.`);
|
|
2349
|
+
if (process.getuid && stats.uid !== process.getuid()) throw new Error(`Refusing to use ${dir}: owned by another user.`);
|
|
2350
|
+
};
|
|
2351
|
+
const prepareWorkDir = (dir) => {
|
|
2352
|
+
ensureSafeDir(dir);
|
|
2353
|
+
node_fs.default.mkdirSync(dir, {
|
|
2354
|
+
recursive: true,
|
|
2355
|
+
mode: 448
|
|
2356
|
+
});
|
|
2357
|
+
const gitignore = node_path.default.join(dir, ".gitignore");
|
|
2358
|
+
if (!node_fs.default.existsSync(gitignore)) node_fs.default.writeFileSync(gitignore, "*\n");
|
|
2359
|
+
};
|
|
2360
|
+
const watchForNextGrab = async (options) => {
|
|
2361
|
+
const { reader, dir, intervalMs, replayLast, onWarn, signal } = options;
|
|
2362
|
+
const logPath = node_path.default.join(dir, "history.jsonl");
|
|
2363
|
+
const { read } = reader;
|
|
2364
|
+
let lastChangeCount = null;
|
|
2365
|
+
let lastTimestamp = 0;
|
|
2366
|
+
let lastTextHash = "";
|
|
2367
|
+
let lastErrorMessage = "";
|
|
2368
|
+
let sequence = 0;
|
|
2369
|
+
const initial = read();
|
|
2370
|
+
if (initial && !replayLast) {
|
|
2371
|
+
lastChangeCount = initial.changeCount;
|
|
2372
|
+
if (initial.text) lastTextHash = shortHash(initial.text);
|
|
2373
|
+
if (initial.grab) try {
|
|
2374
|
+
lastTimestamp = JSON.parse(initial.grab).timestamp ?? 0;
|
|
2375
|
+
} catch {}
|
|
2376
|
+
}
|
|
2377
|
+
while (!signal?.aborted) {
|
|
2378
|
+
await sleep(intervalMs);
|
|
2379
|
+
if (signal?.aborted) return null;
|
|
2380
|
+
try {
|
|
2381
|
+
const snapshot = read();
|
|
2382
|
+
if (!snapshot) continue;
|
|
2383
|
+
if (snapshot.changeCount !== null && snapshot.changeCount === lastChangeCount) continue;
|
|
2384
|
+
const textHash = snapshot.text ? shortHash(snapshot.text) : "";
|
|
2385
|
+
const didTextChange = textHash !== lastTextHash;
|
|
2386
|
+
let record = null;
|
|
2387
|
+
let nextTimestamp = lastTimestamp;
|
|
2388
|
+
if (snapshot.grab) {
|
|
2389
|
+
let parsed = null;
|
|
2390
|
+
try {
|
|
2391
|
+
parsed = JSON.parse(snapshot.grab);
|
|
2392
|
+
} catch {}
|
|
2393
|
+
if (parsed && typeof parsed.timestamp === "number" && parsed.timestamp > lastTimestamp) {
|
|
2394
|
+
nextTimestamp = parsed.timestamp;
|
|
2395
|
+
record = {
|
|
2396
|
+
source: "custom",
|
|
2397
|
+
timestamp: parsed.timestamp,
|
|
2398
|
+
version: typeof parsed.version === "string" ? parsed.version : void 0,
|
|
2399
|
+
content: typeof parsed.content === "string" ? parsed.content : "",
|
|
2400
|
+
entries: Array.isArray(parsed.entries) ? parsed.entries : []
|
|
2401
|
+
};
|
|
2402
|
+
}
|
|
2403
|
+
} else if (didTextChange && snapshot.text && isGrabText(snapshot.text)) record = {
|
|
2404
|
+
source: "text",
|
|
2405
|
+
timestamp: Date.now(),
|
|
2406
|
+
content: snapshot.text,
|
|
2407
|
+
entries: []
|
|
2408
|
+
};
|
|
2409
|
+
if (!record) {
|
|
2410
|
+
lastChangeCount = snapshot.changeCount;
|
|
2411
|
+
lastTextHash = textHash;
|
|
2412
|
+
continue;
|
|
2413
|
+
}
|
|
2414
|
+
const prompt = extractPrompt(record);
|
|
2415
|
+
if (prompt) record.prompt = prompt;
|
|
2416
|
+
const captured = {
|
|
2417
|
+
id: `${record.timestamp}-${(sequence += 1).toString(ID_RADIX)}`,
|
|
2418
|
+
receivedAt: Date.now(),
|
|
2419
|
+
...record
|
|
2420
|
+
};
|
|
2421
|
+
node_fs.default.appendFileSync(logPath, `${JSON.stringify(captured)}\n`);
|
|
2422
|
+
lastChangeCount = snapshot.changeCount;
|
|
2423
|
+
lastTextHash = textHash;
|
|
2424
|
+
lastTimestamp = nextTimestamp;
|
|
2425
|
+
return captured;
|
|
2426
|
+
} catch (error) {
|
|
2427
|
+
const message = String(error?.message ?? error);
|
|
2428
|
+
if (message !== lastErrorMessage) {
|
|
2429
|
+
lastErrorMessage = message;
|
|
2430
|
+
onWarn?.(message);
|
|
2431
|
+
}
|
|
2432
|
+
}
|
|
2433
|
+
}
|
|
2434
|
+
return null;
|
|
2435
|
+
};
|
|
2436
|
+
//#endregion
|
|
2437
|
+
//#region src/commands/watch.ts
|
|
2438
|
+
const DEFAULT_INTERVAL_MS = 800;
|
|
2439
|
+
const DEFAULT_DIR = ".react-grab";
|
|
2440
|
+
const readersDir = () => node_path.default.dirname((0, node_url.fileURLToPath)(require("url").pathToFileURL(__filename).href));
|
|
2441
|
+
const watch = new commander.Command().name("watch").description("watch the clipboard for the next React Grab selection, print it, then exit").option("-d, --dir <dir>", "work dir for history.jsonl + cursor.txt", DEFAULT_DIR).option("-i, --interval <ms>", "clipboard poll interval in ms", String(DEFAULT_INTERVAL_MS)).option("--text-only", "skip the native reader and use the plain-text fallback").option("--replay-last", "also capture the grab already on the clipboard at startup").action((options) => {
|
|
2442
|
+
const dir = node_path.default.resolve(options.dir);
|
|
2443
|
+
const intervalMs = Number(options.interval);
|
|
2444
|
+
const textOnly = Boolean(options.textOnly);
|
|
2445
|
+
try {
|
|
2446
|
+
prepareWorkDir(dir);
|
|
2447
|
+
} catch (error) {
|
|
2448
|
+
process.stderr.write(`react-grab watch: ${String(error?.message ?? error)}\n`);
|
|
2449
|
+
process.exit(1);
|
|
2450
|
+
}
|
|
2451
|
+
const reader = createReader({
|
|
2452
|
+
textOnly,
|
|
2453
|
+
readersDir: readersDir(),
|
|
2454
|
+
workDir: dir
|
|
2455
|
+
});
|
|
2456
|
+
if (!reader) {
|
|
2457
|
+
process.stderr.write("react-grab watch: no clipboard reader available. Linux: install xclip or wl-clipboard. macOS: install Xcode CLI tools (swiftc) or rely on pbpaste. Windows: ensure PowerShell is on PATH.\n");
|
|
2458
|
+
process.exit(1);
|
|
2459
|
+
}
|
|
2460
|
+
process.stderr.write(`react-grab watch: watching clipboard via ${reader.mode}; history → ${node_path.default.join(dir, "history.jsonl")} (Ctrl+C to stop)\n`);
|
|
2461
|
+
watchForNextGrab({
|
|
2462
|
+
reader,
|
|
2463
|
+
dir,
|
|
2464
|
+
intervalMs: Number.isFinite(intervalMs) && intervalMs > 0 ? intervalMs : DEFAULT_INTERVAL_MS,
|
|
2465
|
+
replayLast: Boolean(options.replayLast),
|
|
2466
|
+
onWarn: (message) => process.stderr.write(`react-grab watch: ${message}\n`)
|
|
2467
|
+
}).then((grab) => {
|
|
2468
|
+
if (!grab) process.exit(0);
|
|
2469
|
+
process.stdout.write(`${JSON.stringify(grab)}\n`);
|
|
2470
|
+
process.exit(0);
|
|
2471
|
+
}).catch((error) => {
|
|
2472
|
+
process.stderr.write(`react-grab watch: ${String(error?.message ?? error)}\n`);
|
|
2473
|
+
process.exit(1);
|
|
2474
|
+
});
|
|
2475
|
+
});
|
|
2476
|
+
//#endregion
|
|
2321
2477
|
//#region src/cli.ts
|
|
2322
|
-
const VERSION = "0.1.
|
|
2478
|
+
const VERSION = "0.1.38";
|
|
2323
2479
|
const VERSION_API_URL = "https://www.react-grab.com/api/version";
|
|
2324
2480
|
process.on("SIGINT", () => process.exit(0));
|
|
2325
2481
|
process.on("SIGTERM", () => process.exit(0));
|
|
@@ -2332,6 +2488,7 @@ program.addCommand(add);
|
|
|
2332
2488
|
program.addCommand(remove);
|
|
2333
2489
|
program.addCommand(configure);
|
|
2334
2490
|
program.addCommand(upgrade);
|
|
2491
|
+
program.addCommand(watch);
|
|
2335
2492
|
const main = async () => {
|
|
2336
2493
|
await program.parseAsync();
|
|
2337
2494
|
};
|