@react-grab/cli 0.1.46 → 0.1.47-dev.d5e8fc8

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.
@@ -0,0 +1,864 @@
1
+ #!/usr/bin/env node
2
+ import { basename, delimiter, dirname, join, resolve } from "node:path";
3
+ import { accessSync, constants, existsSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
4
+ import { detect } from "package-manager-detector/detect";
5
+ import ignore from "ignore";
6
+ import { add, detectInstalledSkillAgents, getCanonicalSkillsDir, getSkillAgentConfig, getSkillAgentDir, getSkillAgentTypes, isUniversalSkillAgent } from "agent-install/skill";
7
+ import { fileURLToPath } from "node:url";
8
+ import { x } from "tinyexec";
9
+ //#region src/utils/react-grab-code.ts
10
+ const REACT_GRAB_SPECIFIER_PATTERN = String.raw`react-grab(?:\/[^"']+)?`;
11
+ const stripComments = (content) => content.replace(/<!--[\s\S]*?-->/g, "").replace(/\{\/\*[\s\S]*?\*\/\}/g, "").replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|\s)\/\/.*$/gm, "$1");
12
+ const stripTypeOnlyReactGrabImports = (content) => {
13
+ return content.replace(new RegExp(String.raw`import\s+type\s+[^;]+from\s+["']${REACT_GRAB_SPECIFIER_PATTERN}["'];?`, "g"), "").replace(new RegExp(String.raw`import\s*\{\s*type\s+[^,}]+(?:\s*,\s*type\s+[^,}]+)*\s*,?\s*\}\s*from\s+["']${REACT_GRAB_SPECIFIER_PATTERN}["'];?`, "g"), "");
14
+ };
15
+ const hasReactGrabSetupCode = (content) => {
16
+ const setupCandidateContent = stripTypeOnlyReactGrabImports(stripComments(content));
17
+ return [
18
+ new RegExp(String.raw`import\s*\(\s*["']${REACT_GRAB_SPECIFIER_PATTERN}["']\s*\)`),
19
+ new RegExp(String.raw`import\s+(?!type\b)(?:[^"';]+from\s+)?["']${REACT_GRAB_SPECIFIER_PATTERN}["']`),
20
+ new RegExp(String.raw`require\s*\(\s*["']${REACT_GRAB_SPECIFIER_PATTERN}["']\s*\)`),
21
+ /<Script[\s\S]*?src\s*=\s*(?:["'][^"']*react-grab[^"']*["']|\{(?:["'][^"']*react-grab[^"']*["']|`[^`]*react-grab[^`]*`)\})/i,
22
+ /<script[\s\S]*?src\s*=\s*["'][^"']*react-grab[^"']*["']/i
23
+ ].some((pattern) => pattern.test(setupCandidateContent));
24
+ };
25
+ //#endregion
26
+ //#region src/utils/react-grab-setup-files.ts
27
+ const COMPONENT_EXTENSIONS = [
28
+ "tsx",
29
+ "jsx",
30
+ "ts",
31
+ "js"
32
+ ];
33
+ const INSTRUMENTATION_EXTENSIONS = [
34
+ "ts",
35
+ "tsx",
36
+ "js",
37
+ "jsx",
38
+ "mts",
39
+ "cts",
40
+ "mjs",
41
+ "cjs"
42
+ ];
43
+ const ROUTE_EXTENSIONS = ["tsx", "jsx"];
44
+ const createFileCandidates = (projectRoot, directories, baseName, extensions) => {
45
+ const fileCandidates = [];
46
+ for (const directory of directories) for (const extension of extensions) fileCandidates.push(join(projectRoot, directory, `${baseName}.${extension}`));
47
+ return fileCandidates;
48
+ };
49
+ const findExistingFile = (fileCandidates) => {
50
+ for (const filePath of fileCandidates) if (existsSync(filePath)) return filePath;
51
+ return null;
52
+ };
53
+ const getLayoutFileCandidates = (projectRoot) => createFileCandidates(projectRoot, ["app", "src/app"], "layout", COMPONENT_EXTENSIONS);
54
+ const getDocumentFileCandidates = (projectRoot) => createFileCandidates(projectRoot, ["pages", "src/pages"], "_document", COMPONENT_EXTENSIONS);
55
+ const getInstrumentationFileCandidates = (projectRoot) => createFileCandidates(projectRoot, ["", "src"], "instrumentation-client", INSTRUMENTATION_EXTENSIONS);
56
+ const getIndexHtmlCandidates = (projectRoot) => [join(projectRoot, "index.html"), join(projectRoot, "public", "index.html")];
57
+ const getEntryFileCandidates = (projectRoot) => [...createFileCandidates(projectRoot, ["src"], "index", COMPONENT_EXTENSIONS), ...createFileCandidates(projectRoot, ["src"], "main", COMPONENT_EXTENSIONS)];
58
+ const getTanStackRootFileCandidates = (projectRoot) => createFileCandidates(projectRoot, ["src/routes", "app/routes"], "__root", ROUTE_EXTENSIONS);
59
+ const getReactGrabSetupFileCandidates = (projectRoot) => [
60
+ ...getLayoutFileCandidates(projectRoot),
61
+ ...getDocumentFileCandidates(projectRoot),
62
+ ...getInstrumentationFileCandidates(projectRoot),
63
+ ...getIndexHtmlCandidates(projectRoot),
64
+ ...getEntryFileCandidates(projectRoot),
65
+ ...getTanStackRootFileCandidates(projectRoot)
66
+ ];
67
+ const findLayoutFile = (projectRoot) => findExistingFile(getLayoutFileCandidates(projectRoot));
68
+ const findDocumentFile = (projectRoot) => findExistingFile(getDocumentFileCandidates(projectRoot));
69
+ const findIndexHtml = (projectRoot) => findExistingFile(getIndexHtmlCandidates(projectRoot));
70
+ const findEntryFile = (projectRoot) => findExistingFile(getEntryFileCandidates(projectRoot));
71
+ const findTanStackRootFile = (projectRoot) => findExistingFile(getTanStackRootFileCandidates(projectRoot));
72
+ const isInstrumentationFile = (filePath) => /(?:^|[/\\])instrumentation-client\.[cm]?[jt]sx?$/.test(filePath);
73
+ //#endregion
74
+ //#region src/utils/detect.ts
75
+ const VALID_PACKAGE_MANAGERS = new Set([
76
+ "npm",
77
+ "yarn",
78
+ "pnpm",
79
+ "bun"
80
+ ]);
81
+ const detectPackageManager = async (projectRoot) => {
82
+ const result = await detect({ cwd: projectRoot });
83
+ if (result?.agent) {
84
+ const managerName = result.agent.split("@")[0];
85
+ if (VALID_PACKAGE_MANAGERS.has(managerName)) return managerName;
86
+ }
87
+ return "npm";
88
+ };
89
+ const CONFIG_EXTENSIONS = [
90
+ "ts",
91
+ "mts",
92
+ "cts",
93
+ "js",
94
+ "mjs",
95
+ "cjs"
96
+ ];
97
+ const hasConfigFile = (projectRoot, configBaseName) => CONFIG_EXTENSIONS.some((extension) => existsSync(join(projectRoot, `${configBaseName}.${extension}`)));
98
+ const readMergedDependencies = (projectRoot) => {
99
+ const packageJsonPath = join(projectRoot, "package.json");
100
+ if (!existsSync(packageJsonPath)) return null;
101
+ try {
102
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
103
+ return {
104
+ ...packageJson.dependencies,
105
+ ...packageJson.devDependencies
106
+ };
107
+ } catch {
108
+ return null;
109
+ }
110
+ };
111
+ const detectFrameworkFromDependencies = (dependencies) => {
112
+ if (!dependencies) return "unknown";
113
+ if (dependencies["next"]) return "next";
114
+ if (dependencies["@tanstack/react-start"]) return "tanstack";
115
+ if (dependencies["vite"]) return "vite";
116
+ if (dependencies["webpack"]) return "webpack";
117
+ return "unknown";
118
+ };
119
+ const detectFrameworkFromConfigFiles = (projectRoot) => {
120
+ if (hasConfigFile(projectRoot, "next.config")) return "next";
121
+ if (hasConfigFile(projectRoot, "app.config")) return "tanstack";
122
+ if (hasConfigFile(projectRoot, "vite.config")) return "vite";
123
+ if (hasConfigFile(projectRoot, "webpack.config")) return "webpack";
124
+ return "unknown";
125
+ };
126
+ const findEnclosingMonorepoRoot = (projectRoot) => {
127
+ let currentDirectory = dirname(projectRoot);
128
+ while (currentDirectory !== dirname(currentDirectory)) {
129
+ if (detectMonorepo(currentDirectory)) return currentDirectory;
130
+ currentDirectory = dirname(currentDirectory);
131
+ }
132
+ return null;
133
+ };
134
+ const detectFramework = (projectRoot) => {
135
+ const localFramework = detectFrameworkFromDependencies(readMergedDependencies(projectRoot));
136
+ if (localFramework !== "unknown") return localFramework;
137
+ return detectFrameworkFromConfigFiles(projectRoot);
138
+ };
139
+ const detectFrameworkFromMonorepoRoot = (projectRoot) => {
140
+ const monorepoRoot = findEnclosingMonorepoRoot(projectRoot);
141
+ if (!monorepoRoot) return "unknown";
142
+ return detectFrameworkFromDependencies(readMergedDependencies(monorepoRoot));
143
+ };
144
+ const detectNextRouterType = (projectRoot) => {
145
+ const hasAppDir = existsSync(join(projectRoot, "app"));
146
+ const hasSrcAppDir = existsSync(join(projectRoot, "src", "app"));
147
+ const hasPagesDir = existsSync(join(projectRoot, "pages"));
148
+ const hasSrcPagesDir = existsSync(join(projectRoot, "src", "pages"));
149
+ if (hasAppDir || hasSrcAppDir) return "app";
150
+ if (hasPagesDir || hasSrcPagesDir) return "pages";
151
+ return "unknown";
152
+ };
153
+ const detectMonorepo = (projectRoot) => {
154
+ if (existsSync(join(projectRoot, "pnpm-workspace.yaml"))) return true;
155
+ if (existsSync(join(projectRoot, "lerna.json"))) return true;
156
+ const packageJsonPath = join(projectRoot, "package.json");
157
+ if (existsSync(packageJsonPath)) try {
158
+ if (JSON.parse(readFileSync(packageJsonPath, "utf-8")).workspaces) return true;
159
+ } catch {
160
+ return false;
161
+ }
162
+ return false;
163
+ };
164
+ const getWorkspacePatterns = (projectRoot) => {
165
+ const patterns = [];
166
+ const pnpmWorkspacePath = join(projectRoot, "pnpm-workspace.yaml");
167
+ if (existsSync(pnpmWorkspacePath)) {
168
+ const lines = readFileSync(pnpmWorkspacePath, "utf-8").split("\n");
169
+ let inPackages = false;
170
+ for (const line of lines) {
171
+ if (line.match(/^packages:\s*$/)) {
172
+ inPackages = true;
173
+ continue;
174
+ }
175
+ if (inPackages) {
176
+ if (line.match(/^[a-zA-Z]/) || line.trim() === "") {
177
+ if (line.match(/^[a-zA-Z]/)) inPackages = false;
178
+ continue;
179
+ }
180
+ const match = line.match(/^\s*-\s*['"]?([^'"#\n]+?)['"]?\s*$/);
181
+ if (match) patterns.push(match[1].trim());
182
+ }
183
+ }
184
+ }
185
+ const lernaJsonPath = join(projectRoot, "lerna.json");
186
+ if (existsSync(lernaJsonPath)) try {
187
+ const lernaJson = JSON.parse(readFileSync(lernaJsonPath, "utf-8"));
188
+ if (Array.isArray(lernaJson.packages)) patterns.push(...lernaJson.packages);
189
+ } catch {}
190
+ const packageJsonPath = join(projectRoot, "package.json");
191
+ if (existsSync(packageJsonPath)) try {
192
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
193
+ if (Array.isArray(packageJson.workspaces)) patterns.push(...packageJson.workspaces);
194
+ else if (packageJson.workspaces?.packages) patterns.push(...packageJson.workspaces.packages);
195
+ } catch {}
196
+ return [...new Set(patterns)];
197
+ };
198
+ const expandWorkspacePattern = (projectRoot, pattern) => {
199
+ const isGlob = pattern.endsWith("/*");
200
+ const basePath = join(projectRoot, pattern.replace(/\/\*$/, ""));
201
+ if (!existsSync(basePath)) return [];
202
+ if (!isGlob) return existsSync(join(basePath, "package.json")) ? [basePath] : [];
203
+ const results = [];
204
+ try {
205
+ const entries = readdirSync(basePath, { withFileTypes: true });
206
+ for (const entry of entries) {
207
+ if (!entry.isDirectory()) continue;
208
+ if (existsSync(join(basePath, entry.name, "package.json"))) results.push(join(basePath, entry.name));
209
+ }
210
+ } catch {
211
+ return results;
212
+ }
213
+ return results;
214
+ };
215
+ const hasReactDependency = (projectPath) => {
216
+ const dependencies = readMergedDependencies(projectPath);
217
+ if (!dependencies) return false;
218
+ return Boolean(dependencies["react"] || dependencies["react-dom"]);
219
+ };
220
+ const buildReactProject = (projectPath) => {
221
+ const framework = detectFramework(projectPath);
222
+ if (!hasReactDependency(projectPath) && framework === "unknown") return null;
223
+ let name = basename(projectPath);
224
+ const packageJsonPath = join(projectPath, "package.json");
225
+ try {
226
+ name = JSON.parse(readFileSync(packageJsonPath, "utf-8")).name || name;
227
+ } catch {}
228
+ return {
229
+ name,
230
+ path: projectPath,
231
+ framework
232
+ };
233
+ };
234
+ const findWorkspaceProjects = (projectRoot) => {
235
+ const patterns = getWorkspacePatterns(projectRoot);
236
+ const projects = [];
237
+ for (const pattern of patterns) for (const projectPath of expandWorkspacePattern(projectRoot, pattern)) {
238
+ const project = buildReactProject(projectPath);
239
+ if (project) projects.push(project);
240
+ }
241
+ return projects;
242
+ };
243
+ const ALWAYS_IGNORED_DIRECTORIES = [
244
+ "node_modules",
245
+ ".git",
246
+ ".next",
247
+ ".cache",
248
+ ".turbo",
249
+ "dist",
250
+ "build",
251
+ "coverage",
252
+ "test-results"
253
+ ];
254
+ const loadGitignore = (projectRoot) => {
255
+ const ignorer = ignore().add(ALWAYS_IGNORED_DIRECTORIES);
256
+ const gitignorePath = join(projectRoot, ".gitignore");
257
+ if (existsSync(gitignorePath)) try {
258
+ ignorer.add(readFileSync(gitignorePath, "utf-8"));
259
+ } catch {}
260
+ return ignorer;
261
+ };
262
+ const scanDirectoryForProjects = (rootDirectory, ignorer, maxDepth, currentDepth = 0) => {
263
+ if (currentDepth >= maxDepth) return [];
264
+ if (!existsSync(rootDirectory)) return [];
265
+ const projects = [];
266
+ try {
267
+ const entries = readdirSync(rootDirectory, { withFileTypes: true });
268
+ for (const entry of entries) {
269
+ if (!entry.isDirectory()) continue;
270
+ if (ignorer.ignores(entry.name)) continue;
271
+ const entryPath = join(rootDirectory, entry.name);
272
+ if (existsSync(join(entryPath, "package.json"))) {
273
+ const project = buildReactProject(entryPath);
274
+ if (project) {
275
+ projects.push(project);
276
+ continue;
277
+ }
278
+ }
279
+ projects.push(...scanDirectoryForProjects(entryPath, ignorer, maxDepth, currentDepth + 1));
280
+ }
281
+ } catch {
282
+ return projects;
283
+ }
284
+ return projects;
285
+ };
286
+ const MAX_SCAN_DEPTH = 2;
287
+ const normalizePathForComparison = (filePath) => filePath.replace(/\\/g, "/");
288
+ const findReactProjects = (projectRoot) => {
289
+ const monorepoRoot = detectMonorepo(projectRoot) ? projectRoot : findEnclosingMonorepoRoot(projectRoot);
290
+ if (monorepoRoot) {
291
+ const workspaceProjects = findWorkspaceProjects(monorepoRoot);
292
+ const localProject = projectRoot === monorepoRoot ? null : buildReactProject(projectRoot);
293
+ const projects = localProject ? [localProject, ...workspaceProjects.filter((project) => normalizePathForComparison(project.path) !== normalizePathForComparison(localProject.path))] : workspaceProjects;
294
+ if (projects.length > 0) return projects;
295
+ }
296
+ const scannedProjects = scanDirectoryForProjects(projectRoot, loadGitignore(projectRoot), MAX_SCAN_DEPTH);
297
+ if (scannedProjects.length > 0) return scannedProjects;
298
+ let currentDirectory = dirname(projectRoot);
299
+ while (currentDirectory !== dirname(currentDirectory)) {
300
+ const parentProject = buildReactProject(currentDirectory);
301
+ if (parentProject) return [parentProject];
302
+ currentDirectory = dirname(currentDirectory);
303
+ }
304
+ return [];
305
+ };
306
+ const hasReactGrabSetupInFile = (filePath) => {
307
+ if (!existsSync(filePath)) return false;
308
+ try {
309
+ return hasReactGrabSetupCode(readFileSync(filePath, "utf-8"));
310
+ } catch {
311
+ return false;
312
+ }
313
+ };
314
+ const detectReactGrabDependency = (projectRoot) => {
315
+ const dependencies = readMergedDependencies(projectRoot);
316
+ return Boolean(dependencies?.["react-grab"]);
317
+ };
318
+ const detectReactGrabConfigured = (projectRoot) => {
319
+ return getReactGrabSetupFileCandidates(projectRoot).some(hasReactGrabSetupInFile);
320
+ };
321
+ const detectReactGrab = (projectRoot) => detectReactGrabDependency(projectRoot) || detectReactGrabConfigured(projectRoot);
322
+ const detectUnsupportedFramework = (projectRoot) => {
323
+ const dependencies = readMergedDependencies(projectRoot);
324
+ if (!dependencies) return null;
325
+ if (dependencies["@remix-run/react"] || dependencies["remix"]) return "remix";
326
+ if (dependencies["astro"]) return "astro";
327
+ if (dependencies["@sveltejs/kit"]) return "sveltekit";
328
+ if (dependencies["gatsby"]) return "gatsby";
329
+ return null;
330
+ };
331
+ const detectReactGrabVersion = (projectRoot) => {
332
+ const installedPackageJsonPath = join(projectRoot, "node_modules", "react-grab", "package.json");
333
+ if (existsSync(installedPackageJsonPath)) try {
334
+ return JSON.parse(readFileSync(installedPackageJsonPath, "utf-8")).version ?? null;
335
+ } catch {}
336
+ return null;
337
+ };
338
+ const detectProject = async (projectRoot = process.cwd()) => {
339
+ const localFramework = detectFramework(projectRoot);
340
+ const framework = localFramework === "unknown" ? detectFrameworkFromMonorepoRoot(projectRoot) : localFramework;
341
+ const packageManager = await detectPackageManager(projectRoot);
342
+ const isMonorepo = detectMonorepo(projectRoot) || findEnclosingMonorepoRoot(projectRoot) !== null;
343
+ const isReactGrabConfigured = detectReactGrabConfigured(projectRoot);
344
+ return {
345
+ packageManager,
346
+ framework,
347
+ nextRouterType: framework === "next" ? detectNextRouterType(projectRoot) : "unknown",
348
+ isMonorepo,
349
+ projectRoot,
350
+ hasReactGrab: detectReactGrabDependency(projectRoot) || isReactGrabConfigured,
351
+ isReactGrabConfigured,
352
+ reactGrabVersion: detectReactGrabVersion(projectRoot),
353
+ unsupportedFramework: detectUnsupportedFramework(projectRoot)
354
+ };
355
+ };
356
+ //#endregion
357
+ //#region src/utils/detect-agents.ts
358
+ const PATH_BINARIES = {
359
+ "claude-code": ["claude"],
360
+ codex: ["codex"],
361
+ cursor: ["cursor", "cursor-agent"],
362
+ droid: ["droid"],
363
+ "gemini-cli": ["gemini"],
364
+ "github-copilot": ["copilot"],
365
+ opencode: ["opencode"],
366
+ pi: ["pi", "omegon"]
367
+ };
368
+ const isCommandAvailable = (command) => {
369
+ const pathDirectories = (process.env.PATH ?? "").split(delimiter).filter(Boolean);
370
+ for (const directory of pathDirectories) {
371
+ const binaryPath = join(directory, command);
372
+ try {
373
+ if (statSync(binaryPath).isFile()) {
374
+ accessSync(binaryPath, constants.X_OK);
375
+ return true;
376
+ }
377
+ } catch {}
378
+ }
379
+ return false;
380
+ };
381
+ const detectAvailableAgents = async () => {
382
+ const installedAgents = new Set(await detectInstalledSkillAgents());
383
+ return getSkillAgentTypes().filter((agent) => {
384
+ if (agent === "universal") return false;
385
+ if (installedAgents.has(agent)) return true;
386
+ return PATH_BINARIES[agent]?.some(isCommandAvailable) ?? false;
387
+ });
388
+ };
389
+ //#endregion
390
+ //#region src/utils/install-skill.ts
391
+ const SKILL_NAME = "react-grab";
392
+ const SKILL_SOURCE = fileURLToPath(new URL("../skills/react-grab", import.meta.url));
393
+ const agentLabel = (agent) => getSkillAgentConfig(agent).displayName;
394
+ const installedSkillDir = (agent, global, cwd) => join(isUniversalSkillAgent(agent) ? getCanonicalSkillsDir(global, cwd) : getSkillAgentDir(agent, {
395
+ global,
396
+ cwd
397
+ }), SKILL_NAME);
398
+ const installSkill = async ({ agents, global = false, cwd = process.cwd() } = {}) => {
399
+ return add({
400
+ source: SKILL_SOURCE,
401
+ agents: agents ?? await detectAvailableAgents(),
402
+ global,
403
+ cwd,
404
+ mode: "copy"
405
+ });
406
+ };
407
+ const removeSkill = async ({ cwd = process.cwd(), global = false } = {}) => {
408
+ const agents = await detectAvailableAgents();
409
+ const removedAgents = [];
410
+ const dirsToRemove = /* @__PURE__ */ new Set();
411
+ for (const agent of agents) {
412
+ const skillDir = installedSkillDir(agent, global, cwd);
413
+ if (!existsSync(skillDir)) continue;
414
+ removedAgents.push(agent);
415
+ dirsToRemove.add(skillDir);
416
+ }
417
+ for (const skillDir of dirsToRemove) rmSync(skillDir, {
418
+ recursive: true,
419
+ force: true
420
+ });
421
+ return removedAgents;
422
+ };
423
+ //#endregion
424
+ //#region src/utils/templates.ts
425
+ const NEXT_APP_ROUTER_SCRIPT = `{process.env.NODE_ENV === "development" && (
426
+ <Script
427
+ src="//unpkg.com/react-grab/dist/index.global.js"
428
+ crossOrigin="anonymous"
429
+ strategy="beforeInteractive"
430
+ />
431
+ )}`;
432
+ const VITE_IMPORT = `if (import.meta.env.DEV) {
433
+ import("react-grab");
434
+ }`;
435
+ const WEBPACK_IMPORT = `if (process.env.NODE_ENV === "development") {
436
+ import("react-grab");
437
+ }`;
438
+ const TANSTACK_EFFECT = `useEffect(() => {
439
+ if (import.meta.env.DEV) {
440
+ void import("react-grab");
441
+ }
442
+ }, []);`;
443
+ const SCRIPT_IMPORT = "import Script from \"next/script\";";
444
+ //#endregion
445
+ //#region src/utils/transform.ts
446
+ const hasReactGrabInInstrumentation = (projectRoot) => {
447
+ return findFileWithReactGrabSetup(getInstrumentationFileCandidates(projectRoot)) !== null;
448
+ };
449
+ const findFileWithReactGrabSetup = (fileCandidates) => {
450
+ for (const filePath of fileCandidates) {
451
+ if (!existsSync(filePath)) continue;
452
+ if (hasReactGrabSetupCode(readFileSync(filePath, "utf-8"))) return filePath;
453
+ }
454
+ return null;
455
+ };
456
+ const alreadyConfiguredResult = (filePath) => ({
457
+ success: true,
458
+ filePath,
459
+ message: "React Grab is already configured",
460
+ noChanges: true
461
+ });
462
+ const transformNextAppRouter = (projectRoot, reactGrabAlreadyConfigured) => {
463
+ const layoutPath = findLayoutFile(projectRoot);
464
+ if (!layoutPath) return {
465
+ success: false,
466
+ filePath: "",
467
+ message: "Could not find app/layout.tsx, app/layout.jsx, app/layout.ts, or app/layout.js"
468
+ };
469
+ const originalContent = readFileSync(layoutPath, "utf-8");
470
+ let newContent = originalContent;
471
+ const hasReactGrabInFile = hasReactGrabSetupCode(originalContent);
472
+ const hasReactGrabInInstrumentationFile = hasReactGrabInInstrumentation(projectRoot);
473
+ if (hasReactGrabInFile && reactGrabAlreadyConfigured) return alreadyConfiguredResult(layoutPath);
474
+ if (hasReactGrabInFile || hasReactGrabInInstrumentationFile) return {
475
+ success: true,
476
+ filePath: layoutPath,
477
+ message: "React Grab is already installed" + (hasReactGrabInInstrumentationFile ? " in instrumentation-client" : " in this file"),
478
+ noChanges: true
479
+ };
480
+ if (!newContent.includes("import Script from \"next/script\"")) {
481
+ const importMatch = newContent.match(/^import .+ from ['"].+['"];?\s*$/m);
482
+ if (importMatch) newContent = newContent.replace(importMatch[0], `${importMatch[0]}\n${SCRIPT_IMPORT}`);
483
+ else newContent = `${SCRIPT_IMPORT}\n\n${newContent}`;
484
+ }
485
+ const headMatch = newContent.match(/<head[^>]*>/);
486
+ if (headMatch) newContent = newContent.replace(headMatch[0], `${headMatch[0]}\n ${NEXT_APP_ROUTER_SCRIPT}`);
487
+ else {
488
+ const htmlMatch = newContent.match(/<html[^>]*>/);
489
+ if (htmlMatch) newContent = newContent.replace(htmlMatch[0], `${htmlMatch[0]}\n <head>\n ${NEXT_APP_ROUTER_SCRIPT}\n </head>`);
490
+ }
491
+ return {
492
+ success: true,
493
+ filePath: layoutPath,
494
+ message: "Add React Grab",
495
+ originalContent,
496
+ newContent
497
+ };
498
+ };
499
+ const transformNextPagesRouter = (projectRoot, reactGrabAlreadyConfigured) => {
500
+ const documentPath = findDocumentFile(projectRoot);
501
+ if (!documentPath) return {
502
+ success: false,
503
+ filePath: "",
504
+ message: "Could not find pages/_document.tsx, pages/_document.jsx, pages/_document.ts, or pages/_document.js.\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 }"
505
+ };
506
+ const originalContent = readFileSync(documentPath, "utf-8");
507
+ let newContent = originalContent;
508
+ const hasReactGrabInFile = hasReactGrabSetupCode(originalContent);
509
+ const hasReactGrabInInstrumentationFile = hasReactGrabInInstrumentation(projectRoot);
510
+ if (hasReactGrabInFile && reactGrabAlreadyConfigured) return alreadyConfiguredResult(documentPath);
511
+ if (hasReactGrabInFile || hasReactGrabInInstrumentationFile) return {
512
+ success: true,
513
+ filePath: documentPath,
514
+ message: "React Grab is already installed" + (hasReactGrabInInstrumentationFile ? " in instrumentation-client" : " in this file"),
515
+ noChanges: true
516
+ };
517
+ if (!newContent.includes("import Script from \"next/script\"")) {
518
+ const importMatch = newContent.match(/^import .+ from ['"].+['"];?\s*$/m);
519
+ if (importMatch) newContent = newContent.replace(importMatch[0], `${importMatch[0]}\n${SCRIPT_IMPORT}`);
520
+ }
521
+ const headMatch = newContent.match(/<Head[^>]*>/);
522
+ if (headMatch) newContent = newContent.replace(headMatch[0], `${headMatch[0]}\n ${NEXT_APP_ROUTER_SCRIPT}`);
523
+ return {
524
+ success: true,
525
+ filePath: documentPath,
526
+ message: "Add React Grab",
527
+ originalContent,
528
+ newContent
529
+ };
530
+ };
531
+ const checkExistingInstallation = (filePath, reactGrabAlreadyConfigured) => {
532
+ if (!hasReactGrabSetupCode(readFileSync(filePath, "utf-8"))) return null;
533
+ return {
534
+ success: true,
535
+ filePath,
536
+ message: reactGrabAlreadyConfigured ? "React Grab is already configured" : "React Grab is already installed in this file",
537
+ noChanges: true
538
+ };
539
+ };
540
+ const transformVite = (projectRoot, reactGrabAlreadyConfigured) => {
541
+ const entryPath = findEntryFile(projectRoot);
542
+ const indexPath = findIndexHtml(projectRoot);
543
+ if (indexPath) {
544
+ const existingResult = checkExistingInstallation(indexPath, reactGrabAlreadyConfigured);
545
+ if (existingResult) return existingResult;
546
+ }
547
+ if (!entryPath) return {
548
+ success: false,
549
+ filePath: "",
550
+ message: "Could not find entry file (src/index.tsx, src/main.tsx, etc.)"
551
+ };
552
+ const existingResult = checkExistingInstallation(entryPath, reactGrabAlreadyConfigured);
553
+ if (existingResult) return existingResult;
554
+ const originalContent = readFileSync(entryPath, "utf-8");
555
+ return {
556
+ success: true,
557
+ filePath: entryPath,
558
+ message: "Add React Grab",
559
+ originalContent,
560
+ newContent: `${VITE_IMPORT}\n\n${originalContent}`
561
+ };
562
+ };
563
+ const transformWebpack = (projectRoot, reactGrabAlreadyConfigured) => {
564
+ const entryPath = findEntryFile(projectRoot);
565
+ if (!entryPath) return {
566
+ success: false,
567
+ filePath: "",
568
+ message: "Could not find entry file (src/index.tsx, src/main.tsx, etc.)"
569
+ };
570
+ const existingResult = checkExistingInstallation(entryPath, reactGrabAlreadyConfigured);
571
+ if (existingResult) return existingResult;
572
+ const originalContent = readFileSync(entryPath, "utf-8");
573
+ return {
574
+ success: true,
575
+ filePath: entryPath,
576
+ message: "Add React Grab",
577
+ originalContent,
578
+ newContent: `${WEBPACK_IMPORT}\n\n${originalContent}`
579
+ };
580
+ };
581
+ const transformTanStack = (projectRoot, reactGrabAlreadyConfigured) => {
582
+ const rootPath = findTanStackRootFile(projectRoot);
583
+ if (!rootPath) return {
584
+ success: false,
585
+ filePath: "",
586
+ message: "Could not find src/routes/__root.tsx or app/routes/__root.tsx.\n\nTo set up React Grab with TanStack Start, add this to your root route component:\n\n import { useEffect } from \"react\";\n\n useEffect(() => {\n if (import.meta.env.DEV) {\n void import(\"react-grab\");\n }\n }, []);"
587
+ };
588
+ const originalContent = readFileSync(rootPath, "utf-8");
589
+ let newContent = originalContent;
590
+ const hasReactGrabInFile = hasReactGrabSetupCode(originalContent);
591
+ if (hasReactGrabInFile && reactGrabAlreadyConfigured) return alreadyConfiguredResult(rootPath);
592
+ if (hasReactGrabInFile) return {
593
+ success: true,
594
+ filePath: rootPath,
595
+ message: "React Grab is already installed in this file",
596
+ noChanges: true
597
+ };
598
+ if (!/import\s+\{[^}]*useEffect[^}]*\}\s+from\s+["']react["']/.test(newContent)) {
599
+ const reactImportMatch = newContent.match(/import\s+\{([^}]*)\}\s+from\s+["']react["'];?/);
600
+ if (reactImportMatch) {
601
+ const existingImports = reactImportMatch[1];
602
+ newContent = newContent.replace(reactImportMatch[0], `import { ${existingImports.trim()}, useEffect } from "react";`);
603
+ } else {
604
+ const firstImportMatch = newContent.match(/^import .+ from ['"].+['"];?\s*$/m);
605
+ if (firstImportMatch) newContent = newContent.replace(firstImportMatch[0], `import { useEffect } from "react";\n${firstImportMatch[0]}`);
606
+ else newContent = `import { useEffect } from "react";\n\n${newContent}`;
607
+ }
608
+ }
609
+ const componentMatch = newContent.match(/function\s+(\w+)\s*\([^)]*\)\s*\{/);
610
+ if (componentMatch) {
611
+ const insertPosition = componentMatch.index + componentMatch[0].length;
612
+ newContent = newContent.slice(0, insertPosition) + `\n ${TANSTACK_EFFECT}\n` + newContent.slice(insertPosition);
613
+ } else return {
614
+ success: false,
615
+ filePath: rootPath,
616
+ message: "Could not find a component function in the root file"
617
+ };
618
+ return {
619
+ success: true,
620
+ filePath: rootPath,
621
+ message: "Add React Grab",
622
+ originalContent,
623
+ newContent
624
+ };
625
+ };
626
+ const hasFrameworkEntryPoint = (projectRoot, framework, nextRouterType) => {
627
+ switch (framework) {
628
+ case "next": return nextRouterType === "app" ? findLayoutFile(projectRoot) !== null : findDocumentFile(projectRoot) !== null;
629
+ case "vite":
630
+ case "webpack": return findEntryFile(projectRoot) !== null;
631
+ case "tanstack": return findTanStackRootFile(projectRoot) !== null;
632
+ default: return false;
633
+ }
634
+ };
635
+ const previewTransform = (projectRoot, framework, nextRouterType, reactGrabAlreadyConfigured = false) => {
636
+ switch (framework) {
637
+ case "next":
638
+ if (nextRouterType === "app") return transformNextAppRouter(projectRoot, reactGrabAlreadyConfigured);
639
+ return transformNextPagesRouter(projectRoot, reactGrabAlreadyConfigured);
640
+ case "vite": return transformVite(projectRoot, reactGrabAlreadyConfigured);
641
+ case "tanstack": return transformTanStack(projectRoot, reactGrabAlreadyConfigured);
642
+ case "webpack": return transformWebpack(projectRoot, reactGrabAlreadyConfigured);
643
+ default: return {
644
+ success: false,
645
+ filePath: "",
646
+ message: `Unknown framework: ${framework}. Please add React Grab manually.`
647
+ };
648
+ }
649
+ };
650
+ const canWriteToFile = (filePath) => {
651
+ try {
652
+ accessSync(filePath, constants.W_OK);
653
+ return true;
654
+ } catch {
655
+ return false;
656
+ }
657
+ };
658
+ const applyTransform = (result) => {
659
+ if (result.success && result.newContent && result.filePath) {
660
+ if (!canWriteToFile(result.filePath)) return {
661
+ success: false,
662
+ error: `Cannot write to ${result.filePath}. Check file permissions.`
663
+ };
664
+ try {
665
+ writeFileSync(result.filePath, result.newContent);
666
+ return { success: true };
667
+ } catch (error) {
668
+ return {
669
+ success: false,
670
+ error: `Failed to write to ${result.filePath}: ${error instanceof Error ? error.message : "Unknown error"}`
671
+ };
672
+ }
673
+ }
674
+ return { success: true };
675
+ };
676
+ const formatOptionsForNextjs = (options) => {
677
+ const parts = [];
678
+ if (options.activationKey) parts.push(`activationKey: ${JSON.stringify(options.activationKey)}`);
679
+ if (options.activationMode) parts.push(`activationMode: "${options.activationMode}"`);
680
+ if (options.keyHoldDuration !== void 0) parts.push(`keyHoldDuration: ${options.keyHoldDuration}`);
681
+ if (options.allowActivationInsideInput !== void 0) parts.push(`allowActivationInsideInput: ${options.allowActivationInsideInput}`);
682
+ if (options.maxContextLines !== void 0) parts.push(`maxContextLines: ${options.maxContextLines}`);
683
+ return `{ ${parts.join(", ")} }`;
684
+ };
685
+ const formatOptionsAsJson = (options) => {
686
+ const cleanOptions = {};
687
+ if (options.activationKey) cleanOptions.activationKey = options.activationKey;
688
+ if (options.activationMode) cleanOptions.activationMode = options.activationMode;
689
+ if (options.keyHoldDuration !== void 0) cleanOptions.keyHoldDuration = options.keyHoldDuration;
690
+ if (options.allowActivationInsideInput !== void 0) cleanOptions.allowActivationInsideInput = options.allowActivationInsideInput;
691
+ if (options.maxContextLines !== void 0) cleanOptions.maxContextLines = options.maxContextLines;
692
+ return JSON.stringify(cleanOptions);
693
+ };
694
+ const findReactGrabFile = (projectRoot, framework, nextRouterType) => {
695
+ switch (framework) {
696
+ case "next": {
697
+ const primaryFile = nextRouterType === "app" ? findLayoutFile(projectRoot) : findDocumentFile(projectRoot);
698
+ const primarySetupFile = findFileWithReactGrabSetup(nextRouterType === "app" ? getLayoutFileCandidates(projectRoot) : getDocumentFileCandidates(projectRoot));
699
+ if (primarySetupFile) return primarySetupFile;
700
+ const instrumentationFile = findFileWithReactGrabSetup(getInstrumentationFileCandidates(projectRoot));
701
+ if (instrumentationFile) return instrumentationFile;
702
+ return primaryFile;
703
+ }
704
+ case "vite": {
705
+ const entryFile = findEntryFile(projectRoot);
706
+ const entrySetupFile = findFileWithReactGrabSetup(getEntryFileCandidates(projectRoot));
707
+ if (entrySetupFile) return entrySetupFile;
708
+ const indexHtml = findFileWithReactGrabSetup(getIndexHtmlCandidates(projectRoot));
709
+ if (indexHtml) return indexHtml;
710
+ return entryFile;
711
+ }
712
+ case "tanstack": return findFileWithReactGrabSetup(getTanStackRootFileCandidates(projectRoot)) ?? findTanStackRootFile(projectRoot);
713
+ case "webpack": return findFileWithReactGrabSetup(getEntryFileCandidates(projectRoot)) ?? findEntryFile(projectRoot);
714
+ default: return null;
715
+ }
716
+ };
717
+ const addOptionsToNextScript = (originalContent, options, filePath) => {
718
+ const reactGrabScriptMatch = originalContent.match(/(<Script[\s\S]*?react-grab[\s\S]*?)\s*(\/?>)/i);
719
+ if (!reactGrabScriptMatch) return {
720
+ success: false,
721
+ filePath,
722
+ message: "Could not find React Grab Script tag"
723
+ };
724
+ const scriptTag = reactGrabScriptMatch[0];
725
+ const scriptOpening = reactGrabScriptMatch[1];
726
+ const scriptClosing = reactGrabScriptMatch[2];
727
+ const existingDataOptionsMatch = scriptTag.match(/data-options=\{JSON\.stringify\([^)]+\)\}/);
728
+ const dataOptionsAttr = `data-options={JSON.stringify(\n ${formatOptionsForNextjs(options)}\n )}`;
729
+ let newScriptTag;
730
+ if (existingDataOptionsMatch) newScriptTag = scriptTag.replace(existingDataOptionsMatch[0], dataOptionsAttr);
731
+ else newScriptTag = `${scriptOpening}\n ${dataOptionsAttr}\n ${scriptClosing}`;
732
+ return {
733
+ success: true,
734
+ filePath,
735
+ message: "Update React Grab options",
736
+ originalContent,
737
+ newContent: originalContent.replace(scriptTag, newScriptTag)
738
+ };
739
+ };
740
+ const addOptionsToDynamicImport = (originalContent, options, filePath) => {
741
+ const reactGrabImportWithInitMatch = originalContent.match(/(void\s+)?import\s*\(\s*["']react-grab(?:\/[^"']+)?["']\s*\)(?:\.then\s*\(\s*(?:\(m\)\s*=>\s*m\.init\s*\([^)]*\)|\(\{\s*init\s*\}\)\s*=>\s*init\s*\([^)]*\))\s*\))?/);
742
+ if (!reactGrabImportWithInitMatch) return {
743
+ success: false,
744
+ filePath,
745
+ message: "Could not find React Grab import"
746
+ };
747
+ const optionsJson = formatOptionsAsJson(options);
748
+ const newImport = `${reactGrabImportWithInitMatch[1] ?? ""}import("react-grab").then((m) => m.init(${optionsJson}))`;
749
+ return {
750
+ success: true,
751
+ filePath,
752
+ message: "Update React Grab options",
753
+ originalContent,
754
+ newContent: originalContent.replace(reactGrabImportWithInitMatch[0], newImport)
755
+ };
756
+ };
757
+ const addOptionsToTanStackImport = (originalContent, options, filePath) => {
758
+ const reactGrabImportWithInitMatch = originalContent.match(/(?:(void\s+)?import\s*\(\s*["']react-grab\/core["']\s*\)\.then\s*\(\s*(?:\(\s*\{\s*init\s*\}\s*\)\s*=>\s*init\s*\([^)]*\)|\(m\)\s*=>\s*m\.init\s*\([^)]*\))\s*\)|(void\s+)?import\s*\(\s*["']react-grab(?!\/core)(?:\/[^"']+)?["']\s*\))/);
759
+ if (!reactGrabImportWithInitMatch) return {
760
+ success: false,
761
+ filePath,
762
+ message: "Could not find React Grab import"
763
+ };
764
+ const optionsJson = formatOptionsAsJson(options);
765
+ const newImport = `${reactGrabImportWithInitMatch[1] ?? reactGrabImportWithInitMatch[2] ?? ""}import("react-grab/core").then(({ init }) => init(${optionsJson}))`;
766
+ return {
767
+ success: true,
768
+ filePath,
769
+ message: "Update React Grab options",
770
+ originalContent,
771
+ newContent: originalContent.replace(reactGrabImportWithInitMatch[0], newImport)
772
+ };
773
+ };
774
+ const addOptionsToAnyImport = (originalContent, options, filePath) => {
775
+ const dynamicImportResult = addOptionsToDynamicImport(originalContent, options, filePath);
776
+ if (dynamicImportResult.success) return dynamicImportResult;
777
+ return addOptionsToTanStackImport(originalContent, options, filePath);
778
+ };
779
+ const previewOptionsTransform = (projectRoot, framework, nextRouterType, options) => {
780
+ const filePath = findReactGrabFile(projectRoot, framework, nextRouterType);
781
+ if (!filePath) return {
782
+ success: false,
783
+ filePath: "",
784
+ message: "Could not find file containing React Grab configuration"
785
+ };
786
+ const originalContent = readFileSync(filePath, "utf-8");
787
+ if (!hasReactGrabSetupCode(originalContent)) return {
788
+ success: false,
789
+ filePath,
790
+ message: "Could not find React Grab code in the file"
791
+ };
792
+ switch (framework) {
793
+ case "next":
794
+ if (isInstrumentationFile(filePath)) return addOptionsToAnyImport(originalContent, options, filePath);
795
+ return addOptionsToNextScript(originalContent, options, filePath);
796
+ case "vite": return addOptionsToDynamicImport(originalContent, options, filePath);
797
+ case "tanstack": return addOptionsToTanStackImport(originalContent, options, filePath);
798
+ case "webpack": return addOptionsToDynamicImport(originalContent, options, filePath);
799
+ default: return {
800
+ success: false,
801
+ filePath,
802
+ message: `Unknown framework: ${framework}`
803
+ };
804
+ }
805
+ };
806
+ const previewCdnTransform = (projectRoot, framework, nextRouterType, targetCdnDomain) => {
807
+ const filePath = findReactGrabFile(projectRoot, framework, nextRouterType);
808
+ if (!filePath) return {
809
+ success: false,
810
+ filePath: "",
811
+ message: "Could not find React Grab file"
812
+ };
813
+ const originalContent = readFileSync(filePath, "utf-8");
814
+ const newContent = originalContent.replace(/(https?:)?\/\/[^/\s"']+(?=\/(?:@?react-grab))/g, `//${targetCdnDomain}`).replace(/(https?:)?\/\/[^/\s"']*react-grab[^/\s"']*\.com(?=\/script\.js)/g, `//${targetCdnDomain}`);
815
+ if (newContent === originalContent) return {
816
+ success: true,
817
+ filePath,
818
+ message: "CDN already set",
819
+ noChanges: true
820
+ };
821
+ return {
822
+ success: true,
823
+ filePath,
824
+ message: "Update CDN",
825
+ originalContent,
826
+ newContent
827
+ };
828
+ };
829
+ //#endregion
830
+ //#region src/utils/install.ts
831
+ const installPackages = async (packages, options = {}) => {
832
+ if (packages.length === 0) return;
833
+ const detectedAgent = options.packageManager ?? await detectPackageManager(options.cwd ?? process.cwd());
834
+ const args = [];
835
+ if (options.preferOffline) args.push("--prefer-offline");
836
+ if (detectedAgent === "pnpm") {
837
+ args.push("--prod=false");
838
+ if (existsSync(resolve(options.cwd ?? process.cwd(), "pnpm-workspace.yaml"))) args.push("-w");
839
+ }
840
+ if (options.additionalArgs) args.push(...options.additionalArgs);
841
+ await x(detectedAgent, [
842
+ detectedAgent === "npm" ? "install" : "add",
843
+ ...options.isDev !== false ? ["-D"] : [],
844
+ ...args,
845
+ ...packages
846
+ ], {
847
+ nodeOptions: {
848
+ stdio: options.silent ? "ignore" : "inherit",
849
+ cwd: options.cwd,
850
+ env: {
851
+ ...process.env,
852
+ REACT_GRAB_INIT: "1"
853
+ }
854
+ },
855
+ throwOnError: true
856
+ });
857
+ };
858
+ const getPackagesToInstall = (includeReactGrab = true) => {
859
+ return includeReactGrab ? ["react-grab"] : [];
860
+ };
861
+ //#endregion
862
+ export { detectReactGrabConfigured as _, previewCdnTransform as a, agentLabel as c, detectAvailableAgents as d, detectFramework as f, detectReactGrab as g, detectProject as h, hasFrameworkEntryPoint as i, installSkill as l, detectPackageManager as m, installPackages as n, previewOptionsTransform as o, detectNextRouterType as p, applyTransform as r, previewTransform as s, getPackagesToInstall as t, removeSkill as u, detectUnsupportedFramework as v, findReactProjects as y };
863
+
864
+ //# sourceMappingURL=install-Car5vKPk.js.map