@react-grab/cli 0.1.36 → 0.1.38
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.cjs +493 -336
- package/dist/cli.js +495 -334
- package/dist/cli.js.map +1 -1
- package/dist/read-clipboard.ps1 +111 -0
- package/dist/read-clipboard.swift +35 -0
- package/package.json +7 -4
- package/skills/react-grab/SKILL.md +49 -0
package/dist/cli.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";
|
|
4
|
+
import fs, { accessSync, constants, existsSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
5
5
|
import path, { basename, 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, 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,
|
|
@@ -366,204 +379,60 @@ const prompts = (questions) => {
|
|
|
366
379
|
//#region src/utils/spinner.ts
|
|
367
380
|
const spinner = (text) => ora({ text });
|
|
368
381
|
//#endregion
|
|
369
|
-
//#region src/utils/install-
|
|
370
|
-
const
|
|
371
|
-
const
|
|
372
|
-
const
|
|
373
|
-
const
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
if (
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
if (
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
const jsonPath = path.join(configDir, "opencode.json");
|
|
387
|
-
if (fs.existsSync(jsoncPath)) return jsoncPath;
|
|
388
|
-
if (fs.existsSync(jsonPath)) return jsonPath;
|
|
389
|
-
return jsoncPath;
|
|
390
|
-
};
|
|
391
|
-
const getClients = () => {
|
|
392
|
-
const homeDir = os.homedir();
|
|
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
|
|
382
|
+
//#region src/utils/install-skill.ts
|
|
383
|
+
const SKILL_NAME = "react-grab";
|
|
384
|
+
const SKILL_SOURCE = fileURLToPath(new URL("../skills/react-grab", import.meta.url));
|
|
385
|
+
const agentLabel = (agent) => getSkillAgentConfig(agent).displayName;
|
|
386
|
+
const installedSkillDir = (agent) => join(isUniversalSkillAgent(agent) ? getCanonicalSkillsDir(true) : getSkillAgentDir(agent, { global: true }), SKILL_NAME);
|
|
387
|
+
const promptSkillInstall = async ({ yes = false } = {}) => {
|
|
388
|
+
const agents = await detectInstalledSkillAgents();
|
|
389
|
+
if (agents.length === 0) {
|
|
390
|
+
logger.warn("No supported agents detected.");
|
|
391
|
+
return false;
|
|
392
|
+
}
|
|
393
|
+
if (!yes) {
|
|
394
|
+
const { confirmed } = await prompts({
|
|
395
|
+
type: "confirm",
|
|
396
|
+
name: "confirmed",
|
|
397
|
+
message: `Install the React Grab skill for ${highlighter.info(agents.map(agentLabel).join(", "))}?`,
|
|
398
|
+
initial: true
|
|
532
399
|
});
|
|
400
|
+
if (!confirmed) return false;
|
|
533
401
|
}
|
|
534
|
-
const
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
402
|
+
const installSpinner = spinner("Installing React Grab skill.").start();
|
|
403
|
+
const { installed, failed } = await add({
|
|
404
|
+
source: SKILL_SOURCE,
|
|
405
|
+
agents,
|
|
406
|
+
global: true,
|
|
407
|
+
mode: "copy"
|
|
408
|
+
});
|
|
409
|
+
if (installed.length === 0) {
|
|
410
|
+
installSpinner.fail("Failed to install React Grab skill.");
|
|
411
|
+
return false;
|
|
412
|
+
}
|
|
413
|
+
installSpinner.succeed(`Installed React Grab skill for ${installed.map((record) => agentLabel(record.agent)).join(", ")}.`);
|
|
414
|
+
for (const record of failed) logger.log(` ${highlighter.error("✗")} ${agentLabel(record.agent)} ${record.error}`);
|
|
415
|
+
return true;
|
|
540
416
|
};
|
|
541
|
-
const
|
|
542
|
-
const
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
choices: getMcpClientNames().map((name) => ({
|
|
547
|
-
title: name,
|
|
548
|
-
value: name,
|
|
549
|
-
selected: true
|
|
550
|
-
}))
|
|
417
|
+
const removeSkill = async () => {
|
|
418
|
+
const agentsWithSkill = (await detectInstalledSkillAgents()).filter((agent) => existsSync(installedSkillDir(agent)));
|
|
419
|
+
for (const skillDir of new Set(agentsWithSkill.map(installedSkillDir))) rmSync(skillDir, {
|
|
420
|
+
recursive: true,
|
|
421
|
+
force: true
|
|
551
422
|
});
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
return installMcpServers(selectedAgents).some((result) => result.success);
|
|
423
|
+
for (const agent of agentsWithSkill) logger.log(` ${highlighter.success("✓")} ${agentLabel(agent)}`);
|
|
424
|
+
return agentsWithSkill.length;
|
|
555
425
|
};
|
|
556
426
|
//#endregion
|
|
557
427
|
//#region src/commands/add.ts
|
|
558
|
-
const VERSION$5 = "0.1.
|
|
559
|
-
const add = new Command().name("add").alias("install").description("
|
|
428
|
+
const VERSION$5 = "0.1.38";
|
|
429
|
+
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
430
|
console.log(`${pc.magenta("✿")} ${pc.bold("React Grab")} ${pc.gray(VERSION$5)}`);
|
|
561
431
|
console.log();
|
|
562
432
|
try {
|
|
563
|
-
const cwd = opts.cwd;
|
|
564
433
|
const isNonInteractive = detectNonInteractive(opts.yes);
|
|
565
434
|
const preflightSpinner = spinner("Preflight checks.").start();
|
|
566
|
-
if (!(await detectProject(cwd)).hasReactGrab) {
|
|
435
|
+
if (!(await detectProject(opts.cwd)).hasReactGrab) {
|
|
567
436
|
preflightSpinner.fail("React Grab is not installed.");
|
|
568
437
|
logger.break();
|
|
569
438
|
logger.error(`Run ${highlighter.info("react-grab init")} first to install React Grab.`);
|
|
@@ -571,39 +440,12 @@ const add = new Command().name("add").alias("install").description("connect Reac
|
|
|
571
440
|
process.exit(1);
|
|
572
441
|
}
|
|
573
442
|
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.`);
|
|
443
|
+
logger.break();
|
|
444
|
+
if (!await promptSkillInstall({ yes: isNonInteractive }) && isNonInteractive) {
|
|
578
445
|
logger.break();
|
|
579
446
|
process.exit(1);
|
|
580
447
|
}
|
|
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
|
-
}
|
|
448
|
+
logger.break();
|
|
607
449
|
} catch (error) {
|
|
608
450
|
handleError(error);
|
|
609
451
|
}
|
|
@@ -1185,7 +1027,7 @@ const formatActivationKeyDisplay = (activationKey) => {
|
|
|
1185
1027
|
};
|
|
1186
1028
|
//#endregion
|
|
1187
1029
|
//#region src/commands/configure.ts
|
|
1188
|
-
const VERSION$4 = "0.1.
|
|
1030
|
+
const VERSION$4 = "0.1.38";
|
|
1189
1031
|
const isMac = process.platform === "darwin";
|
|
1190
1032
|
const META_LABEL = isMac ? "Cmd" : "Win";
|
|
1191
1033
|
const ALT_LABEL = isMac ? "Option" : "Alt";
|
|
@@ -1804,7 +1646,7 @@ const installPackagesWithFeedback = async (packages, packageManager, projectRoot
|
|
|
1804
1646
|
};
|
|
1805
1647
|
//#endregion
|
|
1806
1648
|
//#region src/commands/init.ts
|
|
1807
|
-
const VERSION$3 = "0.1.
|
|
1649
|
+
const VERSION$3 = "0.1.38";
|
|
1808
1650
|
const REPORT_URL = "https://react-grab.com/api/report-cli";
|
|
1809
1651
|
const DOCS_URL = "https://github.com/aidenybai/react-grab";
|
|
1810
1652
|
const reportToCli = (type, config, error) => {
|
|
@@ -2020,25 +1862,7 @@ const init = new Command().name("init").alias("setup").description("initialize R
|
|
|
2020
1862
|
}
|
|
2021
1863
|
}
|
|
2022
1864
|
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
|
-
}
|
|
1865
|
+
await promptSkillInstall({ yes: isNonInteractive });
|
|
2042
1866
|
logger.break();
|
|
2043
1867
|
process.exit(0);
|
|
2044
1868
|
}
|
|
@@ -2090,7 +1914,16 @@ const init = new Command().name("init").alias("setup").description("initialize R
|
|
|
2090
1914
|
process.chdir(selectedProject);
|
|
2091
1915
|
const newProjectInfo = await detectProject(selectedProject);
|
|
2092
1916
|
Object.assign(projectInfo, newProjectInfo);
|
|
2093
|
-
spinner("Verifying framework.").start()
|
|
1917
|
+
const newFrameworkSpinner = spinner("Verifying framework.").start();
|
|
1918
|
+
if (newProjectInfo.framework === "unknown") {
|
|
1919
|
+
newFrameworkSpinner.fail("Could not detect a supported framework in this project.");
|
|
1920
|
+
logger.break();
|
|
1921
|
+
logger.log("React Grab supports Next.js, Vite, TanStack Start, and Webpack projects.");
|
|
1922
|
+
logger.log(`Visit ${highlighter.info(DOCS_URL)} for manual setup.`);
|
|
1923
|
+
logger.break();
|
|
1924
|
+
process.exit(1);
|
|
1925
|
+
}
|
|
1926
|
+
newFrameworkSpinner.succeed(`Verifying framework. Found ${highlighter.info(FRAMEWORK_NAMES[newProjectInfo.framework])}.`);
|
|
2094
1927
|
} else {
|
|
2095
1928
|
frameworkSpinner.fail("Could not detect a supported framework.");
|
|
2096
1929
|
logger.break();
|
|
@@ -2105,30 +1938,10 @@ const init = new Command().name("init").alias("setup").description("initialize R
|
|
|
2105
1938
|
const finalFramework = projectInfo.framework;
|
|
2106
1939
|
const finalPackageManager = projectInfo.packageManager;
|
|
2107
1940
|
const finalNextRouterType = projectInfo.nextRouterType;
|
|
2108
|
-
let
|
|
1941
|
+
let didInstallSkill = false;
|
|
2109
1942
|
if (!isNonInteractive) {
|
|
2110
1943
|
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
|
-
}
|
|
1944
|
+
didInstallSkill = await promptSkillInstall({ yes: isNonInteractive });
|
|
2132
1945
|
}
|
|
2133
1946
|
const result = previewTransform(projectInfo.projectRoot, finalFramework, finalNextRouterType, false, opts.force);
|
|
2134
1947
|
if (!result.success) {
|
|
@@ -2172,7 +1985,7 @@ const init = new Command().name("init").alias("setup").description("initialize R
|
|
|
2172
1985
|
framework: finalFramework,
|
|
2173
1986
|
packageManager: finalPackageManager,
|
|
2174
1987
|
router: finalNextRouterType,
|
|
2175
|
-
agent:
|
|
1988
|
+
agent: didInstallSkill ? "skill" : void 0,
|
|
2176
1989
|
isMonorepo: projectInfo.isMonorepo
|
|
2177
1990
|
});
|
|
2178
1991
|
} catch (error) {
|
|
@@ -2182,29 +1995,16 @@ const init = new Command().name("init").alias("setup").description("initialize R
|
|
|
2182
1995
|
});
|
|
2183
1996
|
//#endregion
|
|
2184
1997
|
//#region src/commands/remove.ts
|
|
2185
|
-
const VERSION$2 = "0.1.
|
|
2186
|
-
const remove = new Command().name("remove").description("
|
|
1998
|
+
const VERSION$2 = "0.1.38";
|
|
1999
|
+
const remove = new Command().name("remove").description("uninstall the React Grab skill from your agent").action(async () => {
|
|
2187
2000
|
console.log(`${pc.magenta("✿")} ${pc.bold("React Grab")} ${pc.gray(VERSION$2)}`);
|
|
2188
2001
|
console.log();
|
|
2189
2002
|
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
2003
|
logger.break();
|
|
2207
|
-
|
|
2004
|
+
const removedCount = await removeSkill();
|
|
2005
|
+
logger.break();
|
|
2006
|
+
if (removedCount === 0) logger.log("React Grab skill is not installed in any detected agent.");
|
|
2007
|
+
else logger.log(`${highlighter.success("Removed")} the React Grab skill from ${removedCount} agent${removedCount === 1 ? "" : "s"}.`);
|
|
2208
2008
|
logger.break();
|
|
2209
2009
|
} catch (error) {
|
|
2210
2010
|
handleError(error);
|
|
@@ -2212,7 +2012,7 @@ const remove = new Command().name("remove").description("disconnect React Grab f
|
|
|
2212
2012
|
});
|
|
2213
2013
|
//#endregion
|
|
2214
2014
|
//#region src/commands/upgrade.ts
|
|
2215
|
-
const VERSION$1 = "0.1.
|
|
2015
|
+
const VERSION$1 = "0.1.38";
|
|
2216
2016
|
const NPM_REGISTRY_URL = "https://registry.npmjs.org/react-grab/latest";
|
|
2217
2017
|
const fetchLatestVersion = async () => {
|
|
2218
2018
|
try {
|
|
@@ -2286,8 +2086,368 @@ const upgrade = new Command().name("upgrade").alias("update").description("upgra
|
|
|
2286
2086
|
}
|
|
2287
2087
|
});
|
|
2288
2088
|
//#endregion
|
|
2089
|
+
//#region src/utils/clipboard.ts
|
|
2090
|
+
const READ_TIMEOUT_MS = 2500;
|
|
2091
|
+
const MAX_CLIPBOARD_BYTES = 64 * 1024 * 1024;
|
|
2092
|
+
const ID_RADIX = 36;
|
|
2093
|
+
const HASH_LENGTH = 12;
|
|
2094
|
+
const PICKLE_HEADER_BYTES = 4;
|
|
2095
|
+
const PICKLE_ALIGN_BYTES = 4;
|
|
2096
|
+
const SIGNATURE_SCAN_CHARS = 32 * 1024;
|
|
2097
|
+
const GRAB_MIME = "application/x-react-grab";
|
|
2098
|
+
const CHROMIUM_CUSTOM_FORMAT = "chromium/x-web-custom-data";
|
|
2099
|
+
const GRAB_TEXT_SIGNATURE = /\bin\s+\S+\s+\(at\s+[^\n]{1,400}?:\d+:\d+\)/;
|
|
2100
|
+
const sleep = (durationMs) => new Promise((resolve) => setTimeout(resolve, durationMs));
|
|
2101
|
+
const shortHash = (text) => createHash("sha1").update(text).digest("hex").slice(0, HASH_LENGTH);
|
|
2102
|
+
const alignUp = (value) => value + PICKLE_ALIGN_BYTES - 1 & ~(PICKLE_ALIGN_BYTES - 1);
|
|
2103
|
+
const parseChromiumPickle = (buffer) => {
|
|
2104
|
+
const formats = {};
|
|
2105
|
+
if (!buffer || buffer.length < PICKLE_HEADER_BYTES + 4) return formats;
|
|
2106
|
+
let offset = PICKLE_HEADER_BYTES;
|
|
2107
|
+
const pairCount = buffer.readUInt32LE(offset);
|
|
2108
|
+
offset += 4;
|
|
2109
|
+
for (let pairIndex = 0; pairIndex < pairCount; pairIndex += 1) {
|
|
2110
|
+
if (offset + 4 > buffer.length) break;
|
|
2111
|
+
const formatCodeUnits = buffer.readUInt32LE(offset);
|
|
2112
|
+
offset += 4;
|
|
2113
|
+
if (offset + formatCodeUnits * 2 > buffer.length) break;
|
|
2114
|
+
const format = buffer.toString("utf16le", offset, offset + formatCodeUnits * 2);
|
|
2115
|
+
offset = alignUp(offset + formatCodeUnits * 2);
|
|
2116
|
+
if (offset + 4 > buffer.length) break;
|
|
2117
|
+
const dataCodeUnits = buffer.readUInt32LE(offset);
|
|
2118
|
+
offset += 4;
|
|
2119
|
+
if (offset + dataCodeUnits * 2 > buffer.length) break;
|
|
2120
|
+
const value = buffer.toString("utf16le", offset, offset + dataCodeUnits * 2);
|
|
2121
|
+
offset = alignUp(offset + dataCodeUnits * 2);
|
|
2122
|
+
formats[format] = value;
|
|
2123
|
+
}
|
|
2124
|
+
return formats;
|
|
2125
|
+
};
|
|
2126
|
+
const extractGrab = (raw) => {
|
|
2127
|
+
if (raw.grab) return raw.grab;
|
|
2128
|
+
if (raw.pickleBase64) return parseChromiumPickle(Buffer.from(raw.pickleBase64, "base64"))[GRAB_MIME];
|
|
2129
|
+
};
|
|
2130
|
+
const isGrabText = (text) => GRAB_TEXT_SIGNATURE.test(text.length > SIGNATURE_SCAN_CHARS ? text.slice(0, SIGNATURE_SCAN_CHARS) : text);
|
|
2131
|
+
const extractPrompt = (record) => {
|
|
2132
|
+
const comments = (Array.isArray(record.entries) ? record.entries : []).map((entry) => entry?.commentText?.trim?.()).filter(Boolean);
|
|
2133
|
+
if (comments.length > 0) return comments.join("\n");
|
|
2134
|
+
const lines = (typeof record.content === "string" ? record.content : "").split("\n");
|
|
2135
|
+
const firstReferenceLine = lines.findIndex((line) => line.startsWith("["));
|
|
2136
|
+
if (firstReferenceLine <= 0) return void 0;
|
|
2137
|
+
return lines.slice(0, firstReferenceLine).join("\n").trim() || void 0;
|
|
2138
|
+
};
|
|
2139
|
+
const hasCommand = (name) => {
|
|
2140
|
+
return spawnSync(process.platform === "win32" ? "where" : "which", [name], { stdio: "ignore" }).status === 0;
|
|
2141
|
+
};
|
|
2142
|
+
const runText = (command, args) => {
|
|
2143
|
+
const output = spawnSync(command, args, {
|
|
2144
|
+
encoding: "utf8",
|
|
2145
|
+
maxBuffer: MAX_CLIPBOARD_BYTES,
|
|
2146
|
+
timeout: READ_TIMEOUT_MS
|
|
2147
|
+
});
|
|
2148
|
+
return output.status === 0 ? output.stdout : null;
|
|
2149
|
+
};
|
|
2150
|
+
const runBuffer = (command, args) => {
|
|
2151
|
+
const output = spawnSync(command, args, {
|
|
2152
|
+
maxBuffer: MAX_CLIPBOARD_BYTES,
|
|
2153
|
+
timeout: READ_TIMEOUT_MS
|
|
2154
|
+
});
|
|
2155
|
+
return output.status === 0 && output.stdout?.length ? output.stdout : null;
|
|
2156
|
+
};
|
|
2157
|
+
const runJson = (command, args) => {
|
|
2158
|
+
const output = spawnSync(command, args, {
|
|
2159
|
+
encoding: "utf8",
|
|
2160
|
+
maxBuffer: MAX_CLIPBOARD_BYTES,
|
|
2161
|
+
timeout: READ_TIMEOUT_MS
|
|
2162
|
+
});
|
|
2163
|
+
if (output.status !== 0 || !output.stdout) return null;
|
|
2164
|
+
try {
|
|
2165
|
+
return JSON.parse(output.stdout);
|
|
2166
|
+
} catch {
|
|
2167
|
+
return null;
|
|
2168
|
+
}
|
|
2169
|
+
};
|
|
2170
|
+
const compileSwiftReader = (readersDir, workDir) => {
|
|
2171
|
+
if (!hasCommand("swiftc")) return null;
|
|
2172
|
+
const source = path.join(readersDir, "read-clipboard.swift");
|
|
2173
|
+
if (!fs.existsSync(source)) return null;
|
|
2174
|
+
const binary = path.join(workDir, "pbread");
|
|
2175
|
+
if ((!fs.existsSync(binary) || fs.statSync(source).mtimeMs > fs.statSync(binary).mtimeMs) && spawnSync("swiftc", [
|
|
2176
|
+
"-O",
|
|
2177
|
+
source,
|
|
2178
|
+
"-o",
|
|
2179
|
+
binary
|
|
2180
|
+
]).status !== 0) return null;
|
|
2181
|
+
return binary;
|
|
2182
|
+
};
|
|
2183
|
+
const createDarwinReader = (options) => {
|
|
2184
|
+
const binary = options.textOnly ? null : compileSwiftReader(options.readersDir, options.workDir);
|
|
2185
|
+
if (binary) return {
|
|
2186
|
+
mode: "darwin-native",
|
|
2187
|
+
read: () => {
|
|
2188
|
+
const raw = runJson(binary, []);
|
|
2189
|
+
if (!raw) return null;
|
|
2190
|
+
return {
|
|
2191
|
+
changeCount: raw.changeCount ?? null,
|
|
2192
|
+
text: raw.text,
|
|
2193
|
+
grab: extractGrab(raw)
|
|
2194
|
+
};
|
|
2195
|
+
}
|
|
2196
|
+
};
|
|
2197
|
+
if (!hasCommand("pbpaste")) return null;
|
|
2198
|
+
return {
|
|
2199
|
+
mode: "darwin-text",
|
|
2200
|
+
read: () => {
|
|
2201
|
+
const text = runText("pbpaste", []);
|
|
2202
|
+
return text == null ? null : {
|
|
2203
|
+
changeCount: null,
|
|
2204
|
+
text,
|
|
2205
|
+
grab: void 0
|
|
2206
|
+
};
|
|
2207
|
+
}
|
|
2208
|
+
};
|
|
2209
|
+
};
|
|
2210
|
+
const detectLinuxTool = () => {
|
|
2211
|
+
if (Boolean(process.env.WAYLAND_DISPLAY) && hasCommand("wl-paste") || hasCommand("wl-paste") && !hasCommand("xclip")) return {
|
|
2212
|
+
name: "wl-paste",
|
|
2213
|
+
readText: () => runText("wl-paste", [
|
|
2214
|
+
"-n",
|
|
2215
|
+
"-t",
|
|
2216
|
+
"text/plain"
|
|
2217
|
+
]) ?? runText("wl-paste", ["-n"]),
|
|
2218
|
+
readCustom: () => runBuffer("wl-paste", [
|
|
2219
|
+
"-n",
|
|
2220
|
+
"-t",
|
|
2221
|
+
CHROMIUM_CUSTOM_FORMAT
|
|
2222
|
+
])
|
|
2223
|
+
};
|
|
2224
|
+
if (hasCommand("xclip")) return {
|
|
2225
|
+
name: "xclip",
|
|
2226
|
+
readText: () => runText("xclip", [
|
|
2227
|
+
"-selection",
|
|
2228
|
+
"clipboard",
|
|
2229
|
+
"-o"
|
|
2230
|
+
]),
|
|
2231
|
+
readCustom: () => runBuffer("xclip", [
|
|
2232
|
+
"-selection",
|
|
2233
|
+
"clipboard",
|
|
2234
|
+
"-t",
|
|
2235
|
+
CHROMIUM_CUSTOM_FORMAT,
|
|
2236
|
+
"-o"
|
|
2237
|
+
])
|
|
2238
|
+
};
|
|
2239
|
+
if (hasCommand("xsel")) return {
|
|
2240
|
+
name: "xsel",
|
|
2241
|
+
readText: () => runText("xsel", ["--clipboard", "--output"]),
|
|
2242
|
+
readCustom: null
|
|
2243
|
+
};
|
|
2244
|
+
return null;
|
|
2245
|
+
};
|
|
2246
|
+
const createLinuxReader = (options) => {
|
|
2247
|
+
const tool = detectLinuxTool();
|
|
2248
|
+
if (!tool) return null;
|
|
2249
|
+
const useCustom = !options.textOnly && Boolean(tool.readCustom);
|
|
2250
|
+
return {
|
|
2251
|
+
mode: useCustom ? `linux-${tool.name}` : `linux-${tool.name}-text`,
|
|
2252
|
+
read: () => {
|
|
2253
|
+
const text = tool.readText();
|
|
2254
|
+
if (text == null) return null;
|
|
2255
|
+
return {
|
|
2256
|
+
changeCount: null,
|
|
2257
|
+
text,
|
|
2258
|
+
grab: useCustom && tool.readCustom ? parseChromiumPickle(tool.readCustom())[GRAB_MIME] : void 0
|
|
2259
|
+
};
|
|
2260
|
+
}
|
|
2261
|
+
};
|
|
2262
|
+
};
|
|
2263
|
+
const detectPowershell = () => hasCommand("pwsh") ? "pwsh" : hasCommand("powershell") ? "powershell" : null;
|
|
2264
|
+
const createWindowsReader = (options) => {
|
|
2265
|
+
const shell = detectPowershell();
|
|
2266
|
+
if (!shell) return null;
|
|
2267
|
+
const scriptPath = path.join(options.readersDir, "read-clipboard.ps1");
|
|
2268
|
+
if (options.textOnly || !fs.existsSync(scriptPath)) return {
|
|
2269
|
+
mode: "win-text",
|
|
2270
|
+
read: () => {
|
|
2271
|
+
const text = runText(shell, [
|
|
2272
|
+
"-NoProfile",
|
|
2273
|
+
"-Command",
|
|
2274
|
+
"Get-Clipboard -Raw"
|
|
2275
|
+
]);
|
|
2276
|
+
return text == null ? null : {
|
|
2277
|
+
changeCount: null,
|
|
2278
|
+
text,
|
|
2279
|
+
grab: void 0
|
|
2280
|
+
};
|
|
2281
|
+
}
|
|
2282
|
+
};
|
|
2283
|
+
const args = [
|
|
2284
|
+
"-NoProfile",
|
|
2285
|
+
"-ExecutionPolicy",
|
|
2286
|
+
"Bypass",
|
|
2287
|
+
"-File",
|
|
2288
|
+
scriptPath
|
|
2289
|
+
];
|
|
2290
|
+
spawnSync(shell, args, {
|
|
2291
|
+
stdio: "ignore",
|
|
2292
|
+
maxBuffer: MAX_CLIPBOARD_BYTES
|
|
2293
|
+
});
|
|
2294
|
+
return {
|
|
2295
|
+
mode: "win-native",
|
|
2296
|
+
read: () => {
|
|
2297
|
+
const raw = runJson(shell, args);
|
|
2298
|
+
if (!raw) return null;
|
|
2299
|
+
return {
|
|
2300
|
+
changeCount: raw.changeCount ?? null,
|
|
2301
|
+
text: raw.text ?? void 0,
|
|
2302
|
+
grab: extractGrab(raw)
|
|
2303
|
+
};
|
|
2304
|
+
}
|
|
2305
|
+
};
|
|
2306
|
+
};
|
|
2307
|
+
const createReader = (options) => {
|
|
2308
|
+
if (process.platform === "darwin") return createDarwinReader(options);
|
|
2309
|
+
if (process.platform === "linux") return createLinuxReader(options);
|
|
2310
|
+
if (process.platform === "win32") return createWindowsReader(options);
|
|
2311
|
+
return null;
|
|
2312
|
+
};
|
|
2313
|
+
const ensureSafeDir = (dir) => {
|
|
2314
|
+
let stats;
|
|
2315
|
+
try {
|
|
2316
|
+
stats = fs.lstatSync(dir);
|
|
2317
|
+
} catch {
|
|
2318
|
+
return;
|
|
2319
|
+
}
|
|
2320
|
+
if (stats.isSymbolicLink()) throw new Error(`Refusing to use ${dir}: it is a symlink.`);
|
|
2321
|
+
if (process.getuid && stats.uid !== process.getuid()) throw new Error(`Refusing to use ${dir}: owned by another user.`);
|
|
2322
|
+
};
|
|
2323
|
+
const prepareWorkDir = (dir) => {
|
|
2324
|
+
ensureSafeDir(dir);
|
|
2325
|
+
fs.mkdirSync(dir, {
|
|
2326
|
+
recursive: true,
|
|
2327
|
+
mode: 448
|
|
2328
|
+
});
|
|
2329
|
+
const gitignore = path.join(dir, ".gitignore");
|
|
2330
|
+
if (!fs.existsSync(gitignore)) fs.writeFileSync(gitignore, "*\n");
|
|
2331
|
+
};
|
|
2332
|
+
const watchForNextGrab = async (options) => {
|
|
2333
|
+
const { reader, dir, intervalMs, replayLast, onWarn, signal } = options;
|
|
2334
|
+
const logPath = path.join(dir, "history.jsonl");
|
|
2335
|
+
const { read } = reader;
|
|
2336
|
+
let lastChangeCount = null;
|
|
2337
|
+
let lastTimestamp = 0;
|
|
2338
|
+
let lastTextHash = "";
|
|
2339
|
+
let lastErrorMessage = "";
|
|
2340
|
+
let sequence = 0;
|
|
2341
|
+
const initial = read();
|
|
2342
|
+
if (initial && !replayLast) {
|
|
2343
|
+
lastChangeCount = initial.changeCount;
|
|
2344
|
+
if (initial.text) lastTextHash = shortHash(initial.text);
|
|
2345
|
+
if (initial.grab) try {
|
|
2346
|
+
lastTimestamp = JSON.parse(initial.grab).timestamp ?? 0;
|
|
2347
|
+
} catch {}
|
|
2348
|
+
}
|
|
2349
|
+
while (!signal?.aborted) {
|
|
2350
|
+
await sleep(intervalMs);
|
|
2351
|
+
if (signal?.aborted) return null;
|
|
2352
|
+
try {
|
|
2353
|
+
const snapshot = read();
|
|
2354
|
+
if (!snapshot) continue;
|
|
2355
|
+
if (snapshot.changeCount !== null && snapshot.changeCount === lastChangeCount) continue;
|
|
2356
|
+
const textHash = snapshot.text ? shortHash(snapshot.text) : "";
|
|
2357
|
+
const didTextChange = textHash !== lastTextHash;
|
|
2358
|
+
let record = null;
|
|
2359
|
+
let nextTimestamp = lastTimestamp;
|
|
2360
|
+
if (snapshot.grab) {
|
|
2361
|
+
let parsed = null;
|
|
2362
|
+
try {
|
|
2363
|
+
parsed = JSON.parse(snapshot.grab);
|
|
2364
|
+
} catch {}
|
|
2365
|
+
if (parsed && typeof parsed.timestamp === "number" && parsed.timestamp > lastTimestamp) {
|
|
2366
|
+
nextTimestamp = parsed.timestamp;
|
|
2367
|
+
record = {
|
|
2368
|
+
source: "custom",
|
|
2369
|
+
timestamp: parsed.timestamp,
|
|
2370
|
+
version: typeof parsed.version === "string" ? parsed.version : void 0,
|
|
2371
|
+
content: typeof parsed.content === "string" ? parsed.content : "",
|
|
2372
|
+
entries: Array.isArray(parsed.entries) ? parsed.entries : []
|
|
2373
|
+
};
|
|
2374
|
+
}
|
|
2375
|
+
} else if (didTextChange && snapshot.text && isGrabText(snapshot.text)) record = {
|
|
2376
|
+
source: "text",
|
|
2377
|
+
timestamp: Date.now(),
|
|
2378
|
+
content: snapshot.text,
|
|
2379
|
+
entries: []
|
|
2380
|
+
};
|
|
2381
|
+
if (!record) {
|
|
2382
|
+
lastChangeCount = snapshot.changeCount;
|
|
2383
|
+
lastTextHash = textHash;
|
|
2384
|
+
continue;
|
|
2385
|
+
}
|
|
2386
|
+
const prompt = extractPrompt(record);
|
|
2387
|
+
if (prompt) record.prompt = prompt;
|
|
2388
|
+
const captured = {
|
|
2389
|
+
id: `${record.timestamp}-${(sequence += 1).toString(ID_RADIX)}`,
|
|
2390
|
+
receivedAt: Date.now(),
|
|
2391
|
+
...record
|
|
2392
|
+
};
|
|
2393
|
+
fs.appendFileSync(logPath, `${JSON.stringify(captured)}\n`);
|
|
2394
|
+
lastChangeCount = snapshot.changeCount;
|
|
2395
|
+
lastTextHash = textHash;
|
|
2396
|
+
lastTimestamp = nextTimestamp;
|
|
2397
|
+
return captured;
|
|
2398
|
+
} catch (error) {
|
|
2399
|
+
const message = String(error?.message ?? error);
|
|
2400
|
+
if (message !== lastErrorMessage) {
|
|
2401
|
+
lastErrorMessage = message;
|
|
2402
|
+
onWarn?.(message);
|
|
2403
|
+
}
|
|
2404
|
+
}
|
|
2405
|
+
}
|
|
2406
|
+
return null;
|
|
2407
|
+
};
|
|
2408
|
+
//#endregion
|
|
2409
|
+
//#region src/commands/watch.ts
|
|
2410
|
+
const DEFAULT_INTERVAL_MS = 800;
|
|
2411
|
+
const DEFAULT_DIR = ".react-grab";
|
|
2412
|
+
const readersDir = () => path.dirname(fileURLToPath(import.meta.url));
|
|
2413
|
+
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) => {
|
|
2414
|
+
const dir = path.resolve(options.dir);
|
|
2415
|
+
const intervalMs = Number(options.interval);
|
|
2416
|
+
const textOnly = Boolean(options.textOnly);
|
|
2417
|
+
try {
|
|
2418
|
+
prepareWorkDir(dir);
|
|
2419
|
+
} catch (error) {
|
|
2420
|
+
process.stderr.write(`react-grab watch: ${String(error?.message ?? error)}\n`);
|
|
2421
|
+
process.exit(1);
|
|
2422
|
+
}
|
|
2423
|
+
const reader = createReader({
|
|
2424
|
+
textOnly,
|
|
2425
|
+
readersDir: readersDir(),
|
|
2426
|
+
workDir: dir
|
|
2427
|
+
});
|
|
2428
|
+
if (!reader) {
|
|
2429
|
+
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");
|
|
2430
|
+
process.exit(1);
|
|
2431
|
+
}
|
|
2432
|
+
process.stderr.write(`react-grab watch: watching clipboard via ${reader.mode}; history → ${path.join(dir, "history.jsonl")} (Ctrl+C to stop)\n`);
|
|
2433
|
+
watchForNextGrab({
|
|
2434
|
+
reader,
|
|
2435
|
+
dir,
|
|
2436
|
+
intervalMs: Number.isFinite(intervalMs) && intervalMs > 0 ? intervalMs : DEFAULT_INTERVAL_MS,
|
|
2437
|
+
replayLast: Boolean(options.replayLast),
|
|
2438
|
+
onWarn: (message) => process.stderr.write(`react-grab watch: ${message}\n`)
|
|
2439
|
+
}).then((grab) => {
|
|
2440
|
+
if (!grab) process.exit(0);
|
|
2441
|
+
process.stdout.write(`${JSON.stringify(grab)}\n`);
|
|
2442
|
+
process.exit(0);
|
|
2443
|
+
}).catch((error) => {
|
|
2444
|
+
process.stderr.write(`react-grab watch: ${String(error?.message ?? error)}\n`);
|
|
2445
|
+
process.exit(1);
|
|
2446
|
+
});
|
|
2447
|
+
});
|
|
2448
|
+
//#endregion
|
|
2289
2449
|
//#region src/cli.ts
|
|
2290
|
-
const VERSION = "0.1.
|
|
2450
|
+
const VERSION = "0.1.38";
|
|
2291
2451
|
const VERSION_API_URL = "https://www.react-grab.com/api/version";
|
|
2292
2452
|
process.on("SIGINT", () => process.exit(0));
|
|
2293
2453
|
process.on("SIGTERM", () => process.exit(0));
|
|
@@ -2296,10 +2456,11 @@ try {
|
|
|
2296
2456
|
} catch {}
|
|
2297
2457
|
const program = new Command().name("grab").description("add React Grab to your project").version(VERSION, "-v, --version", "display the version number");
|
|
2298
2458
|
program.addCommand(init);
|
|
2299
|
-
program.addCommand(add);
|
|
2459
|
+
program.addCommand(add$1);
|
|
2300
2460
|
program.addCommand(remove);
|
|
2301
2461
|
program.addCommand(configure);
|
|
2302
2462
|
program.addCommand(upgrade);
|
|
2463
|
+
program.addCommand(watch);
|
|
2303
2464
|
const main = async () => {
|
|
2304
2465
|
await program.parseAsync();
|
|
2305
2466
|
};
|