@react-grab/cli 0.1.0-beta.5 → 0.1.0-beta.6

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.
Files changed (3) hide show
  1. package/dist/cli.cjs +1933 -1934
  2. package/dist/cli.js +1934 -1935
  3. package/package.json +2 -3
package/dist/cli.js CHANGED
@@ -3,13 +3,12 @@ import { Command } from 'commander';
3
3
  import e from 'assert';
4
4
  import pc from 'picocolors';
5
5
  import prompts3 from 'prompts';
6
- import { execSync, spawn } from 'child_process';
7
- import { rmSync, mkdirSync, writeFileSync, readFileSync, existsSync, accessSync, constants, readdirSync } from 'fs';
6
+ import { existsSync, readFileSync, writeFileSync, accessSync, constants, readdirSync } from 'fs';
8
7
  import { join, dirname, basename } from 'path';
9
- import { detect } from '@antfu/ni';
8
+ import { execSync } from 'child_process';
10
9
  import ora from 'ora';
10
+ import { detect } from '@antfu/ni';
11
11
  import { createRequire } from 'module';
12
- import { fileURLToPath } from 'url';
13
12
  import { chromium } from 'playwright-core';
14
13
  import { findInstalledBrowsers, BROWSER_DISPLAY_NAMES, dumpCookies, COOKIE_PREVIEW_LIMIT, DEFAULT_SERVER_PORT, isServerRunning, getServerInfo, spawnServer, serve, toPlaywrightCookies, applyStealthScripts, deleteServerInfo, DEFAULT_NAVIGATION_TIMEOUT_MS, findPageByTargetId, isLinux, installLinuxDeps, getDefaultBrowser, SUPPORTED_BROWSERS, isServerHealthy, stopServer, getSnapshotScript } from '@react-grab/browser';
15
14
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
@@ -1050,670 +1049,224 @@ function h(e3, t2, n2) {
1050
1049
  n2.set(t2, e3);
1051
1050
  for (let r2 of e3.commands) r2.name() !== `complete` && h(r2, t2 ? `${t2} ${r2.name()}` : r2.name(), n2);
1052
1051
  }
1053
- var detectPackageManager = async (projectRoot) => {
1054
- const detected = await detect({ cwd: projectRoot });
1055
- if (detected && ["npm", "yarn", "pnpm", "bun"].includes(detected)) {
1056
- return detected;
1057
- }
1058
- return "npm";
1052
+
1053
+ // src/utils/templates.ts
1054
+ var AGENTS = [
1055
+ "claude-code",
1056
+ "cursor",
1057
+ "opencode",
1058
+ "codex",
1059
+ "gemini",
1060
+ "amp",
1061
+ "ami",
1062
+ "visual-edit"
1063
+ ];
1064
+ var AGENT_NAMES = {
1065
+ "claude-code": "Claude Code",
1066
+ cursor: "Cursor",
1067
+ opencode: "OpenCode",
1068
+ codex: "Codex",
1069
+ gemini: "Gemini",
1070
+ amp: "Amp",
1071
+ ami: "Ami",
1072
+ "visual-edit": "Visual Edit"
1059
1073
  };
1060
- var detectFramework = (projectRoot) => {
1061
- const packageJsonPath = join(projectRoot, "package.json");
1062
- if (!existsSync(packageJsonPath)) {
1063
- return "unknown";
1064
- }
1065
- try {
1066
- const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
1067
- const allDependencies = {
1068
- ...packageJson.dependencies,
1069
- ...packageJson.devDependencies
1070
- };
1071
- if (allDependencies["next"]) {
1072
- return "next";
1073
- }
1074
- if (allDependencies["vite"]) {
1075
- return "vite";
1076
- }
1077
- if (allDependencies["webpack"]) {
1078
- return "webpack";
1074
+ AGENTS.filter((agent) => agent !== "ami").map(
1075
+ (agent) => `@react-grab/${agent}`
1076
+ );
1077
+ var NEXT_APP_ROUTER_SCRIPT = `{process.env.NODE_ENV === "development" && (
1078
+ <Script
1079
+ src="//unpkg.com/react-grab/dist/index.global.js"
1080
+ crossOrigin="anonymous"
1081
+ strategy="beforeInteractive"
1082
+ />
1083
+ )}`;
1084
+ var NEXT_APP_ROUTER_SCRIPT_WITH_AGENT = (agent) => {
1085
+ if (agent === "none") return NEXT_APP_ROUTER_SCRIPT;
1086
+ return `{process.env.NODE_ENV === "development" && (
1087
+ <Script
1088
+ src="//unpkg.com/react-grab/dist/index.global.js"
1089
+ crossOrigin="anonymous"
1090
+ strategy="beforeInteractive"
1091
+ />
1092
+ )}
1093
+ {process.env.NODE_ENV === "development" && (
1094
+ <Script
1095
+ src="//unpkg.com/@react-grab/${agent}/dist/client.global.js"
1096
+ strategy="lazyOnload"
1097
+ />
1098
+ )}`;
1099
+ };
1100
+ var NEXT_PAGES_ROUTER_SCRIPT = `{process.env.NODE_ENV === "development" && (
1101
+ <Script
1102
+ src="//unpkg.com/react-grab/dist/index.global.js"
1103
+ crossOrigin="anonymous"
1104
+ strategy="beforeInteractive"
1105
+ />
1106
+ )}`;
1107
+ var NEXT_PAGES_ROUTER_SCRIPT_WITH_AGENT = (agent) => {
1108
+ if (agent === "none") return NEXT_PAGES_ROUTER_SCRIPT;
1109
+ return `{process.env.NODE_ENV === "development" && (
1110
+ <Script
1111
+ src="//unpkg.com/react-grab/dist/index.global.js"
1112
+ crossOrigin="anonymous"
1113
+ strategy="beforeInteractive"
1114
+ />
1115
+ )}
1116
+ {process.env.NODE_ENV === "development" && (
1117
+ <Script
1118
+ src="//unpkg.com/@react-grab/${agent}/dist/client.global.js"
1119
+ strategy="lazyOnload"
1120
+ />
1121
+ )}`;
1122
+ };
1123
+ var VITE_SCRIPT = `<script type="module">
1124
+ if (import.meta.env.DEV) {
1125
+ import("react-grab");
1126
+ }
1127
+ </script>`;
1128
+ var VITE_SCRIPT_WITH_AGENT = (agent) => {
1129
+ if (agent === "none") return VITE_SCRIPT;
1130
+ return `<script type="module">
1131
+ if (import.meta.env.DEV) {
1132
+ import("react-grab");
1133
+ import("@react-grab/${agent}/client");
1134
+ }
1135
+ </script>`;
1136
+ };
1137
+ var WEBPACK_IMPORT = `if (process.env.NODE_ENV === "development") {
1138
+ import("react-grab");
1139
+ }`;
1140
+ var WEBPACK_IMPORT_WITH_AGENT = (agent) => {
1141
+ if (agent === "none") return WEBPACK_IMPORT;
1142
+ return `if (process.env.NODE_ENV === "development") {
1143
+ import("react-grab");
1144
+ import("@react-grab/${agent}/client");
1145
+ }`;
1146
+ };
1147
+ var SCRIPT_IMPORT = 'import Script from "next/script";';
1148
+ var MCP_CLIENTS = [
1149
+ "cursor",
1150
+ "claude-code",
1151
+ "vscode",
1152
+ "opencode",
1153
+ "codex",
1154
+ "gemini-cli",
1155
+ // "cline",
1156
+ // "roo-cline",
1157
+ "windsurf",
1158
+ "zed",
1159
+ // "warp",
1160
+ "droid"
1161
+ // "claude",
1162
+ ];
1163
+ var MCP_CLIENT_NAMES = {
1164
+ "cursor": "Cursor",
1165
+ "claude-code": "Claude Code",
1166
+ "vscode": "VSCode",
1167
+ "opencode": "OpenCode",
1168
+ "codex": "Codex",
1169
+ "gemini-cli": "Gemini CLI",
1170
+ // "cline": "Cline",
1171
+ // "roo-cline": "Roo Cline",
1172
+ "windsurf": "Windsurf",
1173
+ "zed": "Zed",
1174
+ // "warp": "Warp",
1175
+ "droid": "Droid"
1176
+ // "claude": "Claude Desktop",
1177
+ };
1178
+ var SKILL_AGENTS = [
1179
+ { id: "opencode", name: "OpenCode", folder: ".opencode" },
1180
+ { id: "claude-code", name: "Claude Code", folder: ".claude" },
1181
+ { id: "codex", name: "Codex", folder: ".codex" },
1182
+ { id: "cursor", name: "Cursor", folder: ".cursor" },
1183
+ { id: "vscode", name: "VSCode", folder: ".github" }
1184
+ ];
1185
+
1186
+ // src/utils/transform.ts
1187
+ var hasReactGrabCode = (content) => {
1188
+ const fuzzyPatterns = [
1189
+ /["'`][^"'`]*react-grab/,
1190
+ /react-grab[^"'`]*["'`]/,
1191
+ /<[^>]*react-grab/i,
1192
+ /import[^;]*react-grab/i,
1193
+ /require[^)]*react-grab/i,
1194
+ /from\s+[^;]*react-grab/i,
1195
+ /src[^>]*react-grab/i,
1196
+ /href[^>]*react-grab/i
1197
+ ];
1198
+ return fuzzyPatterns.some((pattern) => pattern.test(content));
1199
+ };
1200
+ var findLayoutFile = (projectRoot) => {
1201
+ const possiblePaths = [
1202
+ join(projectRoot, "app", "layout.tsx"),
1203
+ join(projectRoot, "app", "layout.jsx"),
1204
+ join(projectRoot, "src", "app", "layout.tsx"),
1205
+ join(projectRoot, "src", "app", "layout.jsx")
1206
+ ];
1207
+ for (const filePath of possiblePaths) {
1208
+ if (existsSync(filePath)) {
1209
+ return filePath;
1079
1210
  }
1080
- return "unknown";
1081
- } catch {
1082
- return "unknown";
1083
1211
  }
1212
+ return null;
1084
1213
  };
1085
- var detectNextRouterType = (projectRoot) => {
1086
- const hasAppDir = existsSync(join(projectRoot, "app"));
1087
- const hasSrcAppDir = existsSync(join(projectRoot, "src", "app"));
1088
- const hasPagesDir = existsSync(join(projectRoot, "pages"));
1089
- const hasSrcPagesDir = existsSync(join(projectRoot, "src", "pages"));
1090
- if (hasAppDir || hasSrcAppDir) {
1091
- return "app";
1092
- }
1093
- if (hasPagesDir || hasSrcPagesDir) {
1094
- return "pages";
1214
+ var findInstrumentationFile = (projectRoot) => {
1215
+ const possiblePaths = [
1216
+ join(projectRoot, "instrumentation-client.ts"),
1217
+ join(projectRoot, "instrumentation-client.js"),
1218
+ join(projectRoot, "src", "instrumentation-client.ts"),
1219
+ join(projectRoot, "src", "instrumentation-client.js")
1220
+ ];
1221
+ for (const filePath of possiblePaths) {
1222
+ if (existsSync(filePath)) {
1223
+ return filePath;
1224
+ }
1095
1225
  }
1096
- return "unknown";
1226
+ return null;
1097
1227
  };
1098
- var detectMonorepo = (projectRoot) => {
1099
- if (existsSync(join(projectRoot, "pnpm-workspace.yaml"))) {
1100
- return true;
1101
- }
1102
- if (existsSync(join(projectRoot, "lerna.json"))) {
1103
- return true;
1228
+ var hasReactGrabInInstrumentation = (projectRoot) => {
1229
+ const instrumentationPath = findInstrumentationFile(projectRoot);
1230
+ if (!instrumentationPath) return false;
1231
+ const content = readFileSync(instrumentationPath, "utf-8");
1232
+ return hasReactGrabCode(content);
1233
+ };
1234
+ var findDocumentFile = (projectRoot) => {
1235
+ const possiblePaths = [
1236
+ join(projectRoot, "pages", "_document.tsx"),
1237
+ join(projectRoot, "pages", "_document.jsx"),
1238
+ join(projectRoot, "src", "pages", "_document.tsx"),
1239
+ join(projectRoot, "src", "pages", "_document.jsx")
1240
+ ];
1241
+ for (const filePath of possiblePaths) {
1242
+ if (existsSync(filePath)) {
1243
+ return filePath;
1244
+ }
1104
1245
  }
1105
- const packageJsonPath = join(projectRoot, "package.json");
1106
- if (existsSync(packageJsonPath)) {
1107
- try {
1108
- const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
1109
- if (packageJson.workspaces) {
1110
- return true;
1111
- }
1112
- } catch {
1113
- return false;
1246
+ return null;
1247
+ };
1248
+ var findIndexHtml = (projectRoot) => {
1249
+ const possiblePaths = [
1250
+ join(projectRoot, "index.html"),
1251
+ join(projectRoot, "public", "index.html")
1252
+ ];
1253
+ for (const filePath of possiblePaths) {
1254
+ if (existsSync(filePath)) {
1255
+ return filePath;
1114
1256
  }
1115
1257
  }
1116
- return false;
1258
+ return null;
1117
1259
  };
1118
- var getWorkspacePatterns = (projectRoot) => {
1119
- const patterns = [];
1120
- const pnpmWorkspacePath = join(projectRoot, "pnpm-workspace.yaml");
1121
- if (existsSync(pnpmWorkspacePath)) {
1122
- const content = readFileSync(pnpmWorkspacePath, "utf-8");
1123
- const lines = content.split("\n");
1124
- let inPackages = false;
1125
- for (const line of lines) {
1126
- if (line.match(/^packages:\s*$/)) {
1127
- inPackages = true;
1128
- continue;
1129
- }
1130
- if (inPackages) {
1131
- if (line.match(/^[a-zA-Z]/) || line.trim() === "") {
1132
- if (line.match(/^[a-zA-Z]/)) inPackages = false;
1133
- continue;
1134
- }
1135
- const match = line.match(/^\s*-\s*['"]?([^'"#\n]+?)['"]?\s*$/);
1136
- if (match) {
1137
- patterns.push(match[1].trim());
1138
- }
1139
- }
1140
- }
1141
- }
1142
- const lernaJsonPath = join(projectRoot, "lerna.json");
1143
- if (existsSync(lernaJsonPath)) {
1144
- try {
1145
- const lernaJson = JSON.parse(readFileSync(lernaJsonPath, "utf-8"));
1146
- if (Array.isArray(lernaJson.packages)) {
1147
- patterns.push(...lernaJson.packages);
1148
- }
1149
- } catch {
1150
- }
1151
- }
1152
- const packageJsonPath = join(projectRoot, "package.json");
1153
- if (existsSync(packageJsonPath)) {
1154
- try {
1155
- const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
1156
- if (Array.isArray(packageJson.workspaces)) {
1157
- patterns.push(...packageJson.workspaces);
1158
- } else if (packageJson.workspaces?.packages) {
1159
- patterns.push(...packageJson.workspaces.packages);
1160
- }
1161
- } catch {
1162
- }
1163
- }
1164
- return [...new Set(patterns)];
1165
- };
1166
- var expandWorkspacePattern = (projectRoot, pattern) => {
1167
- const results = [];
1168
- const cleanPattern = pattern.replace(/\/\*$/, "");
1169
- const basePath = join(projectRoot, cleanPattern);
1170
- if (!existsSync(basePath)) return results;
1171
- try {
1172
- const entries = readdirSync(basePath, { withFileTypes: true });
1173
- for (const entry of entries) {
1174
- if (entry.isDirectory()) {
1175
- const packageJsonPath = join(basePath, entry.name, "package.json");
1176
- if (existsSync(packageJsonPath)) {
1177
- results.push(join(basePath, entry.name));
1178
- }
1179
- }
1180
- }
1181
- } catch {
1182
- return results;
1183
- }
1184
- return results;
1185
- };
1186
- var hasReactDependency = (projectPath) => {
1187
- const packageJsonPath = join(projectPath, "package.json");
1188
- if (!existsSync(packageJsonPath)) return false;
1189
- try {
1190
- const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
1191
- const allDeps = {
1192
- ...packageJson.dependencies,
1193
- ...packageJson.devDependencies
1194
- };
1195
- return Boolean(allDeps["react"] || allDeps["react-dom"]);
1196
- } catch {
1197
- return false;
1198
- }
1199
- };
1200
- var findWorkspaceProjects = (projectRoot) => {
1201
- const patterns = getWorkspacePatterns(projectRoot);
1202
- const projects = [];
1203
- for (const pattern of patterns) {
1204
- const projectPaths = expandWorkspacePattern(projectRoot, pattern);
1205
- for (const projectPath of projectPaths) {
1206
- const framework = detectFramework(projectPath);
1207
- const hasReact = hasReactDependency(projectPath);
1208
- if (hasReact || framework !== "unknown") {
1209
- const packageJsonPath = join(projectPath, "package.json");
1210
- let name = basename(projectPath);
1211
- try {
1212
- const packageJson = JSON.parse(
1213
- readFileSync(packageJsonPath, "utf-8")
1214
- );
1215
- name = packageJson.name || name;
1216
- } catch {
1217
- }
1218
- projects.push({
1219
- name,
1220
- path: projectPath,
1221
- framework,
1222
- hasReact
1223
- });
1224
- }
1225
- }
1226
- }
1227
- return projects;
1228
- };
1229
- var hasReactGrabInFile = (filePath) => {
1230
- if (!existsSync(filePath)) return false;
1231
- try {
1232
- const content = readFileSync(filePath, "utf-8");
1233
- const fuzzyPatterns = [
1234
- /["'`][^"'`]*react-grab/,
1235
- /react-grab[^"'`]*["'`]/,
1236
- /<[^>]*react-grab/i,
1237
- /import[^;]*react-grab/i,
1238
- /require[^)]*react-grab/i,
1239
- /from\s+[^;]*react-grab/i,
1240
- /src[^>]*react-grab/i
1241
- ];
1242
- return fuzzyPatterns.some((pattern) => pattern.test(content));
1243
- } catch {
1244
- return false;
1245
- }
1246
- };
1247
- var detectReactGrab = (projectRoot) => {
1248
- const packageJsonPath = join(projectRoot, "package.json");
1249
- if (existsSync(packageJsonPath)) {
1250
- try {
1251
- const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
1252
- const allDependencies = {
1253
- ...packageJson.dependencies,
1254
- ...packageJson.devDependencies
1255
- };
1256
- if (allDependencies["react-grab"]) {
1257
- return true;
1258
- }
1259
- } catch {
1260
- }
1261
- }
1262
- const filesToCheck = [
1263
- join(projectRoot, "app", "layout.tsx"),
1264
- join(projectRoot, "app", "layout.jsx"),
1265
- join(projectRoot, "src", "app", "layout.tsx"),
1266
- join(projectRoot, "src", "app", "layout.jsx"),
1267
- join(projectRoot, "pages", "_document.tsx"),
1268
- join(projectRoot, "pages", "_document.jsx"),
1269
- join(projectRoot, "instrumentation-client.ts"),
1270
- join(projectRoot, "instrumentation-client.js"),
1271
- join(projectRoot, "src", "instrumentation-client.ts"),
1272
- join(projectRoot, "src", "instrumentation-client.js"),
1273
- join(projectRoot, "index.html"),
1274
- join(projectRoot, "public", "index.html"),
1275
- join(projectRoot, "src", "index.tsx"),
1276
- join(projectRoot, "src", "index.ts"),
1277
- join(projectRoot, "src", "main.tsx"),
1278
- join(projectRoot, "src", "main.ts")
1279
- ];
1280
- return filesToCheck.some(hasReactGrabInFile);
1281
- };
1282
- var AGENT_PACKAGES = [
1283
- "@react-grab/claude-code",
1284
- "@react-grab/cursor",
1285
- "@react-grab/opencode",
1286
- "@react-grab/codex",
1287
- "@react-grab/gemini",
1288
- "@react-grab/amp",
1289
- "@react-grab/ami",
1290
- "@react-grab/visual-edit"
1291
- ];
1292
- var detectUnsupportedFramework = (projectRoot) => {
1293
- const packageJsonPath = join(projectRoot, "package.json");
1294
- if (!existsSync(packageJsonPath)) {
1295
- return null;
1296
- }
1297
- try {
1298
- const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
1299
- const allDependencies = {
1300
- ...packageJson.dependencies,
1301
- ...packageJson.devDependencies
1302
- };
1303
- if (allDependencies["@remix-run/react"] || allDependencies["remix"]) {
1304
- return "remix";
1305
- }
1306
- if (allDependencies["astro"]) {
1307
- return "astro";
1308
- }
1309
- if (allDependencies["@sveltejs/kit"]) {
1310
- return "sveltekit";
1311
- }
1312
- if (allDependencies["gatsby"]) {
1313
- return "gatsby";
1314
- }
1315
- return null;
1316
- } catch {
1317
- return null;
1318
- }
1319
- };
1320
- var detectInstalledAgents = (projectRoot) => {
1321
- const packageJsonPath = join(projectRoot, "package.json");
1322
- if (!existsSync(packageJsonPath)) {
1323
- return [];
1324
- }
1325
- try {
1326
- const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
1327
- const allDependencies = {
1328
- ...packageJson.dependencies,
1329
- ...packageJson.devDependencies
1330
- };
1331
- return AGENT_PACKAGES.filter(
1332
- (agent) => Boolean(allDependencies[agent])
1333
- ).map((agent) => agent.replace("@react-grab/", ""));
1334
- } catch {
1335
- return [];
1336
- }
1337
- };
1338
- var detectProject = async (projectRoot = process.cwd()) => {
1339
- const framework = detectFramework(projectRoot);
1340
- const packageManager = await detectPackageManager(projectRoot);
1341
- return {
1342
- packageManager,
1343
- framework,
1344
- nextRouterType: framework === "next" ? detectNextRouterType(projectRoot) : "unknown",
1345
- isMonorepo: detectMonorepo(projectRoot),
1346
- projectRoot,
1347
- hasReactGrab: detectReactGrab(projectRoot),
1348
- installedAgents: detectInstalledAgents(projectRoot),
1349
- unsupportedFramework: detectUnsupportedFramework(projectRoot)
1350
- };
1351
- };
1352
-
1353
- // src/utils/diff.ts
1354
- var RED = "\x1B[31m";
1355
- var GREEN = "\x1B[32m";
1356
- var GRAY = "\x1B[90m";
1357
- var RESET = "\x1B[0m";
1358
- var BOLD = "\x1B[1m";
1359
- var generateDiff = (originalContent, newContent) => {
1360
- const originalLines = originalContent.split("\n");
1361
- const newLines = newContent.split("\n");
1362
- const diff = [];
1363
- Math.max(originalLines.length, newLines.length);
1364
- let originalIndex = 0;
1365
- let newIndex = 0;
1366
- while (originalIndex < originalLines.length || newIndex < newLines.length) {
1367
- const originalLine = originalLines[originalIndex];
1368
- const newLine = newLines[newIndex];
1369
- if (originalLine === newLine) {
1370
- diff.push({
1371
- type: "unchanged",
1372
- content: originalLine,
1373
- lineNumber: newIndex + 1
1374
- });
1375
- originalIndex++;
1376
- newIndex++;
1377
- } else if (originalLine === void 0) {
1378
- diff.push({ type: "added", content: newLine, lineNumber: newIndex + 1 });
1379
- newIndex++;
1380
- } else if (newLine === void 0) {
1381
- diff.push({ type: "removed", content: originalLine });
1382
- originalIndex++;
1383
- } else {
1384
- const originalInNew = newLines.indexOf(originalLine, newIndex);
1385
- const newInOriginal = originalLines.indexOf(newLine, originalIndex);
1386
- if (originalInNew !== -1 && (newInOriginal === -1 || originalInNew - newIndex < newInOriginal - originalIndex)) {
1387
- while (newIndex < originalInNew) {
1388
- diff.push({
1389
- type: "added",
1390
- content: newLines[newIndex],
1391
- lineNumber: newIndex + 1
1392
- });
1393
- newIndex++;
1394
- }
1395
- } else if (newInOriginal !== -1) {
1396
- while (originalIndex < newInOriginal) {
1397
- diff.push({ type: "removed", content: originalLines[originalIndex] });
1398
- originalIndex++;
1399
- }
1400
- } else {
1401
- diff.push({ type: "removed", content: originalLine });
1402
- diff.push({
1403
- type: "added",
1404
- content: newLine,
1405
- lineNumber: newIndex + 1
1406
- });
1407
- originalIndex++;
1408
- newIndex++;
1409
- }
1410
- }
1411
- }
1412
- return diff;
1413
- };
1414
- var formatDiff = (diff, contextLines = 3) => {
1415
- const lines = [];
1416
- let lastPrintedIndex = -1;
1417
- let hasChanges = false;
1418
- const changedIndices = diff.map((line, index) => line.type !== "unchanged" ? index : -1).filter((index) => index !== -1);
1419
- if (changedIndices.length === 0) {
1420
- return `${GRAY}No changes${RESET}`;
1421
- }
1422
- for (const changedIndex of changedIndices) {
1423
- const startContext = Math.max(0, changedIndex - contextLines);
1424
- const endContext = Math.min(diff.length - 1, changedIndex + contextLines);
1425
- if (startContext > lastPrintedIndex + 1 && lastPrintedIndex !== -1) {
1426
- lines.push(`${GRAY} ...${RESET}`);
1427
- }
1428
- for (let lineIndex = Math.max(startContext, lastPrintedIndex + 1); lineIndex <= endContext; lineIndex++) {
1429
- const diffLine = diff[lineIndex];
1430
- if (diffLine.type === "added") {
1431
- lines.push(`${GREEN}+ ${diffLine.content}${RESET}`);
1432
- hasChanges = true;
1433
- } else if (diffLine.type === "removed") {
1434
- lines.push(`${RED}- ${diffLine.content}${RESET}`);
1435
- hasChanges = true;
1436
- } else {
1437
- lines.push(`${GRAY} ${diffLine.content}${RESET}`);
1438
- }
1439
- lastPrintedIndex = lineIndex;
1440
- }
1441
- }
1442
- return hasChanges ? lines.join("\n") : `${GRAY}No changes${RESET}`;
1443
- };
1444
- var printDiff = (filePath, originalContent, newContent) => {
1445
- console.log(`
1446
- ${BOLD}File: ${filePath}${RESET}`);
1447
- console.log("\u2500".repeat(60));
1448
- const diff = generateDiff(originalContent, newContent);
1449
- console.log(formatDiff(diff));
1450
- console.log("\u2500".repeat(60));
1451
- };
1452
- var highlighter = {
1453
- error: pc.red,
1454
- warn: pc.yellow,
1455
- info: pc.cyan,
1456
- success: pc.green,
1457
- dim: pc.dim
1458
- };
1459
-
1460
- // src/utils/logger.ts
1461
- var logger = {
1462
- error(...args) {
1463
- console.log(highlighter.error(args.join(" ")));
1464
- },
1465
- warn(...args) {
1466
- console.log(highlighter.warn(args.join(" ")));
1467
- },
1468
- info(...args) {
1469
- console.log(highlighter.info(args.join(" ")));
1470
- },
1471
- success(...args) {
1472
- console.log(highlighter.success(args.join(" ")));
1473
- },
1474
- dim(...args) {
1475
- console.log(highlighter.dim(args.join(" ")));
1476
- },
1477
- log(...args) {
1478
- console.log(args.join(" "));
1479
- },
1480
- break() {
1481
- console.log("");
1482
- }
1483
- };
1484
-
1485
- // src/utils/handle-error.ts
1486
- var handleError = (error48) => {
1487
- logger.break();
1488
- logger.error(
1489
- "Something went wrong. Please check the error below for more details."
1490
- );
1491
- logger.error("If the problem persists, please open an issue on GitHub.");
1492
- logger.error("");
1493
- if (error48 instanceof Error) {
1494
- logger.error(error48.message);
1495
- }
1496
- logger.break();
1497
- process.exit(1);
1498
- };
1499
- var INSTALL_COMMANDS = {
1500
- npm: "npm install",
1501
- yarn: "yarn add",
1502
- pnpm: "pnpm add",
1503
- bun: "bun add"
1504
- };
1505
- var UNINSTALL_COMMANDS = {
1506
- npm: "npm uninstall",
1507
- yarn: "yarn remove",
1508
- pnpm: "pnpm remove",
1509
- bun: "bun remove"
1510
- };
1511
- var installPackages = (packages, packageManager, projectRoot, isDev = true) => {
1512
- if (packages.length === 0) {
1513
- return;
1514
- }
1515
- const command = INSTALL_COMMANDS[packageManager];
1516
- const devFlag = isDev ? " -D" : "";
1517
- const fullCommand = `${command}${devFlag} ${packages.join(" ")}`;
1518
- console.log(`Running: ${fullCommand}
1519
- `);
1520
- execSync(fullCommand, {
1521
- cwd: projectRoot,
1522
- stdio: "inherit"
1523
- });
1524
- };
1525
- var getPackagesToInstall = (agent, includeReactGrab = true) => {
1526
- const packages = [];
1527
- if (includeReactGrab) {
1528
- packages.push("react-grab");
1529
- }
1530
- if (agent !== "none") {
1531
- packages.push(`@react-grab/${agent}`);
1532
- }
1533
- return packages;
1534
- };
1535
- var uninstallPackages = (packages, packageManager, projectRoot) => {
1536
- if (packages.length === 0) {
1537
- return;
1538
- }
1539
- const command = UNINSTALL_COMMANDS[packageManager];
1540
- const fullCommand = `${command} ${packages.join(" ")}`;
1541
- console.log(`Running: ${fullCommand}
1542
- `);
1543
- execSync(fullCommand, {
1544
- cwd: projectRoot,
1545
- stdio: "inherit"
1546
- });
1547
- };
1548
- var getPackagesToUninstall = (agent) => {
1549
- return [`@react-grab/${agent}`];
1550
- };
1551
- var spinner = (text, options2) => ora({ text, isSilent: options2?.silent });
1552
-
1553
- // src/utils/templates.ts
1554
- var AGENTS = [
1555
- "claude-code",
1556
- "cursor",
1557
- "opencode",
1558
- "codex",
1559
- "gemini",
1560
- "amp",
1561
- "ami",
1562
- "visual-edit"
1563
- ];
1564
- var AGENT_NAMES = {
1565
- "claude-code": "Claude Code",
1566
- cursor: "Cursor",
1567
- opencode: "OpenCode",
1568
- codex: "Codex",
1569
- gemini: "Gemini",
1570
- amp: "Amp",
1571
- ami: "Ami",
1572
- "visual-edit": "Visual Edit"
1573
- };
1574
- AGENTS.filter((agent) => agent !== "ami").map(
1575
- (agent) => `@react-grab/${agent}`
1576
- );
1577
- var NEXT_APP_ROUTER_SCRIPT = `{process.env.NODE_ENV === "development" && (
1578
- <Script
1579
- src="//unpkg.com/react-grab/dist/index.global.js"
1580
- crossOrigin="anonymous"
1581
- strategy="beforeInteractive"
1582
- />
1583
- )}`;
1584
- var NEXT_APP_ROUTER_SCRIPT_WITH_AGENT = (agent) => {
1585
- if (agent === "none") return NEXT_APP_ROUTER_SCRIPT;
1586
- return `{process.env.NODE_ENV === "development" && (
1587
- <Script
1588
- src="//unpkg.com/react-grab/dist/index.global.js"
1589
- crossOrigin="anonymous"
1590
- strategy="beforeInteractive"
1591
- />
1592
- )}
1593
- {process.env.NODE_ENV === "development" && (
1594
- <Script
1595
- src="//unpkg.com/@react-grab/${agent}/dist/client.global.js"
1596
- strategy="lazyOnload"
1597
- />
1598
- )}`;
1599
- };
1600
- var NEXT_PAGES_ROUTER_SCRIPT = `{process.env.NODE_ENV === "development" && (
1601
- <Script
1602
- src="//unpkg.com/react-grab/dist/index.global.js"
1603
- crossOrigin="anonymous"
1604
- strategy="beforeInteractive"
1605
- />
1606
- )}`;
1607
- var NEXT_PAGES_ROUTER_SCRIPT_WITH_AGENT = (agent) => {
1608
- if (agent === "none") return NEXT_PAGES_ROUTER_SCRIPT;
1609
- return `{process.env.NODE_ENV === "development" && (
1610
- <Script
1611
- src="//unpkg.com/react-grab/dist/index.global.js"
1612
- crossOrigin="anonymous"
1613
- strategy="beforeInteractive"
1614
- />
1615
- )}
1616
- {process.env.NODE_ENV === "development" && (
1617
- <Script
1618
- src="//unpkg.com/@react-grab/${agent}/dist/client.global.js"
1619
- strategy="lazyOnload"
1620
- />
1621
- )}`;
1622
- };
1623
- var VITE_SCRIPT = `<script type="module">
1624
- if (import.meta.env.DEV) {
1625
- import("react-grab");
1626
- }
1627
- </script>`;
1628
- var VITE_SCRIPT_WITH_AGENT = (agent) => {
1629
- if (agent === "none") return VITE_SCRIPT;
1630
- return `<script type="module">
1631
- if (import.meta.env.DEV) {
1632
- import("react-grab");
1633
- import("@react-grab/${agent}/client");
1634
- }
1635
- </script>`;
1636
- };
1637
- var WEBPACK_IMPORT = `if (process.env.NODE_ENV === "development") {
1638
- import("react-grab");
1639
- }`;
1640
- var WEBPACK_IMPORT_WITH_AGENT = (agent) => {
1641
- if (agent === "none") return WEBPACK_IMPORT;
1642
- return `if (process.env.NODE_ENV === "development") {
1643
- import("react-grab");
1644
- import("@react-grab/${agent}/client");
1645
- }`;
1646
- };
1647
- var SCRIPT_IMPORT = 'import Script from "next/script";';
1648
- var MCP_CLIENTS = [
1649
- "cursor",
1650
- "claude-code",
1651
- "vscode",
1652
- "opencode",
1653
- "codex",
1654
- "gemini-cli",
1655
- // "cline",
1656
- // "roo-cline",
1657
- "windsurf",
1658
- "zed",
1659
- // "warp",
1660
- "droid"
1661
- // "claude",
1662
- ];
1663
- var MCP_CLIENT_NAMES = {
1664
- "cursor": "Cursor",
1665
- "claude-code": "Claude Code",
1666
- "vscode": "VSCode",
1667
- "opencode": "OpenCode",
1668
- "codex": "Codex",
1669
- "gemini-cli": "Gemini CLI",
1670
- // "cline": "Cline",
1671
- // "roo-cline": "Roo Cline",
1672
- "windsurf": "Windsurf",
1673
- "zed": "Zed",
1674
- // "warp": "Warp",
1675
- "droid": "Droid"
1676
- // "claude": "Claude Desktop",
1677
- };
1678
-
1679
- // src/utils/skill-files.ts
1680
- var REPO = "aidenybai/react-grab";
1681
- var BRANCH = "main";
1682
- var BASE_URL = `https://raw.githubusercontent.com/${REPO}/${BRANCH}`;
1683
- var fetchSkillFile = async () => {
1684
- const skillUrl = `${BASE_URL}/skills/react-grab-browser/SKILL.md`;
1685
- const response = await fetch(skillUrl);
1686
- if (!response.ok) {
1687
- throw new Error(`Failed to fetch SKILL.md: ${response.status}`);
1688
- }
1689
- return response.text();
1690
- };
1691
- var AGENT_TARGETS = {
1692
- claude: ".claude/skills/react-grab-browser",
1693
- codex: ".codex/skills/react-grab-browser",
1694
- amp: ".agents/skills/react-grab-browser",
1695
- vscode: ".vscode/skills/react-grab-browser"
1696
- };
1697
- var SUPPORTED_TARGETS = Object.keys(AGENT_TARGETS);
1698
- var hasReactGrabCode = (content) => {
1699
- const fuzzyPatterns = [
1700
- /["'`][^"'`]*react-grab/,
1701
- /react-grab[^"'`]*["'`]/,
1702
- /<[^>]*react-grab/i,
1703
- /import[^;]*react-grab/i,
1704
- /require[^)]*react-grab/i,
1705
- /from\s+[^;]*react-grab/i,
1706
- /src[^>]*react-grab/i,
1707
- /href[^>]*react-grab/i
1708
- ];
1709
- return fuzzyPatterns.some((pattern) => pattern.test(content));
1710
- };
1711
- var findLayoutFile = (projectRoot) => {
1260
+ var findEntryFile = (projectRoot) => {
1712
1261
  const possiblePaths = [
1713
- join(projectRoot, "app", "layout.tsx"),
1714
- join(projectRoot, "app", "layout.jsx"),
1715
- join(projectRoot, "src", "app", "layout.tsx"),
1716
- join(projectRoot, "src", "app", "layout.jsx")
1262
+ join(projectRoot, "src", "index.tsx"),
1263
+ join(projectRoot, "src", "index.jsx"),
1264
+ join(projectRoot, "src", "index.ts"),
1265
+ join(projectRoot, "src", "index.js"),
1266
+ join(projectRoot, "src", "main.tsx"),
1267
+ join(projectRoot, "src", "main.jsx"),
1268
+ join(projectRoot, "src", "main.ts"),
1269
+ join(projectRoot, "src", "main.js")
1717
1270
  ];
1718
1271
  for (const filePath of possiblePaths) {
1719
1272
  if (existsSync(filePath)) {
@@ -1722,585 +1275,904 @@ var findLayoutFile = (projectRoot) => {
1722
1275
  }
1723
1276
  return null;
1724
1277
  };
1725
- var findInstrumentationFile = (projectRoot) => {
1726
- const possiblePaths = [
1727
- join(projectRoot, "instrumentation-client.ts"),
1728
- join(projectRoot, "instrumentation-client.js"),
1729
- join(projectRoot, "src", "instrumentation-client.ts"),
1730
- join(projectRoot, "src", "instrumentation-client.js")
1731
- ];
1732
- for (const filePath of possiblePaths) {
1733
- if (existsSync(filePath)) {
1734
- return filePath;
1735
- }
1278
+ var addAgentToExistingNextApp = (originalContent, agent, filePath) => {
1279
+ if (agent === "none") {
1280
+ return {
1281
+ success: true,
1282
+ filePath,
1283
+ message: "React Grab is already configured",
1284
+ noChanges: true
1285
+ };
1736
1286
  }
1737
- return null;
1287
+ const agentPackage = `@react-grab/${agent}`;
1288
+ if (originalContent.includes(agentPackage)) {
1289
+ return {
1290
+ success: true,
1291
+ filePath,
1292
+ message: `Agent ${agent} is already configured`,
1293
+ noChanges: true
1294
+ };
1295
+ }
1296
+ const agentScript = `{process.env.NODE_ENV === "development" && (
1297
+ <Script
1298
+ src="//unpkg.com/${agentPackage}/dist/client.global.js"
1299
+ strategy="lazyOnload"
1300
+ />
1301
+ )}`;
1302
+ const reactGrabBlockMatch = originalContent.match(
1303
+ /\{process\.env\.NODE_ENV\s*===\s*["']development["']\s*&&\s*\(\s*<Script[^>]*react-grab[^>]*\/>\s*\)\}/is
1304
+ );
1305
+ if (reactGrabBlockMatch) {
1306
+ const newContent = originalContent.replace(
1307
+ reactGrabBlockMatch[0],
1308
+ `${reactGrabBlockMatch[0]}
1309
+ ${agentScript}`
1310
+ );
1311
+ return {
1312
+ success: true,
1313
+ filePath,
1314
+ message: `Add ${agent} agent`,
1315
+ originalContent,
1316
+ newContent
1317
+ };
1318
+ }
1319
+ const bareScriptMatch = originalContent.match(
1320
+ /<Script[^>]*react-grab[^>]*\/>/i
1321
+ );
1322
+ if (bareScriptMatch) {
1323
+ const newContent = originalContent.replace(
1324
+ bareScriptMatch[0],
1325
+ `${bareScriptMatch[0]}
1326
+ <Script src="//unpkg.com/${agentPackage}/dist/client.global.js" strategy="lazyOnload" />`
1327
+ );
1328
+ return {
1329
+ success: true,
1330
+ filePath,
1331
+ message: `Add ${agent} agent`,
1332
+ originalContent,
1333
+ newContent
1334
+ };
1335
+ }
1336
+ return {
1337
+ success: false,
1338
+ filePath,
1339
+ message: "Could not find React Grab script to add agent after"
1340
+ };
1738
1341
  };
1739
- var hasReactGrabInInstrumentation = (projectRoot) => {
1740
- const instrumentationPath = findInstrumentationFile(projectRoot);
1741
- if (!instrumentationPath) return false;
1742
- const content = readFileSync(instrumentationPath, "utf-8");
1743
- return hasReactGrabCode(content);
1342
+ var addAgentToExistingVite = (originalContent, agent, filePath) => {
1343
+ if (agent === "none") {
1344
+ return {
1345
+ success: true,
1346
+ filePath,
1347
+ message: "React Grab is already configured",
1348
+ noChanges: true
1349
+ };
1350
+ }
1351
+ const agentPackage = `@react-grab/${agent}`;
1352
+ if (originalContent.includes(agentPackage)) {
1353
+ return {
1354
+ success: true,
1355
+ filePath,
1356
+ message: `Agent ${agent} is already configured`,
1357
+ noChanges: true
1358
+ };
1359
+ }
1360
+ const agentImport = `import("${agentPackage}/client");`;
1361
+ const reactGrabImportMatch = originalContent.match(
1362
+ /import\s*\(\s*["']react-grab["']\s*\);?/
1363
+ );
1364
+ if (reactGrabImportMatch) {
1365
+ const matchedText = reactGrabImportMatch[0];
1366
+ const hasSemicolon = matchedText.endsWith(";");
1367
+ const newContent = originalContent.replace(
1368
+ matchedText,
1369
+ `${hasSemicolon ? matchedText.slice(0, -1) : matchedText};
1370
+ ${agentImport}`
1371
+ );
1372
+ return {
1373
+ success: true,
1374
+ filePath,
1375
+ message: `Add ${agent} agent`,
1376
+ originalContent,
1377
+ newContent
1378
+ };
1379
+ }
1380
+ return {
1381
+ success: false,
1382
+ filePath,
1383
+ message: "Could not find React Grab import to add agent after"
1384
+ };
1744
1385
  };
1745
- var findDocumentFile = (projectRoot) => {
1746
- const possiblePaths = [
1747
- join(projectRoot, "pages", "_document.tsx"),
1748
- join(projectRoot, "pages", "_document.jsx"),
1749
- join(projectRoot, "src", "pages", "_document.tsx"),
1750
- join(projectRoot, "src", "pages", "_document.jsx")
1751
- ];
1752
- for (const filePath of possiblePaths) {
1753
- if (existsSync(filePath)) {
1754
- return filePath;
1755
- }
1386
+ var addAgentToExistingWebpack = (originalContent, agent, filePath) => {
1387
+ if (agent === "none") {
1388
+ return {
1389
+ success: true,
1390
+ filePath,
1391
+ message: "React Grab is already configured",
1392
+ noChanges: true
1393
+ };
1756
1394
  }
1757
- return null;
1395
+ const agentPackage = `@react-grab/${agent}`;
1396
+ if (originalContent.includes(agentPackage)) {
1397
+ return {
1398
+ success: true,
1399
+ filePath,
1400
+ message: `Agent ${agent} is already configured`,
1401
+ noChanges: true
1402
+ };
1403
+ }
1404
+ const agentImport = `import("${agentPackage}/client");`;
1405
+ const reactGrabImportMatch = originalContent.match(
1406
+ /import\s*\(\s*["']react-grab["']\s*\);?/
1407
+ );
1408
+ if (reactGrabImportMatch) {
1409
+ const matchedText = reactGrabImportMatch[0];
1410
+ const hasSemicolon = matchedText.endsWith(";");
1411
+ const newContent = originalContent.replace(
1412
+ matchedText,
1413
+ `${hasSemicolon ? matchedText.slice(0, -1) : matchedText};
1414
+ ${agentImport}`
1415
+ );
1416
+ return {
1417
+ success: true,
1418
+ filePath,
1419
+ message: `Add ${agent} agent`,
1420
+ originalContent,
1421
+ newContent
1422
+ };
1423
+ }
1424
+ return {
1425
+ success: false,
1426
+ filePath,
1427
+ message: "Could not find React Grab import to add agent after"
1428
+ };
1758
1429
  };
1759
- var findIndexHtml = (projectRoot) => {
1760
- const possiblePaths = [
1761
- join(projectRoot, "index.html"),
1762
- join(projectRoot, "public", "index.html")
1763
- ];
1764
- for (const filePath of possiblePaths) {
1765
- if (existsSync(filePath)) {
1766
- return filePath;
1430
+ var transformNextAppRouter = (projectRoot, agent, reactGrabAlreadyConfigured) => {
1431
+ const layoutPath = findLayoutFile(projectRoot);
1432
+ if (!layoutPath) {
1433
+ return {
1434
+ success: false,
1435
+ filePath: "",
1436
+ message: "Could not find app/layout.tsx or app/layout.jsx"
1437
+ };
1438
+ }
1439
+ const originalContent = readFileSync(layoutPath, "utf-8");
1440
+ let newContent = originalContent;
1441
+ const hasReactGrabInFile2 = hasReactGrabCode(originalContent);
1442
+ const hasReactGrabInInstrumentationFile = hasReactGrabInInstrumentation(projectRoot);
1443
+ if (hasReactGrabInFile2 && reactGrabAlreadyConfigured) {
1444
+ return addAgentToExistingNextApp(originalContent, agent, layoutPath);
1445
+ }
1446
+ if (hasReactGrabInFile2 || hasReactGrabInInstrumentationFile) {
1447
+ return {
1448
+ success: true,
1449
+ filePath: layoutPath,
1450
+ message: "React Grab is already installed" + (hasReactGrabInInstrumentationFile ? " in instrumentation-client" : " in this file"),
1451
+ noChanges: true
1452
+ };
1453
+ }
1454
+ if (!newContent.includes('import Script from "next/script"')) {
1455
+ const importMatch = newContent.match(/^import .+ from ['"].+['"];?\s*$/m);
1456
+ if (importMatch) {
1457
+ newContent = newContent.replace(
1458
+ importMatch[0],
1459
+ `${importMatch[0]}
1460
+ ${SCRIPT_IMPORT}`
1461
+ );
1462
+ } else {
1463
+ newContent = `${SCRIPT_IMPORT}
1464
+
1465
+ ${newContent}`;
1767
1466
  }
1768
1467
  }
1769
- return null;
1770
- };
1771
- var findEntryFile = (projectRoot) => {
1772
- const possiblePaths = [
1773
- join(projectRoot, "src", "index.tsx"),
1774
- join(projectRoot, "src", "index.jsx"),
1775
- join(projectRoot, "src", "index.ts"),
1776
- join(projectRoot, "src", "index.js"),
1777
- join(projectRoot, "src", "main.tsx"),
1778
- join(projectRoot, "src", "main.jsx"),
1779
- join(projectRoot, "src", "main.ts"),
1780
- join(projectRoot, "src", "main.js")
1781
- ];
1782
- for (const filePath of possiblePaths) {
1783
- if (existsSync(filePath)) {
1784
- return filePath;
1468
+ const scriptBlock = NEXT_APP_ROUTER_SCRIPT_WITH_AGENT(agent);
1469
+ const headMatch = newContent.match(/<head[^>]*>/);
1470
+ if (headMatch) {
1471
+ newContent = newContent.replace(
1472
+ headMatch[0],
1473
+ `${headMatch[0]}
1474
+ ${scriptBlock}`
1475
+ );
1476
+ } else {
1477
+ const htmlMatch = newContent.match(/<html[^>]*>/);
1478
+ if (htmlMatch) {
1479
+ newContent = newContent.replace(
1480
+ htmlMatch[0],
1481
+ `${htmlMatch[0]}
1482
+ <head>
1483
+ ${scriptBlock}
1484
+ </head>`
1485
+ );
1785
1486
  }
1786
1487
  }
1787
- return null;
1488
+ return {
1489
+ success: true,
1490
+ filePath: layoutPath,
1491
+ message: "Add React Grab" + (agent !== "none" ? ` with ${agent} agent` : ""),
1492
+ originalContent,
1493
+ newContent
1494
+ };
1788
1495
  };
1789
- var addAgentToExistingNextApp = (originalContent, agent, filePath) => {
1790
- if (agent === "none") {
1496
+ var transformNextPagesRouter = (projectRoot, agent, reactGrabAlreadyConfigured) => {
1497
+ const documentPath = findDocumentFile(projectRoot);
1498
+ if (!documentPath) {
1791
1499
  return {
1792
- success: true,
1793
- filePath,
1794
- message: "React Grab is already configured",
1795
- noChanges: true
1500
+ success: false,
1501
+ filePath: "",
1502
+ message: 'Could not find pages/_document.tsx or pages/_document.jsx.\n\nTo set up React Grab with Pages Router, create pages/_document.tsx with:\n\n import { Html, Head, Main, NextScript } from "next/document";\n import Script from "next/script";\n\n export default function Document() {\n return (\n <Html>\n <Head>\n {process.env.NODE_ENV === "development" && (\n <Script src="//unpkg.com/react-grab/dist/index.global.js" strategy="beforeInteractive" />\n )}\n </Head>\n <body>\n <Main />\n <NextScript />\n </body>\n </Html>\n );\n }'
1796
1503
  };
1797
1504
  }
1798
- const agentPackage = `@react-grab/${agent}`;
1799
- if (originalContent.includes(agentPackage)) {
1505
+ const originalContent = readFileSync(documentPath, "utf-8");
1506
+ let newContent = originalContent;
1507
+ const hasReactGrabInFile2 = hasReactGrabCode(originalContent);
1508
+ const hasReactGrabInInstrumentationFile = hasReactGrabInInstrumentation(projectRoot);
1509
+ if (hasReactGrabInFile2 && reactGrabAlreadyConfigured) {
1510
+ return addAgentToExistingNextApp(originalContent, agent, documentPath);
1511
+ }
1512
+ if (hasReactGrabInFile2 || hasReactGrabInInstrumentationFile) {
1800
1513
  return {
1801
1514
  success: true,
1802
- filePath,
1803
- message: `Agent ${agent} is already configured`,
1515
+ filePath: documentPath,
1516
+ message: "React Grab is already installed" + (hasReactGrabInInstrumentationFile ? " in instrumentation-client" : " in this file"),
1804
1517
  noChanges: true
1805
1518
  };
1806
1519
  }
1807
- const agentScript = `{process.env.NODE_ENV === "development" && (
1808
- <Script
1809
- src="//unpkg.com/${agentPackage}/dist/client.global.js"
1810
- strategy="lazyOnload"
1811
- />
1812
- )}`;
1813
- const reactGrabBlockMatch = originalContent.match(
1814
- /\{process\.env\.NODE_ENV\s*===\s*["']development["']\s*&&\s*\(\s*<Script[^>]*react-grab[^>]*\/>\s*\)\}/is
1815
- );
1816
- if (reactGrabBlockMatch) {
1817
- const newContent = originalContent.replace(
1818
- reactGrabBlockMatch[0],
1819
- `${reactGrabBlockMatch[0]}
1820
- ${agentScript}`
1520
+ if (!newContent.includes('import Script from "next/script"')) {
1521
+ const importMatch = newContent.match(/^import .+ from ['"].+['"];?\s*$/m);
1522
+ if (importMatch) {
1523
+ newContent = newContent.replace(
1524
+ importMatch[0],
1525
+ `${importMatch[0]}
1526
+ ${SCRIPT_IMPORT}`
1527
+ );
1528
+ }
1529
+ }
1530
+ const scriptBlock = NEXT_PAGES_ROUTER_SCRIPT_WITH_AGENT(agent);
1531
+ const headMatch = newContent.match(/<Head[^>]*>/);
1532
+ if (headMatch) {
1533
+ newContent = newContent.replace(
1534
+ headMatch[0],
1535
+ `${headMatch[0]}
1536
+ ${scriptBlock}`
1821
1537
  );
1538
+ }
1539
+ return {
1540
+ success: true,
1541
+ filePath: documentPath,
1542
+ message: "Add React Grab" + (agent !== "none" ? ` with ${agent} agent` : ""),
1543
+ originalContent,
1544
+ newContent
1545
+ };
1546
+ };
1547
+ var transformVite = (projectRoot, agent, reactGrabAlreadyConfigured) => {
1548
+ const indexPath = findIndexHtml(projectRoot);
1549
+ if (!indexPath) {
1822
1550
  return {
1823
- success: true,
1824
- filePath,
1825
- message: `Add ${agent} agent`,
1826
- originalContent,
1827
- newContent
1551
+ success: false,
1552
+ filePath: "",
1553
+ message: "Could not find index.html"
1828
1554
  };
1829
1555
  }
1830
- const bareScriptMatch = originalContent.match(
1831
- /<Script[^>]*react-grab[^>]*\/>/i
1832
- );
1833
- if (bareScriptMatch) {
1834
- const newContent = originalContent.replace(
1835
- bareScriptMatch[0],
1836
- `${bareScriptMatch[0]}
1837
- <Script src="//unpkg.com/${agentPackage}/dist/client.global.js" strategy="lazyOnload" />`
1838
- );
1556
+ const originalContent = readFileSync(indexPath, "utf-8");
1557
+ let newContent = originalContent;
1558
+ const hasReactGrabInFile2 = hasReactGrabCode(originalContent);
1559
+ if (hasReactGrabInFile2 && reactGrabAlreadyConfigured) {
1560
+ return addAgentToExistingVite(originalContent, agent, indexPath);
1561
+ }
1562
+ if (hasReactGrabInFile2) {
1839
1563
  return {
1840
1564
  success: true,
1841
- filePath,
1842
- message: `Add ${agent} agent`,
1843
- originalContent,
1844
- newContent
1565
+ filePath: indexPath,
1566
+ message: "React Grab is already installed in this file",
1567
+ noChanges: true
1845
1568
  };
1846
1569
  }
1570
+ const scriptBlock = VITE_SCRIPT_WITH_AGENT(agent);
1571
+ const headMatch = newContent.match(/<head[^>]*>/i);
1572
+ if (headMatch) {
1573
+ newContent = newContent.replace(
1574
+ headMatch[0],
1575
+ `${headMatch[0]}
1576
+ ${scriptBlock}`
1577
+ );
1578
+ }
1847
1579
  return {
1848
- success: false,
1849
- filePath,
1850
- message: "Could not find React Grab script to add agent after"
1580
+ success: true,
1581
+ filePath: indexPath,
1582
+ message: "Add React Grab" + (agent !== "none" ? ` with ${agent} agent` : ""),
1583
+ originalContent,
1584
+ newContent
1851
1585
  };
1852
1586
  };
1853
- var addAgentToExistingVite = (originalContent, agent, filePath) => {
1854
- if (agent === "none") {
1587
+ var transformWebpack = (projectRoot, agent, reactGrabAlreadyConfigured) => {
1588
+ const entryPath = findEntryFile(projectRoot);
1589
+ if (!entryPath) {
1590
+ return {
1591
+ success: false,
1592
+ filePath: "",
1593
+ message: "Could not find entry file (src/index.tsx, src/main.tsx, etc.)"
1594
+ };
1595
+ }
1596
+ const originalContent = readFileSync(entryPath, "utf-8");
1597
+ const hasReactGrabInFile2 = hasReactGrabCode(originalContent);
1598
+ if (hasReactGrabInFile2 && reactGrabAlreadyConfigured) {
1599
+ return addAgentToExistingWebpack(originalContent, agent, entryPath);
1600
+ }
1601
+ if (hasReactGrabInFile2) {
1855
1602
  return {
1856
1603
  success: true,
1857
- filePath,
1858
- message: "React Grab is already configured",
1604
+ filePath: entryPath,
1605
+ message: "React Grab is already installed in this file",
1859
1606
  noChanges: true
1860
1607
  };
1861
1608
  }
1862
- const agentPackage = `@react-grab/${agent}`;
1863
- if (originalContent.includes(agentPackage)) {
1609
+ const importBlock = WEBPACK_IMPORT_WITH_AGENT(agent);
1610
+ const newContent = `${importBlock}
1611
+
1612
+ ${originalContent}`;
1613
+ return {
1614
+ success: true,
1615
+ filePath: entryPath,
1616
+ message: "Add React Grab" + (agent !== "none" ? ` with ${agent} agent` : ""),
1617
+ originalContent,
1618
+ newContent
1619
+ };
1620
+ };
1621
+ var previewTransform = (projectRoot, framework, nextRouterType, agent, reactGrabAlreadyConfigured = false) => {
1622
+ switch (framework) {
1623
+ case "next":
1624
+ if (nextRouterType === "app") {
1625
+ return transformNextAppRouter(
1626
+ projectRoot,
1627
+ agent,
1628
+ reactGrabAlreadyConfigured
1629
+ );
1630
+ }
1631
+ return transformNextPagesRouter(
1632
+ projectRoot,
1633
+ agent,
1634
+ reactGrabAlreadyConfigured
1635
+ );
1636
+ case "vite":
1637
+ return transformVite(projectRoot, agent, reactGrabAlreadyConfigured);
1638
+ case "webpack":
1639
+ return transformWebpack(projectRoot, agent, reactGrabAlreadyConfigured);
1640
+ default:
1641
+ return {
1642
+ success: false,
1643
+ filePath: "",
1644
+ message: `Unknown framework: ${framework}. Please add React Grab manually.`
1645
+ };
1646
+ }
1647
+ };
1648
+ var canWriteToFile = (filePath) => {
1649
+ try {
1650
+ accessSync(filePath, constants.W_OK);
1651
+ return true;
1652
+ } catch {
1653
+ return false;
1654
+ }
1655
+ };
1656
+ var applyTransform = (result) => {
1657
+ if (result.success && result.newContent && result.filePath) {
1658
+ if (!canWriteToFile(result.filePath)) {
1659
+ return {
1660
+ success: false,
1661
+ error: `Cannot write to ${result.filePath}. Check file permissions.`
1662
+ };
1663
+ }
1664
+ try {
1665
+ writeFileSync(result.filePath, result.newContent);
1666
+ return { success: true };
1667
+ } catch (error48) {
1668
+ return {
1669
+ success: false,
1670
+ error: `Failed to write to ${result.filePath}: ${error48 instanceof Error ? error48.message : "Unknown error"}`
1671
+ };
1672
+ }
1673
+ }
1674
+ return { success: true };
1675
+ };
1676
+ var getPackageExecutor = (packageManager) => {
1677
+ switch (packageManager) {
1678
+ case "bun":
1679
+ return "bunx";
1680
+ case "pnpm":
1681
+ return "pnpm dlx";
1682
+ case "yarn":
1683
+ return "npx";
1684
+ case "npm":
1685
+ default:
1686
+ return "npx";
1687
+ }
1688
+ };
1689
+ var AGENT_PACKAGES = {
1690
+ "claude-code": "@react-grab/claude-code@latest",
1691
+ cursor: "@react-grab/cursor@latest",
1692
+ opencode: "@react-grab/opencode@latest",
1693
+ codex: "@react-grab/codex@latest",
1694
+ gemini: "@react-grab/gemini@latest",
1695
+ amp: "@react-grab/amp@latest"
1696
+ };
1697
+ var getAgentPrefix = (agent, packageManager) => {
1698
+ const agentPackage = AGENT_PACKAGES[agent];
1699
+ if (!agentPackage) return null;
1700
+ const executor = getPackageExecutor(packageManager);
1701
+ return `${executor} ${agentPackage} &&`;
1702
+ };
1703
+ var getAllAgentPrefixVariants = (agent) => {
1704
+ const agentPackage = AGENT_PACKAGES[agent];
1705
+ if (!agentPackage) return [];
1706
+ return [
1707
+ `npx ${agentPackage} &&`,
1708
+ `bunx ${agentPackage} &&`,
1709
+ `pnpm dlx ${agentPackage} &&`,
1710
+ `yarn dlx ${agentPackage} &&`
1711
+ ];
1712
+ };
1713
+ var previewPackageJsonTransform = (projectRoot, agent, installedAgents, packageManager = "npm") => {
1714
+ if (agent === "none" || agent === "visual-edit") {
1864
1715
  return {
1865
1716
  success: true,
1866
- filePath,
1867
- message: `Agent ${agent} is already configured`,
1717
+ filePath: "",
1718
+ message: agent === "visual-edit" ? "Visual Edit does not require package.json modification" : "No agent selected, skipping package.json modification",
1868
1719
  noChanges: true
1869
1720
  };
1870
1721
  }
1871
- const agentImport = `import("${agentPackage}/client");`;
1872
- const reactGrabImportMatch = originalContent.match(
1873
- /import\s*\(\s*["']react-grab["']\s*\);?/
1874
- );
1875
- if (reactGrabImportMatch) {
1876
- const matchedText = reactGrabImportMatch[0];
1877
- const hasSemicolon = matchedText.endsWith(";");
1878
- const newContent = originalContent.replace(
1879
- matchedText,
1880
- `${hasSemicolon ? matchedText.slice(0, -1) : matchedText};
1881
- ${agentImport}`
1882
- );
1722
+ const packageJsonPath = join(projectRoot, "package.json");
1723
+ if (!existsSync(packageJsonPath)) {
1883
1724
  return {
1884
- success: true,
1885
- filePath,
1886
- message: `Add ${agent} agent`,
1887
- originalContent,
1888
- newContent
1725
+ success: false,
1726
+ filePath: "",
1727
+ message: "Could not find package.json"
1889
1728
  };
1890
1729
  }
1891
- return {
1892
- success: false,
1893
- filePath,
1894
- message: "Could not find React Grab import to add agent after"
1895
- };
1896
- };
1897
- var addAgentToExistingWebpack = (originalContent, agent, filePath) => {
1898
- if (agent === "none") {
1730
+ const originalContent = readFileSync(packageJsonPath, "utf-8");
1731
+ const agentPrefix = getAgentPrefix(agent, packageManager);
1732
+ if (!agentPrefix) {
1899
1733
  return {
1900
- success: true,
1901
- filePath,
1902
- message: "React Grab is already configured",
1903
- noChanges: true
1734
+ success: false,
1735
+ filePath: packageJsonPath,
1736
+ message: `Unknown agent: ${agent}`
1904
1737
  };
1905
1738
  }
1906
- const agentPackage = `@react-grab/${agent}`;
1907
- if (originalContent.includes(agentPackage)) {
1739
+ const allPrefixVariants = getAllAgentPrefixVariants(agent);
1740
+ const hasExistingPrefix = allPrefixVariants.some(
1741
+ (prefix) => originalContent.includes(prefix)
1742
+ );
1743
+ if (hasExistingPrefix) {
1908
1744
  return {
1909
1745
  success: true,
1910
- filePath,
1911
- message: `Agent ${agent} is already configured`,
1746
+ filePath: packageJsonPath,
1747
+ message: `Agent ${agent} dev script is already configured`,
1912
1748
  noChanges: true
1913
1749
  };
1914
1750
  }
1915
- const agentImport = `import("${agentPackage}/client");`;
1916
- const reactGrabImportMatch = originalContent.match(
1917
- /import\s*\(\s*["']react-grab["']\s*\);?/
1918
- );
1919
- if (reactGrabImportMatch) {
1920
- const matchedText = reactGrabImportMatch[0];
1921
- const hasSemicolon = matchedText.endsWith(";");
1922
- const newContent = originalContent.replace(
1923
- matchedText,
1924
- `${hasSemicolon ? matchedText.slice(0, -1) : matchedText};
1925
- ${agentImport}`
1926
- );
1751
+ try {
1752
+ const packageJson = JSON.parse(originalContent);
1753
+ let targetScriptKey = "dev";
1754
+ if (!packageJson.scripts?.dev) {
1755
+ const devScriptKeys = Object.keys(packageJson.scripts || {}).filter(
1756
+ (key) => key.startsWith("dev")
1757
+ );
1758
+ if (devScriptKeys.length > 0) {
1759
+ targetScriptKey = devScriptKeys[0];
1760
+ } else {
1761
+ return {
1762
+ success: true,
1763
+ filePath: packageJsonPath,
1764
+ message: "No dev script found in package.json",
1765
+ noChanges: true,
1766
+ warning: `Could not inject agent into package.json (no dev script found).
1767
+ Run this command manually before starting your dev server:
1768
+ ${agentPrefix} <your dev command>`
1769
+ };
1770
+ }
1771
+ }
1772
+ const currentDevScript = packageJson.scripts[targetScriptKey];
1773
+ for (const installedAgent of installedAgents) {
1774
+ const installedPrefixVariants = getAllAgentPrefixVariants(installedAgent);
1775
+ const hasInstalledAgentPrefix = installedPrefixVariants.some(
1776
+ (prefix) => currentDevScript.includes(prefix)
1777
+ );
1778
+ if (hasInstalledAgentPrefix) {
1779
+ return {
1780
+ success: true,
1781
+ filePath: packageJsonPath,
1782
+ message: `Agent ${installedAgent} is already in ${targetScriptKey} script`,
1783
+ noChanges: true
1784
+ };
1785
+ }
1786
+ }
1787
+ packageJson.scripts[targetScriptKey] = `${agentPrefix} ${currentDevScript}`;
1788
+ const newContent = JSON.stringify(packageJson, null, 2) + "\n";
1927
1789
  return {
1928
1790
  success: true,
1929
- filePath,
1930
- message: `Add ${agent} agent`,
1791
+ filePath: packageJsonPath,
1792
+ message: `Add ${agent} server to ${targetScriptKey} script`,
1931
1793
  originalContent,
1932
1794
  newContent
1933
1795
  };
1934
- }
1935
- return {
1936
- success: false,
1937
- filePath,
1938
- message: "Could not find React Grab import to add agent after"
1939
- };
1940
- };
1941
- var transformNextAppRouter = (projectRoot, agent, reactGrabAlreadyConfigured) => {
1942
- const layoutPath = findLayoutFile(projectRoot);
1943
- if (!layoutPath) {
1796
+ } catch {
1944
1797
  return {
1945
1798
  success: false,
1946
- filePath: "",
1947
- message: "Could not find app/layout.tsx or app/layout.jsx"
1799
+ filePath: packageJsonPath,
1800
+ message: "Failed to parse package.json"
1948
1801
  };
1949
1802
  }
1950
- const originalContent = readFileSync(layoutPath, "utf-8");
1951
- let newContent = originalContent;
1952
- const hasReactGrabInFile2 = hasReactGrabCode(originalContent);
1953
- const hasReactGrabInInstrumentationFile = hasReactGrabInInstrumentation(projectRoot);
1954
- if (hasReactGrabInFile2 && reactGrabAlreadyConfigured) {
1955
- return addAgentToExistingNextApp(originalContent, agent, layoutPath);
1803
+ };
1804
+ var applyPackageJsonTransform = (result) => {
1805
+ if (result.success && result.newContent && result.filePath) {
1806
+ if (!canWriteToFile(result.filePath)) {
1807
+ return {
1808
+ success: false,
1809
+ error: `Cannot write to ${result.filePath}. Check file permissions.`
1810
+ };
1811
+ }
1812
+ try {
1813
+ writeFileSync(result.filePath, result.newContent);
1814
+ return { success: true };
1815
+ } catch (error48) {
1816
+ return {
1817
+ success: false,
1818
+ error: `Failed to write to ${result.filePath}: ${error48 instanceof Error ? error48.message : "Unknown error"}`
1819
+ };
1820
+ }
1956
1821
  }
1957
- if (hasReactGrabInFile2 || hasReactGrabInInstrumentationFile) {
1822
+ return { success: true };
1823
+ };
1824
+ var formatOptionsForNextjs = (options2) => {
1825
+ const parts = [];
1826
+ if (options2.activationKey) {
1827
+ parts.push(`activationKey: ${JSON.stringify(options2.activationKey)}`);
1828
+ }
1829
+ if (options2.activationMode) {
1830
+ parts.push(`activationMode: "${options2.activationMode}"`);
1831
+ }
1832
+ if (options2.keyHoldDuration !== void 0) {
1833
+ parts.push(`keyHoldDuration: ${options2.keyHoldDuration}`);
1834
+ }
1835
+ if (options2.allowActivationInsideInput !== void 0) {
1836
+ parts.push(
1837
+ `allowActivationInsideInput: ${options2.allowActivationInsideInput}`
1838
+ );
1839
+ }
1840
+ if (options2.maxContextLines !== void 0) {
1841
+ parts.push(`maxContextLines: ${options2.maxContextLines}`);
1842
+ }
1843
+ return `{ ${parts.join(", ")} }`;
1844
+ };
1845
+ var formatOptionsAsJson = (options2) => {
1846
+ const cleanOptions = {};
1847
+ if (options2.activationKey) {
1848
+ cleanOptions.activationKey = options2.activationKey;
1849
+ }
1850
+ if (options2.activationMode) {
1851
+ cleanOptions.activationMode = options2.activationMode;
1852
+ }
1853
+ if (options2.keyHoldDuration !== void 0) {
1854
+ cleanOptions.keyHoldDuration = options2.keyHoldDuration;
1855
+ }
1856
+ if (options2.allowActivationInsideInput !== void 0) {
1857
+ cleanOptions.allowActivationInsideInput = options2.allowActivationInsideInput;
1858
+ }
1859
+ if (options2.maxContextLines !== void 0) {
1860
+ cleanOptions.maxContextLines = options2.maxContextLines;
1861
+ }
1862
+ return JSON.stringify(cleanOptions);
1863
+ };
1864
+ var findReactGrabFile = (projectRoot, framework, nextRouterType) => {
1865
+ switch (framework) {
1866
+ case "next":
1867
+ if (nextRouterType === "app") {
1868
+ return findLayoutFile(projectRoot);
1869
+ }
1870
+ return findDocumentFile(projectRoot);
1871
+ case "vite":
1872
+ return findIndexHtml(projectRoot);
1873
+ case "webpack":
1874
+ return findEntryFile(projectRoot);
1875
+ default:
1876
+ return null;
1877
+ }
1878
+ };
1879
+ var addOptionsToNextScript = (originalContent, options2, filePath) => {
1880
+ const reactGrabScriptMatch = originalContent.match(
1881
+ /(<Script[^>]*react-grab[^>]*)(\/?>)/is
1882
+ );
1883
+ if (!reactGrabScriptMatch) {
1958
1884
  return {
1959
- success: true,
1960
- filePath: layoutPath,
1961
- message: "React Grab is already installed" + (hasReactGrabInInstrumentationFile ? " in instrumentation-client" : " in this file"),
1962
- noChanges: true
1885
+ success: false,
1886
+ filePath,
1887
+ message: "Could not find React Grab Script tag"
1963
1888
  };
1964
1889
  }
1965
- if (!newContent.includes('import Script from "next/script"')) {
1966
- const importMatch = newContent.match(/^import .+ from ['"].+['"];?\s*$/m);
1967
- if (importMatch) {
1968
- newContent = newContent.replace(
1969
- importMatch[0],
1970
- `${importMatch[0]}
1971
- ${SCRIPT_IMPORT}`
1972
- );
1973
- } else {
1974
- newContent = `${SCRIPT_IMPORT}
1975
-
1976
- ${newContent}`;
1977
- }
1978
- }
1979
- const scriptBlock = NEXT_APP_ROUTER_SCRIPT_WITH_AGENT(agent);
1980
- const headMatch = newContent.match(/<head[^>]*>/);
1981
- if (headMatch) {
1982
- newContent = newContent.replace(
1983
- headMatch[0],
1984
- `${headMatch[0]}
1985
- ${scriptBlock}`
1890
+ const scriptTag = reactGrabScriptMatch[0];
1891
+ const scriptOpening = reactGrabScriptMatch[1];
1892
+ const scriptClosing = reactGrabScriptMatch[2];
1893
+ const existingDataOptionsMatch = scriptTag.match(
1894
+ /data-options=\{JSON\.stringify\([^)]+\)\}/
1895
+ );
1896
+ const dataOptionsAttr = `data-options={JSON.stringify(
1897
+ ${formatOptionsForNextjs(options2)}
1898
+ )}`;
1899
+ let newScriptTag;
1900
+ if (existingDataOptionsMatch) {
1901
+ newScriptTag = scriptTag.replace(
1902
+ existingDataOptionsMatch[0],
1903
+ dataOptionsAttr
1986
1904
  );
1987
1905
  } else {
1988
- const htmlMatch = newContent.match(/<html[^>]*>/);
1989
- if (htmlMatch) {
1990
- newContent = newContent.replace(
1991
- htmlMatch[0],
1992
- `${htmlMatch[0]}
1993
- <head>
1994
- ${scriptBlock}
1995
- </head>`
1996
- );
1997
- }
1906
+ newScriptTag = `${scriptOpening}
1907
+ ${dataOptionsAttr}
1908
+ ${scriptClosing}`;
1998
1909
  }
1910
+ const newContent = originalContent.replace(scriptTag, newScriptTag);
1999
1911
  return {
2000
1912
  success: true,
2001
- filePath: layoutPath,
2002
- message: "Add React Grab" + (agent !== "none" ? ` with ${agent} agent` : ""),
1913
+ filePath,
1914
+ message: "Update React Grab options",
2003
1915
  originalContent,
2004
1916
  newContent
2005
1917
  };
2006
1918
  };
2007
- var transformNextPagesRouter = (projectRoot, agent, reactGrabAlreadyConfigured) => {
2008
- const documentPath = findDocumentFile(projectRoot);
2009
- if (!documentPath) {
1919
+ var addOptionsToViteScript = (originalContent, options2, filePath) => {
1920
+ const reactGrabImportMatch = originalContent.match(
1921
+ /import\s*\(\s*["']react-grab["']\s*\)/
1922
+ );
1923
+ if (!reactGrabImportMatch) {
2010
1924
  return {
2011
1925
  success: false,
2012
- filePath: "",
2013
- message: 'Could not find pages/_document.tsx or pages/_document.jsx.\n\nTo set up React Grab with Pages Router, create pages/_document.tsx with:\n\n import { Html, Head, Main, NextScript } from "next/document";\n import Script from "next/script";\n\n export default function Document() {\n return (\n <Html>\n <Head>\n {process.env.NODE_ENV === "development" && (\n <Script src="//unpkg.com/react-grab/dist/index.global.js" strategy="beforeInteractive" />\n )}\n </Head>\n <body>\n <Main />\n <NextScript />\n </body>\n </Html>\n );\n }'
2014
- };
2015
- }
2016
- const originalContent = readFileSync(documentPath, "utf-8");
2017
- let newContent = originalContent;
2018
- const hasReactGrabInFile2 = hasReactGrabCode(originalContent);
2019
- const hasReactGrabInInstrumentationFile = hasReactGrabInInstrumentation(projectRoot);
2020
- if (hasReactGrabInFile2 && reactGrabAlreadyConfigured) {
2021
- return addAgentToExistingNextApp(originalContent, agent, documentPath);
2022
- }
2023
- if (hasReactGrabInFile2 || hasReactGrabInInstrumentationFile) {
2024
- return {
2025
- success: true,
2026
- filePath: documentPath,
2027
- message: "React Grab is already installed" + (hasReactGrabInInstrumentationFile ? " in instrumentation-client" : " in this file"),
2028
- noChanges: true
1926
+ filePath,
1927
+ message: "Could not find React Grab import"
2029
1928
  };
2030
1929
  }
2031
- if (!newContent.includes('import Script from "next/script"')) {
2032
- const importMatch = newContent.match(/^import .+ from ['"].+['"];?\s*$/m);
2033
- if (importMatch) {
2034
- newContent = newContent.replace(
2035
- importMatch[0],
2036
- `${importMatch[0]}
2037
- ${SCRIPT_IMPORT}`
2038
- );
2039
- }
2040
- }
2041
- const scriptBlock = NEXT_PAGES_ROUTER_SCRIPT_WITH_AGENT(agent);
2042
- const headMatch = newContent.match(/<Head[^>]*>/);
2043
- if (headMatch) {
2044
- newContent = newContent.replace(
2045
- headMatch[0],
2046
- `${headMatch[0]}
2047
- ${scriptBlock}`
2048
- );
2049
- }
1930
+ const optionsJson = formatOptionsAsJson(options2);
1931
+ const newImport = `import("react-grab").then((m) => m.init(${optionsJson}))`;
1932
+ const newContent = originalContent.replace(
1933
+ reactGrabImportMatch[0],
1934
+ newImport
1935
+ );
2050
1936
  return {
2051
1937
  success: true,
2052
- filePath: documentPath,
2053
- message: "Add React Grab" + (agent !== "none" ? ` with ${agent} agent` : ""),
1938
+ filePath,
1939
+ message: "Update React Grab options",
2054
1940
  originalContent,
2055
1941
  newContent
2056
1942
  };
2057
1943
  };
2058
- var transformVite = (projectRoot, agent, reactGrabAlreadyConfigured) => {
2059
- const indexPath = findIndexHtml(projectRoot);
2060
- if (!indexPath) {
1944
+ var addOptionsToWebpackImport = (originalContent, options2, filePath) => {
1945
+ const reactGrabImportMatch = originalContent.match(
1946
+ /import\s*\(\s*["']react-grab["']\s*\)/
1947
+ );
1948
+ if (!reactGrabImportMatch) {
2061
1949
  return {
2062
1950
  success: false,
2063
- filePath: "",
2064
- message: "Could not find index.html"
2065
- };
2066
- }
2067
- const originalContent = readFileSync(indexPath, "utf-8");
2068
- let newContent = originalContent;
2069
- const hasReactGrabInFile2 = hasReactGrabCode(originalContent);
2070
- if (hasReactGrabInFile2 && reactGrabAlreadyConfigured) {
2071
- return addAgentToExistingVite(originalContent, agent, indexPath);
2072
- }
2073
- if (hasReactGrabInFile2) {
2074
- return {
2075
- success: true,
2076
- filePath: indexPath,
2077
- message: "React Grab is already installed in this file",
2078
- noChanges: true
1951
+ filePath,
1952
+ message: "Could not find React Grab import"
2079
1953
  };
2080
1954
  }
2081
- const scriptBlock = VITE_SCRIPT_WITH_AGENT(agent);
2082
- const headMatch = newContent.match(/<head[^>]*>/i);
2083
- if (headMatch) {
2084
- newContent = newContent.replace(
2085
- headMatch[0],
2086
- `${headMatch[0]}
2087
- ${scriptBlock}`
2088
- );
2089
- }
1955
+ const optionsJson = formatOptionsAsJson(options2);
1956
+ const newImport = `import("react-grab").then((m) => m.init(${optionsJson}))`;
1957
+ const newContent = originalContent.replace(
1958
+ reactGrabImportMatch[0],
1959
+ newImport
1960
+ );
2090
1961
  return {
2091
1962
  success: true,
2092
- filePath: indexPath,
2093
- message: "Add React Grab" + (agent !== "none" ? ` with ${agent} agent` : ""),
1963
+ filePath,
1964
+ message: "Update React Grab options",
2094
1965
  originalContent,
2095
1966
  newContent
2096
1967
  };
2097
1968
  };
2098
- var transformWebpack = (projectRoot, agent, reactGrabAlreadyConfigured) => {
2099
- const entryPath = findEntryFile(projectRoot);
2100
- if (!entryPath) {
1969
+ var previewOptionsTransform = (projectRoot, framework, nextRouterType, options2) => {
1970
+ const filePath = findReactGrabFile(projectRoot, framework, nextRouterType);
1971
+ if (!filePath) {
2101
1972
  return {
2102
1973
  success: false,
2103
1974
  filePath: "",
2104
- message: "Could not find entry file (src/index.tsx, src/main.tsx, etc.)"
1975
+ message: "Could not find file containing React Grab configuration"
2105
1976
  };
2106
1977
  }
2107
- const originalContent = readFileSync(entryPath, "utf-8");
2108
- const hasReactGrabInFile2 = hasReactGrabCode(originalContent);
2109
- if (hasReactGrabInFile2 && reactGrabAlreadyConfigured) {
2110
- return addAgentToExistingWebpack(originalContent, agent, entryPath);
2111
- }
2112
- if (hasReactGrabInFile2) {
1978
+ const originalContent = readFileSync(filePath, "utf-8");
1979
+ if (!hasReactGrabCode(originalContent)) {
2113
1980
  return {
2114
- success: true,
2115
- filePath: entryPath,
2116
- message: "React Grab is already installed in this file",
2117
- noChanges: true
1981
+ success: false,
1982
+ filePath,
1983
+ message: "Could not find React Grab code in the file"
2118
1984
  };
2119
1985
  }
2120
- const importBlock = WEBPACK_IMPORT_WITH_AGENT(agent);
2121
- const newContent = `${importBlock}
2122
-
2123
- ${originalContent}`;
2124
- return {
2125
- success: true,
2126
- filePath: entryPath,
2127
- message: "Add React Grab" + (agent !== "none" ? ` with ${agent} agent` : ""),
2128
- originalContent,
2129
- newContent
2130
- };
2131
- };
2132
- var previewTransform = (projectRoot, framework, nextRouterType, agent, reactGrabAlreadyConfigured = false) => {
2133
1986
  switch (framework) {
2134
1987
  case "next":
2135
- if (nextRouterType === "app") {
2136
- return transformNextAppRouter(
2137
- projectRoot,
2138
- agent,
2139
- reactGrabAlreadyConfigured
2140
- );
2141
- }
2142
- return transformNextPagesRouter(
2143
- projectRoot,
2144
- agent,
2145
- reactGrabAlreadyConfigured
2146
- );
1988
+ return addOptionsToNextScript(originalContent, options2, filePath);
2147
1989
  case "vite":
2148
- return transformVite(projectRoot, agent, reactGrabAlreadyConfigured);
1990
+ return addOptionsToViteScript(originalContent, options2, filePath);
2149
1991
  case "webpack":
2150
- return transformWebpack(projectRoot, agent, reactGrabAlreadyConfigured);
1992
+ return addOptionsToWebpackImport(originalContent, options2, filePath);
2151
1993
  default:
2152
1994
  return {
2153
1995
  success: false,
2154
- filePath: "",
2155
- message: `Unknown framework: ${framework}. Please add React Grab manually.`
1996
+ filePath,
1997
+ message: `Unknown framework: ${framework}`
2156
1998
  };
2157
1999
  }
2158
2000
  };
2159
- var canWriteToFile = (filePath) => {
2160
- try {
2161
- accessSync(filePath, constants.W_OK);
2162
- return true;
2163
- } catch {
2164
- return false;
2001
+ var applyOptionsTransform = (result) => {
2002
+ return applyTransform(result);
2003
+ };
2004
+ var removeAgentFromNextApp = (originalContent, agent, filePath) => {
2005
+ const agentPackage = `@react-grab/${agent}`;
2006
+ if (!originalContent.includes(agentPackage)) {
2007
+ return {
2008
+ success: true,
2009
+ filePath,
2010
+ message: `Agent ${agent} is not configured in this file`,
2011
+ noChanges: true
2012
+ };
2013
+ }
2014
+ const agentScriptPattern = new RegExp(
2015
+ `\\s*\\{process\\.env\\.NODE_ENV === "development" && \\(\\s*<Script[^>]*${agentPackage.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[^>]*\\/>\\s*\\)\\}`,
2016
+ "gs"
2017
+ );
2018
+ const simpleScriptPattern = new RegExp(
2019
+ `\\s*<Script[^>]*${agentPackage.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[^>]*\\/>`,
2020
+ "gi"
2021
+ );
2022
+ let newContent = originalContent.replace(agentScriptPattern, "");
2023
+ if (newContent === originalContent) {
2024
+ newContent = originalContent.replace(simpleScriptPattern, "");
2025
+ }
2026
+ if (newContent === originalContent) {
2027
+ return {
2028
+ success: false,
2029
+ filePath,
2030
+ message: `Could not find agent ${agent} script to remove`
2031
+ };
2165
2032
  }
2033
+ return {
2034
+ success: true,
2035
+ filePath,
2036
+ message: `Remove ${agent} agent`,
2037
+ originalContent,
2038
+ newContent
2039
+ };
2166
2040
  };
2167
- var applyTransform = (result) => {
2168
- if (result.success && result.newContent && result.filePath) {
2169
- if (!canWriteToFile(result.filePath)) {
2170
- return {
2171
- success: false,
2172
- error: `Cannot write to ${result.filePath}. Check file permissions.`
2173
- };
2174
- }
2175
- try {
2176
- writeFileSync(result.filePath, result.newContent);
2177
- return { success: true };
2178
- } catch (error48) {
2179
- return {
2180
- success: false,
2181
- error: `Failed to write to ${result.filePath}: ${error48 instanceof Error ? error48.message : "Unknown error"}`
2182
- };
2183
- }
2041
+ var removeAgentFromVite = (originalContent, agent, filePath) => {
2042
+ const agentPackage = `@react-grab/${agent}`;
2043
+ if (!originalContent.includes(agentPackage)) {
2044
+ return {
2045
+ success: true,
2046
+ filePath,
2047
+ message: `Agent ${agent} is not configured in this file`,
2048
+ noChanges: true
2049
+ };
2050
+ }
2051
+ const agentImportPattern = new RegExp(
2052
+ `\\s*import\\s*\\(\\s*["']${agentPackage.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}/client["']\\s*\\);?`,
2053
+ "g"
2054
+ );
2055
+ const newContent = originalContent.replace(agentImportPattern, "");
2056
+ if (newContent === originalContent) {
2057
+ return {
2058
+ success: false,
2059
+ filePath,
2060
+ message: `Could not find agent ${agent} import to remove`
2061
+ };
2184
2062
  }
2185
- return { success: true };
2063
+ return {
2064
+ success: true,
2065
+ filePath,
2066
+ message: `Remove ${agent} agent`,
2067
+ originalContent,
2068
+ newContent
2069
+ };
2186
2070
  };
2187
- var getPackageExecutor = (packageManager) => {
2188
- switch (packageManager) {
2189
- case "bun":
2190
- return "bunx";
2191
- case "pnpm":
2192
- return "pnpm dlx";
2193
- case "yarn":
2194
- return "npx";
2195
- case "npm":
2196
- default:
2197
- return "npx";
2071
+ var removeAgentFromWebpack = (originalContent, agent, filePath) => {
2072
+ const agentPackage = `@react-grab/${agent}`;
2073
+ if (!originalContent.includes(agentPackage)) {
2074
+ return {
2075
+ success: true,
2076
+ filePath,
2077
+ message: `Agent ${agent} is not configured in this file`,
2078
+ noChanges: true
2079
+ };
2198
2080
  }
2081
+ const agentImportPattern = new RegExp(
2082
+ `\\s*import\\s*\\(\\s*["']${agentPackage.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}/client["']\\s*\\);?`,
2083
+ "g"
2084
+ );
2085
+ const newContent = originalContent.replace(agentImportPattern, "");
2086
+ if (newContent === originalContent) {
2087
+ return {
2088
+ success: false,
2089
+ filePath,
2090
+ message: `Could not find agent ${agent} import to remove`
2091
+ };
2092
+ }
2093
+ return {
2094
+ success: true,
2095
+ filePath,
2096
+ message: `Remove ${agent} agent`,
2097
+ originalContent,
2098
+ newContent
2099
+ };
2199
2100
  };
2200
- var AGENT_PACKAGES2 = {
2201
- "claude-code": "@react-grab/claude-code@latest",
2202
- cursor: "@react-grab/cursor@latest",
2203
- opencode: "@react-grab/opencode@latest",
2204
- codex: "@react-grab/codex@latest",
2205
- gemini: "@react-grab/gemini@latest",
2206
- amp: "@react-grab/amp@latest"
2207
- };
2208
- var getAgentPrefix = (agent, packageManager) => {
2209
- const agentPackage = AGENT_PACKAGES2[agent];
2210
- if (!agentPackage) return null;
2211
- const executor = getPackageExecutor(packageManager);
2212
- return `${executor} ${agentPackage} &&`;
2213
- };
2214
- var getAllAgentPrefixVariants = (agent) => {
2215
- const agentPackage = AGENT_PACKAGES2[agent];
2216
- if (!agentPackage) return [];
2217
- return [
2218
- `npx ${agentPackage} &&`,
2219
- `bunx ${agentPackage} &&`,
2220
- `pnpm dlx ${agentPackage} &&`,
2221
- `yarn dlx ${agentPackage} &&`
2222
- ];
2223
- };
2224
- var previewPackageJsonTransform = (projectRoot, agent, installedAgents, packageManager = "npm") => {
2225
- if (agent === "none" || agent === "visual-edit") {
2101
+ var previewAgentRemoval = (projectRoot, framework, nextRouterType, agent) => {
2102
+ const filePath = findReactGrabFile(projectRoot, framework, nextRouterType);
2103
+ if (!filePath) {
2226
2104
  return {
2227
2105
  success: true,
2228
2106
  filePath: "",
2229
- message: agent === "visual-edit" ? "Visual Edit does not require package.json modification" : "No agent selected, skipping package.json modification",
2107
+ message: "Could not find file containing React Grab configuration",
2230
2108
  noChanges: true
2231
2109
  };
2232
2110
  }
2111
+ const originalContent = readFileSync(filePath, "utf-8");
2112
+ switch (framework) {
2113
+ case "next":
2114
+ return removeAgentFromNextApp(originalContent, agent, filePath);
2115
+ case "vite":
2116
+ return removeAgentFromVite(originalContent, agent, filePath);
2117
+ case "webpack":
2118
+ return removeAgentFromWebpack(originalContent, agent, filePath);
2119
+ default:
2120
+ return {
2121
+ success: false,
2122
+ filePath,
2123
+ message: `Unknown framework: ${framework}`
2124
+ };
2125
+ }
2126
+ };
2127
+ var previewPackageJsonAgentRemoval = (projectRoot, agent) => {
2233
2128
  const packageJsonPath = join(projectRoot, "package.json");
2234
2129
  if (!existsSync(packageJsonPath)) {
2235
2130
  return {
2236
- success: false,
2131
+ success: true,
2237
2132
  filePath: "",
2238
- message: "Could not find package.json"
2133
+ message: "Could not find package.json",
2134
+ noChanges: true
2239
2135
  };
2240
2136
  }
2241
2137
  const originalContent = readFileSync(packageJsonPath, "utf-8");
2242
- const agentPrefix = getAgentPrefix(agent, packageManager);
2243
- if (!agentPrefix) {
2138
+ const allPrefixVariants = getAllAgentPrefixVariants(agent);
2139
+ if (allPrefixVariants.length === 0) {
2244
2140
  return {
2245
- success: false,
2141
+ success: true,
2246
2142
  filePath: packageJsonPath,
2247
- message: `Unknown agent: ${agent}`
2143
+ message: `Unknown agent: ${agent}`,
2144
+ noChanges: true
2248
2145
  };
2249
2146
  }
2250
- const allPrefixVariants = getAllAgentPrefixVariants(agent);
2251
- const hasExistingPrefix = allPrefixVariants.some(
2147
+ const hasAnyPrefix = allPrefixVariants.some(
2252
2148
  (prefix) => originalContent.includes(prefix)
2253
2149
  );
2254
- if (hasExistingPrefix) {
2150
+ if (!hasAnyPrefix) {
2255
2151
  return {
2256
2152
  success: true,
2257
2153
  filePath: packageJsonPath,
2258
- message: `Agent ${agent} dev script is already configured`,
2154
+ message: `Agent ${agent} dev script is not configured`,
2259
2155
  noChanges: true
2260
2156
  };
2261
2157
  }
2262
2158
  try {
2263
2159
  const packageJson = JSON.parse(originalContent);
2264
- let targetScriptKey = "dev";
2265
- if (!packageJson.scripts?.dev) {
2266
- const devScriptKeys = Object.keys(packageJson.scripts || {}).filter(
2267
- (key) => key.startsWith("dev")
2268
- );
2269
- if (devScriptKeys.length > 0) {
2270
- targetScriptKey = devScriptKeys[0];
2271
- } else {
2272
- return {
2273
- success: true,
2274
- filePath: packageJsonPath,
2275
- message: "No dev script found in package.json",
2276
- noChanges: true,
2277
- warning: `Could not inject agent into package.json (no dev script found).
2278
- Run this command manually before starting your dev server:
2279
- ${agentPrefix} <your dev command>`
2280
- };
2281
- }
2282
- }
2283
- const currentDevScript = packageJson.scripts[targetScriptKey];
2284
- for (const installedAgent of installedAgents) {
2285
- const installedPrefixVariants = getAllAgentPrefixVariants(installedAgent);
2286
- const hasInstalledAgentPrefix = installedPrefixVariants.some(
2287
- (prefix) => currentDevScript.includes(prefix)
2288
- );
2289
- if (hasInstalledAgentPrefix) {
2290
- return {
2291
- success: true,
2292
- filePath: packageJsonPath,
2293
- message: `Agent ${installedAgent} is already in ${targetScriptKey} script`,
2294
- noChanges: true
2295
- };
2160
+ for (const scriptKey of Object.keys(packageJson.scripts || {})) {
2161
+ let scriptValue = packageJson.scripts[scriptKey];
2162
+ if (typeof scriptValue === "string") {
2163
+ for (const prefix of allPrefixVariants) {
2164
+ if (scriptValue.includes(prefix)) {
2165
+ scriptValue = scriptValue.replace(prefix + " ", "").replace(prefix, "");
2166
+ }
2167
+ }
2168
+ packageJson.scripts[scriptKey] = scriptValue;
2296
2169
  }
2297
2170
  }
2298
- packageJson.scripts[targetScriptKey] = `${agentPrefix} ${currentDevScript}`;
2299
2171
  const newContent = JSON.stringify(packageJson, null, 2) + "\n";
2300
2172
  return {
2301
2173
  success: true,
2302
2174
  filePath: packageJsonPath,
2303
- message: `Add ${agent} server to ${targetScriptKey} script`,
2175
+ message: `Remove ${agent} server from dev script`,
2304
2176
  originalContent,
2305
2177
  newContent
2306
2178
  };
@@ -2312,393 +2184,596 @@ Run this command manually before starting your dev server:
2312
2184
  };
2313
2185
  }
2314
2186
  };
2315
- var applyPackageJsonTransform = (result) => {
2316
- if (result.success && result.newContent && result.filePath) {
2317
- if (!canWriteToFile(result.filePath)) {
2318
- return {
2319
- success: false,
2320
- error: `Cannot write to ${result.filePath}. Check file permissions.`
2321
- };
2322
- }
2323
- try {
2324
- writeFileSync(result.filePath, result.newContent);
2325
- return { success: true };
2326
- } catch (error48) {
2327
- return {
2328
- success: false,
2329
- error: `Failed to write to ${result.filePath}: ${error48 instanceof Error ? error48.message : "Unknown error"}`
2330
- };
2331
- }
2187
+ var highlighter = {
2188
+ error: pc.red,
2189
+ warn: pc.yellow,
2190
+ info: pc.cyan,
2191
+ success: pc.green,
2192
+ dim: pc.dim
2193
+ };
2194
+
2195
+ // src/utils/logger.ts
2196
+ var logger = {
2197
+ error(...args) {
2198
+ console.log(highlighter.error(args.join(" ")));
2199
+ },
2200
+ warn(...args) {
2201
+ console.log(highlighter.warn(args.join(" ")));
2202
+ },
2203
+ info(...args) {
2204
+ console.log(highlighter.info(args.join(" ")));
2205
+ },
2206
+ success(...args) {
2207
+ console.log(highlighter.success(args.join(" ")));
2208
+ },
2209
+ dim(...args) {
2210
+ console.log(highlighter.dim(args.join(" ")));
2211
+ },
2212
+ log(...args) {
2213
+ console.log(args.join(" "));
2214
+ },
2215
+ break() {
2216
+ console.log("");
2217
+ }
2218
+ };
2219
+
2220
+ // src/utils/handle-error.ts
2221
+ var handleError = (error48) => {
2222
+ logger.break();
2223
+ logger.error(
2224
+ "Something went wrong. Please check the error below for more details."
2225
+ );
2226
+ logger.error("If the problem persists, please open an issue on GitHub.");
2227
+ logger.error("");
2228
+ if (error48 instanceof Error) {
2229
+ logger.error(error48.message);
2230
+ }
2231
+ logger.break();
2232
+ process.exit(1);
2233
+ };
2234
+ var INSTALL_COMMANDS = {
2235
+ npm: "npm install",
2236
+ yarn: "yarn add",
2237
+ pnpm: "pnpm add",
2238
+ bun: "bun add"
2239
+ };
2240
+ var UNINSTALL_COMMANDS = {
2241
+ npm: "npm uninstall",
2242
+ yarn: "yarn remove",
2243
+ pnpm: "pnpm remove",
2244
+ bun: "bun remove"
2245
+ };
2246
+ var installPackages = (packages, packageManager, projectRoot, isDev = true) => {
2247
+ if (packages.length === 0) {
2248
+ return;
2249
+ }
2250
+ const command = INSTALL_COMMANDS[packageManager];
2251
+ const devFlag = isDev ? " -D" : "";
2252
+ const fullCommand = `${command}${devFlag} ${packages.join(" ")}`;
2253
+ console.log(`Running: ${fullCommand}
2254
+ `);
2255
+ execSync(fullCommand, {
2256
+ cwd: projectRoot,
2257
+ stdio: "inherit"
2258
+ });
2259
+ };
2260
+ var getPackagesToInstall = (agent, includeReactGrab = true) => {
2261
+ const packages = [];
2262
+ if (includeReactGrab) {
2263
+ packages.push("react-grab");
2332
2264
  }
2333
- return { success: true };
2265
+ if (agent !== "none") {
2266
+ packages.push(`@react-grab/${agent}`);
2267
+ }
2268
+ return packages;
2334
2269
  };
2335
- var formatOptionsForNextjs = (options2) => {
2336
- const parts = [];
2337
- if (options2.activationKey) {
2338
- parts.push(`activationKey: ${JSON.stringify(options2.activationKey)}`);
2270
+ var uninstallPackages = (packages, packageManager, projectRoot) => {
2271
+ if (packages.length === 0) {
2272
+ return;
2339
2273
  }
2340
- if (options2.activationMode) {
2341
- parts.push(`activationMode: "${options2.activationMode}"`);
2274
+ const command = UNINSTALL_COMMANDS[packageManager];
2275
+ const fullCommand = `${command} ${packages.join(" ")}`;
2276
+ console.log(`Running: ${fullCommand}
2277
+ `);
2278
+ execSync(fullCommand, {
2279
+ cwd: projectRoot,
2280
+ stdio: "inherit"
2281
+ });
2282
+ };
2283
+ var getPackagesToUninstall = (agent) => {
2284
+ return [`@react-grab/${agent}`];
2285
+ };
2286
+ var spinner = (text, options2) => ora({ text, isSilent: options2?.silent });
2287
+ var detectSkillAgents = (cwd) => {
2288
+ return SKILL_AGENTS.filter((agent) => existsSync(join(cwd, agent.folder)));
2289
+ };
2290
+ var findSkillAgent = (id) => {
2291
+ return SKILL_AGENTS.find((agent) => agent.id === id);
2292
+ };
2293
+ var formatInstalledAgentNames = (agents) => agents.map((agent) => AGENT_NAMES[agent] ?? agent).join(", ");
2294
+ var applyTransformWithFeedback = (result, message) => {
2295
+ const writeSpinner = spinner(message ?? `Applying changes to ${result.filePath}.`).start();
2296
+ const writeResult = applyTransform(result);
2297
+ if (!writeResult.success) {
2298
+ writeSpinner.fail();
2299
+ logger.break();
2300
+ logger.error(writeResult.error || "Failed to write file.");
2301
+ logger.break();
2302
+ process.exit(1);
2342
2303
  }
2343
- if (options2.keyHoldDuration !== void 0) {
2344
- parts.push(`keyHoldDuration: ${options2.keyHoldDuration}`);
2304
+ writeSpinner.succeed();
2305
+ };
2306
+ var applyPackageJsonWithFeedback = (result, message) => {
2307
+ const writeSpinner = spinner(message ?? `Applying changes to ${result.filePath}.`).start();
2308
+ const writeResult = applyPackageJsonTransform(result);
2309
+ if (!writeResult.success) {
2310
+ writeSpinner.fail();
2311
+ logger.break();
2312
+ logger.error(writeResult.error || "Failed to write file.");
2313
+ logger.break();
2314
+ process.exit(1);
2345
2315
  }
2346
- if (options2.allowActivationInsideInput !== void 0) {
2347
- parts.push(
2348
- `allowActivationInsideInput: ${options2.allowActivationInsideInput}`
2349
- );
2316
+ writeSpinner.succeed();
2317
+ };
2318
+ var installPackagesWithFeedback = (packages, packageManager, projectRoot) => {
2319
+ if (packages.length === 0) return;
2320
+ const installSpinner = spinner(`Installing ${packages.join(", ")}.`).start();
2321
+ try {
2322
+ installPackages(packages, packageManager, projectRoot);
2323
+ installSpinner.succeed();
2324
+ } catch (error48) {
2325
+ installSpinner.fail();
2326
+ handleError(error48);
2350
2327
  }
2351
- if (options2.maxContextLines !== void 0) {
2352
- parts.push(`maxContextLines: ${options2.maxContextLines}`);
2328
+ };
2329
+ var uninstallPackagesWithFeedback = (packages, packageManager, projectRoot) => {
2330
+ if (packages.length === 0) return;
2331
+ const uninstallSpinner = spinner(`Removing ${packages.join(", ")}.`).start();
2332
+ try {
2333
+ uninstallPackages(packages, packageManager, projectRoot);
2334
+ uninstallSpinner.succeed();
2335
+ } catch (error48) {
2336
+ uninstallSpinner.fail();
2337
+ handleError(error48);
2353
2338
  }
2354
- return `{ ${parts.join(", ")} }`;
2355
2339
  };
2356
- var formatOptionsAsJson = (options2) => {
2357
- const cleanOptions = {};
2358
- if (options2.activationKey) {
2359
- cleanOptions.activationKey = options2.activationKey;
2340
+ var detectPackageManager = async (projectRoot) => {
2341
+ const detected = await detect({ cwd: projectRoot });
2342
+ if (detected && ["npm", "yarn", "pnpm", "bun"].includes(detected)) {
2343
+ return detected;
2360
2344
  }
2361
- if (options2.activationMode) {
2362
- cleanOptions.activationMode = options2.activationMode;
2345
+ return "npm";
2346
+ };
2347
+ var detectFramework = (projectRoot) => {
2348
+ const packageJsonPath = join(projectRoot, "package.json");
2349
+ if (!existsSync(packageJsonPath)) {
2350
+ return "unknown";
2363
2351
  }
2364
- if (options2.keyHoldDuration !== void 0) {
2365
- cleanOptions.keyHoldDuration = options2.keyHoldDuration;
2352
+ try {
2353
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
2354
+ const allDependencies = {
2355
+ ...packageJson.dependencies,
2356
+ ...packageJson.devDependencies
2357
+ };
2358
+ if (allDependencies["next"]) {
2359
+ return "next";
2360
+ }
2361
+ if (allDependencies["vite"]) {
2362
+ return "vite";
2363
+ }
2364
+ if (allDependencies["webpack"]) {
2365
+ return "webpack";
2366
+ }
2367
+ return "unknown";
2368
+ } catch {
2369
+ return "unknown";
2366
2370
  }
2367
- if (options2.allowActivationInsideInput !== void 0) {
2368
- cleanOptions.allowActivationInsideInput = options2.allowActivationInsideInput;
2371
+ };
2372
+ var detectNextRouterType = (projectRoot) => {
2373
+ const hasAppDir = existsSync(join(projectRoot, "app"));
2374
+ const hasSrcAppDir = existsSync(join(projectRoot, "src", "app"));
2375
+ const hasPagesDir = existsSync(join(projectRoot, "pages"));
2376
+ const hasSrcPagesDir = existsSync(join(projectRoot, "src", "pages"));
2377
+ if (hasAppDir || hasSrcAppDir) {
2378
+ return "app";
2369
2379
  }
2370
- if (options2.maxContextLines !== void 0) {
2371
- cleanOptions.maxContextLines = options2.maxContextLines;
2380
+ if (hasPagesDir || hasSrcPagesDir) {
2381
+ return "pages";
2372
2382
  }
2373
- return JSON.stringify(cleanOptions);
2383
+ return "unknown";
2374
2384
  };
2375
- var findReactGrabFile = (projectRoot, framework, nextRouterType) => {
2376
- switch (framework) {
2377
- case "next":
2378
- if (nextRouterType === "app") {
2379
- return findLayoutFile(projectRoot);
2385
+ var detectMonorepo = (projectRoot) => {
2386
+ if (existsSync(join(projectRoot, "pnpm-workspace.yaml"))) {
2387
+ return true;
2388
+ }
2389
+ if (existsSync(join(projectRoot, "lerna.json"))) {
2390
+ return true;
2391
+ }
2392
+ const packageJsonPath = join(projectRoot, "package.json");
2393
+ if (existsSync(packageJsonPath)) {
2394
+ try {
2395
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
2396
+ if (packageJson.workspaces) {
2397
+ return true;
2380
2398
  }
2381
- return findDocumentFile(projectRoot);
2382
- case "vite":
2383
- return findIndexHtml(projectRoot);
2384
- case "webpack":
2385
- return findEntryFile(projectRoot);
2386
- default:
2387
- return null;
2399
+ } catch {
2400
+ return false;
2401
+ }
2388
2402
  }
2403
+ return false;
2389
2404
  };
2390
- var addOptionsToNextScript = (originalContent, options2, filePath) => {
2391
- const reactGrabScriptMatch = originalContent.match(
2392
- /(<Script[^>]*react-grab[^>]*)(\/?>)/is
2393
- );
2394
- if (!reactGrabScriptMatch) {
2395
- return {
2396
- success: false,
2397
- filePath,
2398
- message: "Could not find React Grab Script tag"
2399
- };
2405
+ var getWorkspacePatterns = (projectRoot) => {
2406
+ const patterns = [];
2407
+ const pnpmWorkspacePath = join(projectRoot, "pnpm-workspace.yaml");
2408
+ if (existsSync(pnpmWorkspacePath)) {
2409
+ const content = readFileSync(pnpmWorkspacePath, "utf-8");
2410
+ const lines = content.split("\n");
2411
+ let inPackages = false;
2412
+ for (const line of lines) {
2413
+ if (line.match(/^packages:\s*$/)) {
2414
+ inPackages = true;
2415
+ continue;
2416
+ }
2417
+ if (inPackages) {
2418
+ if (line.match(/^[a-zA-Z]/) || line.trim() === "") {
2419
+ if (line.match(/^[a-zA-Z]/)) inPackages = false;
2420
+ continue;
2421
+ }
2422
+ const match = line.match(/^\s*-\s*['"]?([^'"#\n]+?)['"]?\s*$/);
2423
+ if (match) {
2424
+ patterns.push(match[1].trim());
2425
+ }
2426
+ }
2427
+ }
2400
2428
  }
2401
- const scriptTag = reactGrabScriptMatch[0];
2402
- const scriptOpening = reactGrabScriptMatch[1];
2403
- const scriptClosing = reactGrabScriptMatch[2];
2404
- const existingDataOptionsMatch = scriptTag.match(
2405
- /data-options=\{JSON\.stringify\([^)]+\)\}/
2406
- );
2407
- const dataOptionsAttr = `data-options={JSON.stringify(
2408
- ${formatOptionsForNextjs(options2)}
2409
- )}`;
2410
- let newScriptTag;
2411
- if (existingDataOptionsMatch) {
2412
- newScriptTag = scriptTag.replace(
2413
- existingDataOptionsMatch[0],
2414
- dataOptionsAttr
2415
- );
2416
- } else {
2417
- newScriptTag = `${scriptOpening}
2418
- ${dataOptionsAttr}
2419
- ${scriptClosing}`;
2429
+ const lernaJsonPath = join(projectRoot, "lerna.json");
2430
+ if (existsSync(lernaJsonPath)) {
2431
+ try {
2432
+ const lernaJson = JSON.parse(readFileSync(lernaJsonPath, "utf-8"));
2433
+ if (Array.isArray(lernaJson.packages)) {
2434
+ patterns.push(...lernaJson.packages);
2435
+ }
2436
+ } catch {
2437
+ }
2420
2438
  }
2421
- const newContent = originalContent.replace(scriptTag, newScriptTag);
2422
- return {
2423
- success: true,
2424
- filePath,
2425
- message: "Update React Grab options",
2426
- originalContent,
2427
- newContent
2428
- };
2429
- };
2430
- var addOptionsToViteScript = (originalContent, options2, filePath) => {
2431
- const reactGrabImportMatch = originalContent.match(
2432
- /import\s*\(\s*["']react-grab["']\s*\)/
2433
- );
2434
- if (!reactGrabImportMatch) {
2435
- return {
2436
- success: false,
2437
- filePath,
2438
- message: "Could not find React Grab import"
2439
- };
2439
+ const packageJsonPath = join(projectRoot, "package.json");
2440
+ if (existsSync(packageJsonPath)) {
2441
+ try {
2442
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
2443
+ if (Array.isArray(packageJson.workspaces)) {
2444
+ patterns.push(...packageJson.workspaces);
2445
+ } else if (packageJson.workspaces?.packages) {
2446
+ patterns.push(...packageJson.workspaces.packages);
2447
+ }
2448
+ } catch {
2449
+ }
2440
2450
  }
2441
- const optionsJson = formatOptionsAsJson(options2);
2442
- const newImport = `import("react-grab").then((m) => m.init(${optionsJson}))`;
2443
- const newContent = originalContent.replace(
2444
- reactGrabImportMatch[0],
2445
- newImport
2446
- );
2447
- return {
2448
- success: true,
2449
- filePath,
2450
- message: "Update React Grab options",
2451
- originalContent,
2452
- newContent
2453
- };
2451
+ return [...new Set(patterns)];
2454
2452
  };
2455
- var addOptionsToWebpackImport = (originalContent, options2, filePath) => {
2456
- const reactGrabImportMatch = originalContent.match(
2457
- /import\s*\(\s*["']react-grab["']\s*\)/
2458
- );
2459
- if (!reactGrabImportMatch) {
2460
- return {
2461
- success: false,
2462
- filePath,
2463
- message: "Could not find React Grab import"
2464
- };
2453
+ var expandWorkspacePattern = (projectRoot, pattern) => {
2454
+ const results = [];
2455
+ const cleanPattern = pattern.replace(/\/\*$/, "");
2456
+ const basePath = join(projectRoot, cleanPattern);
2457
+ if (!existsSync(basePath)) return results;
2458
+ try {
2459
+ const entries = readdirSync(basePath, { withFileTypes: true });
2460
+ for (const entry of entries) {
2461
+ if (entry.isDirectory()) {
2462
+ const packageJsonPath = join(basePath, entry.name, "package.json");
2463
+ if (existsSync(packageJsonPath)) {
2464
+ results.push(join(basePath, entry.name));
2465
+ }
2466
+ }
2467
+ }
2468
+ } catch {
2469
+ return results;
2465
2470
  }
2466
- const optionsJson = formatOptionsAsJson(options2);
2467
- const newImport = `import("react-grab").then((m) => m.init(${optionsJson}))`;
2468
- const newContent = originalContent.replace(
2469
- reactGrabImportMatch[0],
2470
- newImport
2471
- );
2472
- return {
2473
- success: true,
2474
- filePath,
2475
- message: "Update React Grab options",
2476
- originalContent,
2477
- newContent
2478
- };
2471
+ return results;
2479
2472
  };
2480
- var previewOptionsTransform = (projectRoot, framework, nextRouterType, options2) => {
2481
- const filePath = findReactGrabFile(projectRoot, framework, nextRouterType);
2482
- if (!filePath) {
2483
- return {
2484
- success: false,
2485
- filePath: "",
2486
- message: "Could not find file containing React Grab configuration"
2487
- };
2488
- }
2489
- const originalContent = readFileSync(filePath, "utf-8");
2490
- if (!hasReactGrabCode(originalContent)) {
2491
- return {
2492
- success: false,
2493
- filePath,
2494
- message: "Could not find React Grab code in the file"
2473
+ var hasReactDependency = (projectPath) => {
2474
+ const packageJsonPath = join(projectPath, "package.json");
2475
+ if (!existsSync(packageJsonPath)) return false;
2476
+ try {
2477
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
2478
+ const allDeps = {
2479
+ ...packageJson.dependencies,
2480
+ ...packageJson.devDependencies
2495
2481
  };
2482
+ return Boolean(allDeps["react"] || allDeps["react-dom"]);
2483
+ } catch {
2484
+ return false;
2496
2485
  }
2497
- switch (framework) {
2498
- case "next":
2499
- return addOptionsToNextScript(originalContent, options2, filePath);
2500
- case "vite":
2501
- return addOptionsToViteScript(originalContent, options2, filePath);
2502
- case "webpack":
2503
- return addOptionsToWebpackImport(originalContent, options2, filePath);
2504
- default:
2505
- return {
2506
- success: false,
2507
- filePath,
2508
- message: `Unknown framework: ${framework}`
2509
- };
2510
- }
2511
- };
2512
- var applyOptionsTransform = (result) => {
2513
- return applyTransform(result);
2514
2486
  };
2515
- var removeAgentFromNextApp = (originalContent, agent, filePath) => {
2516
- const agentPackage = `@react-grab/${agent}`;
2517
- if (!originalContent.includes(agentPackage)) {
2518
- return {
2519
- success: true,
2520
- filePath,
2521
- message: `Agent ${agent} is not configured in this file`,
2522
- noChanges: true
2523
- };
2487
+ var findWorkspaceProjects = (projectRoot) => {
2488
+ const patterns = getWorkspacePatterns(projectRoot);
2489
+ const projects = [];
2490
+ for (const pattern of patterns) {
2491
+ const projectPaths = expandWorkspacePattern(projectRoot, pattern);
2492
+ for (const projectPath of projectPaths) {
2493
+ const framework = detectFramework(projectPath);
2494
+ const hasReact = hasReactDependency(projectPath);
2495
+ if (hasReact || framework !== "unknown") {
2496
+ const packageJsonPath = join(projectPath, "package.json");
2497
+ let name = basename(projectPath);
2498
+ try {
2499
+ const packageJson = JSON.parse(
2500
+ readFileSync(packageJsonPath, "utf-8")
2501
+ );
2502
+ name = packageJson.name || name;
2503
+ } catch {
2504
+ }
2505
+ projects.push({
2506
+ name,
2507
+ path: projectPath,
2508
+ framework,
2509
+ hasReact
2510
+ });
2511
+ }
2512
+ }
2524
2513
  }
2525
- const agentScriptPattern = new RegExp(
2526
- `\\s*\\{process\\.env\\.NODE_ENV === "development" && \\(\\s*<Script[^>]*${agentPackage.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[^>]*\\/>\\s*\\)\\}`,
2527
- "gs"
2528
- );
2529
- const simpleScriptPattern = new RegExp(
2530
- `\\s*<Script[^>]*${agentPackage.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[^>]*\\/>`,
2531
- "gi"
2532
- );
2533
- let newContent = originalContent.replace(agentScriptPattern, "");
2534
- if (newContent === originalContent) {
2535
- newContent = originalContent.replace(simpleScriptPattern, "");
2514
+ return projects;
2515
+ };
2516
+ var hasReactGrabInFile = (filePath) => {
2517
+ if (!existsSync(filePath)) return false;
2518
+ try {
2519
+ const content = readFileSync(filePath, "utf-8");
2520
+ const fuzzyPatterns = [
2521
+ /["'`][^"'`]*react-grab/,
2522
+ /react-grab[^"'`]*["'`]/,
2523
+ /<[^>]*react-grab/i,
2524
+ /import[^;]*react-grab/i,
2525
+ /require[^)]*react-grab/i,
2526
+ /from\s+[^;]*react-grab/i,
2527
+ /src[^>]*react-grab/i
2528
+ ];
2529
+ return fuzzyPatterns.some((pattern) => pattern.test(content));
2530
+ } catch {
2531
+ return false;
2536
2532
  }
2537
- if (newContent === originalContent) {
2538
- return {
2539
- success: false,
2540
- filePath,
2541
- message: `Could not find agent ${agent} script to remove`
2542
- };
2533
+ };
2534
+ var detectReactGrab = (projectRoot) => {
2535
+ const packageJsonPath = join(projectRoot, "package.json");
2536
+ if (existsSync(packageJsonPath)) {
2537
+ try {
2538
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
2539
+ const allDependencies = {
2540
+ ...packageJson.dependencies,
2541
+ ...packageJson.devDependencies
2542
+ };
2543
+ if (allDependencies["react-grab"]) {
2544
+ return true;
2545
+ }
2546
+ } catch {
2547
+ }
2543
2548
  }
2544
- return {
2545
- success: true,
2546
- filePath,
2547
- message: `Remove ${agent} agent`,
2548
- originalContent,
2549
- newContent
2550
- };
2549
+ const filesToCheck = [
2550
+ join(projectRoot, "app", "layout.tsx"),
2551
+ join(projectRoot, "app", "layout.jsx"),
2552
+ join(projectRoot, "src", "app", "layout.tsx"),
2553
+ join(projectRoot, "src", "app", "layout.jsx"),
2554
+ join(projectRoot, "pages", "_document.tsx"),
2555
+ join(projectRoot, "pages", "_document.jsx"),
2556
+ join(projectRoot, "instrumentation-client.ts"),
2557
+ join(projectRoot, "instrumentation-client.js"),
2558
+ join(projectRoot, "src", "instrumentation-client.ts"),
2559
+ join(projectRoot, "src", "instrumentation-client.js"),
2560
+ join(projectRoot, "index.html"),
2561
+ join(projectRoot, "public", "index.html"),
2562
+ join(projectRoot, "src", "index.tsx"),
2563
+ join(projectRoot, "src", "index.ts"),
2564
+ join(projectRoot, "src", "main.tsx"),
2565
+ join(projectRoot, "src", "main.ts")
2566
+ ];
2567
+ return filesToCheck.some(hasReactGrabInFile);
2551
2568
  };
2552
- var removeAgentFromVite = (originalContent, agent, filePath) => {
2553
- const agentPackage = `@react-grab/${agent}`;
2554
- if (!originalContent.includes(agentPackage)) {
2555
- return {
2556
- success: true,
2557
- filePath,
2558
- message: `Agent ${agent} is not configured in this file`,
2559
- noChanges: true
2560
- };
2569
+ var AGENT_PACKAGES2 = [
2570
+ "@react-grab/claude-code",
2571
+ "@react-grab/cursor",
2572
+ "@react-grab/opencode",
2573
+ "@react-grab/codex",
2574
+ "@react-grab/gemini",
2575
+ "@react-grab/amp",
2576
+ "@react-grab/ami",
2577
+ "@react-grab/visual-edit"
2578
+ ];
2579
+ var detectUnsupportedFramework = (projectRoot) => {
2580
+ const packageJsonPath = join(projectRoot, "package.json");
2581
+ if (!existsSync(packageJsonPath)) {
2582
+ return null;
2561
2583
  }
2562
- const agentImportPattern = new RegExp(
2563
- `\\s*import\\s*\\(\\s*["']${agentPackage.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}/client["']\\s*\\);?`,
2564
- "g"
2565
- );
2566
- const newContent = originalContent.replace(agentImportPattern, "");
2567
- if (newContent === originalContent) {
2568
- return {
2569
- success: false,
2570
- filePath,
2571
- message: `Could not find agent ${agent} import to remove`
2584
+ try {
2585
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
2586
+ const allDependencies = {
2587
+ ...packageJson.dependencies,
2588
+ ...packageJson.devDependencies
2572
2589
  };
2590
+ if (allDependencies["@remix-run/react"] || allDependencies["remix"]) {
2591
+ return "remix";
2592
+ }
2593
+ if (allDependencies["astro"]) {
2594
+ return "astro";
2595
+ }
2596
+ if (allDependencies["@sveltejs/kit"]) {
2597
+ return "sveltekit";
2598
+ }
2599
+ if (allDependencies["gatsby"]) {
2600
+ return "gatsby";
2601
+ }
2602
+ return null;
2603
+ } catch {
2604
+ return null;
2573
2605
  }
2574
- return {
2575
- success: true,
2576
- filePath,
2577
- message: `Remove ${agent} agent`,
2578
- originalContent,
2579
- newContent
2580
- };
2581
2606
  };
2582
- var removeAgentFromWebpack = (originalContent, agent, filePath) => {
2583
- const agentPackage = `@react-grab/${agent}`;
2584
- if (!originalContent.includes(agentPackage)) {
2585
- return {
2586
- success: true,
2587
- filePath,
2588
- message: `Agent ${agent} is not configured in this file`,
2589
- noChanges: true
2590
- };
2607
+ var detectInstalledAgents = (projectRoot) => {
2608
+ const packageJsonPath = join(projectRoot, "package.json");
2609
+ if (!existsSync(packageJsonPath)) {
2610
+ return [];
2591
2611
  }
2592
- const agentImportPattern = new RegExp(
2593
- `\\s*import\\s*\\(\\s*["']${agentPackage.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}/client["']\\s*\\);?`,
2594
- "g"
2595
- );
2596
- const newContent = originalContent.replace(agentImportPattern, "");
2597
- if (newContent === originalContent) {
2598
- return {
2599
- success: false,
2600
- filePath,
2601
- message: `Could not find agent ${agent} import to remove`
2612
+ try {
2613
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
2614
+ const allDependencies = {
2615
+ ...packageJson.dependencies,
2616
+ ...packageJson.devDependencies
2602
2617
  };
2618
+ return AGENT_PACKAGES2.filter(
2619
+ (agent) => Boolean(allDependencies[agent])
2620
+ ).map((agent) => agent.replace("@react-grab/", ""));
2621
+ } catch {
2622
+ return [];
2603
2623
  }
2624
+ };
2625
+ var detectProject = async (projectRoot = process.cwd()) => {
2626
+ const framework = detectFramework(projectRoot);
2627
+ const packageManager = await detectPackageManager(projectRoot);
2604
2628
  return {
2605
- success: true,
2606
- filePath,
2607
- message: `Remove ${agent} agent`,
2608
- originalContent,
2609
- newContent
2629
+ packageManager,
2630
+ framework,
2631
+ nextRouterType: framework === "next" ? detectNextRouterType(projectRoot) : "unknown",
2632
+ isMonorepo: detectMonorepo(projectRoot),
2633
+ projectRoot,
2634
+ hasReactGrab: detectReactGrab(projectRoot),
2635
+ installedAgents: detectInstalledAgents(projectRoot),
2636
+ unsupportedFramework: detectUnsupportedFramework(projectRoot)
2610
2637
  };
2611
2638
  };
2612
- var previewAgentRemoval = (projectRoot, framework, nextRouterType, agent) => {
2613
- const filePath = findReactGrabFile(projectRoot, framework, nextRouterType);
2614
- if (!filePath) {
2615
- return {
2616
- success: true,
2617
- filePath: "",
2618
- message: "Could not find file containing React Grab configuration",
2619
- noChanges: true
2620
- };
2621
- }
2622
- const originalContent = readFileSync(filePath, "utf-8");
2623
- switch (framework) {
2624
- case "next":
2625
- return removeAgentFromNextApp(originalContent, agent, filePath);
2626
- case "vite":
2627
- return removeAgentFromVite(originalContent, agent, filePath);
2628
- case "webpack":
2629
- return removeAgentFromWebpack(originalContent, agent, filePath);
2630
- default:
2631
- return {
2632
- success: false,
2633
- filePath,
2634
- message: `Unknown framework: ${framework}`
2635
- };
2639
+
2640
+ // src/utils/diff.ts
2641
+ var RED = "\x1B[31m";
2642
+ var GREEN = "\x1B[32m";
2643
+ var GRAY = "\x1B[90m";
2644
+ var RESET = "\x1B[0m";
2645
+ var BOLD = "\x1B[1m";
2646
+ var generateDiff = (originalContent, newContent) => {
2647
+ const originalLines = originalContent.split("\n");
2648
+ const newLines = newContent.split("\n");
2649
+ const diff = [];
2650
+ Math.max(originalLines.length, newLines.length);
2651
+ let originalIndex = 0;
2652
+ let newIndex = 0;
2653
+ while (originalIndex < originalLines.length || newIndex < newLines.length) {
2654
+ const originalLine = originalLines[originalIndex];
2655
+ const newLine = newLines[newIndex];
2656
+ if (originalLine === newLine) {
2657
+ diff.push({
2658
+ type: "unchanged",
2659
+ content: originalLine,
2660
+ lineNumber: newIndex + 1
2661
+ });
2662
+ originalIndex++;
2663
+ newIndex++;
2664
+ } else if (originalLine === void 0) {
2665
+ diff.push({ type: "added", content: newLine, lineNumber: newIndex + 1 });
2666
+ newIndex++;
2667
+ } else if (newLine === void 0) {
2668
+ diff.push({ type: "removed", content: originalLine });
2669
+ originalIndex++;
2670
+ } else {
2671
+ const originalInNew = newLines.indexOf(originalLine, newIndex);
2672
+ const newInOriginal = originalLines.indexOf(newLine, originalIndex);
2673
+ if (originalInNew !== -1 && (newInOriginal === -1 || originalInNew - newIndex < newInOriginal - originalIndex)) {
2674
+ while (newIndex < originalInNew) {
2675
+ diff.push({
2676
+ type: "added",
2677
+ content: newLines[newIndex],
2678
+ lineNumber: newIndex + 1
2679
+ });
2680
+ newIndex++;
2681
+ }
2682
+ } else if (newInOriginal !== -1) {
2683
+ while (originalIndex < newInOriginal) {
2684
+ diff.push({ type: "removed", content: originalLines[originalIndex] });
2685
+ originalIndex++;
2686
+ }
2687
+ } else {
2688
+ diff.push({ type: "removed", content: originalLine });
2689
+ diff.push({
2690
+ type: "added",
2691
+ content: newLine,
2692
+ lineNumber: newIndex + 1
2693
+ });
2694
+ originalIndex++;
2695
+ newIndex++;
2696
+ }
2697
+ }
2636
2698
  }
2699
+ return diff;
2637
2700
  };
2638
- var previewPackageJsonAgentRemoval = (projectRoot, agent) => {
2639
- const packageJsonPath = join(projectRoot, "package.json");
2640
- if (!existsSync(packageJsonPath)) {
2641
- return {
2642
- success: true,
2643
- filePath: "",
2644
- message: "Could not find package.json",
2645
- noChanges: true
2646
- };
2701
+ var formatDiff = (diff, contextLines = 3) => {
2702
+ const lines = [];
2703
+ let lastPrintedIndex = -1;
2704
+ let hasChanges = false;
2705
+ const changedIndices = diff.map((line, index) => line.type !== "unchanged" ? index : -1).filter((index) => index !== -1);
2706
+ if (changedIndices.length === 0) {
2707
+ return `${GRAY}No changes${RESET}`;
2647
2708
  }
2648
- const originalContent = readFileSync(packageJsonPath, "utf-8");
2649
- const allPrefixVariants = getAllAgentPrefixVariants(agent);
2650
- if (allPrefixVariants.length === 0) {
2651
- return {
2652
- success: true,
2653
- filePath: packageJsonPath,
2654
- message: `Unknown agent: ${agent}`,
2655
- noChanges: true
2656
- };
2709
+ for (const changedIndex of changedIndices) {
2710
+ const startContext = Math.max(0, changedIndex - contextLines);
2711
+ const endContext = Math.min(diff.length - 1, changedIndex + contextLines);
2712
+ if (startContext > lastPrintedIndex + 1 && lastPrintedIndex !== -1) {
2713
+ lines.push(`${GRAY} ...${RESET}`);
2714
+ }
2715
+ for (let lineIndex = Math.max(startContext, lastPrintedIndex + 1); lineIndex <= endContext; lineIndex++) {
2716
+ const diffLine = diff[lineIndex];
2717
+ if (diffLine.type === "added") {
2718
+ lines.push(`${GREEN}+ ${diffLine.content}${RESET}`);
2719
+ hasChanges = true;
2720
+ } else if (diffLine.type === "removed") {
2721
+ lines.push(`${RED}- ${diffLine.content}${RESET}`);
2722
+ hasChanges = true;
2723
+ } else {
2724
+ lines.push(`${GRAY} ${diffLine.content}${RESET}`);
2725
+ }
2726
+ lastPrintedIndex = lineIndex;
2727
+ }
2657
2728
  }
2658
- const hasAnyPrefix = allPrefixVariants.some(
2659
- (prefix) => originalContent.includes(prefix)
2660
- );
2661
- if (!hasAnyPrefix) {
2662
- return {
2663
- success: true,
2664
- filePath: packageJsonPath,
2665
- message: `Agent ${agent} dev script is not configured`,
2666
- noChanges: true
2667
- };
2729
+ return hasChanges ? lines.join("\n") : `${GRAY}No changes${RESET}`;
2730
+ };
2731
+ var printDiff = (filePath, originalContent, newContent) => {
2732
+ console.log(`
2733
+ ${BOLD}File: ${filePath}${RESET}`);
2734
+ console.log("\u2500".repeat(60));
2735
+ const diff = generateDiff(originalContent, newContent);
2736
+ console.log(formatDiff(diff));
2737
+ console.log("\u2500".repeat(60));
2738
+ };
2739
+ var VERSION = "0.1.0-beta.5";
2740
+ var configureMcp = (mcpClient, cwd, customPkg) => {
2741
+ const mcpCommand = customPkg ? `npx -y ${customPkg} browser mcp` : `npx -y @react-grab/cli browser mcp`;
2742
+ const mcpSpinner = spinner(`Installing MCP server for ${MCP_CLIENT_NAMES[mcpClient]}`).start();
2743
+ try {
2744
+ execSync(
2745
+ `npx -y install-mcp '${mcpCommand}' --client ${mcpClient} --yes`,
2746
+ { stdio: "ignore", cwd }
2747
+ );
2748
+ mcpSpinner.succeed(`MCP server installed for ${MCP_CLIENT_NAMES[mcpClient]}`);
2749
+ logger.break();
2750
+ process.exit(0);
2751
+ } catch {
2752
+ mcpSpinner.fail(`Failed to configure MCP for ${MCP_CLIENT_NAMES[mcpClient]}`);
2753
+ logger.dim(`Try manually: npx -y install-mcp '${mcpCommand}' --client ${mcpClient}`);
2754
+ logger.break();
2755
+ process.exit(1);
2668
2756
  }
2757
+ };
2758
+ var installSkill = (agent, cwd) => {
2759
+ logger.break();
2760
+ const skillSpinner = spinner(`Installing skill for ${agent.name}`).start();
2669
2761
  try {
2670
- const packageJson = JSON.parse(originalContent);
2671
- for (const scriptKey of Object.keys(packageJson.scripts || {})) {
2672
- let scriptValue = packageJson.scripts[scriptKey];
2673
- if (typeof scriptValue === "string") {
2674
- for (const prefix of allPrefixVariants) {
2675
- if (scriptValue.includes(prefix)) {
2676
- scriptValue = scriptValue.replace(prefix + " ", "").replace(prefix, "");
2677
- }
2678
- }
2679
- packageJson.scripts[scriptKey] = scriptValue;
2680
- }
2681
- }
2682
- const newContent = JSON.stringify(packageJson, null, 2) + "\n";
2683
- return {
2684
- success: true,
2685
- filePath: packageJsonPath,
2686
- message: `Remove ${agent} server from dev script`,
2687
- originalContent,
2688
- newContent
2689
- };
2762
+ execSync(`npx -y add-skill aidenybai/react-grab -y --agent ${agent.id}`, {
2763
+ stdio: "ignore",
2764
+ cwd
2765
+ });
2766
+ skillSpinner.succeed(`Skill installed for ${agent.name}`);
2767
+ logger.break();
2768
+ process.exit(0);
2690
2769
  } catch {
2691
- return {
2692
- success: false,
2693
- filePath: packageJsonPath,
2694
- message: "Failed to parse package.json"
2695
- };
2770
+ skillSpinner.fail(`Failed to install skill for ${agent.name}`);
2771
+ logger.warn(`Try manually: npx -y add-skill aidenybai/react-grab --agent ${agent.id}`);
2772
+ logger.break();
2773
+ process.exit(1);
2696
2774
  }
2697
2775
  };
2698
-
2699
- // src/commands/add.ts
2700
- var VERSION = "0.1.0-beta.4";
2701
- var add = new Command().name("add").alias("install").description("add an agent integration or MCP server").argument(
2776
+ var add = new Command().name("add").alias("install").description("add browser automation for your AI agent").argument(
2702
2777
  "[agent]",
2703
2778
  `agent to add (${AGENTS.join(", ")}, mcp, skill)`
2704
2779
  ).option("-y, --yes", "skip confirmation prompts", false).option(
@@ -2750,79 +2825,59 @@ var add = new Command().name("add").alias("install").description("add an agent i
2750
2825
  }
2751
2826
  if (!mcpClient) {
2752
2827
  logger.break();
2753
- logger.error("Please specify an MCP client with --client");
2754
- logger.error(`Available clients: ${MCP_CLIENTS.join(", ")}`);
2755
- logger.break();
2756
- process.exit(1);
2757
- }
2758
- const customPkg = opts2.pkg;
2759
- const mcpCommand = customPkg ? `npx -y ${customPkg} browser mcp` : `npx -y @react-grab/cli browser mcp`;
2760
- const mcpSpinner = spinner(`Configuring MCP for ${MCP_CLIENT_NAMES[mcpClient]}`).start();
2761
- try {
2762
- execSync(
2763
- `npx -y install-mcp '${mcpCommand}' --client ${mcpClient} --yes`,
2764
- { stdio: "ignore", cwd }
2765
- );
2766
- mcpSpinner.succeed(`MCP configured for ${MCP_CLIENT_NAMES[mcpClient]}`);
2767
- logger.break();
2768
- process.exit(0);
2769
- } catch {
2770
- mcpSpinner.fail(`Failed to configure MCP for ${MCP_CLIENT_NAMES[mcpClient]}`);
2771
- logger.dim(`Try manually: npx -y install-mcp '${mcpCommand}' --client ${mcpClient}`);
2828
+ logger.error("Please specify an MCP client with --client");
2829
+ logger.error(`Available clients: ${MCP_CLIENTS.join(", ")}`);
2772
2830
  logger.break();
2773
2831
  process.exit(1);
2774
2832
  }
2833
+ configureMcp(mcpClient, cwd, opts2.pkg);
2775
2834
  }
2776
2835
  if (agentArg === "skill") {
2777
- let skillTarget = opts2.client;
2778
- if (skillTarget && !AGENT_TARGETS[skillTarget]) {
2836
+ const clientArg = opts2.client;
2837
+ let selectedAgent = clientArg ? findSkillAgent(clientArg) : void 0;
2838
+ const detectedAgents = detectSkillAgents(cwd);
2839
+ if (clientArg && !selectedAgent) {
2779
2840
  logger.break();
2780
- logger.error(`Invalid skill target: ${skillTarget}`);
2781
- logger.error(`Available targets: ${SUPPORTED_TARGETS.join(", ")}`);
2841
+ logger.error(`Invalid skill agent: ${clientArg}`);
2842
+ logger.error(`Available agents: ${SKILL_AGENTS.map((a2) => a2.id).join(", ")}`);
2782
2843
  logger.break();
2783
2844
  process.exit(1);
2784
2845
  }
2785
- if (!skillTarget && !isNonInteractive) {
2846
+ if (!selectedAgent && !isNonInteractive) {
2847
+ if (detectedAgents.length === 0) {
2848
+ logger.break();
2849
+ logger.warn("No supported agent folders detected.");
2850
+ logger.log("Supported agents: " + SKILL_AGENTS.map((a2) => a2.id).join(", "));
2851
+ logger.break();
2852
+ process.exit(0);
2853
+ }
2786
2854
  logger.break();
2787
- const { target } = await prompts3({
2855
+ const { agent } = await prompts3({
2788
2856
  type: "select",
2789
- name: "target",
2857
+ name: "agent",
2790
2858
  message: `Which ${highlighter.info("agent")} would you like to install the skill for?`,
2791
- choices: SUPPORTED_TARGETS.map((innerTarget) => ({
2792
- title: innerTarget,
2793
- value: innerTarget
2794
- }))
2859
+ choices: [
2860
+ ...detectedAgents.map((innerAgent) => ({
2861
+ title: innerAgent.name,
2862
+ value: innerAgent
2863
+ })),
2864
+ { title: "Skip", value: null }
2865
+ ]
2795
2866
  });
2796
- if (!target) {
2867
+ if (!agent) {
2797
2868
  logger.break();
2798
- process.exit(1);
2869
+ process.exit(0);
2799
2870
  }
2800
- skillTarget = target;
2871
+ selectedAgent = agent;
2801
2872
  }
2802
- if (!skillTarget) {
2873
+ if (!selectedAgent) {
2803
2874
  logger.break();
2804
- logger.error("Please specify a target with --client");
2805
- logger.error(`Available targets: ${SUPPORTED_TARGETS.join(", ")}`);
2806
- logger.break();
2807
- process.exit(1);
2808
- }
2809
- logger.break();
2810
- const installSpinner = spinner("Fetching skill file").start();
2811
- try {
2812
- const skill = await fetchSkillFile();
2813
- const skillDir = join(cwd, AGENT_TARGETS[skillTarget]);
2814
- rmSync(skillDir, { recursive: true, force: true });
2815
- mkdirSync(skillDir, { recursive: true });
2816
- writeFileSync(join(skillDir, "SKILL.md"), skill);
2817
- installSpinner.succeed(`Skill installed to ${AGENT_TARGETS[skillTarget]}/`);
2818
- } catch (error48) {
2819
- installSpinner.fail("Failed to install skill");
2820
- logger.error(error48 instanceof Error ? error48.message : "Unknown error");
2875
+ logger.error("Please specify an agent with --client");
2876
+ logger.error(`Available agents: ${SKILL_AGENTS.map((a2) => a2.id).join(", ")}`);
2821
2877
  logger.break();
2822
2878
  process.exit(1);
2823
2879
  }
2824
- logger.break();
2825
- process.exit(0);
2880
+ installSkill(selectedAgent, cwd);
2826
2881
  }
2827
2882
  if (!agentArg && !isNonInteractive) {
2828
2883
  logger.break();
@@ -2832,18 +2887,18 @@ var add = new Command().name("add").alias("install").description("add an agent i
2832
2887
  message: "What would you like to add?",
2833
2888
  choices: [
2834
2889
  {
2835
- title: "MCP Server",
2836
- description: "Give your agent access to your browser",
2837
- value: "mcp"
2890
+ title: "Skill (recommended)",
2891
+ description: "Instructions for your agent to use the browser",
2892
+ value: "skill"
2838
2893
  },
2839
2894
  {
2840
- title: "Skill",
2841
- description: "Install browser automation skill for AI agents",
2842
- value: "skill"
2895
+ title: "MCP Server",
2896
+ description: "A server that provides browser tools to your agent",
2897
+ value: "mcp"
2843
2898
  },
2844
2899
  {
2845
2900
  title: "Agent Integration",
2846
- description: "Run agents through the React Grab interface",
2901
+ description: "Add Claude Code, Cursor, etc. to the React Grab UI",
2847
2902
  value: "agent"
2848
2903
  }
2849
2904
  ]
@@ -2866,55 +2921,34 @@ var add = new Command().name("add").alias("install").description("add an agent i
2866
2921
  logger.break();
2867
2922
  process.exit(1);
2868
2923
  }
2869
- const mcpClient = client;
2870
- const customPkg = opts2.pkg;
2871
- const mcpCommand = customPkg ? `npx -y ${customPkg} browser mcp` : `npx -y @react-grab/cli browser mcp`;
2872
- const mcpSpinner = spinner(`Configuring MCP for ${MCP_CLIENT_NAMES[mcpClient]}`).start();
2873
- try {
2874
- execSync(
2875
- `npx -y install-mcp '${mcpCommand}' --client ${mcpClient} --yes`,
2876
- { stdio: "ignore", cwd }
2877
- );
2878
- mcpSpinner.succeed(`MCP configured for ${MCP_CLIENT_NAMES[mcpClient]}`);
2924
+ configureMcp(client, cwd, opts2.pkg);
2925
+ }
2926
+ if (addType === "skill") {
2927
+ const detectedAgents = detectSkillAgents(cwd);
2928
+ if (detectedAgents.length === 0) {
2879
2929
  logger.break();
2880
- process.exit(0);
2881
- } catch {
2882
- mcpSpinner.fail(`Failed to configure MCP for ${MCP_CLIENT_NAMES[mcpClient]}`);
2883
- logger.dim(`Try manually: npx install-mcp '${mcpCommand}' --client ${mcpClient}`);
2930
+ logger.warn("No supported agent folders detected.");
2931
+ logger.log("Supported agents: " + SKILL_AGENTS.map((a2) => a2.id).join(", "));
2884
2932
  logger.break();
2885
- process.exit(1);
2933
+ process.exit(0);
2886
2934
  }
2887
- }
2888
- if (addType === "skill") {
2889
- const { target } = await prompts3({
2935
+ const { selectedAgent } = await prompts3({
2890
2936
  type: "select",
2891
- name: "target",
2937
+ name: "selectedAgent",
2892
2938
  message: `Which ${highlighter.info("agent")} would you like to install the skill for?`,
2893
- choices: SUPPORTED_TARGETS.map((innerTarget) => ({
2894
- title: innerTarget,
2895
- value: innerTarget
2896
- }))
2939
+ choices: [
2940
+ ...detectedAgents.map((innerAgent) => ({
2941
+ title: innerAgent.name,
2942
+ value: innerAgent
2943
+ })),
2944
+ { title: "Skip", value: null }
2945
+ ]
2897
2946
  });
2898
- if (!target) {
2947
+ if (!selectedAgent) {
2899
2948
  logger.break();
2900
- process.exit(1);
2901
- }
2902
- const installSpinner = spinner("Fetching skill file").start();
2903
- try {
2904
- const skill = await fetchSkillFile();
2905
- const skillDir = join(cwd, AGENT_TARGETS[target]);
2906
- rmSync(skillDir, { recursive: true, force: true });
2907
- mkdirSync(skillDir, { recursive: true });
2908
- writeFileSync(join(skillDir, "SKILL.md"), skill);
2909
- installSpinner.succeed(`Skill installed to ${AGENT_TARGETS[target]}/`);
2910
- } catch (error48) {
2911
- installSpinner.fail("Failed to install skill");
2912
- logger.error(error48 instanceof Error ? error48.message : "Unknown error");
2913
- logger.break();
2914
- process.exit(1);
2949
+ process.exit(0);
2915
2950
  }
2916
- logger.break();
2917
- process.exit(0);
2951
+ installSkill(selectedAgent, cwd);
2918
2952
  }
2919
2953
  }
2920
2954
  const preflightSpinner = spinner("Preflight checks.").start();
@@ -3232,9 +3266,11 @@ var MAX_SUGGESTIONS_COUNT = 30;
3232
3266
  var MAX_KEY_HOLD_DURATION_MS = 2e3;
3233
3267
  var MAX_CONTEXT_LINES = 50;
3234
3268
  var COMPONENT_STACK_MAX_DEPTH = 10;
3269
+ var DEFAULT_COMPONENT_TREE_MAX_DEPTH = 50;
3270
+ var MAX_PROPS_DISPLAY_LENGTH = 100;
3271
+ var LOAD_STATES = /* @__PURE__ */ new Set(["load", "domcontentloaded", "networkidle"]);
3235
3272
 
3236
3273
  // src/utils/browser-automation.ts
3237
- var LOAD_STATES = /* @__PURE__ */ new Set(["load", "domcontentloaded", "networkidle"]);
3238
3274
  var ensureHealthyServer = async (options2 = {}) => {
3239
3275
  const cliPath = process.argv[1];
3240
3276
  const serverRunning = await isServerRunning();
@@ -3309,13 +3345,7 @@ var getReactContextForActiveElement = async (page) => {
3309
3345
  var createOutputJson = (getPage, pageName) => {
3310
3346
  return async (ok, result, error48) => {
3311
3347
  const page = getPage();
3312
- let reactContext;
3313
- if (page && ok) {
3314
- const context = await getReactContextForActiveElement(page);
3315
- if (context) {
3316
- reactContext = context;
3317
- }
3318
- }
3348
+ const reactContext = page && ok ? await getReactContextForActiveElement(page) ?? void 0 : void 0;
3319
3349
  return {
3320
3350
  ok,
3321
3351
  url: page?.url() ?? "",
@@ -3356,7 +3386,7 @@ var createRefHelper = (getActivePage2) => {
3356
3386
  const g2 = globalThis;
3357
3387
  const refs = g2.__REACT_GRAB_REFS__;
3358
3388
  if (!refs) {
3359
- throw new Error("No refs found. Call snapshot() first.");
3389
+ throw new Error("No refs found. Call getSnapshot() first.");
3360
3390
  }
3361
3391
  const element2 = refs[id];
3362
3392
  if (!element2) {
@@ -3413,38 +3443,33 @@ var createRefHelper = (getActivePage2) => {
3413
3443
  }
3414
3444
  };
3415
3445
  return (refId) => {
3446
+ const customMethods = {
3447
+ then: () => (resolve, reject) => getElement(refId).then(resolve, reject),
3448
+ source: () => () => getSource(refId),
3449
+ props: () => () => getProps(refId),
3450
+ state: () => () => getState(refId),
3451
+ screenshot: () => async (options2) => {
3452
+ const element = await getElement(refId);
3453
+ try {
3454
+ return await element.screenshot({ scale: "css", ...options2 });
3455
+ } finally {
3456
+ await element.dispose();
3457
+ }
3458
+ }
3459
+ };
3416
3460
  return new Proxy(
3417
3461
  {},
3418
3462
  {
3419
3463
  get(_, prop) {
3420
- if (prop === "then") {
3421
- return (resolve, reject) => getElement(refId).then(resolve, reject);
3422
- }
3423
- if (prop === "source") {
3424
- return () => getSource(refId);
3425
- }
3426
- if (prop === "props") {
3427
- return () => getProps(refId);
3428
- }
3429
- if (prop === "state") {
3430
- return () => getState(refId);
3431
- }
3432
- if (prop === "screenshot") {
3433
- return async (options2) => {
3434
- const el = await getElement(refId);
3435
- try {
3436
- return await el.screenshot({ scale: "css", ...options2 });
3437
- } finally {
3438
- await el.dispose();
3439
- }
3440
- };
3464
+ if (prop in customMethods) {
3465
+ return customMethods[prop]();
3441
3466
  }
3442
3467
  return async (...args) => {
3443
- const el = await getElement(refId);
3468
+ const element = await getElement(refId);
3444
3469
  try {
3445
- return await el[prop](...args);
3470
+ return await element[prop](...args);
3446
3471
  } finally {
3447
- await el.dispose();
3472
+ await element.dispose();
3448
3473
  }
3449
3474
  };
3450
3475
  }
@@ -3496,9 +3521,9 @@ var createComponentHelper = (getActivePage2) => {
3496
3521
  return handles;
3497
3522
  };
3498
3523
  };
3499
- var createFillHelper = (ref, getActivePage2) => {
3524
+ var createFillHelper = (getRef, getActivePage2) => {
3500
3525
  return async (refId, text) => {
3501
- const element = await ref(refId);
3526
+ const element = await getRef(refId);
3502
3527
  await element.click();
3503
3528
  const currentPage2 = getActivePage2();
3504
3529
  const isMac2 = process.platform === "darwin";
@@ -3644,7 +3669,7 @@ var createWaitForHelper = (getActivePage2) => {
3644
3669
  await currentPage2.waitForLoadState(selectorOrState, { timeout });
3645
3670
  return;
3646
3671
  }
3647
- if (selectorOrState.startsWith("e") && /^e\d+$/.test(selectorOrState)) {
3672
+ if (/^e\d+$/.test(selectorOrState)) {
3648
3673
  await currentPage2.waitForSelector(`[aria-ref="${selectorOrState}"]`, { timeout });
3649
3674
  return;
3650
3675
  }
@@ -17447,56 +17472,93 @@ var startMcpServer = async () => {
17447
17472
  server.registerTool(
17448
17473
  "browser_snapshot",
17449
17474
  {
17450
- description: `Get ARIA accessibility tree with element refs (e1, e2...) and React component info.
17475
+ description: `Get ARIA accessibility tree with element refs and React component info.
17476
+
17477
+ OUTPUT FORMAT:
17478
+ - button "Submit" [ref=e1] [component=Button] [source=form.tsx:42]
17479
+ - ComponentName [ref=e2] [source=file.tsx:10]: text content
17451
17480
 
17452
- OUTPUT INCLUDES:
17453
- - ARIA roles and accessible names
17454
- - Element refs (e1, e2...) for interaction
17455
- - [component=ComponentName] for React components
17456
- - [source=file.tsx:line] for source location
17481
+ REACT INFO IS ALREADY INCLUDED (no extra calls needed):
17482
+ - [component=X] \u2014 React component name (replaces "generic" when available)
17483
+ - [source=file.tsx:line] \u2014 Source file location
17484
+ Just parse these from the snapshot string.
17457
17485
 
17458
- SCREENSHOT STRATEGY - ALWAYS prefer element screenshots over full page:
17486
+ FOR MORE REACT DETAILS on a specific element:
17487
+ Use browser_execute: return await getRef('e1').source()
17488
+ Returns: { filePath, lineNumber, componentName }
17489
+
17490
+ SCREENSHOT STRATEGY - prefer element screenshots:
17459
17491
  1. First: Get refs with snapshot (this tool)
17460
- 2. Then: Screenshot specific element via browser_execute: return await ref('e1').screenshot()
17492
+ 2. Then: Screenshot specific element via browser_execute: return await getRef('e1').screenshot()
17461
17493
 
17462
- USE ELEMENT SCREENSHOTS (ref('eX').screenshot()) FOR:
17463
- - Visual bugs: "wrong color", "broken", "misaligned", "styling issue", "CSS bug"
17464
- - Appearance checks: "how does X look", "show me the button", "what does Y display"
17465
- - UI verification: "is it visible", "check the layout", "verify the design"
17466
- - Any visual concern about a SPECIFIC component
17494
+ USE ELEMENT SCREENSHOTS (getRef('eX').screenshot()) FOR:
17495
+ - Visual bugs: "wrong color", "broken", "misaligned", "styling issue"
17496
+ - Appearance checks: "how does X look", "show me the button"
17497
+ - UI verification: "is it visible", "check the layout"
17467
17498
 
17468
17499
  USE VIEWPORT screenshot=true ONLY FOR:
17469
17500
  - "screenshot the page", "what's on screen"
17470
- - No specific element mentioned AND need visual context
17471
-
17472
- PERFORMANCE:
17473
- - interactableOnly:true = much smaller output (recommended)
17474
- - maxDepth = limit tree depth
17501
+ - No specific element mentioned
17475
17502
 
17476
- After getting refs, use browser_execute with: ref('e1').click()`,
17503
+ After getting refs, use browser_execute with: getRef('e1').click()`,
17477
17504
  inputSchema: {
17478
17505
  page: external_exports.string().optional().default("default").describe("Named page context"),
17479
17506
  maxDepth: external_exports.number().optional().describe("Limit tree depth"),
17480
- interactableOnly: external_exports.boolean().optional().describe("Only clickable/input elements (recommended)"),
17481
17507
  screenshot: external_exports.boolean().optional().default(false).describe(
17482
- "Viewport screenshot. For element screenshots (PREFERRED), use browser_execute: ref('eX').screenshot()"
17483
- )
17508
+ "Viewport screenshot. For element screenshots (PREFERRED), use browser_execute: getRef('eX').screenshot()"
17509
+ ),
17510
+ reactTree: external_exports.boolean().optional().default(false).describe("(Experimental) React tree view. Note: Regular snapshot already includes [component=X] and [source=X] - prefer that."),
17511
+ includeProps: external_exports.boolean().optional().default(false).describe("Include props when reactTree=true")
17484
17512
  }
17485
17513
  },
17486
17514
  async ({
17487
17515
  page: pageName,
17488
17516
  maxDepth,
17489
- interactableOnly,
17490
- screenshot
17517
+ screenshot,
17518
+ reactTree,
17519
+ includeProps
17491
17520
  }) => {
17492
17521
  let browser2 = null;
17493
17522
  try {
17494
17523
  const connection = await connectToBrowserPage(pageName);
17495
17524
  browser2 = connection.browser;
17496
17525
  const activePage = connection.page;
17497
- const getActivePage2 = () => activePage;
17498
- const snapshot = createSnapshotHelper(getActivePage2);
17499
- const snapshotResult = await snapshot({ maxDepth, interactableOnly });
17526
+ let textResult;
17527
+ if (reactTree) {
17528
+ const componentTree = await activePage.evaluate(
17529
+ async (opts2) => {
17530
+ const g2 = globalThis;
17531
+ if (g2.__REACT_GRAB_SNAPSHOT__) {
17532
+ await g2.__REACT_GRAB_SNAPSHOT__();
17533
+ }
17534
+ if (!g2.__REACT_GRAB_GET_COMPONENT_TREE__) {
17535
+ return [];
17536
+ }
17537
+ return g2.__REACT_GRAB_GET_COMPONENT_TREE__(opts2);
17538
+ },
17539
+ { maxDepth: maxDepth ?? DEFAULT_COMPONENT_TREE_MAX_DEPTH, includeProps: includeProps ?? false }
17540
+ );
17541
+ const renderTree = (nodes) => {
17542
+ const lines = [];
17543
+ for (const node of nodes) {
17544
+ const indent = " ".repeat(node.depth);
17545
+ let line = `${indent}- ${node.name}`;
17546
+ if (node.ref) line += ` [ref=${node.ref}]`;
17547
+ if (node.source) line += ` [source=${node.source}]`;
17548
+ if (node.props && Object.keys(node.props).length > 0) {
17549
+ const propsStr = JSON.stringify(node.props);
17550
+ if (propsStr.length < MAX_PROPS_DISPLAY_LENGTH) {
17551
+ line += ` [props=${propsStr}]`;
17552
+ }
17553
+ }
17554
+ lines.push(line);
17555
+ }
17556
+ return lines.join("\n");
17557
+ };
17558
+ textResult = renderTree(componentTree) || "No React components found. Make sure react-grab is installed and the page uses React.";
17559
+ } else {
17560
+ textResult = await createSnapshotHelper(() => activePage)({ maxDepth });
17561
+ }
17500
17562
  if (screenshot) {
17501
17563
  const screenshotBuffer = await activePage.screenshot({
17502
17564
  fullPage: false,
@@ -17504,7 +17566,7 @@ After getting refs, use browser_execute with: ref('e1').click()`,
17504
17566
  });
17505
17567
  return {
17506
17568
  content: [
17507
- { type: "text", text: snapshotResult },
17569
+ { type: "text", text: textResult },
17508
17570
  {
17509
17571
  type: "image",
17510
17572
  data: screenshotBuffer.toString("base64"),
@@ -17514,7 +17576,7 @@ After getting refs, use browser_execute with: ref('e1').click()`,
17514
17576
  };
17515
17577
  }
17516
17578
  return {
17517
- content: [{ type: "text", text: snapshotResult }]
17579
+ content: [{ type: "text", text: textResult }]
17518
17580
  };
17519
17581
  } catch (error48) {
17520
17582
  return createMcpErrorResponse(error48);
@@ -17528,40 +17590,38 @@ After getting refs, use browser_execute with: ref('e1').click()`,
17528
17590
  {
17529
17591
  description: `Execute Playwright code with helpers for element interaction.
17530
17592
 
17531
- IMPORTANT: Always call snapshot() first to get element refs from the a11y tree (e1, e2...), then use ref('e1') to interact.
17593
+ IMPORTANT: Always call getSnapshot() first to get element refs (e1, e2...), then use getRef('e1') to interact.
17532
17594
 
17533
17595
  AVAILABLE HELPERS:
17534
- - page: Playwright Page object (https://playwright.dev/docs/api/class-page)
17535
- - snapshot(opts?): Get ARIA tree with React component info. opts: {maxDepth, interactableOnly}
17536
- - ref(id): Get element by ref ID, chainable with all ElementHandle methods
17537
- - ref(id).source(): Get React component source {filePath, lineNumber, componentName}
17538
- - ref(id).props(): Get React component props (serialized)
17539
- - ref(id).state(): Get React component state/hooks (serialized)
17540
- - component(name, opts?): Find elements by React component name. opts: {nth: number}
17541
- - fill(id, text): Clear and fill input (works with rich text editors)
17596
+ - page: Playwright Page object
17597
+ - getSnapshot(opts?): Get ARIA tree with React info. opts: {maxDepth}
17598
+ - getRef(id): Get element by ref ID, chainable with ElementHandle methods
17599
+ - getRef(id).source(): Get React source {filePath, lineNumber, componentName}
17600
+ - getRef(id).props(): Get React component props
17601
+ - getRef(id).state(): Get React component state/hooks
17602
+ - fill(id, text): Clear and fill input
17542
17603
  - drag({from, to, dataTransfer?}): Drag with custom MIME types
17543
17604
  - dispatch({target, event, dataTransfer?, detail?}): Dispatch custom events
17544
17605
  - waitFor(target): Wait for selector/ref/state. e.g. waitFor('e1'), waitFor('networkidle')
17545
17606
  - grab: React Grab client API (activate, deactivate, toggle, isActive, copyElement, getState)
17546
17607
 
17547
- REACT-SPECIFIC PATTERNS:
17548
- - Get React source: return await ref('e1').source()
17549
- - Get component props: return await ref('e1').props()
17550
- - Get component state: return await ref('e1').state()
17551
- - Find by component: const btn = await component('Button', {nth: 0})
17608
+ GETTING REACT INFO (ranked by preference):
17609
+ 1. Parse snapshot \u2014 [component=X] and [source=file:line] are already in the output
17610
+ 2. getRef('eX').source() \u2014 for detailed info on a specific element
17552
17611
 
17553
- ELEMENT SCREENSHOTS (PREFERRED for visual issues):
17554
- - return await ref('e1').screenshot()
17555
- Use for: wrong color, broken styling, visual bugs, "how does X look", UI verification
17612
+ ELEMENT SCREENSHOTS (for visual issues):
17613
+ - return await getRef('e1').screenshot()
17614
+ Use for: wrong color, broken styling, visual bugs, UI verification
17556
17615
 
17557
17616
  COMMON PATTERNS:
17558
- - Click: await ref('e1').click()
17617
+ - Click: await getRef('e1').click()
17559
17618
  - Fill input: await fill('e1', 'hello')
17560
- - Get attribute: return await ref('e1').getAttribute('href')
17619
+ - Get attribute: return await getRef('e1').getAttribute('href')
17561
17620
  - Navigate: await page.goto('https://example.com')
17562
- - Full page screenshot (rare): return await page.screenshot()
17563
17621
 
17564
- PERFORMANCE: Batch multiple actions in one execute call to minimize round-trips.`,
17622
+ DON'T manually traverse __reactFiber$ \u2014 use getRef('eX').source() instead.
17623
+
17624
+ PERFORMANCE: Batch multiple actions in one execute call.`,
17565
17625
  inputSchema: {
17566
17626
  code: external_exports.string().describe(
17567
17627
  "JavaScript code. Use 'page' for Playwright, 'ref(id)' for elements, 'return' for output"
@@ -17592,19 +17652,19 @@ PERFORMANCE: Batch multiple actions in one execute call to minimize round-trips.
17592
17652
  };
17593
17653
  context.on("page", pageOpenHandler);
17594
17654
  const getActivePage2 = createActivePageGetter(context, () => activePage);
17595
- const snapshot = createSnapshotHelper(getActivePage2);
17596
- const ref = createRefHelper(getActivePage2);
17597
- const fill = createFillHelper(ref, getActivePage2);
17655
+ const getSnapshot = createSnapshotHelper(getActivePage2);
17656
+ const getRef = createRefHelper(getActivePage2);
17657
+ const fill = createFillHelper(getRef, getActivePage2);
17598
17658
  const drag = createDragHelper(getActivePage2);
17599
17659
  const dispatch = createDispatchHelper(getActivePage2);
17600
- const grab = createGrabHelper(ref, getActivePage2);
17660
+ const grab = createGrabHelper(getRef, getActivePage2);
17601
17661
  const waitFor = createWaitForHelper(getActivePage2);
17602
17662
  const component = createComponentHelper(getActivePage2);
17603
17663
  const executeFunction = new Function(
17604
17664
  "page",
17605
17665
  "getActivePage",
17606
- "snapshot",
17607
- "ref",
17666
+ "getSnapshot",
17667
+ "getRef",
17608
17668
  "fill",
17609
17669
  "drag",
17610
17670
  "dispatch",
@@ -17616,8 +17676,8 @@ PERFORMANCE: Batch multiple actions in one execute call to minimize round-trips.
17616
17676
  const result = await executeFunction(
17617
17677
  getActivePage2(),
17618
17678
  getActivePage2,
17619
- snapshot,
17620
- ref,
17679
+ getSnapshot,
17680
+ getRef,
17621
17681
  fill,
17622
17682
  drag,
17623
17683
  dispatch,
@@ -17660,82 +17720,12 @@ PERFORMANCE: Batch multiple actions in one execute call to minimize round-trips.
17660
17720
  }
17661
17721
  }
17662
17722
  );
17663
- server.registerTool(
17664
- "browser_react_tree",
17665
- {
17666
- description: `Get React component tree hierarchy (separate from ARIA tree).
17667
-
17668
- Shows the React component structure with:
17669
- - Component names and nesting
17670
- - Source file locations
17671
- - Element refs where available
17672
- - Optional props (serialized)
17673
-
17674
- Use this when you need to understand React component architecture rather than accessibility tree.
17675
- For interacting with elements, use browser_snapshot to get refs first.`,
17676
- inputSchema: {
17677
- page: external_exports.string().optional().default("default").describe("Named page context"),
17678
- maxDepth: external_exports.number().optional().default(50).describe("Maximum tree depth"),
17679
- includeProps: external_exports.boolean().optional().default(false).describe("Include component props (increases output size)")
17680
- }
17681
- },
17682
- async ({ page: pageName, maxDepth, includeProps }) => {
17683
- let browser2 = null;
17684
- try {
17685
- const connection = await connectToBrowserPage(pageName);
17686
- browser2 = connection.browser;
17687
- const activePage = connection.page;
17688
- const componentTree = await activePage.evaluate(
17689
- async (opts2) => {
17690
- const g2 = globalThis;
17691
- if (!g2.__REACT_GRAB_GET_COMPONENT_TREE__) {
17692
- return [];
17693
- }
17694
- return g2.__REACT_GRAB_GET_COMPONENT_TREE__(opts2);
17695
- },
17696
- { maxDepth: maxDepth ?? 50, includeProps: includeProps ?? false }
17697
- );
17698
- const renderTree = (nodes) => {
17699
- const lines = [];
17700
- for (const node of nodes) {
17701
- const indent = " ".repeat(node.depth);
17702
- let line = `${indent}- ${node.name}`;
17703
- if (node.ref) line += ` [ref=${node.ref}]`;
17704
- if (node.source) line += ` [source=${node.source}]`;
17705
- if (node.props && Object.keys(node.props).length > 0) {
17706
- const propsStr = JSON.stringify(node.props);
17707
- if (propsStr.length < 100) {
17708
- line += ` [props=${propsStr}]`;
17709
- } else {
17710
- line += ` [props=...]`;
17711
- }
17712
- }
17713
- lines.push(line);
17714
- }
17715
- return lines.join("\n");
17716
- };
17717
- const treeOutput = renderTree(componentTree);
17718
- return {
17719
- content: [
17720
- {
17721
- type: "text",
17722
- text: treeOutput || "No React components found. Make sure react-grab is installed and the page uses React."
17723
- }
17724
- ]
17725
- };
17726
- } catch (error48) {
17727
- return createMcpErrorResponse(error48);
17728
- } finally {
17729
- await browser2?.close();
17730
- }
17731
- }
17732
- );
17733
17723
  const transport = new StdioServerTransport();
17734
17724
  await server.connect(transport);
17735
17725
  };
17736
17726
 
17737
17727
  // src/commands/browser.ts
17738
- var VERSION2 = "0.1.0-beta.4";
17728
+ var VERSION2 = "0.1.0-beta.5";
17739
17729
  var printHeader = () => {
17740
17730
  console.log(
17741
17731
  `${pc.magenta("\u273F")} ${pc.bold("React Grab")} ${pc.gray(VERSION2)}`
@@ -17746,24 +17736,6 @@ var exitWithError = (error48) => {
17746
17736
  logger.error(error48 instanceof Error ? error48.message : "Failed");
17747
17737
  process.exit(1);
17748
17738
  };
17749
- var rebuildNativeModuleAndRestart = async (browserPkgDir) => {
17750
- execSync("npm rebuild better-sqlite3", {
17751
- stdio: "ignore",
17752
- cwd: browserPkgDir
17753
- });
17754
- const rebuildSpinner = spinner("Restarting server").start();
17755
- rebuildSpinner.succeed("Restarting server");
17756
- const args = process.argv.slice(2);
17757
- const child = spawn(process.argv[0], [process.argv[1], ...args], {
17758
- stdio: "inherit",
17759
- detached: false
17760
- });
17761
- child.on("error", (error48) => {
17762
- console.error(`Failed to restart: ${error48.message}`);
17763
- process.exit(1);
17764
- });
17765
- child.on("exit", (code) => process.exit(code ?? 0));
17766
- };
17767
17739
  var isSupportedBrowser = (value) => {
17768
17740
  return SUPPORTED_BROWSERS.includes(value);
17769
17741
  };
@@ -17820,16 +17792,23 @@ var dump = new Command().name("dump").description("dump cookies from a browser")
17820
17792
  }
17821
17793
  });
17822
17794
  var start = new Command().name("start").description("start browser server manually (auto-starts on first execute)").option("-p, --port <port>", "HTTP API port", String(DEFAULT_SERVER_PORT)).option("--headed", "show browser window (default is headless)").option("-b, --browser <browser>", "source browser for cookies (chrome, edge, brave, arc)").option("-d, --domain <domain>", "only load cookies matching this domain").option("--foreground", "run in foreground instead of detaching").action(async (options2) => {
17823
- printHeader();
17795
+ const isForeground = options2.foreground;
17796
+ if (!isForeground) {
17797
+ printHeader();
17798
+ }
17824
17799
  if (await isServerRunning()) {
17825
17800
  const info = getServerInfo();
17826
- logger.error(`Server already running on port ${info?.port}`);
17801
+ if (isForeground) {
17802
+ console.error(`Server already running on port ${info?.port}`);
17803
+ } else {
17804
+ logger.error(`Server already running on port ${info?.port}`);
17805
+ }
17827
17806
  process.exit(1);
17828
17807
  }
17829
17808
  const sourceBrowser = resolveSourceBrowser(options2.browser);
17830
17809
  const port = parseInt(options2.port, 10);
17831
- if (!options2.foreground) {
17832
- const serverSpinner2 = spinner("Starting server").start();
17810
+ if (!isForeground) {
17811
+ const serverSpinner = spinner("Starting server").start();
17833
17812
  try {
17834
17813
  const browserServer = await spawnServer({
17835
17814
  port,
@@ -17838,55 +17817,30 @@ var start = new Command().name("start").description("start browser server manual
17838
17817
  browser: options2.browser,
17839
17818
  domain: options2.domain
17840
17819
  });
17841
- serverSpinner2.succeed(`Server running on port ${browserServer.port}`);
17820
+ serverSpinner.succeed(`Server running on port ${browserServer.port}`);
17842
17821
  logger.dim(`CDP: ${browserServer.wsEndpoint}`);
17843
17822
  } catch (error48) {
17844
- serverSpinner2.fail();
17823
+ serverSpinner.fail();
17845
17824
  exitWithError(error48);
17846
17825
  }
17847
17826
  return;
17848
17827
  }
17849
- const serverSpinner = spinner("Starting server").start();
17850
17828
  try {
17851
17829
  const browserServer = await serve({
17852
17830
  port,
17853
17831
  headless: !options2.headed
17854
17832
  });
17855
- serverSpinner.succeed(`Server running on port ${browserServer.port}`);
17856
- logger.dim(`CDP: ${browserServer.wsEndpoint}`);
17857
- const cookieSpinner = spinner("Loading cookies").start();
17858
- try {
17859
- const cookies = dumpCookies(sourceBrowser, { domain: options2.domain });
17860
- const playwrightCookies = toPlaywrightCookies(cookies);
17861
- const browser2 = await chromium.connectOverCDP(browserServer.wsEndpoint);
17862
- const contexts = browser2.contexts();
17863
- if (contexts.length > 0) {
17864
- if (playwrightCookies.length > 0) {
17865
- await contexts[0].addCookies(playwrightCookies);
17866
- }
17867
- await applyStealthScripts(contexts[0]);
17868
- }
17869
- await browser2.close();
17870
- cookieSpinner.succeed(`Loaded ${playwrightCookies.length} cookies from ${sourceBrowser}`);
17871
- } catch (cookieError) {
17872
- const errorMessage = cookieError instanceof Error ? cookieError.message : "Unknown error";
17873
- const isModuleVersionError = errorMessage.includes("NODE_MODULE_VERSION");
17874
- if (!isModuleVersionError) {
17875
- cookieSpinner.fail(`Failed to load cookies from ${sourceBrowser}`);
17876
- } else {
17877
- cookieSpinner.info("Native module mismatch. Rebuilding...");
17878
- try {
17879
- const currentDir = dirname(fileURLToPath(import.meta.url));
17880
- const browserPkgDir = join(currentDir, "..", "..");
17881
- await browserServer.stop();
17882
- await rebuildNativeModuleAndRestart(browserPkgDir);
17883
- return;
17884
- } catch {
17885
- cookieSpinner.fail("Auto-rebuild failed. Run: npm rebuild better-sqlite3");
17886
- }
17833
+ const cookies = dumpCookies(sourceBrowser, { domain: options2.domain });
17834
+ const playwrightCookies = toPlaywrightCookies(cookies);
17835
+ const browser2 = await chromium.connectOverCDP(browserServer.wsEndpoint);
17836
+ const contexts = browser2.contexts();
17837
+ if (contexts.length > 0) {
17838
+ if (playwrightCookies.length > 0) {
17839
+ await contexts[0].addCookies(playwrightCookies);
17887
17840
  }
17841
+ await applyStealthScripts(contexts[0]);
17888
17842
  }
17889
- spinner("Server ready. Press Ctrl+C to stop.").succeed();
17843
+ await browser2.close();
17890
17844
  const shutdownHandler = () => {
17891
17845
  browserServer.stop().catch(() => {
17892
17846
  }).finally(() => process.exit(0));
@@ -17897,8 +17851,8 @@ var start = new Command().name("start").description("start browser server manual
17897
17851
  await new Promise(() => {
17898
17852
  });
17899
17853
  } catch (error48) {
17900
- serverSpinner.fail();
17901
- exitWithError(error48);
17854
+ console.error(error48 instanceof Error ? error48.message : "Failed to start server");
17855
+ process.exit(1);
17902
17856
  }
17903
17857
  });
17904
17858
  var stop = new Command().name("stop").description("stop the browser server").action(async () => {
@@ -17963,7 +17917,7 @@ var execute = new Command().name("execute").description("run Playwright code wit
17963
17917
  let activePage = null;
17964
17918
  let browser2 = null;
17965
17919
  let pageOpenHandler = null;
17966
- const buildOutput = createOutputJson(() => activePage, pageName);
17920
+ const outputJson = createOutputJson(() => activePage, pageName);
17967
17921
  let exitCode = 0;
17968
17922
  try {
17969
17923
  const { serverUrl } = await ensureHealthyServer({
@@ -17988,19 +17942,19 @@ var execute = new Command().name("execute").description("run Playwright code wit
17988
17942
  };
17989
17943
  context.on("page", pageOpenHandler);
17990
17944
  const getActivePage2 = createActivePageGetter(context, () => activePage);
17991
- const snapshot = createSnapshotHelper(getActivePage2);
17992
- const ref = createRefHelper(getActivePage2);
17993
- const fill = createFillHelper(ref, getActivePage2);
17945
+ const getSnapshot = createSnapshotHelper(getActivePage2);
17946
+ const getRef = createRefHelper(getActivePage2);
17947
+ const fill = createFillHelper(getRef, getActivePage2);
17994
17948
  const drag = createDragHelper(getActivePage2);
17995
17949
  const dispatch = createDispatchHelper(getActivePage2);
17996
- const grab = createGrabHelper(ref, getActivePage2);
17950
+ const grab = createGrabHelper(getRef, getActivePage2);
17997
17951
  const waitFor = createWaitForHelper(getActivePage2);
17998
17952
  const component = createComponentHelper(getActivePage2);
17999
17953
  const executeFunction = new Function(
18000
17954
  "page",
18001
17955
  "getActivePage",
18002
- "snapshot",
18003
- "ref",
17956
+ "getSnapshot",
17957
+ "getRef",
18004
17958
  "fill",
18005
17959
  "drag",
18006
17960
  "dispatch",
@@ -18009,10 +17963,21 @@ var execute = new Command().name("execute").description("run Playwright code wit
18009
17963
  "component",
18010
17964
  `return (async () => { ${code} })();`
18011
17965
  );
18012
- const result = await executeFunction(getActivePage2(), getActivePage2, snapshot, ref, fill, drag, dispatch, grab, waitFor, component);
18013
- console.log(JSON.stringify(await buildOutput(true, result)));
17966
+ const result = await executeFunction(
17967
+ getActivePage2(),
17968
+ getActivePage2,
17969
+ getSnapshot,
17970
+ getRef,
17971
+ fill,
17972
+ drag,
17973
+ dispatch,
17974
+ grab,
17975
+ waitFor,
17976
+ component
17977
+ );
17978
+ console.log(JSON.stringify(await outputJson(true, result)));
18014
17979
  } catch (error48) {
18015
- console.log(JSON.stringify(await buildOutput(false, void 0, error48 instanceof Error ? error48.message : "Failed")));
17980
+ console.log(JSON.stringify(await outputJson(false, void 0, error48 instanceof Error ? error48.message : "Failed")));
18016
17981
  exitCode = 1;
18017
17982
  } finally {
18018
17983
  if (activePage && pageOpenHandler) {
@@ -18067,37 +18032,32 @@ PERFORMANCE TIPS
18067
18032
  1. Batch multiple actions in a single execute call to minimize round-trips.
18068
18033
  Each execute spawns a new connection, so combining actions is 3-5x faster.
18069
18034
 
18070
- 2. Use interactableOnly or limit depth for smaller snapshots (faster, fewer tokens).
18071
- - snapshot({interactableOnly: true}) -> only clickable/input elements
18072
- - snapshot({maxDepth: 5}) -> limit tree depth
18035
+ 2. Use maxDepth to limit tree depth for smaller snapshots (faster, fewer tokens).
18036
+ - getSnapshot({maxDepth: 5}) -> limit tree depth
18073
18037
 
18074
- # SLOW: 3 separate round-trips, full snapshot
18038
+ # SLOW: 3 separate round-trips
18075
18039
  execute "await page.goto('https://example.com')"
18076
- execute "await ref('e1').click()"
18077
- execute "return await snapshot()"
18040
+ execute "await getRef('e1').click()"
18041
+ execute "return await getSnapshot()"
18078
18042
 
18079
- # FAST: 1 round-trip, interactable only
18043
+ # FAST: 1 round-trip (same result, 3x faster)
18080
18044
  execute "
18081
18045
  await page.goto('https://example.com');
18082
- await ref('e1').click();
18083
- return await snapshot({interactableOnly: true});
18046
+ await getRef('e1').click();
18047
+ return await getSnapshot();
18084
18048
  "
18085
18049
 
18086
18050
  HELPERS
18087
18051
  page - Playwright Page object
18088
- snapshot(opts?) - Get ARIA accessibility tree with refs
18052
+ getSnapshot(opts?)- Get ARIA accessibility tree with refs
18089
18053
  opts.maxDepth: limit tree depth (e.g., 5)
18090
- opts.interactableOnly: only clickable/input elements
18091
- ref(id) - Get element by ref ID (chainable - supports all ElementHandle methods)
18092
- Example: await ref('e1').click()
18093
- Example: await ref('e1').getAttribute('data-foo')
18094
- ref(id).source() - Get React component source file info for element
18054
+ getRef(id) - Get element by ref ID (chainable - supports all ElementHandle methods)
18055
+ Example: await getRef('e1').click()
18056
+ Example: await getRef('e1').getAttribute('data-foo')
18057
+ getRef(id).source() - Get React component source file info for element
18095
18058
  Returns { filePath, lineNumber, componentName } or null
18096
- ref(id).props() - Get React component props (serialized)
18097
- ref(id).state() - Get React component state/hooks (serialized)
18098
- component(name, opts?) - Find elements by React component name
18099
- opts.nth: get the nth matching element (0-indexed)
18100
- Example: await component('Button', {nth: 0})
18059
+ getRef(id).props() - Get React component props (serialized)
18060
+ getRef(id).state() - Get React component state/hooks (serialized)
18101
18061
  fill(id, text) - Clear and fill input (works with rich text editors)
18102
18062
  drag(opts) - Drag with custom MIME types
18103
18063
  opts.from: source selector or ref ID (e.g., "e1" or "text=src")
@@ -18116,14 +18076,11 @@ HELPERS
18116
18076
  grab - React Grab client API (activate, copyElement, etc)
18117
18077
 
18118
18078
  SNAPSHOT OPTIONS
18119
- # Full YAML tree (default, can be large)
18120
- execute "return await snapshot()"
18121
-
18122
- # Interactable only (recommended - much smaller!)
18123
- execute "return await snapshot({interactableOnly: true})"
18079
+ # Full YAML tree (default)
18080
+ execute "return await getSnapshot()"
18124
18081
 
18125
- # With depth limit
18126
- execute "return await snapshot({interactableOnly: true, maxDepth: 6})"
18082
+ # With depth limit (smaller output)
18083
+ execute "return await getSnapshot({maxDepth: 6})"
18127
18084
 
18128
18085
  SCREENSHOTS - PREFER ELEMENT OVER FULL PAGE
18129
18086
  For visual issues (wrong color, broken styling, misalignment), ALWAYS screenshot
@@ -18133,8 +18090,8 @@ SCREENSHOTS - PREFER ELEMENT OVER FULL PAGE
18133
18090
  - Easier to compare
18134
18091
 
18135
18092
  # Element screenshot (PREFERRED)
18136
- execute "await ref('e1').screenshot({path: '/tmp/button.png'})"
18137
- execute "await ref('e5').screenshot({path: '/tmp/card.png'})"
18093
+ execute "await getRef('e1').screenshot({path: '/tmp/button.png'})"
18094
+ execute "await getRef('e5').screenshot({path: '/tmp/card.png'})"
18138
18095
 
18139
18096
  # Full page (only when needed)
18140
18097
  execute "await page.screenshot({path: '/tmp/full.png'})"
@@ -18144,10 +18101,10 @@ SCREENSHOTS - PREFER ELEMENT OVER FULL PAGE
18144
18101
 
18145
18102
  COMMON PATTERNS
18146
18103
  # Click by ref (chainable - no double await needed!)
18147
- execute "await ref('e1').click()"
18104
+ execute "await getRef('e1').click()"
18148
18105
 
18149
18106
  # Get element attribute
18150
- execute "return await ref('e1').getAttribute('data-id')"
18107
+ execute "return await getRef('e1').getAttribute('data-id')"
18151
18108
 
18152
18109
  # Fill input (clears existing content)
18153
18110
  execute "await fill('e1', 'text')"
@@ -18174,23 +18131,17 @@ COMMON PATTERNS
18174
18131
 
18175
18132
  REACT-SPECIFIC PATTERNS
18176
18133
  # Get React component source file
18177
- execute "return await ref('e1').source()"
18134
+ execute "return await getRef('e1').source()"
18178
18135
 
18179
18136
  # Get component props
18180
- execute "return await ref('e1').props()"
18137
+ execute "return await getRef('e1').props()"
18181
18138
 
18182
18139
  # Get component state
18183
- execute "return await ref('e1').state()"
18184
-
18185
- # Find elements by React component name
18186
- execute "const buttons = await component('Button'); return buttons.length"
18187
-
18188
- # Get the first Button component and click it
18189
- execute "const btn = await component('Button', {nth: 0}); await btn.click()"
18140
+ execute "return await getRef('e1').state()"
18190
18141
 
18191
18142
  MULTI-PAGE SESSIONS
18192
18143
  execute "await page.goto('https://github.com')" --page github
18193
- execute "return await snapshot({interactableOnly: true})" --page github
18144
+ execute "return await getSnapshot()" --page github
18194
18145
 
18195
18146
  PLAYWRIGHT DOCS: https://playwright.dev/docs/api/class-page
18196
18147
  `;
@@ -18244,7 +18195,7 @@ browser.addCommand(status);
18244
18195
  browser.addCommand(execute);
18245
18196
  browser.addCommand(pages);
18246
18197
  browser.addCommand(mcp);
18247
- var VERSION3 = "0.1.0-beta.4";
18198
+ var VERSION3 = "0.1.0-beta.5";
18248
18199
  var isMac = process.platform === "darwin";
18249
18200
  var META_LABEL = isMac ? "Cmd" : "Win";
18250
18201
  var ALT_LABEL = isMac ? "Option" : "Alt";
@@ -18682,122 +18633,48 @@ var configure = new Command().name("configure").alias("config").description("con
18682
18633
  handleError(error48);
18683
18634
  }
18684
18635
  });
18685
-
18686
- // src/utils/cli-helpers.ts
18687
- var formatInstalledAgentNames = (agents) => agents.map((agent) => AGENT_NAMES[agent] ?? agent).join(", ");
18688
- var applyTransformWithFeedback = (result, message) => {
18689
- const writeSpinner = spinner(message ?? `Applying changes to ${result.filePath}.`).start();
18690
- const writeResult = applyTransform(result);
18691
- if (!writeResult.success) {
18692
- writeSpinner.fail();
18693
- logger.break();
18694
- logger.error(writeResult.error || "Failed to write file.");
18695
- logger.break();
18696
- process.exit(1);
18697
- }
18698
- writeSpinner.succeed();
18699
- };
18700
- var applyPackageJsonWithFeedback = (result, message) => {
18701
- const writeSpinner = spinner(message ?? `Applying changes to ${result.filePath}.`).start();
18702
- const writeResult = applyPackageJsonTransform(result);
18703
- if (!writeResult.success) {
18704
- writeSpinner.fail();
18705
- logger.break();
18706
- logger.error(writeResult.error || "Failed to write file.");
18707
- logger.break();
18708
- process.exit(1);
18709
- }
18710
- writeSpinner.succeed();
18711
- };
18712
- var installPackagesWithFeedback = (packages, packageManager, projectRoot) => {
18713
- if (packages.length === 0) return;
18714
- const installSpinner = spinner(`Installing ${packages.join(", ")}.`).start();
18715
- try {
18716
- installPackages(packages, packageManager, projectRoot);
18717
- installSpinner.succeed();
18718
- } catch (error48) {
18719
- installSpinner.fail();
18720
- handleError(error48);
18721
- }
18722
- };
18723
- var uninstallPackagesWithFeedback = (packages, packageManager, projectRoot) => {
18724
- if (packages.length === 0) return;
18725
- const uninstallSpinner = spinner(`Removing ${packages.join(", ")}.`).start();
18726
- try {
18727
- uninstallPackages(packages, packageManager, projectRoot);
18728
- uninstallSpinner.succeed();
18729
- } catch (error48) {
18730
- uninstallSpinner.fail();
18731
- handleError(error48);
18732
- }
18733
- };
18734
- var VERSION4 = "0.1.0-beta.4";
18636
+ var VERSION4 = "0.1.0-beta.5";
18735
18637
  var REPORT_URL = "https://react-grab.com/api/report-cli";
18736
18638
  var DOCS_URL = "https://github.com/aidenybai/react-grab";
18737
- var promptAgentIntegration = async (cwd, customPkg) => {
18738
- const { integrationType } = await prompts3({
18639
+ var promptSkillInstall = async (cwd) => {
18640
+ const detectedAgents = detectSkillAgents(cwd);
18641
+ if (detectedAgents.length === 0) {
18642
+ return;
18643
+ }
18644
+ logger.log(`The ${highlighter.info("React Grab skill")} gives your agent access to the browser.`);
18645
+ logger.log(`Learn more at ${highlighter.info("https://skill.md")}`);
18646
+ logger.break();
18647
+ const { wantSkill } = await prompts3({
18648
+ type: "confirm",
18649
+ name: "wantSkill",
18650
+ message: `Would you like to install the skill?`,
18651
+ initial: true
18652
+ });
18653
+ if (!wantSkill) return;
18654
+ const { selectedAgent } = await prompts3({
18739
18655
  type: "select",
18740
- name: "integrationType",
18741
- message: `Would you like to add ${highlighter.info("agent integration")}?`,
18656
+ name: "selectedAgent",
18657
+ message: `Which ${highlighter.info("agent")} would you like to install the skill for?`,
18742
18658
  choices: [
18743
- { title: "MCP Server (browser automation for your agent)", value: "mcp" },
18744
- { title: "Skill (for agents like Codex)", value: "skill" },
18745
- { title: "Both", value: "both" },
18746
- { title: "Neither", value: "none" }
18659
+ ...detectedAgents.map((agent) => ({
18660
+ title: agent.name,
18661
+ value: agent
18662
+ })),
18663
+ { title: "Skip", value: null }
18747
18664
  ]
18748
18665
  });
18749
- if (!integrationType || integrationType === "none") return;
18750
- if (integrationType === "mcp" || integrationType === "both") {
18751
- const { mcpClient } = await prompts3({
18752
- type: "select",
18753
- name: "mcpClient",
18754
- message: `Which ${highlighter.info("MCP client")} would you like to configure?`,
18755
- choices: MCP_CLIENTS.map((client) => ({
18756
- title: MCP_CLIENT_NAMES[client],
18757
- value: client
18758
- }))
18759
- });
18760
- if (mcpClient) {
18761
- const mcpCommand = customPkg ? `npx -y ${customPkg} browser mcp` : `npx -y @react-grab/cli browser mcp`;
18762
- logger.break();
18763
- try {
18764
- execSync(
18765
- `npx -y install-mcp '${mcpCommand}' --client ${mcpClient} --yes`,
18766
- { stdio: "inherit", cwd }
18767
- );
18768
- logger.break();
18769
- logger.success("MCP server has been configured.");
18770
- } catch {
18771
- logger.break();
18772
- logger.warn("Failed to configure MCP server. You can try again later with:");
18773
- logger.log(` npx -y install-mcp '${mcpCommand}' --client ${mcpClient}`);
18774
- }
18775
- }
18776
- }
18777
- if (integrationType === "skill" || integrationType === "both") {
18778
- const { skillTarget } = await prompts3({
18779
- type: "select",
18780
- name: "skillTarget",
18781
- message: `Which ${highlighter.info("agent")} would you like to install the skill for?`,
18782
- choices: SUPPORTED_TARGETS.map((target) => ({
18783
- title: target,
18784
- value: target
18785
- }))
18786
- });
18787
- if (skillTarget) {
18788
- logger.break();
18789
- const skillSpinner = spinner("Installing browser automation skill").start();
18790
- try {
18791
- const skill = await fetchSkillFile();
18792
- const skillDir = join(cwd, AGENT_TARGETS[skillTarget]);
18793
- rmSync(skillDir, { recursive: true, force: true });
18794
- mkdirSync(skillDir, { recursive: true });
18795
- writeFileSync(join(skillDir, "SKILL.md"), skill);
18796
- skillSpinner.succeed(`Skill installed to ${AGENT_TARGETS[skillTarget]}/`);
18797
- } catch {
18798
- skillSpinner.fail("Failed to install skill");
18799
- logger.dim("Try manually: npx -y openskills install aidenybai/react-grab");
18800
- }
18666
+ if (selectedAgent) {
18667
+ logger.break();
18668
+ const skillSpinner = spinner(`Installing skill for ${selectedAgent.name}`).start();
18669
+ try {
18670
+ execSync(`npx -y add-skill aidenybai/react-grab -y --agent ${selectedAgent.id}`, {
18671
+ stdio: "ignore",
18672
+ cwd
18673
+ });
18674
+ skillSpinner.succeed(`Skill installed for ${selectedAgent.name}.`);
18675
+ } catch {
18676
+ skillSpinner.fail(`Failed to install skill for ${selectedAgent.name}.`);
18677
+ logger.warn(`Try manually: npx -y add-skill aidenybai/react-grab --agent ${selectedAgent.id}`);
18801
18678
  }
18802
18679
  }
18803
18680
  logger.break();
@@ -18931,7 +18808,7 @@ var init = new Command().name("init").description("initialize React Grab in your
18931
18808
  );
18932
18809
  logger.break();
18933
18810
  }
18934
- await promptAgentIntegration(projectInfo.projectRoot, opts2.pkg);
18811
+ await promptSkillInstall(cwd);
18935
18812
  const { wantCustomizeOptions } = await prompts3({
18936
18813
  type: "confirm",
18937
18814
  name: "wantCustomizeOptions",
@@ -19097,14 +18974,17 @@ var init = new Command().name("init").description("initialize React Grab in your
19097
18974
  type: "select",
19098
18975
  name: "agent",
19099
18976
  message: `Which ${highlighter.info("agent integration")} would you like to add?`,
19100
- choices: availableAgents.map((innerAgent) => ({
19101
- title: getAgentName(innerAgent),
19102
- value: innerAgent
19103
- }))
18977
+ choices: [
18978
+ ...availableAgents.map((innerAgent) => ({
18979
+ title: getAgentName(innerAgent),
18980
+ value: innerAgent
18981
+ })),
18982
+ { title: "Skip", value: "skip" }
18983
+ ]
19104
18984
  });
19105
- if (agent === void 0) {
18985
+ if (agent === void 0 || agent === "skip") {
19106
18986
  logger.break();
19107
- process.exit(1);
18987
+ process.exit(0);
19108
18988
  }
19109
18989
  const agentIntegration2 = agent;
19110
18990
  let agentsToRemove2 = [];
@@ -19267,17 +19147,21 @@ var init = new Command().name("init").description("initialize React Grab in your
19267
19147
  type: "select",
19268
19148
  name: "selectedProject",
19269
19149
  message: "Select a project to install React Grab:",
19270
- choices: sortedProjects.map((project) => {
19271
- const frameworkLabel = project.framework !== "unknown" ? ` ${highlighter.dim(`(${FRAMEWORK_NAMES[project.framework]})`)}` : "";
19272
- return {
19273
- title: `${project.name}${frameworkLabel}`,
19274
- value: project.path
19275
- };
19276
- })
19150
+ choices: [
19151
+ ...sortedProjects.map((project) => {
19152
+ const frameworkLabel = project.framework !== "unknown" ? ` ${highlighter.dim(`(${FRAMEWORK_NAMES[project.framework]})`)}` : "";
19153
+ return {
19154
+ title: `${project.name}${frameworkLabel}`,
19155
+ value: project.path
19156
+ };
19157
+ }),
19158
+ { title: "Skip", value: "skip" }
19159
+ ]
19277
19160
  });
19278
- if (!selectedProject) {
19161
+ if (!selectedProject || selectedProject === "skip") {
19279
19162
  logger.break();
19280
- process.exit(1);
19163
+ await promptSkillInstall(cwd);
19164
+ process.exit(0);
19281
19165
  }
19282
19166
  process.chdir(selectedProject);
19283
19167
  const newProjectInfo = await detectProject(selectedProject);
@@ -19419,7 +19303,7 @@ var init = new Command().name("init").description("initialize React Grab in your
19419
19303
  }
19420
19304
  logger.break();
19421
19305
  if (!isNonInteractive) {
19422
- await promptAgentIntegration(projectInfo.projectRoot, opts2.pkg);
19306
+ await promptSkillInstall(cwd);
19423
19307
  }
19424
19308
  await reportToCli("completed", {
19425
19309
  framework: finalFramework,
@@ -19433,7 +19317,7 @@ var init = new Command().name("init").description("initialize React Grab in your
19433
19317
  await reportToCli("error", void 0, error48);
19434
19318
  }
19435
19319
  });
19436
- var VERSION5 = "0.1.0-beta.4";
19320
+ var VERSION5 = "0.1.0-beta.5";
19437
19321
  var remove = new Command().name("remove").description("remove an agent integration").argument(
19438
19322
  "[agent]",
19439
19323
  "agent to remove (claude-code, cursor, opencode, codex, gemini, amp, ami, visual-edit)"
@@ -19610,9 +19494,123 @@ var remove = new Command().name("remove").description("remove an agent integrati
19610
19494
  handleError(error48);
19611
19495
  }
19612
19496
  });
19497
+ var VERSION6 = "0.1.0-beta.5";
19498
+ var UPDATE_COMMANDS = {
19499
+ npm: "npm install -g grab@latest",
19500
+ yarn: "yarn global add grab@latest",
19501
+ pnpm: "pnpm add -g grab@latest",
19502
+ bun: "bun add -g grab@latest"
19503
+ };
19504
+ var MANAGER_NAMES = {
19505
+ npm: "npm",
19506
+ yarn: "Yarn",
19507
+ pnpm: "pnpm",
19508
+ bun: "Bun"
19509
+ };
19510
+ var execSilent = (command) => {
19511
+ try {
19512
+ return execSync(command, {
19513
+ encoding: "utf-8",
19514
+ stdio: ["pipe", "pipe", "pipe"]
19515
+ }).trim();
19516
+ } catch {
19517
+ return null;
19518
+ }
19519
+ };
19520
+ var getInstalledVersion = () => {
19521
+ const output = execSilent("grab --version");
19522
+ if (!output) return null;
19523
+ const match = output.match(/(\d+\.\d+\.\d+(?:-[a-zA-Z0-9.]+)?)/);
19524
+ return match ? match[1] : null;
19525
+ };
19526
+ var getLatestVersion = () => execSilent("npm view grab version");
19527
+ var isPackageManagerInstalled = (manager) => execSilent(`${manager} --version`) !== null;
19528
+ var detectGlobalPackageManager = () => {
19529
+ const managers = ["pnpm", "yarn", "bun"];
19530
+ for (const manager of managers) {
19531
+ if (isPackageManagerInstalled(manager)) {
19532
+ return manager;
19533
+ }
19534
+ }
19535
+ return "npm";
19536
+ };
19537
+ var update = new Command().name("update").alias("upgrade").description("update grab to the latest version").option("-y, --yes", "skip confirmation prompts", false).option("--check", "only check for updates, don't install", false).action(async (opts2) => {
19538
+ console.log(
19539
+ `${pc.magenta("\u273F")} ${pc.bold("React Grab")} ${pc.gray(VERSION6)}`
19540
+ );
19541
+ console.log();
19542
+ const checkSpinner = spinner("Checking for updates").start();
19543
+ const installedVersion = getInstalledVersion();
19544
+ const latestVersion = getLatestVersion();
19545
+ if (!latestVersion) {
19546
+ checkSpinner.fail("Failed to fetch latest version from npm");
19547
+ logger.break();
19548
+ logger.error("Could not reach npm registry. Check your network connection.");
19549
+ logger.break();
19550
+ process.exit(1);
19551
+ }
19552
+ if (!installedVersion) {
19553
+ checkSpinner.fail("grab is not installed globally");
19554
+ logger.break();
19555
+ logger.log(`Install it with: ${highlighter.info("npm install -g grab")}`);
19556
+ logger.break();
19557
+ process.exit(1);
19558
+ }
19559
+ checkSpinner.succeed(`Current version: ${highlighter.info(installedVersion)}`);
19560
+ if (installedVersion === latestVersion) {
19561
+ logger.break();
19562
+ logger.success(`You're already on the latest version (${latestVersion})`);
19563
+ logger.break();
19564
+ process.exit(0);
19565
+ }
19566
+ logger.log(`Latest version: ${highlighter.success(latestVersion)}`);
19567
+ logger.break();
19568
+ if (opts2.check) {
19569
+ logger.log(`Update available: ${installedVersion} \u2192 ${latestVersion}`);
19570
+ logger.log(`Run ${highlighter.info("grab update")} to install the update.`);
19571
+ logger.break();
19572
+ process.exit(0);
19573
+ }
19574
+ const packageManager = detectGlobalPackageManager();
19575
+ if (!opts2.yes) {
19576
+ const { shouldProceed } = await prompts3({
19577
+ type: "confirm",
19578
+ name: "shouldProceed",
19579
+ message: `Update grab from ${installedVersion} to ${latestVersion} using ${MANAGER_NAMES[packageManager]}?`,
19580
+ initial: true
19581
+ });
19582
+ if (!shouldProceed) {
19583
+ logger.break();
19584
+ logger.log("Update cancelled.");
19585
+ logger.break();
19586
+ process.exit(0);
19587
+ }
19588
+ }
19589
+ logger.break();
19590
+ const updateSpinner = spinner(`Updating grab to ${latestVersion}`).start();
19591
+ try {
19592
+ const command = UPDATE_COMMANDS[packageManager];
19593
+ execSync(command, {
19594
+ stdio: ["pipe", "pipe", "pipe"]
19595
+ });
19596
+ updateSpinner.succeed(`Updated grab to ${latestVersion}`);
19597
+ logger.break();
19598
+ logger.success("Update complete!");
19599
+ logger.break();
19600
+ } catch (error48) {
19601
+ updateSpinner.fail("Failed to update grab");
19602
+ logger.break();
19603
+ logger.error(`Update failed. Try manually: ${highlighter.info(UPDATE_COMMANDS[packageManager])}`);
19604
+ if (error48 instanceof Error && error48.message) {
19605
+ logger.dim(error48.message);
19606
+ }
19607
+ logger.break();
19608
+ process.exit(1);
19609
+ }
19610
+ });
19613
19611
 
19614
19612
  // src/cli.ts
19615
- var VERSION6 = "0.1.0-beta.4";
19613
+ var VERSION7 = "0.1.0-beta.5";
19616
19614
  var VERSION_API_URL = "https://www.react-grab.com/api/version";
19617
19615
  process.on("SIGINT", () => process.exit(0));
19618
19616
  process.on("SIGTERM", () => process.exit(0));
@@ -19621,12 +19619,13 @@ try {
19621
19619
  });
19622
19620
  } catch {
19623
19621
  }
19624
- var program = new Command().name("react-grab").description("add React Grab to your project").version(VERSION6, "-v, --version", "display the version number");
19622
+ var program = new Command().name("grab").description("add React Grab to your project").version(VERSION7, "-v, --version", "display the version number");
19625
19623
  program.addCommand(init);
19626
19624
  program.addCommand(add);
19627
19625
  program.addCommand(remove);
19628
19626
  program.addCommand(configure);
19629
19627
  program.addCommand(browser);
19628
+ program.addCommand(update);
19630
19629
  var completion = f2(program);
19631
19630
  var initCommand = completion.commands.get("init");
19632
19631
  var initAgentOption = initCommand?.options.get("agent");
@@ -19650,8 +19649,8 @@ var addCommand = completion.commands.get("add");
19650
19649
  var addAgentArg = addCommand?.arguments.get("agent");
19651
19650
  if (addAgentArg) {
19652
19651
  addAgentArg.handler = (complete) => {
19653
- complete("mcp", "MCP server for browser automation");
19654
- complete("skill", "Browser automation skill for AI agents");
19652
+ complete("skill", "Instructions for your agent to use the browser (recommended)");
19653
+ complete("mcp", "A server that provides browser tools to your agent");
19655
19654
  for (const agent of AGENTS) {
19656
19655
  complete(agent, "");
19657
19656
  }