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