@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.js
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Command } from "commander";
|
|
3
3
|
import pc from "picocolors";
|
|
4
|
-
import fs, { accessSync, constants, existsSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
5
|
-
import path, { basename, dirname, join, relative, resolve } from "node:path";
|
|
4
|
+
import fs, { accessSync, constants, existsSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
5
|
+
import path, { basename, delimiter, dirname, join, relative, resolve } from "node:path";
|
|
6
6
|
import { detect } from "package-manager-detector/detect";
|
|
7
7
|
import ignore from "ignore";
|
|
8
|
-
import
|
|
9
|
-
import
|
|
10
|
-
import * as jsonc from "jsonc-parser";
|
|
11
|
-
import * as TOML from "smol-toml";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
import { add, detectInstalledSkillAgents, getCanonicalSkillsDir, getSkillAgentConfig, getSkillAgentDir, getSkillAgentTypes, isUniversalSkillAgent } from "agent-install/skill";
|
|
12
10
|
import basePrompts from "prompts";
|
|
13
11
|
import ora from "ora";
|
|
14
12
|
import { x } from "tinyexec";
|
|
13
|
+
import { spawnSync } from "node:child_process";
|
|
14
|
+
import { createHash } from "node:crypto";
|
|
15
15
|
//#region src/utils/is-non-interactive.ts
|
|
16
16
|
const AGENT_ENVIRONMENT_VARIABLES = [
|
|
17
17
|
"CI",
|
|
@@ -40,24 +40,61 @@ const detectPackageManager = async (projectRoot) => {
|
|
|
40
40
|
}
|
|
41
41
|
return "npm";
|
|
42
42
|
};
|
|
43
|
-
const
|
|
43
|
+
const CONFIG_EXTENSIONS = [
|
|
44
|
+
"ts",
|
|
45
|
+
"mts",
|
|
46
|
+
"cts",
|
|
47
|
+
"js",
|
|
48
|
+
"mjs",
|
|
49
|
+
"cjs"
|
|
50
|
+
];
|
|
51
|
+
const hasConfigFile = (projectRoot, configBaseName) => CONFIG_EXTENSIONS.some((extension) => existsSync(join(projectRoot, `${configBaseName}.${extension}`)));
|
|
52
|
+
const readMergedDependencies = (projectRoot) => {
|
|
44
53
|
const packageJsonPath = join(projectRoot, "package.json");
|
|
45
|
-
if (!existsSync(packageJsonPath)) return
|
|
54
|
+
if (!existsSync(packageJsonPath)) return null;
|
|
46
55
|
try {
|
|
47
56
|
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
48
|
-
|
|
57
|
+
return {
|
|
49
58
|
...packageJson.dependencies,
|
|
50
59
|
...packageJson.devDependencies
|
|
51
60
|
};
|
|
52
|
-
if (allDependencies["next"]) return "next";
|
|
53
|
-
if (allDependencies["@tanstack/react-start"]) return "tanstack";
|
|
54
|
-
if (allDependencies["vite"]) return "vite";
|
|
55
|
-
if (allDependencies["webpack"]) return "webpack";
|
|
56
|
-
return "unknown";
|
|
57
61
|
} catch {
|
|
58
|
-
return
|
|
62
|
+
return null;
|
|
59
63
|
}
|
|
60
64
|
};
|
|
65
|
+
const detectFrameworkFromDependencies = (dependencies) => {
|
|
66
|
+
if (!dependencies) return "unknown";
|
|
67
|
+
if (dependencies["next"]) return "next";
|
|
68
|
+
if (dependencies["@tanstack/react-start"]) return "tanstack";
|
|
69
|
+
if (dependencies["vite"]) return "vite";
|
|
70
|
+
if (dependencies["webpack"]) return "webpack";
|
|
71
|
+
return "unknown";
|
|
72
|
+
};
|
|
73
|
+
const detectFrameworkFromConfigFiles = (projectRoot) => {
|
|
74
|
+
if (hasConfigFile(projectRoot, "next.config")) return "next";
|
|
75
|
+
if (hasConfigFile(projectRoot, "app.config")) return "tanstack";
|
|
76
|
+
if (hasConfigFile(projectRoot, "vite.config")) return "vite";
|
|
77
|
+
if (hasConfigFile(projectRoot, "webpack.config")) return "webpack";
|
|
78
|
+
return "unknown";
|
|
79
|
+
};
|
|
80
|
+
const findEnclosingMonorepoRoot = (projectRoot) => {
|
|
81
|
+
let currentDirectory = dirname(projectRoot);
|
|
82
|
+
while (currentDirectory !== dirname(currentDirectory)) {
|
|
83
|
+
if (detectMonorepo(currentDirectory)) return currentDirectory;
|
|
84
|
+
currentDirectory = dirname(currentDirectory);
|
|
85
|
+
}
|
|
86
|
+
return null;
|
|
87
|
+
};
|
|
88
|
+
const detectFramework = (projectRoot) => {
|
|
89
|
+
const localFramework = detectFrameworkFromDependencies(readMergedDependencies(projectRoot));
|
|
90
|
+
if (localFramework !== "unknown") return localFramework;
|
|
91
|
+
return detectFrameworkFromConfigFiles(projectRoot);
|
|
92
|
+
};
|
|
93
|
+
const detectFrameworkFromMonorepoRoot = (projectRoot) => {
|
|
94
|
+
const monorepoRoot = findEnclosingMonorepoRoot(projectRoot);
|
|
95
|
+
if (!monorepoRoot) return "unknown";
|
|
96
|
+
return detectFrameworkFromDependencies(readMergedDependencies(monorepoRoot));
|
|
97
|
+
};
|
|
61
98
|
const detectNextRouterType = (projectRoot) => {
|
|
62
99
|
const hasAppDir = existsSync(join(projectRoot, "app"));
|
|
63
100
|
const hasSrcAppDir = existsSync(join(projectRoot, "src", "app"));
|
|
@@ -130,18 +167,9 @@ const expandWorkspacePattern = (projectRoot, pattern) => {
|
|
|
130
167
|
return results;
|
|
131
168
|
};
|
|
132
169
|
const hasReactDependency = (projectPath) => {
|
|
133
|
-
const
|
|
134
|
-
if (!
|
|
135
|
-
|
|
136
|
-
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
137
|
-
const allDeps = {
|
|
138
|
-
...packageJson.dependencies,
|
|
139
|
-
...packageJson.devDependencies
|
|
140
|
-
};
|
|
141
|
-
return Boolean(allDeps["react"] || allDeps["react-dom"]);
|
|
142
|
-
} catch {
|
|
143
|
-
return false;
|
|
144
|
-
}
|
|
170
|
+
const dependencies = readMergedDependencies(projectPath);
|
|
171
|
+
if (!dependencies) return false;
|
|
172
|
+
return Boolean(dependencies["react"] || dependencies["react-dom"]);
|
|
145
173
|
};
|
|
146
174
|
const buildReactProject = (projectPath) => {
|
|
147
175
|
const framework = detectFramework(projectPath);
|
|
@@ -243,14 +271,7 @@ const hasReactGrabInFile = (filePath) => {
|
|
|
243
271
|
}
|
|
244
272
|
};
|
|
245
273
|
const detectReactGrab = (projectRoot) => {
|
|
246
|
-
|
|
247
|
-
if (existsSync(packageJsonPath)) try {
|
|
248
|
-
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
249
|
-
if ({
|
|
250
|
-
...packageJson.dependencies,
|
|
251
|
-
...packageJson.devDependencies
|
|
252
|
-
}["react-grab"]) return true;
|
|
253
|
-
} catch {}
|
|
274
|
+
if (readMergedDependencies(projectRoot)?.["react-grab"]) return true;
|
|
254
275
|
return [
|
|
255
276
|
join(projectRoot, "app", "layout.tsx"),
|
|
256
277
|
join(projectRoot, "app", "layout.jsx"),
|
|
@@ -275,22 +296,13 @@ const detectReactGrab = (projectRoot) => {
|
|
|
275
296
|
].some(hasReactGrabInFile);
|
|
276
297
|
};
|
|
277
298
|
const detectUnsupportedFramework = (projectRoot) => {
|
|
278
|
-
const
|
|
279
|
-
if (!
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
};
|
|
286
|
-
if (allDependencies["@remix-run/react"] || allDependencies["remix"]) return "remix";
|
|
287
|
-
if (allDependencies["astro"]) return "astro";
|
|
288
|
-
if (allDependencies["@sveltejs/kit"]) return "sveltekit";
|
|
289
|
-
if (allDependencies["gatsby"]) return "gatsby";
|
|
290
|
-
return null;
|
|
291
|
-
} catch {
|
|
292
|
-
return null;
|
|
293
|
-
}
|
|
299
|
+
const dependencies = readMergedDependencies(projectRoot);
|
|
300
|
+
if (!dependencies) return null;
|
|
301
|
+
if (dependencies["@remix-run/react"] || dependencies["remix"]) return "remix";
|
|
302
|
+
if (dependencies["astro"]) return "astro";
|
|
303
|
+
if (dependencies["@sveltejs/kit"]) return "sveltekit";
|
|
304
|
+
if (dependencies["gatsby"]) return "gatsby";
|
|
305
|
+
return null;
|
|
294
306
|
};
|
|
295
307
|
const detectReactGrabVersion = (projectRoot) => {
|
|
296
308
|
const installedPackageJsonPath = join(projectRoot, "node_modules", "react-grab", "package.json");
|
|
@@ -300,7 +312,8 @@ const detectReactGrabVersion = (projectRoot) => {
|
|
|
300
312
|
return null;
|
|
301
313
|
};
|
|
302
314
|
const detectProject = async (projectRoot = process.cwd()) => {
|
|
303
|
-
const
|
|
315
|
+
const localFramework = detectFramework(projectRoot);
|
|
316
|
+
const framework = localFramework === "unknown" ? detectFrameworkFromMonorepoRoot(projectRoot) : localFramework;
|
|
304
317
|
return {
|
|
305
318
|
packageManager: await detectPackageManager(projectRoot),
|
|
306
319
|
framework,
|
|
@@ -352,6 +365,45 @@ const handleError = (error) => {
|
|
|
352
365
|
process.exit(1);
|
|
353
366
|
};
|
|
354
367
|
//#endregion
|
|
368
|
+
//#region src/utils/detect-agents.ts
|
|
369
|
+
const PATH_BINARIES = {
|
|
370
|
+
"claude-code": ["claude"],
|
|
371
|
+
codex: ["codex"],
|
|
372
|
+
cursor: ["cursor", "cursor-agent"],
|
|
373
|
+
droid: ["droid"],
|
|
374
|
+
"gemini-cli": ["gemini"],
|
|
375
|
+
"github-copilot": ["copilot"],
|
|
376
|
+
opencode: ["opencode"],
|
|
377
|
+
pi: ["pi", "omegon"]
|
|
378
|
+
};
|
|
379
|
+
const isCommandAvailable = (command) => {
|
|
380
|
+
const pathDirectories = (process.env.PATH ?? "").split(delimiter).filter(Boolean);
|
|
381
|
+
for (const directory of pathDirectories) {
|
|
382
|
+
const binaryPath = join(directory, command);
|
|
383
|
+
try {
|
|
384
|
+
if (statSync(binaryPath).isFile()) {
|
|
385
|
+
accessSync(binaryPath, constants.X_OK);
|
|
386
|
+
return true;
|
|
387
|
+
}
|
|
388
|
+
} catch {}
|
|
389
|
+
}
|
|
390
|
+
return false;
|
|
391
|
+
};
|
|
392
|
+
const detectAvailableAgents = async () => {
|
|
393
|
+
const installedAgents = new Set(await detectInstalledSkillAgents());
|
|
394
|
+
return getSkillAgentTypes().filter((agent) => {
|
|
395
|
+
if (agent === "universal") return false;
|
|
396
|
+
if (installedAgents.has(agent)) return true;
|
|
397
|
+
return PATH_BINARIES[agent]?.some(isCommandAvailable) ?? false;
|
|
398
|
+
});
|
|
399
|
+
};
|
|
400
|
+
//#endregion
|
|
401
|
+
//#region src/utils/unref-stdin.ts
|
|
402
|
+
const unrefStdin = () => {
|
|
403
|
+
if (process.stdin.isTTY) return;
|
|
404
|
+
process.stdin.unref?.();
|
|
405
|
+
};
|
|
406
|
+
//#endregion
|
|
355
407
|
//#region src/utils/prompts.ts
|
|
356
408
|
const onCancel = () => {
|
|
357
409
|
logger.break();
|
|
@@ -360,210 +412,74 @@ const onCancel = () => {
|
|
|
360
412
|
process.exit(0);
|
|
361
413
|
};
|
|
362
414
|
const prompts = (questions) => {
|
|
363
|
-
return basePrompts(questions, { onCancel });
|
|
415
|
+
return basePrompts(questions, { onCancel }).finally(unrefStdin);
|
|
364
416
|
};
|
|
365
417
|
//#endregion
|
|
366
418
|
//#region src/utils/spinner.ts
|
|
367
419
|
const spinner = (text) => ora({ text });
|
|
368
420
|
//#endregion
|
|
369
|
-
//#region src/utils/install-
|
|
370
|
-
const
|
|
371
|
-
const
|
|
372
|
-
const
|
|
373
|
-
const
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
if (
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
const baseDir = getBaseDir();
|
|
394
|
-
const stdioConfig = {
|
|
395
|
-
command: "npx",
|
|
396
|
-
args: [
|
|
397
|
-
"-y",
|
|
398
|
-
PACKAGE_NAME,
|
|
399
|
-
"--stdio"
|
|
400
|
-
]
|
|
401
|
-
};
|
|
402
|
-
return [
|
|
403
|
-
{
|
|
404
|
-
name: "Claude Code",
|
|
405
|
-
configPath: path.join(homeDir, ".claude.json"),
|
|
406
|
-
configKey: "mcpServers",
|
|
407
|
-
format: "json",
|
|
408
|
-
serverConfig: stdioConfig
|
|
409
|
-
},
|
|
410
|
-
{
|
|
411
|
-
name: "Codex",
|
|
412
|
-
configPath: path.join(process$1.env.CODEX_HOME || path.join(homeDir, ".codex"), "config.toml"),
|
|
413
|
-
configKey: "mcp_servers",
|
|
414
|
-
format: "toml",
|
|
415
|
-
serverConfig: stdioConfig
|
|
416
|
-
},
|
|
417
|
-
{
|
|
418
|
-
name: "Cursor",
|
|
419
|
-
configPath: path.join(homeDir, ".cursor", "mcp.json"),
|
|
420
|
-
configKey: "mcpServers",
|
|
421
|
-
format: "json",
|
|
422
|
-
serverConfig: stdioConfig
|
|
423
|
-
},
|
|
424
|
-
{
|
|
425
|
-
name: "OpenCode",
|
|
426
|
-
configPath: getOpenCodeConfigPath(),
|
|
427
|
-
configKey: "mcp",
|
|
428
|
-
format: "json",
|
|
429
|
-
serverConfig: {
|
|
430
|
-
type: "local",
|
|
431
|
-
command: [
|
|
432
|
-
"npx",
|
|
433
|
-
"-y",
|
|
434
|
-
PACKAGE_NAME,
|
|
435
|
-
"--stdio"
|
|
436
|
-
]
|
|
437
|
-
}
|
|
438
|
-
},
|
|
439
|
-
{
|
|
440
|
-
name: "VS Code",
|
|
441
|
-
configPath: path.join(baseDir, "Code", "User", "mcp.json"),
|
|
442
|
-
configKey: "servers",
|
|
443
|
-
format: "json",
|
|
444
|
-
serverConfig: {
|
|
445
|
-
type: "stdio",
|
|
446
|
-
...stdioConfig
|
|
447
|
-
}
|
|
448
|
-
},
|
|
449
|
-
{
|
|
450
|
-
name: "Amp",
|
|
451
|
-
configPath: path.join(homeDir, ".config", "amp", "settings.json"),
|
|
452
|
-
configKey: "amp.mcpServers",
|
|
453
|
-
format: "json",
|
|
454
|
-
serverConfig: stdioConfig
|
|
455
|
-
},
|
|
456
|
-
{
|
|
457
|
-
name: "Droid",
|
|
458
|
-
configPath: path.join(homeDir, ".factory", "mcp.json"),
|
|
459
|
-
configKey: "mcpServers",
|
|
460
|
-
format: "json",
|
|
461
|
-
serverConfig: {
|
|
462
|
-
type: "stdio",
|
|
463
|
-
...stdioConfig
|
|
464
|
-
}
|
|
465
|
-
},
|
|
466
|
-
{
|
|
467
|
-
name: "Windsurf",
|
|
468
|
-
configPath: path.join(homeDir, ".codeium", "windsurf", "mcp_config.json"),
|
|
469
|
-
configKey: "mcpServers",
|
|
470
|
-
format: "json",
|
|
471
|
-
serverConfig: stdioConfig
|
|
472
|
-
},
|
|
473
|
-
{
|
|
474
|
-
name: "Zed",
|
|
475
|
-
configPath: getZedConfigPath(),
|
|
476
|
-
configKey: "context_servers",
|
|
477
|
-
format: "json",
|
|
478
|
-
serverConfig: {
|
|
479
|
-
source: "custom",
|
|
480
|
-
...stdioConfig,
|
|
481
|
-
env: {}
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
];
|
|
485
|
-
};
|
|
486
|
-
const ensureDirectory = (filePath) => {
|
|
487
|
-
const directory = path.dirname(filePath);
|
|
488
|
-
if (!fs.existsSync(directory)) fs.mkdirSync(directory, { recursive: true });
|
|
489
|
-
};
|
|
490
|
-
const JSONC_FORMAT_OPTIONS = {
|
|
491
|
-
tabSize: 2,
|
|
492
|
-
insertSpaces: true
|
|
493
|
-
};
|
|
494
|
-
const upsertIntoJsonc = (filePath, content, configKey, serverName, serverConfig) => {
|
|
495
|
-
const edits = jsonc.modify(content, [configKey, serverName], serverConfig, { formattingOptions: JSONC_FORMAT_OPTIONS });
|
|
496
|
-
fs.writeFileSync(filePath, jsonc.applyEdits(content, edits));
|
|
497
|
-
};
|
|
498
|
-
const installJsonClient = (client) => {
|
|
499
|
-
ensureDirectory(client.configPath);
|
|
500
|
-
const content = fs.existsSync(client.configPath) ? fs.readFileSync(client.configPath, "utf8") : "{}";
|
|
501
|
-
upsertIntoJsonc(client.configPath, content, client.configKey, SERVER_NAME, client.serverConfig);
|
|
502
|
-
};
|
|
503
|
-
const installTomlClient = (client) => {
|
|
504
|
-
ensureDirectory(client.configPath);
|
|
505
|
-
const existingConfig = fs.existsSync(client.configPath) ? TOML.parse(fs.readFileSync(client.configPath, "utf8")) : {};
|
|
506
|
-
const serverSection = existingConfig[client.configKey] ?? {};
|
|
507
|
-
serverSection[SERVER_NAME] = client.serverConfig;
|
|
508
|
-
existingConfig[client.configKey] = serverSection;
|
|
509
|
-
fs.writeFileSync(client.configPath, TOML.stringify(existingConfig));
|
|
510
|
-
};
|
|
511
|
-
const getMcpClientNames = () => getClients().map((client) => client.name);
|
|
512
|
-
const installMcpServers = (selectedClients) => {
|
|
513
|
-
const allClients = getClients();
|
|
514
|
-
const clients = selectedClients ? allClients.filter((client) => selectedClients.includes(client.name)) : allClients;
|
|
515
|
-
const results = [];
|
|
516
|
-
const installSpinner = spinner("Installing MCP server.").start();
|
|
517
|
-
for (const client of clients) try {
|
|
518
|
-
if (client.format === "toml") installTomlClient(client);
|
|
519
|
-
else installJsonClient(client);
|
|
520
|
-
results.push({
|
|
521
|
-
client: client.name,
|
|
522
|
-
configPath: client.configPath,
|
|
523
|
-
success: true
|
|
524
|
-
});
|
|
525
|
-
} catch (error) {
|
|
526
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
527
|
-
results.push({
|
|
528
|
-
client: client.name,
|
|
529
|
-
configPath: client.configPath,
|
|
530
|
-
success: false,
|
|
531
|
-
error: message
|
|
421
|
+
//#region src/utils/install-skill.ts
|
|
422
|
+
const SKILL_NAME = "react-grab";
|
|
423
|
+
const SKILL_SOURCE = fileURLToPath(new URL("../skills/react-grab", import.meta.url));
|
|
424
|
+
const agentLabel = (agent) => getSkillAgentConfig(agent).displayName;
|
|
425
|
+
const installedSkillDir = (agent) => join(isUniversalSkillAgent(agent) ? getCanonicalSkillsDir(true) : getSkillAgentDir(agent, { global: true }), SKILL_NAME);
|
|
426
|
+
const promptSkillInstall = async ({ yes = false } = {}) => {
|
|
427
|
+
const detectedAgents = await detectAvailableAgents();
|
|
428
|
+
if (detectedAgents.length === 0) {
|
|
429
|
+
logger.warn("No supported agents detected.");
|
|
430
|
+
return false;
|
|
431
|
+
}
|
|
432
|
+
let selectedAgents = detectedAgents;
|
|
433
|
+
if (!yes) {
|
|
434
|
+
const { agents } = await prompts({
|
|
435
|
+
type: "multiselect",
|
|
436
|
+
name: "agents",
|
|
437
|
+
message: "Install the React Grab skill for:",
|
|
438
|
+
choices: detectedAgents.map((agent) => ({
|
|
439
|
+
title: agentLabel(agent),
|
|
440
|
+
value: agent,
|
|
441
|
+
selected: true
|
|
442
|
+
})),
|
|
443
|
+
instructions: false,
|
|
444
|
+
min: 1
|
|
532
445
|
});
|
|
446
|
+
selectedAgents = agents ?? [];
|
|
447
|
+
if (selectedAgents.length === 0) return false;
|
|
533
448
|
}
|
|
534
|
-
const
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
449
|
+
const installSpinner = spinner("Installing React Grab skill.").start();
|
|
450
|
+
const { installed, failed } = await add({
|
|
451
|
+
source: SKILL_SOURCE,
|
|
452
|
+
agents: selectedAgents,
|
|
453
|
+
global: true,
|
|
454
|
+
mode: "copy"
|
|
455
|
+
});
|
|
456
|
+
if (installed.length === 0) {
|
|
457
|
+
installSpinner.fail("Failed to install React Grab skill.");
|
|
458
|
+
return false;
|
|
459
|
+
}
|
|
460
|
+
installSpinner.succeed(`Installed React Grab skill for ${installed.map((record) => agentLabel(record.agent)).join(", ")}.`);
|
|
461
|
+
for (const record of failed) logger.log(` ${highlighter.error("✗")} ${agentLabel(record.agent)} ${record.error}`);
|
|
462
|
+
return true;
|
|
540
463
|
};
|
|
541
|
-
const
|
|
542
|
-
const
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
choices: getMcpClientNames().map((name) => ({
|
|
547
|
-
title: name,
|
|
548
|
-
value: name,
|
|
549
|
-
selected: true
|
|
550
|
-
}))
|
|
464
|
+
const removeSkill = async () => {
|
|
465
|
+
const agentsWithSkill = (await detectAvailableAgents()).filter((agent) => existsSync(installedSkillDir(agent)));
|
|
466
|
+
for (const skillDir of new Set(agentsWithSkill.map(installedSkillDir))) rmSync(skillDir, {
|
|
467
|
+
recursive: true,
|
|
468
|
+
force: true
|
|
551
469
|
});
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
return installMcpServers(selectedAgents).some((result) => result.success);
|
|
470
|
+
for (const agent of agentsWithSkill) logger.log(` ${highlighter.success("✓")} ${agentLabel(agent)}`);
|
|
471
|
+
return agentsWithSkill.length;
|
|
555
472
|
};
|
|
556
473
|
//#endregion
|
|
557
474
|
//#region src/commands/add.ts
|
|
558
|
-
const VERSION$5 = "0.1.
|
|
559
|
-
const add = new Command().name("add").alias("install").description("
|
|
475
|
+
const VERSION$5 = "0.1.39";
|
|
476
|
+
const add$1 = new Command().name("add").alias("install").description("install the React Grab skill for your agent").option("-y, --yes", "skip confirmation prompts", false).option("-c, --cwd <cwd>", "working directory (defaults to current directory)", process.cwd()).action(async (opts) => {
|
|
560
477
|
console.log(`${pc.magenta("✿")} ${pc.bold("React Grab")} ${pc.gray(VERSION$5)}`);
|
|
561
478
|
console.log();
|
|
562
479
|
try {
|
|
563
|
-
const cwd = opts.cwd;
|
|
564
480
|
const isNonInteractive = detectNonInteractive(opts.yes);
|
|
565
481
|
const preflightSpinner = spinner("Preflight checks.").start();
|
|
566
|
-
if (!(await detectProject(cwd)).hasReactGrab) {
|
|
482
|
+
if (!(await detectProject(opts.cwd)).hasReactGrab) {
|
|
567
483
|
preflightSpinner.fail("React Grab is not installed.");
|
|
568
484
|
logger.break();
|
|
569
485
|
logger.error(`Run ${highlighter.info("react-grab init")} first to install React Grab.`);
|
|
@@ -571,39 +487,12 @@ const add = new Command().name("add").alias("install").description("connect Reac
|
|
|
571
487
|
process.exit(1);
|
|
572
488
|
}
|
|
573
489
|
preflightSpinner.succeed();
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
logger.warn(`Legacy agent packages are deprecated. Use ${highlighter.info("mcp")} instead.`);
|
|
577
|
-
logger.log(`Run ${highlighter.info("grab add mcp")} to install the MCP server.`);
|
|
490
|
+
logger.break();
|
|
491
|
+
if (!await promptSkillInstall({ yes: isNonInteractive }) && isNonInteractive) {
|
|
578
492
|
logger.break();
|
|
579
493
|
process.exit(1);
|
|
580
494
|
}
|
|
581
|
-
|
|
582
|
-
if (isNonInteractive) {
|
|
583
|
-
if (!installMcpServers().some((result) => result.success)) {
|
|
584
|
-
logger.break();
|
|
585
|
-
logger.error("Failed to install MCP server.");
|
|
586
|
-
logger.break();
|
|
587
|
-
process.exit(1);
|
|
588
|
-
}
|
|
589
|
-
} else if (!await promptMcpInstall()) {
|
|
590
|
-
logger.break();
|
|
591
|
-
process.exit(0);
|
|
592
|
-
}
|
|
593
|
-
logger.break();
|
|
594
|
-
logger.log(`${highlighter.success("Success!")} MCP server has been configured.`);
|
|
595
|
-
logger.log("Restart your agents to activate.");
|
|
596
|
-
logger.break();
|
|
597
|
-
} else {
|
|
598
|
-
if (!await promptMcpInstall()) {
|
|
599
|
-
logger.break();
|
|
600
|
-
process.exit(0);
|
|
601
|
-
}
|
|
602
|
-
logger.break();
|
|
603
|
-
logger.log(`${highlighter.success("Success!")} MCP server has been configured.`);
|
|
604
|
-
logger.log("Restart your agents to activate.");
|
|
605
|
-
logger.break();
|
|
606
|
-
}
|
|
495
|
+
logger.break();
|
|
607
496
|
} catch (error) {
|
|
608
497
|
handleError(error);
|
|
609
498
|
}
|
|
@@ -986,6 +875,15 @@ const transformTanStack = (projectRoot, reactGrabAlreadyConfigured, force = fals
|
|
|
986
875
|
newContent
|
|
987
876
|
};
|
|
988
877
|
};
|
|
878
|
+
const hasFrameworkEntryPoint = (projectRoot, framework, nextRouterType) => {
|
|
879
|
+
switch (framework) {
|
|
880
|
+
case "next": return nextRouterType === "app" ? findLayoutFile(projectRoot) !== null : findDocumentFile(projectRoot) !== null;
|
|
881
|
+
case "vite":
|
|
882
|
+
case "webpack": return findEntryFile(projectRoot) !== null;
|
|
883
|
+
case "tanstack": return findTanStackRootFile(projectRoot) !== null;
|
|
884
|
+
default: return false;
|
|
885
|
+
}
|
|
886
|
+
};
|
|
989
887
|
const previewTransform = (projectRoot, framework, nextRouterType, reactGrabAlreadyConfigured = false, force = false) => {
|
|
990
888
|
switch (framework) {
|
|
991
889
|
case "next":
|
|
@@ -1185,7 +1083,7 @@ const formatActivationKeyDisplay = (activationKey) => {
|
|
|
1185
1083
|
};
|
|
1186
1084
|
//#endregion
|
|
1187
1085
|
//#region src/commands/configure.ts
|
|
1188
|
-
const VERSION$4 = "0.1.
|
|
1086
|
+
const VERSION$4 = "0.1.39";
|
|
1189
1087
|
const isMac = process.platform === "darwin";
|
|
1190
1088
|
const META_LABEL = isMac ? "Cmd" : "Win";
|
|
1191
1089
|
const ALT_LABEL = isMac ? "Option" : "Alt";
|
|
@@ -1804,7 +1702,7 @@ const installPackagesWithFeedback = async (packages, packageManager, projectRoot
|
|
|
1804
1702
|
};
|
|
1805
1703
|
//#endregion
|
|
1806
1704
|
//#region src/commands/init.ts
|
|
1807
|
-
const VERSION$3 = "0.1.
|
|
1705
|
+
const VERSION$3 = "0.1.39";
|
|
1808
1706
|
const REPORT_URL = "https://react-grab.com/api/report-cli";
|
|
1809
1707
|
const DOCS_URL = "https://github.com/aidenybai/react-grab";
|
|
1810
1708
|
const reportToCli = (type, config, error) => {
|
|
@@ -1862,6 +1760,15 @@ const printSubprojects = (searchRoot, sortedProjects) => {
|
|
|
1862
1760
|
logger.log(` ${highlighter.dim("$")} npx grab@latest init -c ${relative(searchRoot, sortedProjects[0].path)}`);
|
|
1863
1761
|
logger.break();
|
|
1864
1762
|
};
|
|
1763
|
+
const SUPPORTED_FRAMEWORKS_LINE = "React Grab supports Next.js, Vite, TanStack Start, and Webpack projects.";
|
|
1764
|
+
const failWithManualSetup = (failingSpinner, message, { listSupportedFrameworks = false } = {}) => {
|
|
1765
|
+
failingSpinner.fail(message);
|
|
1766
|
+
logger.break();
|
|
1767
|
+
if (listSupportedFrameworks) logger.log(SUPPORTED_FRAMEWORKS_LINE);
|
|
1768
|
+
logger.log(`Visit ${highlighter.info(DOCS_URL)} for manual setup.`);
|
|
1769
|
+
logger.break();
|
|
1770
|
+
process.exit(1);
|
|
1771
|
+
};
|
|
1865
1772
|
const init = new 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) => {
|
|
1866
1773
|
console.log(`${pc.magenta("✿")} ${pc.bold("React Grab")} ${pc.gray(VERSION$3)}`);
|
|
1867
1774
|
console.log();
|
|
@@ -2020,25 +1927,7 @@ const init = new Command().name("init").alias("setup").description("initialize R
|
|
|
2020
1927
|
}
|
|
2021
1928
|
}
|
|
2022
1929
|
logger.break();
|
|
2023
|
-
|
|
2024
|
-
type: "confirm",
|
|
2025
|
-
name: "wantAddMcp",
|
|
2026
|
-
message: `Would you like to ${highlighter.info("connect it to your agent via MCP")}?`,
|
|
2027
|
-
initial: false
|
|
2028
|
-
});
|
|
2029
|
-
if (wantAddMcp === void 0) {
|
|
2030
|
-
logger.break();
|
|
2031
|
-
process.exit(1);
|
|
2032
|
-
}
|
|
2033
|
-
if (wantAddMcp) {
|
|
2034
|
-
if (!await promptMcpInstall()) {
|
|
2035
|
-
logger.break();
|
|
2036
|
-
process.exit(0);
|
|
2037
|
-
}
|
|
2038
|
-
logger.break();
|
|
2039
|
-
logger.success("MCP server has been configured.");
|
|
2040
|
-
logger.log("Restart your agents to activate.");
|
|
2041
|
-
}
|
|
1930
|
+
await promptSkillInstall({ yes: isNonInteractive });
|
|
2042
1931
|
logger.break();
|
|
2043
1932
|
process.exit(0);
|
|
2044
1933
|
}
|
|
@@ -2053,7 +1942,7 @@ const init = new Command().name("init").alias("setup").description("initialize R
|
|
|
2053
1942
|
logger.break();
|
|
2054
1943
|
process.exit(1);
|
|
2055
1944
|
}
|
|
2056
|
-
if (projectInfo.framework === "unknown") {
|
|
1945
|
+
if (projectInfo.framework === "unknown" || projectInfo.isMonorepo && !hasFrameworkEntryPoint(projectInfo.projectRoot, projectInfo.framework, projectInfo.nextRouterType)) {
|
|
2057
1946
|
let searchRoot = cwd;
|
|
2058
1947
|
let reactProjects = findReactProjects(searchRoot);
|
|
2059
1948
|
if (reactProjects.length === 0 && cwd !== process.cwd()) {
|
|
@@ -2090,45 +1979,21 @@ const init = new Command().name("init").alias("setup").description("initialize R
|
|
|
2090
1979
|
process.chdir(selectedProject);
|
|
2091
1980
|
const newProjectInfo = await detectProject(selectedProject);
|
|
2092
1981
|
Object.assign(projectInfo, newProjectInfo);
|
|
2093
|
-
spinner("Verifying framework.").start()
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
logger.log(`Visit ${highlighter.info(DOCS_URL)} for manual setup.`);
|
|
2099
|
-
logger.break();
|
|
2100
|
-
process.exit(1);
|
|
2101
|
-
}
|
|
1982
|
+
const newFrameworkSpinner = spinner("Verifying framework.").start();
|
|
1983
|
+
if (newProjectInfo.framework === "unknown") failWithManualSetup(newFrameworkSpinner, "Could not detect a supported framework in this project.", { listSupportedFrameworks: true });
|
|
1984
|
+
newFrameworkSpinner.succeed(`Verifying framework. Found ${highlighter.info(FRAMEWORK_NAMES[newProjectInfo.framework])}.`);
|
|
1985
|
+
} else if (projectInfo.framework !== "unknown") failWithManualSetup(frameworkSpinner, `Verifying framework. Found ${highlighter.info(FRAMEWORK_NAMES[projectInfo.framework])}, but could not find an entry file.`);
|
|
1986
|
+
else failWithManualSetup(frameworkSpinner, "Could not detect a supported framework.", { listSupportedFrameworks: true });
|
|
2102
1987
|
} else frameworkSpinner.succeed(`Verifying framework. Found ${highlighter.info(FRAMEWORK_NAMES[projectInfo.framework])}.`);
|
|
2103
1988
|
if (projectInfo.framework === "next") spinner("Detecting router type.").start().succeed(`Detecting router type. Found ${highlighter.info(projectInfo.nextRouterType === "app" ? "App Router" : "Pages Router")}.`);
|
|
2104
1989
|
spinner("Detecting package manager.").start().succeed(`Detecting package manager. Found ${highlighter.info(PACKAGE_MANAGER_NAMES[projectInfo.packageManager])}.`);
|
|
2105
1990
|
const finalFramework = projectInfo.framework;
|
|
2106
1991
|
const finalPackageManager = projectInfo.packageManager;
|
|
2107
1992
|
const finalNextRouterType = projectInfo.nextRouterType;
|
|
2108
|
-
let
|
|
1993
|
+
let didInstallSkill = false;
|
|
2109
1994
|
if (!isNonInteractive) {
|
|
2110
1995
|
logger.break();
|
|
2111
|
-
|
|
2112
|
-
type: "confirm",
|
|
2113
|
-
name: "wantAddMcp",
|
|
2114
|
-
message: `Would you like to ${highlighter.info("connect it to your agent via MCP")}?`,
|
|
2115
|
-
initial: false
|
|
2116
|
-
});
|
|
2117
|
-
if (wantAddMcp === void 0) {
|
|
2118
|
-
logger.break();
|
|
2119
|
-
process.exit(1);
|
|
2120
|
-
}
|
|
2121
|
-
if (wantAddMcp) {
|
|
2122
|
-
didInstallMcp = Boolean(await promptMcpInstall());
|
|
2123
|
-
if (!didInstallMcp) {
|
|
2124
|
-
logger.break();
|
|
2125
|
-
process.exit(0);
|
|
2126
|
-
}
|
|
2127
|
-
logger.break();
|
|
2128
|
-
logger.success("MCP server has been configured.");
|
|
2129
|
-
logger.log("Continuing with React Grab installation...");
|
|
2130
|
-
logger.break();
|
|
2131
|
-
}
|
|
1996
|
+
didInstallSkill = await promptSkillInstall({ yes: isNonInteractive });
|
|
2132
1997
|
}
|
|
2133
1998
|
const result = previewTransform(projectInfo.projectRoot, finalFramework, finalNextRouterType, false, opts.force);
|
|
2134
1999
|
if (!result.success) {
|
|
@@ -2172,7 +2037,7 @@ const init = new Command().name("init").alias("setup").description("initialize R
|
|
|
2172
2037
|
framework: finalFramework,
|
|
2173
2038
|
packageManager: finalPackageManager,
|
|
2174
2039
|
router: finalNextRouterType,
|
|
2175
|
-
agent:
|
|
2040
|
+
agent: didInstallSkill ? "skill" : void 0,
|
|
2176
2041
|
isMonorepo: projectInfo.isMonorepo
|
|
2177
2042
|
});
|
|
2178
2043
|
} catch (error) {
|
|
@@ -2182,29 +2047,16 @@ const init = new Command().name("init").alias("setup").description("initialize R
|
|
|
2182
2047
|
});
|
|
2183
2048
|
//#endregion
|
|
2184
2049
|
//#region src/commands/remove.ts
|
|
2185
|
-
const VERSION$2 = "0.1.
|
|
2186
|
-
const remove = new Command().name("remove").description("
|
|
2050
|
+
const VERSION$2 = "0.1.39";
|
|
2051
|
+
const remove = new Command().name("remove").description("uninstall the React Grab skill from your agent").action(async () => {
|
|
2187
2052
|
console.log(`${pc.magenta("✿")} ${pc.bold("React Grab")} ${pc.gray(VERSION$2)}`);
|
|
2188
2053
|
console.log();
|
|
2189
2054
|
try {
|
|
2190
|
-
const cwd = opts.cwd;
|
|
2191
|
-
const preflightSpinner = spinner("Preflight checks.").start();
|
|
2192
|
-
if (!(await detectProject(cwd)).hasReactGrab) {
|
|
2193
|
-
preflightSpinner.fail("React Grab is not installed.");
|
|
2194
|
-
logger.break();
|
|
2195
|
-
logger.error(`Run ${highlighter.info("react-grab init")} first to install React Grab.`);
|
|
2196
|
-
logger.break();
|
|
2197
|
-
process.exit(1);
|
|
2198
|
-
}
|
|
2199
|
-
preflightSpinner.succeed();
|
|
2200
|
-
if (agentArg && agentArg !== "mcp") {
|
|
2201
|
-
logger.break();
|
|
2202
|
-
logger.warn(`Legacy agent packages are deprecated. Uninstall ${highlighter.info(`@react-grab/${agentArg}`)} manually with your package manager.`);
|
|
2203
|
-
logger.break();
|
|
2204
|
-
process.exit(0);
|
|
2205
|
-
}
|
|
2206
2055
|
logger.break();
|
|
2207
|
-
|
|
2056
|
+
const removedCount = await removeSkill();
|
|
2057
|
+
logger.break();
|
|
2058
|
+
if (removedCount === 0) logger.log("React Grab skill is not installed in any detected agent.");
|
|
2059
|
+
else logger.log(`${highlighter.success("Removed")} the React Grab skill from ${removedCount} agent${removedCount === 1 ? "" : "s"}.`);
|
|
2208
2060
|
logger.break();
|
|
2209
2061
|
} catch (error) {
|
|
2210
2062
|
handleError(error);
|
|
@@ -2212,7 +2064,7 @@ const remove = new Command().name("remove").description("disconnect React Grab f
|
|
|
2212
2064
|
});
|
|
2213
2065
|
//#endregion
|
|
2214
2066
|
//#region src/commands/upgrade.ts
|
|
2215
|
-
const VERSION$1 = "0.1.
|
|
2067
|
+
const VERSION$1 = "0.1.39";
|
|
2216
2068
|
const NPM_REGISTRY_URL = "https://registry.npmjs.org/react-grab/latest";
|
|
2217
2069
|
const fetchLatestVersion = async () => {
|
|
2218
2070
|
try {
|
|
@@ -2286,8 +2138,368 @@ const upgrade = new Command().name("upgrade").alias("update").description("upgra
|
|
|
2286
2138
|
}
|
|
2287
2139
|
});
|
|
2288
2140
|
//#endregion
|
|
2141
|
+
//#region src/utils/clipboard.ts
|
|
2142
|
+
const READ_TIMEOUT_MS = 2500;
|
|
2143
|
+
const MAX_CLIPBOARD_BYTES = 64 * 1024 * 1024;
|
|
2144
|
+
const ID_RADIX = 36;
|
|
2145
|
+
const HASH_LENGTH = 12;
|
|
2146
|
+
const PICKLE_HEADER_BYTES = 4;
|
|
2147
|
+
const PICKLE_ALIGN_BYTES = 4;
|
|
2148
|
+
const SIGNATURE_SCAN_CHARS = 32 * 1024;
|
|
2149
|
+
const GRAB_MIME = "application/x-react-grab";
|
|
2150
|
+
const CHROMIUM_CUSTOM_FORMAT = "chromium/x-web-custom-data";
|
|
2151
|
+
const GRAB_TEXT_SIGNATURE = /\bin\s+\S+\s+\(at\s+[^\n]{1,400}?:\d+:\d+\)/;
|
|
2152
|
+
const sleep = (durationMs) => new Promise((resolve) => setTimeout(resolve, durationMs));
|
|
2153
|
+
const shortHash = (text) => createHash("sha1").update(text).digest("hex").slice(0, HASH_LENGTH);
|
|
2154
|
+
const alignUp = (value) => value + PICKLE_ALIGN_BYTES - 1 & ~(PICKLE_ALIGN_BYTES - 1);
|
|
2155
|
+
const parseChromiumPickle = (buffer) => {
|
|
2156
|
+
const formats = {};
|
|
2157
|
+
if (!buffer || buffer.length < PICKLE_HEADER_BYTES + 4) return formats;
|
|
2158
|
+
let offset = PICKLE_HEADER_BYTES;
|
|
2159
|
+
const pairCount = buffer.readUInt32LE(offset);
|
|
2160
|
+
offset += 4;
|
|
2161
|
+
for (let pairIndex = 0; pairIndex < pairCount; pairIndex += 1) {
|
|
2162
|
+
if (offset + 4 > buffer.length) break;
|
|
2163
|
+
const formatCodeUnits = buffer.readUInt32LE(offset);
|
|
2164
|
+
offset += 4;
|
|
2165
|
+
if (offset + formatCodeUnits * 2 > buffer.length) break;
|
|
2166
|
+
const format = buffer.toString("utf16le", offset, offset + formatCodeUnits * 2);
|
|
2167
|
+
offset = alignUp(offset + formatCodeUnits * 2);
|
|
2168
|
+
if (offset + 4 > buffer.length) break;
|
|
2169
|
+
const dataCodeUnits = buffer.readUInt32LE(offset);
|
|
2170
|
+
offset += 4;
|
|
2171
|
+
if (offset + dataCodeUnits * 2 > buffer.length) break;
|
|
2172
|
+
const value = buffer.toString("utf16le", offset, offset + dataCodeUnits * 2);
|
|
2173
|
+
offset = alignUp(offset + dataCodeUnits * 2);
|
|
2174
|
+
formats[format] = value;
|
|
2175
|
+
}
|
|
2176
|
+
return formats;
|
|
2177
|
+
};
|
|
2178
|
+
const extractGrab = (raw) => {
|
|
2179
|
+
if (raw.grab) return raw.grab;
|
|
2180
|
+
if (raw.pickleBase64) return parseChromiumPickle(Buffer.from(raw.pickleBase64, "base64"))[GRAB_MIME];
|
|
2181
|
+
};
|
|
2182
|
+
const isGrabText = (text) => GRAB_TEXT_SIGNATURE.test(text.length > SIGNATURE_SCAN_CHARS ? text.slice(0, SIGNATURE_SCAN_CHARS) : text);
|
|
2183
|
+
const extractPrompt = (record) => {
|
|
2184
|
+
const comments = (Array.isArray(record.entries) ? record.entries : []).map((entry) => entry?.commentText?.trim?.()).filter(Boolean);
|
|
2185
|
+
if (comments.length > 0) return comments.join("\n");
|
|
2186
|
+
const lines = (typeof record.content === "string" ? record.content : "").split("\n");
|
|
2187
|
+
const firstReferenceLine = lines.findIndex((line) => line.startsWith("["));
|
|
2188
|
+
if (firstReferenceLine <= 0) return void 0;
|
|
2189
|
+
return lines.slice(0, firstReferenceLine).join("\n").trim() || void 0;
|
|
2190
|
+
};
|
|
2191
|
+
const hasCommand = (name) => {
|
|
2192
|
+
return spawnSync(process.platform === "win32" ? "where" : "which", [name], { stdio: "ignore" }).status === 0;
|
|
2193
|
+
};
|
|
2194
|
+
const runText = (command, args) => {
|
|
2195
|
+
const output = spawnSync(command, args, {
|
|
2196
|
+
encoding: "utf8",
|
|
2197
|
+
maxBuffer: MAX_CLIPBOARD_BYTES,
|
|
2198
|
+
timeout: READ_TIMEOUT_MS
|
|
2199
|
+
});
|
|
2200
|
+
return output.status === 0 ? output.stdout : null;
|
|
2201
|
+
};
|
|
2202
|
+
const runBuffer = (command, args) => {
|
|
2203
|
+
const output = spawnSync(command, args, {
|
|
2204
|
+
maxBuffer: MAX_CLIPBOARD_BYTES,
|
|
2205
|
+
timeout: READ_TIMEOUT_MS
|
|
2206
|
+
});
|
|
2207
|
+
return output.status === 0 && output.stdout?.length ? output.stdout : null;
|
|
2208
|
+
};
|
|
2209
|
+
const runJson = (command, args) => {
|
|
2210
|
+
const output = spawnSync(command, args, {
|
|
2211
|
+
encoding: "utf8",
|
|
2212
|
+
maxBuffer: MAX_CLIPBOARD_BYTES,
|
|
2213
|
+
timeout: READ_TIMEOUT_MS
|
|
2214
|
+
});
|
|
2215
|
+
if (output.status !== 0 || !output.stdout) return null;
|
|
2216
|
+
try {
|
|
2217
|
+
return JSON.parse(output.stdout);
|
|
2218
|
+
} catch {
|
|
2219
|
+
return null;
|
|
2220
|
+
}
|
|
2221
|
+
};
|
|
2222
|
+
const compileSwiftReader = (readersDir, workDir) => {
|
|
2223
|
+
if (!hasCommand("swiftc")) return null;
|
|
2224
|
+
const source = path.join(readersDir, "read-clipboard.swift");
|
|
2225
|
+
if (!fs.existsSync(source)) return null;
|
|
2226
|
+
const binary = path.join(workDir, "pbread");
|
|
2227
|
+
if ((!fs.existsSync(binary) || fs.statSync(source).mtimeMs > fs.statSync(binary).mtimeMs) && spawnSync("swiftc", [
|
|
2228
|
+
"-O",
|
|
2229
|
+
source,
|
|
2230
|
+
"-o",
|
|
2231
|
+
binary
|
|
2232
|
+
]).status !== 0) return null;
|
|
2233
|
+
return binary;
|
|
2234
|
+
};
|
|
2235
|
+
const createDarwinReader = (options) => {
|
|
2236
|
+
const binary = options.textOnly ? null : compileSwiftReader(options.readersDir, options.workDir);
|
|
2237
|
+
if (binary) return {
|
|
2238
|
+
mode: "darwin-native",
|
|
2239
|
+
read: () => {
|
|
2240
|
+
const raw = runJson(binary, []);
|
|
2241
|
+
if (!raw) return null;
|
|
2242
|
+
return {
|
|
2243
|
+
changeCount: raw.changeCount ?? null,
|
|
2244
|
+
text: raw.text,
|
|
2245
|
+
grab: extractGrab(raw)
|
|
2246
|
+
};
|
|
2247
|
+
}
|
|
2248
|
+
};
|
|
2249
|
+
if (!hasCommand("pbpaste")) return null;
|
|
2250
|
+
return {
|
|
2251
|
+
mode: "darwin-text",
|
|
2252
|
+
read: () => {
|
|
2253
|
+
const text = runText("pbpaste", []);
|
|
2254
|
+
return text == null ? null : {
|
|
2255
|
+
changeCount: null,
|
|
2256
|
+
text,
|
|
2257
|
+
grab: void 0
|
|
2258
|
+
};
|
|
2259
|
+
}
|
|
2260
|
+
};
|
|
2261
|
+
};
|
|
2262
|
+
const detectLinuxTool = () => {
|
|
2263
|
+
if (Boolean(process.env.WAYLAND_DISPLAY) && hasCommand("wl-paste") || hasCommand("wl-paste") && !hasCommand("xclip")) return {
|
|
2264
|
+
name: "wl-paste",
|
|
2265
|
+
readText: () => runText("wl-paste", [
|
|
2266
|
+
"-n",
|
|
2267
|
+
"-t",
|
|
2268
|
+
"text/plain"
|
|
2269
|
+
]) ?? runText("wl-paste", ["-n"]),
|
|
2270
|
+
readCustom: () => runBuffer("wl-paste", [
|
|
2271
|
+
"-n",
|
|
2272
|
+
"-t",
|
|
2273
|
+
CHROMIUM_CUSTOM_FORMAT
|
|
2274
|
+
])
|
|
2275
|
+
};
|
|
2276
|
+
if (hasCommand("xclip")) return {
|
|
2277
|
+
name: "xclip",
|
|
2278
|
+
readText: () => runText("xclip", [
|
|
2279
|
+
"-selection",
|
|
2280
|
+
"clipboard",
|
|
2281
|
+
"-o"
|
|
2282
|
+
]),
|
|
2283
|
+
readCustom: () => runBuffer("xclip", [
|
|
2284
|
+
"-selection",
|
|
2285
|
+
"clipboard",
|
|
2286
|
+
"-t",
|
|
2287
|
+
CHROMIUM_CUSTOM_FORMAT,
|
|
2288
|
+
"-o"
|
|
2289
|
+
])
|
|
2290
|
+
};
|
|
2291
|
+
if (hasCommand("xsel")) return {
|
|
2292
|
+
name: "xsel",
|
|
2293
|
+
readText: () => runText("xsel", ["--clipboard", "--output"]),
|
|
2294
|
+
readCustom: null
|
|
2295
|
+
};
|
|
2296
|
+
return null;
|
|
2297
|
+
};
|
|
2298
|
+
const createLinuxReader = (options) => {
|
|
2299
|
+
const tool = detectLinuxTool();
|
|
2300
|
+
if (!tool) return null;
|
|
2301
|
+
const useCustom = !options.textOnly && Boolean(tool.readCustom);
|
|
2302
|
+
return {
|
|
2303
|
+
mode: useCustom ? `linux-${tool.name}` : `linux-${tool.name}-text`,
|
|
2304
|
+
read: () => {
|
|
2305
|
+
const text = tool.readText();
|
|
2306
|
+
if (text == null) return null;
|
|
2307
|
+
return {
|
|
2308
|
+
changeCount: null,
|
|
2309
|
+
text,
|
|
2310
|
+
grab: useCustom && tool.readCustom ? parseChromiumPickle(tool.readCustom())[GRAB_MIME] : void 0
|
|
2311
|
+
};
|
|
2312
|
+
}
|
|
2313
|
+
};
|
|
2314
|
+
};
|
|
2315
|
+
const detectPowershell = () => hasCommand("pwsh") ? "pwsh" : hasCommand("powershell") ? "powershell" : null;
|
|
2316
|
+
const createWindowsReader = (options) => {
|
|
2317
|
+
const shell = detectPowershell();
|
|
2318
|
+
if (!shell) return null;
|
|
2319
|
+
const scriptPath = path.join(options.readersDir, "read-clipboard.ps1");
|
|
2320
|
+
if (options.textOnly || !fs.existsSync(scriptPath)) return {
|
|
2321
|
+
mode: "win-text",
|
|
2322
|
+
read: () => {
|
|
2323
|
+
const text = runText(shell, [
|
|
2324
|
+
"-NoProfile",
|
|
2325
|
+
"-Command",
|
|
2326
|
+
"Get-Clipboard -Raw"
|
|
2327
|
+
]);
|
|
2328
|
+
return text == null ? null : {
|
|
2329
|
+
changeCount: null,
|
|
2330
|
+
text,
|
|
2331
|
+
grab: void 0
|
|
2332
|
+
};
|
|
2333
|
+
}
|
|
2334
|
+
};
|
|
2335
|
+
const args = [
|
|
2336
|
+
"-NoProfile",
|
|
2337
|
+
"-ExecutionPolicy",
|
|
2338
|
+
"Bypass",
|
|
2339
|
+
"-File",
|
|
2340
|
+
scriptPath
|
|
2341
|
+
];
|
|
2342
|
+
spawnSync(shell, args, {
|
|
2343
|
+
stdio: "ignore",
|
|
2344
|
+
maxBuffer: MAX_CLIPBOARD_BYTES
|
|
2345
|
+
});
|
|
2346
|
+
return {
|
|
2347
|
+
mode: "win-native",
|
|
2348
|
+
read: () => {
|
|
2349
|
+
const raw = runJson(shell, args);
|
|
2350
|
+
if (!raw) return null;
|
|
2351
|
+
return {
|
|
2352
|
+
changeCount: raw.changeCount ?? null,
|
|
2353
|
+
text: raw.text ?? void 0,
|
|
2354
|
+
grab: extractGrab(raw)
|
|
2355
|
+
};
|
|
2356
|
+
}
|
|
2357
|
+
};
|
|
2358
|
+
};
|
|
2359
|
+
const createReader = (options) => {
|
|
2360
|
+
if (process.platform === "darwin") return createDarwinReader(options);
|
|
2361
|
+
if (process.platform === "linux") return createLinuxReader(options);
|
|
2362
|
+
if (process.platform === "win32") return createWindowsReader(options);
|
|
2363
|
+
return null;
|
|
2364
|
+
};
|
|
2365
|
+
const ensureSafeDir = (dir) => {
|
|
2366
|
+
let stats;
|
|
2367
|
+
try {
|
|
2368
|
+
stats = fs.lstatSync(dir);
|
|
2369
|
+
} catch {
|
|
2370
|
+
return;
|
|
2371
|
+
}
|
|
2372
|
+
if (stats.isSymbolicLink()) throw new Error(`Refusing to use ${dir}: it is a symlink.`);
|
|
2373
|
+
if (process.getuid && stats.uid !== process.getuid()) throw new Error(`Refusing to use ${dir}: owned by another user.`);
|
|
2374
|
+
};
|
|
2375
|
+
const prepareWorkDir = (dir) => {
|
|
2376
|
+
ensureSafeDir(dir);
|
|
2377
|
+
fs.mkdirSync(dir, {
|
|
2378
|
+
recursive: true,
|
|
2379
|
+
mode: 448
|
|
2380
|
+
});
|
|
2381
|
+
const gitignore = path.join(dir, ".gitignore");
|
|
2382
|
+
if (!fs.existsSync(gitignore)) fs.writeFileSync(gitignore, "*\n");
|
|
2383
|
+
};
|
|
2384
|
+
const watchForNextGrab = async (options) => {
|
|
2385
|
+
const { reader, dir, intervalMs, replayLast, onWarn, signal } = options;
|
|
2386
|
+
const logPath = path.join(dir, "history.jsonl");
|
|
2387
|
+
const { read } = reader;
|
|
2388
|
+
let lastChangeCount = null;
|
|
2389
|
+
let lastTimestamp = 0;
|
|
2390
|
+
let lastTextHash = "";
|
|
2391
|
+
let lastErrorMessage = "";
|
|
2392
|
+
let sequence = 0;
|
|
2393
|
+
const initial = read();
|
|
2394
|
+
if (initial && !replayLast) {
|
|
2395
|
+
lastChangeCount = initial.changeCount;
|
|
2396
|
+
if (initial.text) lastTextHash = shortHash(initial.text);
|
|
2397
|
+
if (initial.grab) try {
|
|
2398
|
+
lastTimestamp = JSON.parse(initial.grab).timestamp ?? 0;
|
|
2399
|
+
} catch {}
|
|
2400
|
+
}
|
|
2401
|
+
while (!signal?.aborted) {
|
|
2402
|
+
await sleep(intervalMs);
|
|
2403
|
+
if (signal?.aborted) return null;
|
|
2404
|
+
try {
|
|
2405
|
+
const snapshot = read();
|
|
2406
|
+
if (!snapshot) continue;
|
|
2407
|
+
if (snapshot.changeCount !== null && snapshot.changeCount === lastChangeCount) continue;
|
|
2408
|
+
const textHash = snapshot.text ? shortHash(snapshot.text) : "";
|
|
2409
|
+
const didTextChange = textHash !== lastTextHash;
|
|
2410
|
+
let record = null;
|
|
2411
|
+
let nextTimestamp = lastTimestamp;
|
|
2412
|
+
if (snapshot.grab) {
|
|
2413
|
+
let parsed = null;
|
|
2414
|
+
try {
|
|
2415
|
+
parsed = JSON.parse(snapshot.grab);
|
|
2416
|
+
} catch {}
|
|
2417
|
+
if (parsed && typeof parsed.timestamp === "number" && parsed.timestamp > lastTimestamp) {
|
|
2418
|
+
nextTimestamp = parsed.timestamp;
|
|
2419
|
+
record = {
|
|
2420
|
+
source: "custom",
|
|
2421
|
+
timestamp: parsed.timestamp,
|
|
2422
|
+
version: typeof parsed.version === "string" ? parsed.version : void 0,
|
|
2423
|
+
content: typeof parsed.content === "string" ? parsed.content : "",
|
|
2424
|
+
entries: Array.isArray(parsed.entries) ? parsed.entries : []
|
|
2425
|
+
};
|
|
2426
|
+
}
|
|
2427
|
+
} else if (didTextChange && snapshot.text && isGrabText(snapshot.text)) record = {
|
|
2428
|
+
source: "text",
|
|
2429
|
+
timestamp: Date.now(),
|
|
2430
|
+
content: snapshot.text,
|
|
2431
|
+
entries: []
|
|
2432
|
+
};
|
|
2433
|
+
if (!record) {
|
|
2434
|
+
lastChangeCount = snapshot.changeCount;
|
|
2435
|
+
lastTextHash = textHash;
|
|
2436
|
+
continue;
|
|
2437
|
+
}
|
|
2438
|
+
const prompt = extractPrompt(record);
|
|
2439
|
+
if (prompt) record.prompt = prompt;
|
|
2440
|
+
const captured = {
|
|
2441
|
+
id: `${record.timestamp}-${(sequence += 1).toString(ID_RADIX)}`,
|
|
2442
|
+
receivedAt: Date.now(),
|
|
2443
|
+
...record
|
|
2444
|
+
};
|
|
2445
|
+
fs.appendFileSync(logPath, `${JSON.stringify(captured)}\n`);
|
|
2446
|
+
lastChangeCount = snapshot.changeCount;
|
|
2447
|
+
lastTextHash = textHash;
|
|
2448
|
+
lastTimestamp = nextTimestamp;
|
|
2449
|
+
return captured;
|
|
2450
|
+
} catch (error) {
|
|
2451
|
+
const message = String(error?.message ?? error);
|
|
2452
|
+
if (message !== lastErrorMessage) {
|
|
2453
|
+
lastErrorMessage = message;
|
|
2454
|
+
onWarn?.(message);
|
|
2455
|
+
}
|
|
2456
|
+
}
|
|
2457
|
+
}
|
|
2458
|
+
return null;
|
|
2459
|
+
};
|
|
2460
|
+
//#endregion
|
|
2461
|
+
//#region src/commands/watch.ts
|
|
2462
|
+
const DEFAULT_INTERVAL_MS = 800;
|
|
2463
|
+
const DEFAULT_DIR = ".react-grab";
|
|
2464
|
+
const readersDir = () => path.dirname(fileURLToPath(import.meta.url));
|
|
2465
|
+
const watch = new 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) => {
|
|
2466
|
+
const dir = path.resolve(options.dir);
|
|
2467
|
+
const intervalMs = Number(options.interval);
|
|
2468
|
+
const textOnly = Boolean(options.textOnly);
|
|
2469
|
+
try {
|
|
2470
|
+
prepareWorkDir(dir);
|
|
2471
|
+
} catch (error) {
|
|
2472
|
+
process.stderr.write(`react-grab watch: ${String(error?.message ?? error)}\n`);
|
|
2473
|
+
process.exit(1);
|
|
2474
|
+
}
|
|
2475
|
+
const reader = createReader({
|
|
2476
|
+
textOnly,
|
|
2477
|
+
readersDir: readersDir(),
|
|
2478
|
+
workDir: dir
|
|
2479
|
+
});
|
|
2480
|
+
if (!reader) {
|
|
2481
|
+
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");
|
|
2482
|
+
process.exit(1);
|
|
2483
|
+
}
|
|
2484
|
+
process.stderr.write(`react-grab watch: watching clipboard via ${reader.mode}; history → ${path.join(dir, "history.jsonl")} (Ctrl+C to stop)\n`);
|
|
2485
|
+
watchForNextGrab({
|
|
2486
|
+
reader,
|
|
2487
|
+
dir,
|
|
2488
|
+
intervalMs: Number.isFinite(intervalMs) && intervalMs > 0 ? intervalMs : DEFAULT_INTERVAL_MS,
|
|
2489
|
+
replayLast: Boolean(options.replayLast),
|
|
2490
|
+
onWarn: (message) => process.stderr.write(`react-grab watch: ${message}\n`)
|
|
2491
|
+
}).then((grab) => {
|
|
2492
|
+
if (!grab) process.exit(0);
|
|
2493
|
+
process.stdout.write(`${JSON.stringify(grab)}\n`);
|
|
2494
|
+
process.exit(0);
|
|
2495
|
+
}).catch((error) => {
|
|
2496
|
+
process.stderr.write(`react-grab watch: ${String(error?.message ?? error)}\n`);
|
|
2497
|
+
process.exit(1);
|
|
2498
|
+
});
|
|
2499
|
+
});
|
|
2500
|
+
//#endregion
|
|
2289
2501
|
//#region src/cli.ts
|
|
2290
|
-
const VERSION = "0.1.
|
|
2502
|
+
const VERSION = "0.1.39";
|
|
2291
2503
|
const VERSION_API_URL = "https://www.react-grab.com/api/version";
|
|
2292
2504
|
process.on("SIGINT", () => process.exit(0));
|
|
2293
2505
|
process.on("SIGTERM", () => process.exit(0));
|
|
@@ -2296,10 +2508,11 @@ try {
|
|
|
2296
2508
|
} catch {}
|
|
2297
2509
|
const program = new Command().name("grab").description("add React Grab to your project").version(VERSION, "-v, --version", "display the version number");
|
|
2298
2510
|
program.addCommand(init);
|
|
2299
|
-
program.addCommand(add);
|
|
2511
|
+
program.addCommand(add$1);
|
|
2300
2512
|
program.addCommand(remove);
|
|
2301
2513
|
program.addCommand(configure);
|
|
2302
2514
|
program.addCommand(upgrade);
|
|
2515
|
+
program.addCommand(watch);
|
|
2303
2516
|
const main = async () => {
|
|
2304
2517
|
await program.parseAsync();
|
|
2305
2518
|
};
|