@react-grab/cli 0.1.37 → 0.1.39
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 +555 -346
- package/dist/cli.js +558 -345
- 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,
|
|
@@ -384,6 +393,45 @@ const handleError = (error) => {
|
|
|
384
393
|
process.exit(1);
|
|
385
394
|
};
|
|
386
395
|
//#endregion
|
|
396
|
+
//#region src/utils/detect-agents.ts
|
|
397
|
+
const PATH_BINARIES = {
|
|
398
|
+
"claude-code": ["claude"],
|
|
399
|
+
codex: ["codex"],
|
|
400
|
+
cursor: ["cursor", "cursor-agent"],
|
|
401
|
+
droid: ["droid"],
|
|
402
|
+
"gemini-cli": ["gemini"],
|
|
403
|
+
"github-copilot": ["copilot"],
|
|
404
|
+
opencode: ["opencode"],
|
|
405
|
+
pi: ["pi", "omegon"]
|
|
406
|
+
};
|
|
407
|
+
const isCommandAvailable = (command) => {
|
|
408
|
+
const pathDirectories = (process.env.PATH ?? "").split(node_path.delimiter).filter(Boolean);
|
|
409
|
+
for (const directory of pathDirectories) {
|
|
410
|
+
const binaryPath = (0, node_path.join)(directory, command);
|
|
411
|
+
try {
|
|
412
|
+
if ((0, node_fs.statSync)(binaryPath).isFile()) {
|
|
413
|
+
(0, node_fs.accessSync)(binaryPath, node_fs.constants.X_OK);
|
|
414
|
+
return true;
|
|
415
|
+
}
|
|
416
|
+
} catch {}
|
|
417
|
+
}
|
|
418
|
+
return false;
|
|
419
|
+
};
|
|
420
|
+
const detectAvailableAgents = async () => {
|
|
421
|
+
const installedAgents = new Set(await (0, agent_install_skill.detectInstalledSkillAgents)());
|
|
422
|
+
return (0, agent_install_skill.getSkillAgentTypes)().filter((agent) => {
|
|
423
|
+
if (agent === "universal") return false;
|
|
424
|
+
if (installedAgents.has(agent)) return true;
|
|
425
|
+
return PATH_BINARIES[agent]?.some(isCommandAvailable) ?? false;
|
|
426
|
+
});
|
|
427
|
+
};
|
|
428
|
+
//#endregion
|
|
429
|
+
//#region src/utils/unref-stdin.ts
|
|
430
|
+
const unrefStdin = () => {
|
|
431
|
+
if (process.stdin.isTTY) return;
|
|
432
|
+
process.stdin.unref?.();
|
|
433
|
+
};
|
|
434
|
+
//#endregion
|
|
387
435
|
//#region src/utils/prompts.ts
|
|
388
436
|
const onCancel = () => {
|
|
389
437
|
logger.break();
|
|
@@ -392,210 +440,74 @@ const onCancel = () => {
|
|
|
392
440
|
process.exit(0);
|
|
393
441
|
};
|
|
394
442
|
const prompts$1 = (questions) => {
|
|
395
|
-
return (0, prompts.default)(questions, { onCancel });
|
|
443
|
+
return (0, prompts.default)(questions, { onCancel }).finally(unrefStdin);
|
|
396
444
|
};
|
|
397
445
|
//#endregion
|
|
398
446
|
//#region src/utils/spinner.ts
|
|
399
447
|
const spinner = (text) => (0, ora.default)({ text });
|
|
400
448
|
//#endregion
|
|
401
|
-
//#region src/utils/install-
|
|
402
|
-
const
|
|
403
|
-
const
|
|
404
|
-
const
|
|
405
|
-
const
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
if (
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
|
|
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
|
|
449
|
+
//#region src/utils/install-skill.ts
|
|
450
|
+
const SKILL_NAME = "react-grab";
|
|
451
|
+
const SKILL_SOURCE = (0, node_url.fileURLToPath)(new URL("../skills/react-grab", require("url").pathToFileURL(__filename).href));
|
|
452
|
+
const agentLabel = (agent) => (0, agent_install_skill.getSkillAgentConfig)(agent).displayName;
|
|
453
|
+
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);
|
|
454
|
+
const promptSkillInstall = async ({ yes = false } = {}) => {
|
|
455
|
+
const detectedAgents = await detectAvailableAgents();
|
|
456
|
+
if (detectedAgents.length === 0) {
|
|
457
|
+
logger.warn("No supported agents detected.");
|
|
458
|
+
return false;
|
|
459
|
+
}
|
|
460
|
+
let selectedAgents = detectedAgents;
|
|
461
|
+
if (!yes) {
|
|
462
|
+
const { agents } = await prompts$1({
|
|
463
|
+
type: "multiselect",
|
|
464
|
+
name: "agents",
|
|
465
|
+
message: "Install the React Grab skill for:",
|
|
466
|
+
choices: detectedAgents.map((agent) => ({
|
|
467
|
+
title: agentLabel(agent),
|
|
468
|
+
value: agent,
|
|
469
|
+
selected: true
|
|
470
|
+
})),
|
|
471
|
+
instructions: false,
|
|
472
|
+
min: 1
|
|
564
473
|
});
|
|
474
|
+
selectedAgents = agents ?? [];
|
|
475
|
+
if (selectedAgents.length === 0) return false;
|
|
565
476
|
}
|
|
566
|
-
const
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
477
|
+
const installSpinner = spinner("Installing React Grab skill.").start();
|
|
478
|
+
const { installed, failed } = await (0, agent_install_skill.add)({
|
|
479
|
+
source: SKILL_SOURCE,
|
|
480
|
+
agents: selectedAgents,
|
|
481
|
+
global: true,
|
|
482
|
+
mode: "copy"
|
|
483
|
+
});
|
|
484
|
+
if (installed.length === 0) {
|
|
485
|
+
installSpinner.fail("Failed to install React Grab skill.");
|
|
486
|
+
return false;
|
|
487
|
+
}
|
|
488
|
+
installSpinner.succeed(`Installed React Grab skill for ${installed.map((record) => agentLabel(record.agent)).join(", ")}.`);
|
|
489
|
+
for (const record of failed) logger.log(` ${highlighter.error("✗")} ${agentLabel(record.agent)} ${record.error}`);
|
|
490
|
+
return true;
|
|
572
491
|
};
|
|
573
|
-
const
|
|
574
|
-
const
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
choices: getMcpClientNames().map((name) => ({
|
|
579
|
-
title: name,
|
|
580
|
-
value: name,
|
|
581
|
-
selected: true
|
|
582
|
-
}))
|
|
492
|
+
const removeSkill = async () => {
|
|
493
|
+
const agentsWithSkill = (await detectAvailableAgents()).filter((agent) => (0, node_fs.existsSync)(installedSkillDir(agent)));
|
|
494
|
+
for (const skillDir of new Set(agentsWithSkill.map(installedSkillDir))) (0, node_fs.rmSync)(skillDir, {
|
|
495
|
+
recursive: true,
|
|
496
|
+
force: true
|
|
583
497
|
});
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
return installMcpServers(selectedAgents).some((result) => result.success);
|
|
498
|
+
for (const agent of agentsWithSkill) logger.log(` ${highlighter.success("✓")} ${agentLabel(agent)}`);
|
|
499
|
+
return agentsWithSkill.length;
|
|
587
500
|
};
|
|
588
501
|
//#endregion
|
|
589
502
|
//#region src/commands/add.ts
|
|
590
|
-
const VERSION$5 = "0.1.
|
|
591
|
-
const add = new commander.Command().name("add").alias("install").description("
|
|
503
|
+
const VERSION$5 = "0.1.39";
|
|
504
|
+
const add = new commander.Command().name("add").alias("install").description("install the React Grab skill for your agent").option("-y, --yes", "skip confirmation prompts", false).option("-c, --cwd <cwd>", "working directory (defaults to current directory)", process.cwd()).action(async (opts) => {
|
|
592
505
|
console.log(`${picocolors.default.magenta("✿")} ${picocolors.default.bold("React Grab")} ${picocolors.default.gray(VERSION$5)}`);
|
|
593
506
|
console.log();
|
|
594
507
|
try {
|
|
595
|
-
const cwd = opts.cwd;
|
|
596
508
|
const isNonInteractive = detectNonInteractive(opts.yes);
|
|
597
509
|
const preflightSpinner = spinner("Preflight checks.").start();
|
|
598
|
-
if (!(await detectProject(cwd)).hasReactGrab) {
|
|
510
|
+
if (!(await detectProject(opts.cwd)).hasReactGrab) {
|
|
599
511
|
preflightSpinner.fail("React Grab is not installed.");
|
|
600
512
|
logger.break();
|
|
601
513
|
logger.error(`Run ${highlighter.info("react-grab init")} first to install React Grab.`);
|
|
@@ -603,39 +515,12 @@ const add = new commander.Command().name("add").alias("install").description("co
|
|
|
603
515
|
process.exit(1);
|
|
604
516
|
}
|
|
605
517
|
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.`);
|
|
518
|
+
logger.break();
|
|
519
|
+
if (!await promptSkillInstall({ yes: isNonInteractive }) && isNonInteractive) {
|
|
610
520
|
logger.break();
|
|
611
521
|
process.exit(1);
|
|
612
522
|
}
|
|
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
|
-
}
|
|
523
|
+
logger.break();
|
|
639
524
|
} catch (error) {
|
|
640
525
|
handleError(error);
|
|
641
526
|
}
|
|
@@ -1018,6 +903,15 @@ const transformTanStack = (projectRoot, reactGrabAlreadyConfigured, force = fals
|
|
|
1018
903
|
newContent
|
|
1019
904
|
};
|
|
1020
905
|
};
|
|
906
|
+
const hasFrameworkEntryPoint = (projectRoot, framework, nextRouterType) => {
|
|
907
|
+
switch (framework) {
|
|
908
|
+
case "next": return nextRouterType === "app" ? findLayoutFile(projectRoot) !== null : findDocumentFile(projectRoot) !== null;
|
|
909
|
+
case "vite":
|
|
910
|
+
case "webpack": return findEntryFile(projectRoot) !== null;
|
|
911
|
+
case "tanstack": return findTanStackRootFile(projectRoot) !== null;
|
|
912
|
+
default: return false;
|
|
913
|
+
}
|
|
914
|
+
};
|
|
1021
915
|
const previewTransform = (projectRoot, framework, nextRouterType, reactGrabAlreadyConfigured = false, force = false) => {
|
|
1022
916
|
switch (framework) {
|
|
1023
917
|
case "next":
|
|
@@ -1217,7 +1111,7 @@ const formatActivationKeyDisplay = (activationKey) => {
|
|
|
1217
1111
|
};
|
|
1218
1112
|
//#endregion
|
|
1219
1113
|
//#region src/commands/configure.ts
|
|
1220
|
-
const VERSION$4 = "0.1.
|
|
1114
|
+
const VERSION$4 = "0.1.39";
|
|
1221
1115
|
const isMac = process.platform === "darwin";
|
|
1222
1116
|
const META_LABEL = isMac ? "Cmd" : "Win";
|
|
1223
1117
|
const ALT_LABEL = isMac ? "Option" : "Alt";
|
|
@@ -1836,7 +1730,7 @@ const installPackagesWithFeedback = async (packages, packageManager, projectRoot
|
|
|
1836
1730
|
};
|
|
1837
1731
|
//#endregion
|
|
1838
1732
|
//#region src/commands/init.ts
|
|
1839
|
-
const VERSION$3 = "0.1.
|
|
1733
|
+
const VERSION$3 = "0.1.39";
|
|
1840
1734
|
const REPORT_URL = "https://react-grab.com/api/report-cli";
|
|
1841
1735
|
const DOCS_URL = "https://github.com/aidenybai/react-grab";
|
|
1842
1736
|
const reportToCli = (type, config, error) => {
|
|
@@ -1894,6 +1788,15 @@ const printSubprojects = (searchRoot, sortedProjects) => {
|
|
|
1894
1788
|
logger.log(` ${highlighter.dim("$")} npx grab@latest init -c ${(0, node_path.relative)(searchRoot, sortedProjects[0].path)}`);
|
|
1895
1789
|
logger.break();
|
|
1896
1790
|
};
|
|
1791
|
+
const SUPPORTED_FRAMEWORKS_LINE = "React Grab supports Next.js, Vite, TanStack Start, and Webpack projects.";
|
|
1792
|
+
const failWithManualSetup = (failingSpinner, message, { listSupportedFrameworks = false } = {}) => {
|
|
1793
|
+
failingSpinner.fail(message);
|
|
1794
|
+
logger.break();
|
|
1795
|
+
if (listSupportedFrameworks) logger.log(SUPPORTED_FRAMEWORKS_LINE);
|
|
1796
|
+
logger.log(`Visit ${highlighter.info(DOCS_URL)} for manual setup.`);
|
|
1797
|
+
logger.break();
|
|
1798
|
+
process.exit(1);
|
|
1799
|
+
};
|
|
1897
1800
|
const init = new commander.Command().name("init").alias("setup").description("initialize React Grab in your project").option("-y, --yes", "skip confirmation prompts", false).option("-f, --force", "force overwrite existing config", false).option("-k, --key <key>", "activation key (e.g., Meta+K, Ctrl+Shift+G, Space)").option("--skip-install", "skip package installation", false).option("--pkg <pkg>", "custom package URL for CLI (e.g., grab)").option("-c, --cwd <cwd>", "working directory (defaults to current directory)", process.cwd()).action(async (opts) => {
|
|
1898
1801
|
console.log(`${picocolors.default.magenta("✿")} ${picocolors.default.bold("React Grab")} ${picocolors.default.gray(VERSION$3)}`);
|
|
1899
1802
|
console.log();
|
|
@@ -2052,25 +1955,7 @@ const init = new commander.Command().name("init").alias("setup").description("in
|
|
|
2052
1955
|
}
|
|
2053
1956
|
}
|
|
2054
1957
|
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
|
-
}
|
|
1958
|
+
await promptSkillInstall({ yes: isNonInteractive });
|
|
2074
1959
|
logger.break();
|
|
2075
1960
|
process.exit(0);
|
|
2076
1961
|
}
|
|
@@ -2085,7 +1970,7 @@ const init = new commander.Command().name("init").alias("setup").description("in
|
|
|
2085
1970
|
logger.break();
|
|
2086
1971
|
process.exit(1);
|
|
2087
1972
|
}
|
|
2088
|
-
if (projectInfo.framework === "unknown") {
|
|
1973
|
+
if (projectInfo.framework === "unknown" || projectInfo.isMonorepo && !hasFrameworkEntryPoint(projectInfo.projectRoot, projectInfo.framework, projectInfo.nextRouterType)) {
|
|
2089
1974
|
let searchRoot = cwd;
|
|
2090
1975
|
let reactProjects = findReactProjects(searchRoot);
|
|
2091
1976
|
if (reactProjects.length === 0 && cwd !== process.cwd()) {
|
|
@@ -2122,45 +2007,21 @@ const init = new commander.Command().name("init").alias("setup").description("in
|
|
|
2122
2007
|
process.chdir(selectedProject);
|
|
2123
2008
|
const newProjectInfo = await detectProject(selectedProject);
|
|
2124
2009
|
Object.assign(projectInfo, newProjectInfo);
|
|
2125
|
-
spinner("Verifying framework.").start()
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
logger.log(`Visit ${highlighter.info(DOCS_URL)} for manual setup.`);
|
|
2131
|
-
logger.break();
|
|
2132
|
-
process.exit(1);
|
|
2133
|
-
}
|
|
2010
|
+
const newFrameworkSpinner = spinner("Verifying framework.").start();
|
|
2011
|
+
if (newProjectInfo.framework === "unknown") failWithManualSetup(newFrameworkSpinner, "Could not detect a supported framework in this project.", { listSupportedFrameworks: true });
|
|
2012
|
+
newFrameworkSpinner.succeed(`Verifying framework. Found ${highlighter.info(FRAMEWORK_NAMES[newProjectInfo.framework])}.`);
|
|
2013
|
+
} else if (projectInfo.framework !== "unknown") failWithManualSetup(frameworkSpinner, `Verifying framework. Found ${highlighter.info(FRAMEWORK_NAMES[projectInfo.framework])}, but could not find an entry file.`);
|
|
2014
|
+
else failWithManualSetup(frameworkSpinner, "Could not detect a supported framework.", { listSupportedFrameworks: true });
|
|
2134
2015
|
} else frameworkSpinner.succeed(`Verifying framework. Found ${highlighter.info(FRAMEWORK_NAMES[projectInfo.framework])}.`);
|
|
2135
2016
|
if (projectInfo.framework === "next") spinner("Detecting router type.").start().succeed(`Detecting router type. Found ${highlighter.info(projectInfo.nextRouterType === "app" ? "App Router" : "Pages Router")}.`);
|
|
2136
2017
|
spinner("Detecting package manager.").start().succeed(`Detecting package manager. Found ${highlighter.info(PACKAGE_MANAGER_NAMES[projectInfo.packageManager])}.`);
|
|
2137
2018
|
const finalFramework = projectInfo.framework;
|
|
2138
2019
|
const finalPackageManager = projectInfo.packageManager;
|
|
2139
2020
|
const finalNextRouterType = projectInfo.nextRouterType;
|
|
2140
|
-
let
|
|
2021
|
+
let didInstallSkill = false;
|
|
2141
2022
|
if (!isNonInteractive) {
|
|
2142
2023
|
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
|
-
}
|
|
2024
|
+
didInstallSkill = await promptSkillInstall({ yes: isNonInteractive });
|
|
2164
2025
|
}
|
|
2165
2026
|
const result = previewTransform(projectInfo.projectRoot, finalFramework, finalNextRouterType, false, opts.force);
|
|
2166
2027
|
if (!result.success) {
|
|
@@ -2204,7 +2065,7 @@ const init = new commander.Command().name("init").alias("setup").description("in
|
|
|
2204
2065
|
framework: finalFramework,
|
|
2205
2066
|
packageManager: finalPackageManager,
|
|
2206
2067
|
router: finalNextRouterType,
|
|
2207
|
-
agent:
|
|
2068
|
+
agent: didInstallSkill ? "skill" : void 0,
|
|
2208
2069
|
isMonorepo: projectInfo.isMonorepo
|
|
2209
2070
|
});
|
|
2210
2071
|
} catch (error) {
|
|
@@ -2214,29 +2075,16 @@ const init = new commander.Command().name("init").alias("setup").description("in
|
|
|
2214
2075
|
});
|
|
2215
2076
|
//#endregion
|
|
2216
2077
|
//#region src/commands/remove.ts
|
|
2217
|
-
const VERSION$2 = "0.1.
|
|
2218
|
-
const remove = new commander.Command().name("remove").description("
|
|
2078
|
+
const VERSION$2 = "0.1.39";
|
|
2079
|
+
const remove = new commander.Command().name("remove").description("uninstall the React Grab skill from your agent").action(async () => {
|
|
2219
2080
|
console.log(`${picocolors.default.magenta("✿")} ${picocolors.default.bold("React Grab")} ${picocolors.default.gray(VERSION$2)}`);
|
|
2220
2081
|
console.log();
|
|
2221
2082
|
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
2083
|
logger.break();
|
|
2239
|
-
|
|
2084
|
+
const removedCount = await removeSkill();
|
|
2085
|
+
logger.break();
|
|
2086
|
+
if (removedCount === 0) logger.log("React Grab skill is not installed in any detected agent.");
|
|
2087
|
+
else logger.log(`${highlighter.success("Removed")} the React Grab skill from ${removedCount} agent${removedCount === 1 ? "" : "s"}.`);
|
|
2240
2088
|
logger.break();
|
|
2241
2089
|
} catch (error) {
|
|
2242
2090
|
handleError(error);
|
|
@@ -2244,7 +2092,7 @@ const remove = new commander.Command().name("remove").description("disconnect Re
|
|
|
2244
2092
|
});
|
|
2245
2093
|
//#endregion
|
|
2246
2094
|
//#region src/commands/upgrade.ts
|
|
2247
|
-
const VERSION$1 = "0.1.
|
|
2095
|
+
const VERSION$1 = "0.1.39";
|
|
2248
2096
|
const NPM_REGISTRY_URL = "https://registry.npmjs.org/react-grab/latest";
|
|
2249
2097
|
const fetchLatestVersion = async () => {
|
|
2250
2098
|
try {
|
|
@@ -2318,8 +2166,368 @@ const upgrade = new commander.Command().name("upgrade").alias("update").descript
|
|
|
2318
2166
|
}
|
|
2319
2167
|
});
|
|
2320
2168
|
//#endregion
|
|
2169
|
+
//#region src/utils/clipboard.ts
|
|
2170
|
+
const READ_TIMEOUT_MS = 2500;
|
|
2171
|
+
const MAX_CLIPBOARD_BYTES = 64 * 1024 * 1024;
|
|
2172
|
+
const ID_RADIX = 36;
|
|
2173
|
+
const HASH_LENGTH = 12;
|
|
2174
|
+
const PICKLE_HEADER_BYTES = 4;
|
|
2175
|
+
const PICKLE_ALIGN_BYTES = 4;
|
|
2176
|
+
const SIGNATURE_SCAN_CHARS = 32 * 1024;
|
|
2177
|
+
const GRAB_MIME = "application/x-react-grab";
|
|
2178
|
+
const CHROMIUM_CUSTOM_FORMAT = "chromium/x-web-custom-data";
|
|
2179
|
+
const GRAB_TEXT_SIGNATURE = /\bin\s+\S+\s+\(at\s+[^\n]{1,400}?:\d+:\d+\)/;
|
|
2180
|
+
const sleep = (durationMs) => new Promise((resolve) => setTimeout(resolve, durationMs));
|
|
2181
|
+
const shortHash = (text) => (0, node_crypto.createHash)("sha1").update(text).digest("hex").slice(0, HASH_LENGTH);
|
|
2182
|
+
const alignUp = (value) => value + PICKLE_ALIGN_BYTES - 1 & ~(PICKLE_ALIGN_BYTES - 1);
|
|
2183
|
+
const parseChromiumPickle = (buffer) => {
|
|
2184
|
+
const formats = {};
|
|
2185
|
+
if (!buffer || buffer.length < PICKLE_HEADER_BYTES + 4) return formats;
|
|
2186
|
+
let offset = PICKLE_HEADER_BYTES;
|
|
2187
|
+
const pairCount = buffer.readUInt32LE(offset);
|
|
2188
|
+
offset += 4;
|
|
2189
|
+
for (let pairIndex = 0; pairIndex < pairCount; pairIndex += 1) {
|
|
2190
|
+
if (offset + 4 > buffer.length) break;
|
|
2191
|
+
const formatCodeUnits = buffer.readUInt32LE(offset);
|
|
2192
|
+
offset += 4;
|
|
2193
|
+
if (offset + formatCodeUnits * 2 > buffer.length) break;
|
|
2194
|
+
const format = buffer.toString("utf16le", offset, offset + formatCodeUnits * 2);
|
|
2195
|
+
offset = alignUp(offset + formatCodeUnits * 2);
|
|
2196
|
+
if (offset + 4 > buffer.length) break;
|
|
2197
|
+
const dataCodeUnits = buffer.readUInt32LE(offset);
|
|
2198
|
+
offset += 4;
|
|
2199
|
+
if (offset + dataCodeUnits * 2 > buffer.length) break;
|
|
2200
|
+
const value = buffer.toString("utf16le", offset, offset + dataCodeUnits * 2);
|
|
2201
|
+
offset = alignUp(offset + dataCodeUnits * 2);
|
|
2202
|
+
formats[format] = value;
|
|
2203
|
+
}
|
|
2204
|
+
return formats;
|
|
2205
|
+
};
|
|
2206
|
+
const extractGrab = (raw) => {
|
|
2207
|
+
if (raw.grab) return raw.grab;
|
|
2208
|
+
if (raw.pickleBase64) return parseChromiumPickle(Buffer.from(raw.pickleBase64, "base64"))[GRAB_MIME];
|
|
2209
|
+
};
|
|
2210
|
+
const isGrabText = (text) => GRAB_TEXT_SIGNATURE.test(text.length > SIGNATURE_SCAN_CHARS ? text.slice(0, SIGNATURE_SCAN_CHARS) : text);
|
|
2211
|
+
const extractPrompt = (record) => {
|
|
2212
|
+
const comments = (Array.isArray(record.entries) ? record.entries : []).map((entry) => entry?.commentText?.trim?.()).filter(Boolean);
|
|
2213
|
+
if (comments.length > 0) return comments.join("\n");
|
|
2214
|
+
const lines = (typeof record.content === "string" ? record.content : "").split("\n");
|
|
2215
|
+
const firstReferenceLine = lines.findIndex((line) => line.startsWith("["));
|
|
2216
|
+
if (firstReferenceLine <= 0) return void 0;
|
|
2217
|
+
return lines.slice(0, firstReferenceLine).join("\n").trim() || void 0;
|
|
2218
|
+
};
|
|
2219
|
+
const hasCommand = (name) => {
|
|
2220
|
+
return (0, node_child_process.spawnSync)(process.platform === "win32" ? "where" : "which", [name], { stdio: "ignore" }).status === 0;
|
|
2221
|
+
};
|
|
2222
|
+
const runText = (command, args) => {
|
|
2223
|
+
const output = (0, node_child_process.spawnSync)(command, args, {
|
|
2224
|
+
encoding: "utf8",
|
|
2225
|
+
maxBuffer: MAX_CLIPBOARD_BYTES,
|
|
2226
|
+
timeout: READ_TIMEOUT_MS
|
|
2227
|
+
});
|
|
2228
|
+
return output.status === 0 ? output.stdout : null;
|
|
2229
|
+
};
|
|
2230
|
+
const runBuffer = (command, args) => {
|
|
2231
|
+
const output = (0, node_child_process.spawnSync)(command, args, {
|
|
2232
|
+
maxBuffer: MAX_CLIPBOARD_BYTES,
|
|
2233
|
+
timeout: READ_TIMEOUT_MS
|
|
2234
|
+
});
|
|
2235
|
+
return output.status === 0 && output.stdout?.length ? output.stdout : null;
|
|
2236
|
+
};
|
|
2237
|
+
const runJson = (command, args) => {
|
|
2238
|
+
const output = (0, node_child_process.spawnSync)(command, args, {
|
|
2239
|
+
encoding: "utf8",
|
|
2240
|
+
maxBuffer: MAX_CLIPBOARD_BYTES,
|
|
2241
|
+
timeout: READ_TIMEOUT_MS
|
|
2242
|
+
});
|
|
2243
|
+
if (output.status !== 0 || !output.stdout) return null;
|
|
2244
|
+
try {
|
|
2245
|
+
return JSON.parse(output.stdout);
|
|
2246
|
+
} catch {
|
|
2247
|
+
return null;
|
|
2248
|
+
}
|
|
2249
|
+
};
|
|
2250
|
+
const compileSwiftReader = (readersDir, workDir) => {
|
|
2251
|
+
if (!hasCommand("swiftc")) return null;
|
|
2252
|
+
const source = node_path.default.join(readersDir, "read-clipboard.swift");
|
|
2253
|
+
if (!node_fs.default.existsSync(source)) return null;
|
|
2254
|
+
const binary = node_path.default.join(workDir, "pbread");
|
|
2255
|
+
if ((!node_fs.default.existsSync(binary) || node_fs.default.statSync(source).mtimeMs > node_fs.default.statSync(binary).mtimeMs) && (0, node_child_process.spawnSync)("swiftc", [
|
|
2256
|
+
"-O",
|
|
2257
|
+
source,
|
|
2258
|
+
"-o",
|
|
2259
|
+
binary
|
|
2260
|
+
]).status !== 0) return null;
|
|
2261
|
+
return binary;
|
|
2262
|
+
};
|
|
2263
|
+
const createDarwinReader = (options) => {
|
|
2264
|
+
const binary = options.textOnly ? null : compileSwiftReader(options.readersDir, options.workDir);
|
|
2265
|
+
if (binary) return {
|
|
2266
|
+
mode: "darwin-native",
|
|
2267
|
+
read: () => {
|
|
2268
|
+
const raw = runJson(binary, []);
|
|
2269
|
+
if (!raw) return null;
|
|
2270
|
+
return {
|
|
2271
|
+
changeCount: raw.changeCount ?? null,
|
|
2272
|
+
text: raw.text,
|
|
2273
|
+
grab: extractGrab(raw)
|
|
2274
|
+
};
|
|
2275
|
+
}
|
|
2276
|
+
};
|
|
2277
|
+
if (!hasCommand("pbpaste")) return null;
|
|
2278
|
+
return {
|
|
2279
|
+
mode: "darwin-text",
|
|
2280
|
+
read: () => {
|
|
2281
|
+
const text = runText("pbpaste", []);
|
|
2282
|
+
return text == null ? null : {
|
|
2283
|
+
changeCount: null,
|
|
2284
|
+
text,
|
|
2285
|
+
grab: void 0
|
|
2286
|
+
};
|
|
2287
|
+
}
|
|
2288
|
+
};
|
|
2289
|
+
};
|
|
2290
|
+
const detectLinuxTool = () => {
|
|
2291
|
+
if (Boolean(process.env.WAYLAND_DISPLAY) && hasCommand("wl-paste") || hasCommand("wl-paste") && !hasCommand("xclip")) return {
|
|
2292
|
+
name: "wl-paste",
|
|
2293
|
+
readText: () => runText("wl-paste", [
|
|
2294
|
+
"-n",
|
|
2295
|
+
"-t",
|
|
2296
|
+
"text/plain"
|
|
2297
|
+
]) ?? runText("wl-paste", ["-n"]),
|
|
2298
|
+
readCustom: () => runBuffer("wl-paste", [
|
|
2299
|
+
"-n",
|
|
2300
|
+
"-t",
|
|
2301
|
+
CHROMIUM_CUSTOM_FORMAT
|
|
2302
|
+
])
|
|
2303
|
+
};
|
|
2304
|
+
if (hasCommand("xclip")) return {
|
|
2305
|
+
name: "xclip",
|
|
2306
|
+
readText: () => runText("xclip", [
|
|
2307
|
+
"-selection",
|
|
2308
|
+
"clipboard",
|
|
2309
|
+
"-o"
|
|
2310
|
+
]),
|
|
2311
|
+
readCustom: () => runBuffer("xclip", [
|
|
2312
|
+
"-selection",
|
|
2313
|
+
"clipboard",
|
|
2314
|
+
"-t",
|
|
2315
|
+
CHROMIUM_CUSTOM_FORMAT,
|
|
2316
|
+
"-o"
|
|
2317
|
+
])
|
|
2318
|
+
};
|
|
2319
|
+
if (hasCommand("xsel")) return {
|
|
2320
|
+
name: "xsel",
|
|
2321
|
+
readText: () => runText("xsel", ["--clipboard", "--output"]),
|
|
2322
|
+
readCustom: null
|
|
2323
|
+
};
|
|
2324
|
+
return null;
|
|
2325
|
+
};
|
|
2326
|
+
const createLinuxReader = (options) => {
|
|
2327
|
+
const tool = detectLinuxTool();
|
|
2328
|
+
if (!tool) return null;
|
|
2329
|
+
const useCustom = !options.textOnly && Boolean(tool.readCustom);
|
|
2330
|
+
return {
|
|
2331
|
+
mode: useCustom ? `linux-${tool.name}` : `linux-${tool.name}-text`,
|
|
2332
|
+
read: () => {
|
|
2333
|
+
const text = tool.readText();
|
|
2334
|
+
if (text == null) return null;
|
|
2335
|
+
return {
|
|
2336
|
+
changeCount: null,
|
|
2337
|
+
text,
|
|
2338
|
+
grab: useCustom && tool.readCustom ? parseChromiumPickle(tool.readCustom())[GRAB_MIME] : void 0
|
|
2339
|
+
};
|
|
2340
|
+
}
|
|
2341
|
+
};
|
|
2342
|
+
};
|
|
2343
|
+
const detectPowershell = () => hasCommand("pwsh") ? "pwsh" : hasCommand("powershell") ? "powershell" : null;
|
|
2344
|
+
const createWindowsReader = (options) => {
|
|
2345
|
+
const shell = detectPowershell();
|
|
2346
|
+
if (!shell) return null;
|
|
2347
|
+
const scriptPath = node_path.default.join(options.readersDir, "read-clipboard.ps1");
|
|
2348
|
+
if (options.textOnly || !node_fs.default.existsSync(scriptPath)) return {
|
|
2349
|
+
mode: "win-text",
|
|
2350
|
+
read: () => {
|
|
2351
|
+
const text = runText(shell, [
|
|
2352
|
+
"-NoProfile",
|
|
2353
|
+
"-Command",
|
|
2354
|
+
"Get-Clipboard -Raw"
|
|
2355
|
+
]);
|
|
2356
|
+
return text == null ? null : {
|
|
2357
|
+
changeCount: null,
|
|
2358
|
+
text,
|
|
2359
|
+
grab: void 0
|
|
2360
|
+
};
|
|
2361
|
+
}
|
|
2362
|
+
};
|
|
2363
|
+
const args = [
|
|
2364
|
+
"-NoProfile",
|
|
2365
|
+
"-ExecutionPolicy",
|
|
2366
|
+
"Bypass",
|
|
2367
|
+
"-File",
|
|
2368
|
+
scriptPath
|
|
2369
|
+
];
|
|
2370
|
+
(0, node_child_process.spawnSync)(shell, args, {
|
|
2371
|
+
stdio: "ignore",
|
|
2372
|
+
maxBuffer: MAX_CLIPBOARD_BYTES
|
|
2373
|
+
});
|
|
2374
|
+
return {
|
|
2375
|
+
mode: "win-native",
|
|
2376
|
+
read: () => {
|
|
2377
|
+
const raw = runJson(shell, args);
|
|
2378
|
+
if (!raw) return null;
|
|
2379
|
+
return {
|
|
2380
|
+
changeCount: raw.changeCount ?? null,
|
|
2381
|
+
text: raw.text ?? void 0,
|
|
2382
|
+
grab: extractGrab(raw)
|
|
2383
|
+
};
|
|
2384
|
+
}
|
|
2385
|
+
};
|
|
2386
|
+
};
|
|
2387
|
+
const createReader = (options) => {
|
|
2388
|
+
if (process.platform === "darwin") return createDarwinReader(options);
|
|
2389
|
+
if (process.platform === "linux") return createLinuxReader(options);
|
|
2390
|
+
if (process.platform === "win32") return createWindowsReader(options);
|
|
2391
|
+
return null;
|
|
2392
|
+
};
|
|
2393
|
+
const ensureSafeDir = (dir) => {
|
|
2394
|
+
let stats;
|
|
2395
|
+
try {
|
|
2396
|
+
stats = node_fs.default.lstatSync(dir);
|
|
2397
|
+
} catch {
|
|
2398
|
+
return;
|
|
2399
|
+
}
|
|
2400
|
+
if (stats.isSymbolicLink()) throw new Error(`Refusing to use ${dir}: it is a symlink.`);
|
|
2401
|
+
if (process.getuid && stats.uid !== process.getuid()) throw new Error(`Refusing to use ${dir}: owned by another user.`);
|
|
2402
|
+
};
|
|
2403
|
+
const prepareWorkDir = (dir) => {
|
|
2404
|
+
ensureSafeDir(dir);
|
|
2405
|
+
node_fs.default.mkdirSync(dir, {
|
|
2406
|
+
recursive: true,
|
|
2407
|
+
mode: 448
|
|
2408
|
+
});
|
|
2409
|
+
const gitignore = node_path.default.join(dir, ".gitignore");
|
|
2410
|
+
if (!node_fs.default.existsSync(gitignore)) node_fs.default.writeFileSync(gitignore, "*\n");
|
|
2411
|
+
};
|
|
2412
|
+
const watchForNextGrab = async (options) => {
|
|
2413
|
+
const { reader, dir, intervalMs, replayLast, onWarn, signal } = options;
|
|
2414
|
+
const logPath = node_path.default.join(dir, "history.jsonl");
|
|
2415
|
+
const { read } = reader;
|
|
2416
|
+
let lastChangeCount = null;
|
|
2417
|
+
let lastTimestamp = 0;
|
|
2418
|
+
let lastTextHash = "";
|
|
2419
|
+
let lastErrorMessage = "";
|
|
2420
|
+
let sequence = 0;
|
|
2421
|
+
const initial = read();
|
|
2422
|
+
if (initial && !replayLast) {
|
|
2423
|
+
lastChangeCount = initial.changeCount;
|
|
2424
|
+
if (initial.text) lastTextHash = shortHash(initial.text);
|
|
2425
|
+
if (initial.grab) try {
|
|
2426
|
+
lastTimestamp = JSON.parse(initial.grab).timestamp ?? 0;
|
|
2427
|
+
} catch {}
|
|
2428
|
+
}
|
|
2429
|
+
while (!signal?.aborted) {
|
|
2430
|
+
await sleep(intervalMs);
|
|
2431
|
+
if (signal?.aborted) return null;
|
|
2432
|
+
try {
|
|
2433
|
+
const snapshot = read();
|
|
2434
|
+
if (!snapshot) continue;
|
|
2435
|
+
if (snapshot.changeCount !== null && snapshot.changeCount === lastChangeCount) continue;
|
|
2436
|
+
const textHash = snapshot.text ? shortHash(snapshot.text) : "";
|
|
2437
|
+
const didTextChange = textHash !== lastTextHash;
|
|
2438
|
+
let record = null;
|
|
2439
|
+
let nextTimestamp = lastTimestamp;
|
|
2440
|
+
if (snapshot.grab) {
|
|
2441
|
+
let parsed = null;
|
|
2442
|
+
try {
|
|
2443
|
+
parsed = JSON.parse(snapshot.grab);
|
|
2444
|
+
} catch {}
|
|
2445
|
+
if (parsed && typeof parsed.timestamp === "number" && parsed.timestamp > lastTimestamp) {
|
|
2446
|
+
nextTimestamp = parsed.timestamp;
|
|
2447
|
+
record = {
|
|
2448
|
+
source: "custom",
|
|
2449
|
+
timestamp: parsed.timestamp,
|
|
2450
|
+
version: typeof parsed.version === "string" ? parsed.version : void 0,
|
|
2451
|
+
content: typeof parsed.content === "string" ? parsed.content : "",
|
|
2452
|
+
entries: Array.isArray(parsed.entries) ? parsed.entries : []
|
|
2453
|
+
};
|
|
2454
|
+
}
|
|
2455
|
+
} else if (didTextChange && snapshot.text && isGrabText(snapshot.text)) record = {
|
|
2456
|
+
source: "text",
|
|
2457
|
+
timestamp: Date.now(),
|
|
2458
|
+
content: snapshot.text,
|
|
2459
|
+
entries: []
|
|
2460
|
+
};
|
|
2461
|
+
if (!record) {
|
|
2462
|
+
lastChangeCount = snapshot.changeCount;
|
|
2463
|
+
lastTextHash = textHash;
|
|
2464
|
+
continue;
|
|
2465
|
+
}
|
|
2466
|
+
const prompt = extractPrompt(record);
|
|
2467
|
+
if (prompt) record.prompt = prompt;
|
|
2468
|
+
const captured = {
|
|
2469
|
+
id: `${record.timestamp}-${(sequence += 1).toString(ID_RADIX)}`,
|
|
2470
|
+
receivedAt: Date.now(),
|
|
2471
|
+
...record
|
|
2472
|
+
};
|
|
2473
|
+
node_fs.default.appendFileSync(logPath, `${JSON.stringify(captured)}\n`);
|
|
2474
|
+
lastChangeCount = snapshot.changeCount;
|
|
2475
|
+
lastTextHash = textHash;
|
|
2476
|
+
lastTimestamp = nextTimestamp;
|
|
2477
|
+
return captured;
|
|
2478
|
+
} catch (error) {
|
|
2479
|
+
const message = String(error?.message ?? error);
|
|
2480
|
+
if (message !== lastErrorMessage) {
|
|
2481
|
+
lastErrorMessage = message;
|
|
2482
|
+
onWarn?.(message);
|
|
2483
|
+
}
|
|
2484
|
+
}
|
|
2485
|
+
}
|
|
2486
|
+
return null;
|
|
2487
|
+
};
|
|
2488
|
+
//#endregion
|
|
2489
|
+
//#region src/commands/watch.ts
|
|
2490
|
+
const DEFAULT_INTERVAL_MS = 800;
|
|
2491
|
+
const DEFAULT_DIR = ".react-grab";
|
|
2492
|
+
const readersDir = () => node_path.default.dirname((0, node_url.fileURLToPath)(require("url").pathToFileURL(__filename).href));
|
|
2493
|
+
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) => {
|
|
2494
|
+
const dir = node_path.default.resolve(options.dir);
|
|
2495
|
+
const intervalMs = Number(options.interval);
|
|
2496
|
+
const textOnly = Boolean(options.textOnly);
|
|
2497
|
+
try {
|
|
2498
|
+
prepareWorkDir(dir);
|
|
2499
|
+
} catch (error) {
|
|
2500
|
+
process.stderr.write(`react-grab watch: ${String(error?.message ?? error)}\n`);
|
|
2501
|
+
process.exit(1);
|
|
2502
|
+
}
|
|
2503
|
+
const reader = createReader({
|
|
2504
|
+
textOnly,
|
|
2505
|
+
readersDir: readersDir(),
|
|
2506
|
+
workDir: dir
|
|
2507
|
+
});
|
|
2508
|
+
if (!reader) {
|
|
2509
|
+
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");
|
|
2510
|
+
process.exit(1);
|
|
2511
|
+
}
|
|
2512
|
+
process.stderr.write(`react-grab watch: watching clipboard via ${reader.mode}; history → ${node_path.default.join(dir, "history.jsonl")} (Ctrl+C to stop)\n`);
|
|
2513
|
+
watchForNextGrab({
|
|
2514
|
+
reader,
|
|
2515
|
+
dir,
|
|
2516
|
+
intervalMs: Number.isFinite(intervalMs) && intervalMs > 0 ? intervalMs : DEFAULT_INTERVAL_MS,
|
|
2517
|
+
replayLast: Boolean(options.replayLast),
|
|
2518
|
+
onWarn: (message) => process.stderr.write(`react-grab watch: ${message}\n`)
|
|
2519
|
+
}).then((grab) => {
|
|
2520
|
+
if (!grab) process.exit(0);
|
|
2521
|
+
process.stdout.write(`${JSON.stringify(grab)}\n`);
|
|
2522
|
+
process.exit(0);
|
|
2523
|
+
}).catch((error) => {
|
|
2524
|
+
process.stderr.write(`react-grab watch: ${String(error?.message ?? error)}\n`);
|
|
2525
|
+
process.exit(1);
|
|
2526
|
+
});
|
|
2527
|
+
});
|
|
2528
|
+
//#endregion
|
|
2321
2529
|
//#region src/cli.ts
|
|
2322
|
-
const VERSION = "0.1.
|
|
2530
|
+
const VERSION = "0.1.39";
|
|
2323
2531
|
const VERSION_API_URL = "https://www.react-grab.com/api/version";
|
|
2324
2532
|
process.on("SIGINT", () => process.exit(0));
|
|
2325
2533
|
process.on("SIGTERM", () => process.exit(0));
|
|
@@ -2332,6 +2540,7 @@ program.addCommand(add);
|
|
|
2332
2540
|
program.addCommand(remove);
|
|
2333
2541
|
program.addCommand(configure);
|
|
2334
2542
|
program.addCommand(upgrade);
|
|
2543
|
+
program.addCommand(watch);
|
|
2335
2544
|
const main = async () => {
|
|
2336
2545
|
await program.parseAsync();
|
|
2337
2546
|
};
|