@react-grab/cli 0.1.44 → 0.1.45-dev.b85b9b1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -25,6 +25,71 @@ const AGENT_ENVIRONMENT_VARIABLES = [
25
25
  const isEnvironmentVariableSet = (variable) => Boolean(process.env[variable]);
26
26
  const detectNonInteractive = (yesFlag) => yesFlag || AGENT_ENVIRONMENT_VARIABLES.some(isEnvironmentVariableSet) || !process.stdin.isTTY;
27
27
  //#endregion
28
+ //#region src/utils/react-grab-code.ts
29
+ const REACT_GRAB_SPECIFIER_PATTERN = String.raw`react-grab(?:\/[^"']+)?`;
30
+ const stripComments = (content) => content.replace(/<!--[\s\S]*?-->/g, "").replace(/\{\/\*[\s\S]*?\*\/\}/g, "").replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|\s)\/\/.*$/gm, "$1");
31
+ const stripTypeOnlyReactGrabImports = (content) => {
32
+ 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"), "");
33
+ };
34
+ const hasReactGrabSetupCode = (content) => {
35
+ const setupCandidateContent = stripTypeOnlyReactGrabImports(stripComments(content));
36
+ return [
37
+ new RegExp(String.raw`import\s*\(\s*["']${REACT_GRAB_SPECIFIER_PATTERN}["']\s*\)`),
38
+ new RegExp(String.raw`import\s+(?!type\b)(?:[^"';]+from\s+)?["']${REACT_GRAB_SPECIFIER_PATTERN}["']`),
39
+ new RegExp(String.raw`require\s*\(\s*["']${REACT_GRAB_SPECIFIER_PATTERN}["']\s*\)`),
40
+ /<Script[\s\S]*?src\s*=\s*(?:["'][^"']*react-grab[^"']*["']|\{(?:["'][^"']*react-grab[^"']*["']|`[^`]*react-grab[^`]*`)\})/i,
41
+ /<script[\s\S]*?src\s*=\s*["'][^"']*react-grab[^"']*["']/i
42
+ ].some((pattern) => pattern.test(setupCandidateContent));
43
+ };
44
+ //#endregion
45
+ //#region src/utils/react-grab-setup-files.ts
46
+ const COMPONENT_EXTENSIONS = [
47
+ "tsx",
48
+ "jsx",
49
+ "ts",
50
+ "js"
51
+ ];
52
+ const INSTRUMENTATION_EXTENSIONS = [
53
+ "ts",
54
+ "tsx",
55
+ "js",
56
+ "jsx",
57
+ "mts",
58
+ "cts",
59
+ "mjs",
60
+ "cjs"
61
+ ];
62
+ const ROUTE_EXTENSIONS = ["tsx", "jsx"];
63
+ const createFileCandidates = (projectRoot, directories, baseName, extensions) => {
64
+ const fileCandidates = [];
65
+ for (const directory of directories) for (const extension of extensions) fileCandidates.push(join(projectRoot, directory, `${baseName}.${extension}`));
66
+ return fileCandidates;
67
+ };
68
+ const findExistingFile = (fileCandidates) => {
69
+ for (const filePath of fileCandidates) if (existsSync(filePath)) return filePath;
70
+ return null;
71
+ };
72
+ const getLayoutFileCandidates = (projectRoot) => createFileCandidates(projectRoot, ["app", "src/app"], "layout", COMPONENT_EXTENSIONS);
73
+ const getDocumentFileCandidates = (projectRoot) => createFileCandidates(projectRoot, ["pages", "src/pages"], "_document", COMPONENT_EXTENSIONS);
74
+ const getInstrumentationFileCandidates = (projectRoot) => createFileCandidates(projectRoot, ["", "src"], "instrumentation-client", INSTRUMENTATION_EXTENSIONS);
75
+ const getIndexHtmlCandidates = (projectRoot) => [join(projectRoot, "index.html"), join(projectRoot, "public", "index.html")];
76
+ const getEntryFileCandidates = (projectRoot) => [...createFileCandidates(projectRoot, ["src"], "index", COMPONENT_EXTENSIONS), ...createFileCandidates(projectRoot, ["src"], "main", COMPONENT_EXTENSIONS)];
77
+ const getTanStackRootFileCandidates = (projectRoot) => createFileCandidates(projectRoot, ["src/routes", "app/routes"], "__root", ROUTE_EXTENSIONS);
78
+ const getReactGrabSetupFileCandidates = (projectRoot) => [
79
+ ...getLayoutFileCandidates(projectRoot),
80
+ ...getDocumentFileCandidates(projectRoot),
81
+ ...getInstrumentationFileCandidates(projectRoot),
82
+ ...getIndexHtmlCandidates(projectRoot),
83
+ ...getEntryFileCandidates(projectRoot),
84
+ ...getTanStackRootFileCandidates(projectRoot)
85
+ ];
86
+ const findLayoutFile = (projectRoot) => findExistingFile(getLayoutFileCandidates(projectRoot));
87
+ const findDocumentFile = (projectRoot) => findExistingFile(getDocumentFileCandidates(projectRoot));
88
+ const findIndexHtml = (projectRoot) => findExistingFile(getIndexHtmlCandidates(projectRoot));
89
+ const findEntryFile = (projectRoot) => findExistingFile(getEntryFileCandidates(projectRoot));
90
+ const findTanStackRootFile = (projectRoot) => findExistingFile(getTanStackRootFileCandidates(projectRoot));
91
+ const isInstrumentationFile = (filePath) => /(?:^|[/\\])instrumentation-client\.[cm]?[jt]sx?$/.test(filePath);
92
+ //#endregion
28
93
  //#region src/utils/detect.ts
29
94
  const VALID_PACKAGE_MANAGERS = new Set([
30
95
  "npm",
@@ -238,10 +303,14 @@ const scanDirectoryForProjects = (rootDirectory, ignorer, maxDepth, currentDepth
238
303
  return projects;
239
304
  };
240
305
  const MAX_SCAN_DEPTH = 2;
306
+ const normalizePathForComparison = (filePath) => filePath.replace(/\\/g, "/");
241
307
  const findReactProjects = (projectRoot) => {
242
- if (detectMonorepo(projectRoot)) {
243
- const workspaceProjects = findWorkspaceProjects(projectRoot);
244
- if (workspaceProjects.length > 0) return workspaceProjects;
308
+ const monorepoRoot = detectMonorepo(projectRoot) ? projectRoot : findEnclosingMonorepoRoot(projectRoot);
309
+ if (monorepoRoot) {
310
+ const workspaceProjects = findWorkspaceProjects(monorepoRoot);
311
+ const localProject = projectRoot === monorepoRoot ? null : buildReactProject(projectRoot);
312
+ const projects = localProject ? [localProject, ...workspaceProjects.filter((project) => normalizePathForComparison(project.path) !== normalizePathForComparison(localProject.path))] : workspaceProjects;
313
+ if (projects.length > 0) return projects;
245
314
  }
246
315
  const scannedProjects = scanDirectoryForProjects(projectRoot, loadGitignore(projectRoot), MAX_SCAN_DEPTH);
247
316
  if (scannedProjects.length > 0) return scannedProjects;
@@ -253,47 +322,20 @@ const findReactProjects = (projectRoot) => {
253
322
  }
254
323
  return [];
255
324
  };
256
- const hasReactGrabInFile = (filePath) => {
325
+ const hasReactGrabSetupInFile = (filePath) => {
257
326
  if (!existsSync(filePath)) return false;
258
327
  try {
259
- const content = readFileSync(filePath, "utf-8");
260
- return [
261
- /["'`][^"'`]*react-grab/,
262
- /react-grab[^"'`]*["'`]/,
263
- /<[^>]*react-grab/i,
264
- /import[^;]*react-grab/i,
265
- /require[^)]*react-grab/i,
266
- /from\s+[^;]*react-grab/i,
267
- /src[^>]*react-grab/i
268
- ].some((pattern) => pattern.test(content));
328
+ return hasReactGrabSetupCode(readFileSync(filePath, "utf-8"));
269
329
  } catch {
270
330
  return false;
271
331
  }
272
332
  };
273
- const detectReactGrab = (projectRoot) => {
274
- if (readMergedDependencies(projectRoot)?.["react-grab"]) return true;
275
- return [
276
- join(projectRoot, "app", "layout.tsx"),
277
- join(projectRoot, "app", "layout.jsx"),
278
- join(projectRoot, "src", "app", "layout.tsx"),
279
- join(projectRoot, "src", "app", "layout.jsx"),
280
- join(projectRoot, "pages", "_document.tsx"),
281
- join(projectRoot, "pages", "_document.jsx"),
282
- join(projectRoot, "instrumentation-client.ts"),
283
- join(projectRoot, "instrumentation-client.js"),
284
- join(projectRoot, "src", "instrumentation-client.ts"),
285
- join(projectRoot, "src", "instrumentation-client.js"),
286
- join(projectRoot, "index.html"),
287
- join(projectRoot, "public", "index.html"),
288
- join(projectRoot, "src", "index.tsx"),
289
- join(projectRoot, "src", "index.ts"),
290
- join(projectRoot, "src", "main.tsx"),
291
- join(projectRoot, "src", "main.ts"),
292
- join(projectRoot, "src", "routes", "__root.tsx"),
293
- join(projectRoot, "src", "routes", "__root.jsx"),
294
- join(projectRoot, "app", "routes", "__root.tsx"),
295
- join(projectRoot, "app", "routes", "__root.jsx")
296
- ].some(hasReactGrabInFile);
333
+ const detectReactGrabDependency = (projectRoot) => {
334
+ const dependencies = readMergedDependencies(projectRoot);
335
+ return Boolean(dependencies?.["react-grab"]);
336
+ };
337
+ const detectReactGrabConfigured = (projectRoot) => {
338
+ return getReactGrabSetupFileCandidates(projectRoot).some(hasReactGrabSetupInFile);
297
339
  };
298
340
  const detectUnsupportedFramework = (projectRoot) => {
299
341
  const dependencies = readMergedDependencies(projectRoot);
@@ -314,13 +356,17 @@ const detectReactGrabVersion = (projectRoot) => {
314
356
  const detectProject = async (projectRoot = process.cwd()) => {
315
357
  const localFramework = detectFramework(projectRoot);
316
358
  const framework = localFramework === "unknown" ? detectFrameworkFromMonorepoRoot(projectRoot) : localFramework;
359
+ const packageManager = await detectPackageManager(projectRoot);
360
+ const isMonorepo = detectMonorepo(projectRoot) || findEnclosingMonorepoRoot(projectRoot) !== null;
361
+ const isReactGrabConfigured = detectReactGrabConfigured(projectRoot);
317
362
  return {
318
- packageManager: await detectPackageManager(projectRoot),
363
+ packageManager,
319
364
  framework,
320
365
  nextRouterType: framework === "next" ? detectNextRouterType(projectRoot) : "unknown",
321
- isMonorepo: detectMonorepo(projectRoot),
366
+ isMonorepo,
322
367
  projectRoot,
323
- hasReactGrab: detectReactGrab(projectRoot),
368
+ hasReactGrab: detectReactGrabDependency(projectRoot) || isReactGrabConfigured,
369
+ isReactGrabConfigured,
324
370
  reactGrabVersion: detectReactGrabVersion(projectRoot),
325
371
  unsupportedFramework: detectUnsupportedFramework(projectRoot)
326
372
  };
@@ -491,7 +537,7 @@ const removeSkill = async ({ cwd = process.cwd(), global = false } = {}) => {
491
537
  };
492
538
  //#endregion
493
539
  //#region src/commands/add.ts
494
- const VERSION$5 = "0.1.44";
540
+ const VERSION$5 = "0.1.45-dev.b85b9b1";
495
541
  const add$1 = new Command().name("add").alias("install").description("install the React Grab skill for your agent").option("-y, --yes", "skip confirmation prompts", false).option("-c, --cwd <cwd>", "working directory (defaults to current directory)", process.cwd()).option("-g, --global", "install the skill globally instead of in the project", false).action(async (opts) => {
496
542
  console.log(`${pc.magenta("✿")} ${pc.bold("React Grab")} ${pc.gray(VERSION$5)}`);
497
543
  console.log();
@@ -646,80 +692,14 @@ const TANSTACK_EFFECT = `useEffect(() => {
646
692
  const SCRIPT_IMPORT = "import Script from \"next/script\";";
647
693
  //#endregion
648
694
  //#region src/utils/transform.ts
649
- const hasReactGrabCode = (content) => {
650
- return [
651
- /["'`][^"'`]*react-grab/,
652
- /react-grab[^"'`]*["'`]/,
653
- /<[^>]*react-grab/i,
654
- /import[^;]*react-grab/i,
655
- /require[^)]*react-grab/i,
656
- /from\s+[^;]*react-grab/i,
657
- /src[^>]*react-grab/i,
658
- /href[^>]*react-grab/i
659
- ].some((pattern) => pattern.test(content));
660
- };
661
- const findLayoutFile = (projectRoot) => {
662
- const possiblePaths = [
663
- join(projectRoot, "app", "layout.tsx"),
664
- join(projectRoot, "app", "layout.jsx"),
665
- join(projectRoot, "src", "app", "layout.tsx"),
666
- join(projectRoot, "src", "app", "layout.jsx")
667
- ];
668
- for (const filePath of possiblePaths) if (existsSync(filePath)) return filePath;
669
- return null;
670
- };
671
- const findInstrumentationFile = (projectRoot) => {
672
- const possiblePaths = [
673
- join(projectRoot, "instrumentation-client.ts"),
674
- join(projectRoot, "instrumentation-client.js"),
675
- join(projectRoot, "src", "instrumentation-client.ts"),
676
- join(projectRoot, "src", "instrumentation-client.js")
677
- ];
678
- for (const filePath of possiblePaths) if (existsSync(filePath)) return filePath;
679
- return null;
680
- };
681
695
  const hasReactGrabInInstrumentation = (projectRoot) => {
682
- const instrumentationPath = findInstrumentationFile(projectRoot);
683
- if (!instrumentationPath) return false;
684
- return hasReactGrabCode(readFileSync(instrumentationPath, "utf-8"));
685
- };
686
- const findDocumentFile = (projectRoot) => {
687
- const possiblePaths = [
688
- join(projectRoot, "pages", "_document.tsx"),
689
- join(projectRoot, "pages", "_document.jsx"),
690
- join(projectRoot, "src", "pages", "_document.tsx"),
691
- join(projectRoot, "src", "pages", "_document.jsx")
692
- ];
693
- for (const filePath of possiblePaths) if (existsSync(filePath)) return filePath;
694
- return null;
695
- };
696
- const findIndexHtml = (projectRoot) => {
697
- const possiblePaths = [join(projectRoot, "index.html"), join(projectRoot, "public", "index.html")];
698
- for (const filePath of possiblePaths) if (existsSync(filePath)) return filePath;
699
- return null;
700
- };
701
- const findEntryFile = (projectRoot) => {
702
- const possiblePaths = [
703
- join(projectRoot, "src", "index.tsx"),
704
- join(projectRoot, "src", "index.jsx"),
705
- join(projectRoot, "src", "index.ts"),
706
- join(projectRoot, "src", "index.js"),
707
- join(projectRoot, "src", "main.tsx"),
708
- join(projectRoot, "src", "main.jsx"),
709
- join(projectRoot, "src", "main.ts"),
710
- join(projectRoot, "src", "main.js")
711
- ];
712
- for (const filePath of possiblePaths) if (existsSync(filePath)) return filePath;
713
- return null;
696
+ return findFileWithReactGrabSetup(getInstrumentationFileCandidates(projectRoot)) !== null;
714
697
  };
715
- const findTanStackRootFile = (projectRoot) => {
716
- const possiblePaths = [
717
- join(projectRoot, "src", "routes", "__root.tsx"),
718
- join(projectRoot, "src", "routes", "__root.jsx"),
719
- join(projectRoot, "app", "routes", "__root.tsx"),
720
- join(projectRoot, "app", "routes", "__root.jsx")
721
- ];
722
- for (const filePath of possiblePaths) if (existsSync(filePath)) return filePath;
698
+ const findFileWithReactGrabSetup = (fileCandidates) => {
699
+ for (const filePath of fileCandidates) {
700
+ if (!existsSync(filePath)) continue;
701
+ if (hasReactGrabSetupCode(readFileSync(filePath, "utf-8"))) return filePath;
702
+ }
723
703
  return null;
724
704
  };
725
705
  const alreadyConfiguredResult = (filePath) => ({
@@ -728,19 +708,19 @@ const alreadyConfiguredResult = (filePath) => ({
728
708
  message: "React Grab is already configured",
729
709
  noChanges: true
730
710
  });
731
- const transformNextAppRouter = (projectRoot, reactGrabAlreadyConfigured, force = false) => {
711
+ const transformNextAppRouter = (projectRoot, reactGrabAlreadyConfigured) => {
732
712
  const layoutPath = findLayoutFile(projectRoot);
733
713
  if (!layoutPath) return {
734
714
  success: false,
735
715
  filePath: "",
736
- message: "Could not find app/layout.tsx or app/layout.jsx"
716
+ message: "Could not find app/layout.tsx, app/layout.jsx, app/layout.ts, or app/layout.js"
737
717
  };
738
718
  const originalContent = readFileSync(layoutPath, "utf-8");
739
719
  let newContent = originalContent;
740
- const hasReactGrabInFile = hasReactGrabCode(originalContent);
720
+ const hasReactGrabInFile = hasReactGrabSetupCode(originalContent);
741
721
  const hasReactGrabInInstrumentationFile = hasReactGrabInInstrumentation(projectRoot);
742
- if (!force && hasReactGrabInFile && reactGrabAlreadyConfigured) return alreadyConfiguredResult(layoutPath);
743
- if (!force && (hasReactGrabInFile || hasReactGrabInInstrumentationFile)) return {
722
+ if (hasReactGrabInFile && reactGrabAlreadyConfigured) return alreadyConfiguredResult(layoutPath);
723
+ if (hasReactGrabInFile || hasReactGrabInInstrumentationFile) return {
744
724
  success: true,
745
725
  filePath: layoutPath,
746
726
  message: "React Grab is already installed" + (hasReactGrabInInstrumentationFile ? " in instrumentation-client" : " in this file"),
@@ -765,19 +745,19 @@ const transformNextAppRouter = (projectRoot, reactGrabAlreadyConfigured, force =
765
745
  newContent
766
746
  };
767
747
  };
768
- const transformNextPagesRouter = (projectRoot, reactGrabAlreadyConfigured, force = false) => {
748
+ const transformNextPagesRouter = (projectRoot, reactGrabAlreadyConfigured) => {
769
749
  const documentPath = findDocumentFile(projectRoot);
770
750
  if (!documentPath) return {
771
751
  success: false,
772
752
  filePath: "",
773
- 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 }"
753
+ 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 }"
774
754
  };
775
755
  const originalContent = readFileSync(documentPath, "utf-8");
776
756
  let newContent = originalContent;
777
- const hasReactGrabInFile = hasReactGrabCode(originalContent);
757
+ const hasReactGrabInFile = hasReactGrabSetupCode(originalContent);
778
758
  const hasReactGrabInInstrumentationFile = hasReactGrabInInstrumentation(projectRoot);
779
- if (!force && hasReactGrabInFile && reactGrabAlreadyConfigured) return alreadyConfiguredResult(documentPath);
780
- if (!force && (hasReactGrabInFile || hasReactGrabInInstrumentationFile)) return {
759
+ if (hasReactGrabInFile && reactGrabAlreadyConfigured) return alreadyConfiguredResult(documentPath);
760
+ if (hasReactGrabInFile || hasReactGrabInInstrumentationFile) return {
781
761
  success: true,
782
762
  filePath: documentPath,
783
763
  message: "React Grab is already installed" + (hasReactGrabInInstrumentationFile ? " in instrumentation-client" : " in this file"),
@@ -798,7 +778,7 @@ const transformNextPagesRouter = (projectRoot, reactGrabAlreadyConfigured, force
798
778
  };
799
779
  };
800
780
  const checkExistingInstallation = (filePath, reactGrabAlreadyConfigured) => {
801
- if (!hasReactGrabCode(readFileSync(filePath, "utf-8"))) return null;
781
+ if (!hasReactGrabSetupCode(readFileSync(filePath, "utf-8"))) return null;
802
782
  return {
803
783
  success: true,
804
784
  filePath,
@@ -806,24 +786,20 @@ const checkExistingInstallation = (filePath, reactGrabAlreadyConfigured) => {
806
786
  noChanges: true
807
787
  };
808
788
  };
809
- const transformVite = (projectRoot, reactGrabAlreadyConfigured, force = false) => {
789
+ const transformVite = (projectRoot, reactGrabAlreadyConfigured) => {
810
790
  const entryPath = findEntryFile(projectRoot);
811
- if (!force) {
812
- const indexPath = findIndexHtml(projectRoot);
813
- if (indexPath) {
814
- const existingResult = checkExistingInstallation(indexPath, reactGrabAlreadyConfigured);
815
- if (existingResult) return existingResult;
816
- }
791
+ const indexPath = findIndexHtml(projectRoot);
792
+ if (indexPath) {
793
+ const existingResult = checkExistingInstallation(indexPath, reactGrabAlreadyConfigured);
794
+ if (existingResult) return existingResult;
817
795
  }
818
796
  if (!entryPath) return {
819
797
  success: false,
820
798
  filePath: "",
821
799
  message: "Could not find entry file (src/index.tsx, src/main.tsx, etc.)"
822
800
  };
823
- if (!force) {
824
- const existingResult = checkExistingInstallation(entryPath, reactGrabAlreadyConfigured);
825
- if (existingResult) return existingResult;
826
- }
801
+ const existingResult = checkExistingInstallation(entryPath, reactGrabAlreadyConfigured);
802
+ if (existingResult) return existingResult;
827
803
  const originalContent = readFileSync(entryPath, "utf-8");
828
804
  return {
829
805
  success: true,
@@ -833,17 +809,15 @@ const transformVite = (projectRoot, reactGrabAlreadyConfigured, force = false) =
833
809
  newContent: `${VITE_IMPORT}\n\n${originalContent}`
834
810
  };
835
811
  };
836
- const transformWebpack = (projectRoot, reactGrabAlreadyConfigured, force = false) => {
812
+ const transformWebpack = (projectRoot, reactGrabAlreadyConfigured) => {
837
813
  const entryPath = findEntryFile(projectRoot);
838
814
  if (!entryPath) return {
839
815
  success: false,
840
816
  filePath: "",
841
817
  message: "Could not find entry file (src/index.tsx, src/main.tsx, etc.)"
842
818
  };
843
- if (!force) {
844
- const existingResult = checkExistingInstallation(entryPath, reactGrabAlreadyConfigured);
845
- if (existingResult) return existingResult;
846
- }
819
+ const existingResult = checkExistingInstallation(entryPath, reactGrabAlreadyConfigured);
820
+ if (existingResult) return existingResult;
847
821
  const originalContent = readFileSync(entryPath, "utf-8");
848
822
  return {
849
823
  success: true,
@@ -853,7 +827,7 @@ const transformWebpack = (projectRoot, reactGrabAlreadyConfigured, force = false
853
827
  newContent: `${WEBPACK_IMPORT}\n\n${originalContent}`
854
828
  };
855
829
  };
856
- const transformTanStack = (projectRoot, reactGrabAlreadyConfigured, force = false) => {
830
+ const transformTanStack = (projectRoot, reactGrabAlreadyConfigured) => {
857
831
  const rootPath = findTanStackRootFile(projectRoot);
858
832
  if (!rootPath) return {
859
833
  success: false,
@@ -862,9 +836,9 @@ const transformTanStack = (projectRoot, reactGrabAlreadyConfigured, force = fals
862
836
  };
863
837
  const originalContent = readFileSync(rootPath, "utf-8");
864
838
  let newContent = originalContent;
865
- const hasReactGrabInFile = hasReactGrabCode(originalContent);
866
- if (!force && hasReactGrabInFile && reactGrabAlreadyConfigured) return alreadyConfiguredResult(rootPath);
867
- if (!force && hasReactGrabInFile) return {
839
+ const hasReactGrabInFile = hasReactGrabSetupCode(originalContent);
840
+ if (hasReactGrabInFile && reactGrabAlreadyConfigured) return alreadyConfiguredResult(rootPath);
841
+ if (hasReactGrabInFile) return {
868
842
  success: true,
869
843
  filePath: rootPath,
870
844
  message: "React Grab is already installed in this file",
@@ -907,14 +881,14 @@ const hasFrameworkEntryPoint = (projectRoot, framework, nextRouterType) => {
907
881
  default: return false;
908
882
  }
909
883
  };
910
- const previewTransform = (projectRoot, framework, nextRouterType, reactGrabAlreadyConfigured = false, force = false) => {
884
+ const previewTransform = (projectRoot, framework, nextRouterType, reactGrabAlreadyConfigured = false) => {
911
885
  switch (framework) {
912
886
  case "next":
913
- if (nextRouterType === "app") return transformNextAppRouter(projectRoot, reactGrabAlreadyConfigured, force);
914
- return transformNextPagesRouter(projectRoot, reactGrabAlreadyConfigured, force);
915
- case "vite": return transformVite(projectRoot, reactGrabAlreadyConfigured, force);
916
- case "tanstack": return transformTanStack(projectRoot, reactGrabAlreadyConfigured, force);
917
- case "webpack": return transformWebpack(projectRoot, reactGrabAlreadyConfigured, force);
887
+ if (nextRouterType === "app") return transformNextAppRouter(projectRoot, reactGrabAlreadyConfigured);
888
+ return transformNextPagesRouter(projectRoot, reactGrabAlreadyConfigured);
889
+ case "vite": return transformVite(projectRoot, reactGrabAlreadyConfigured);
890
+ case "tanstack": return transformTanStack(projectRoot, reactGrabAlreadyConfigured);
891
+ case "webpack": return transformWebpack(projectRoot, reactGrabAlreadyConfigured);
918
892
  default: return {
919
893
  success: false,
920
894
  filePath: "",
@@ -968,18 +942,24 @@ const formatOptionsAsJson = (options) => {
968
942
  };
969
943
  const findReactGrabFile = (projectRoot, framework, nextRouterType) => {
970
944
  switch (framework) {
971
- case "next":
972
- if (nextRouterType === "app") return findLayoutFile(projectRoot);
973
- return findDocumentFile(projectRoot);
945
+ case "next": {
946
+ const primaryFile = nextRouterType === "app" ? findLayoutFile(projectRoot) : findDocumentFile(projectRoot);
947
+ const primarySetupFile = findFileWithReactGrabSetup(nextRouterType === "app" ? getLayoutFileCandidates(projectRoot) : getDocumentFileCandidates(projectRoot));
948
+ if (primarySetupFile) return primarySetupFile;
949
+ const instrumentationFile = findFileWithReactGrabSetup(getInstrumentationFileCandidates(projectRoot));
950
+ if (instrumentationFile) return instrumentationFile;
951
+ return primaryFile;
952
+ }
974
953
  case "vite": {
975
954
  const entryFile = findEntryFile(projectRoot);
976
- if (entryFile && hasReactGrabCode(readFileSync(entryFile, "utf-8"))) return entryFile;
977
- const indexHtml = findIndexHtml(projectRoot);
978
- if (indexHtml && hasReactGrabCode(readFileSync(indexHtml, "utf-8"))) return indexHtml;
955
+ const entrySetupFile = findFileWithReactGrabSetup(getEntryFileCandidates(projectRoot));
956
+ if (entrySetupFile) return entrySetupFile;
957
+ const indexHtml = findFileWithReactGrabSetup(getIndexHtmlCandidates(projectRoot));
958
+ if (indexHtml) return indexHtml;
979
959
  return entryFile;
980
960
  }
981
- case "tanstack": return findTanStackRootFile(projectRoot);
982
- case "webpack": return findEntryFile(projectRoot);
961
+ case "tanstack": return findFileWithReactGrabSetup(getTanStackRootFileCandidates(projectRoot)) ?? findTanStackRootFile(projectRoot);
962
+ case "webpack": return findFileWithReactGrabSetup(getEntryFileCandidates(projectRoot)) ?? findEntryFile(projectRoot);
983
963
  default: return null;
984
964
  }
985
965
  };
@@ -1007,13 +987,14 @@ const addOptionsToNextScript = (originalContent, options, filePath) => {
1007
987
  };
1008
988
  };
1009
989
  const addOptionsToDynamicImport = (originalContent, options, filePath) => {
1010
- const reactGrabImportWithInitMatch = originalContent.match(/import\s*\(\s*["']react-grab["']\s*\)(?:\.then\s*\(\s*\(m\)\s*=>\s*m\.init\s*\([^)]*\)\s*\))?/);
990
+ 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*\))?/);
1011
991
  if (!reactGrabImportWithInitMatch) return {
1012
992
  success: false,
1013
993
  filePath,
1014
994
  message: "Could not find React Grab import"
1015
995
  };
1016
- const newImport = `import("react-grab").then((m) => m.init(${formatOptionsAsJson(options)}))`;
996
+ const optionsJson = formatOptionsAsJson(options);
997
+ const newImport = `${reactGrabImportWithInitMatch[1] ?? ""}import("react-grab").then((m) => m.init(${optionsJson}))`;
1017
998
  return {
1018
999
  success: true,
1019
1000
  filePath,
@@ -1023,13 +1004,14 @@ const addOptionsToDynamicImport = (originalContent, options, filePath) => {
1023
1004
  };
1024
1005
  };
1025
1006
  const addOptionsToTanStackImport = (originalContent, options, filePath) => {
1026
- const reactGrabImportWithInitMatch = originalContent.match(/(?:void\s+import\s*\(\s*["']react-grab["']\s*\)|import\s*\(\s*["']react-grab\/core["']\s*\)\.then\s*\(\s*\(\s*\{\s*init\s*\}\s*\)\s*=>\s*init\s*\([^)]*\)\s*\))/);
1007
+ 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*\))/);
1027
1008
  if (!reactGrabImportWithInitMatch) return {
1028
1009
  success: false,
1029
1010
  filePath,
1030
1011
  message: "Could not find React Grab import"
1031
1012
  };
1032
- const newImport = `import("react-grab/core").then(({ init }) => init(${formatOptionsAsJson(options)}))`;
1013
+ const optionsJson = formatOptionsAsJson(options);
1014
+ const newImport = `${reactGrabImportWithInitMatch[1] ?? reactGrabImportWithInitMatch[2] ?? ""}import("react-grab/core").then(({ init }) => init(${optionsJson}))`;
1033
1015
  return {
1034
1016
  success: true,
1035
1017
  filePath,
@@ -1038,6 +1020,11 @@ const addOptionsToTanStackImport = (originalContent, options, filePath) => {
1038
1020
  newContent: originalContent.replace(reactGrabImportWithInitMatch[0], newImport)
1039
1021
  };
1040
1022
  };
1023
+ const addOptionsToAnyImport = (originalContent, options, filePath) => {
1024
+ const dynamicImportResult = addOptionsToDynamicImport(originalContent, options, filePath);
1025
+ if (dynamicImportResult.success) return dynamicImportResult;
1026
+ return addOptionsToTanStackImport(originalContent, options, filePath);
1027
+ };
1041
1028
  const previewOptionsTransform = (projectRoot, framework, nextRouterType, options) => {
1042
1029
  const filePath = findReactGrabFile(projectRoot, framework, nextRouterType);
1043
1030
  if (!filePath) return {
@@ -1046,13 +1033,15 @@ const previewOptionsTransform = (projectRoot, framework, nextRouterType, options
1046
1033
  message: "Could not find file containing React Grab configuration"
1047
1034
  };
1048
1035
  const originalContent = readFileSync(filePath, "utf-8");
1049
- if (!hasReactGrabCode(originalContent)) return {
1036
+ if (!hasReactGrabSetupCode(originalContent)) return {
1050
1037
  success: false,
1051
1038
  filePath,
1052
1039
  message: "Could not find React Grab code in the file"
1053
1040
  };
1054
1041
  switch (framework) {
1055
- case "next": return addOptionsToNextScript(originalContent, options, filePath);
1042
+ case "next":
1043
+ if (isInstrumentationFile(filePath)) return addOptionsToAnyImport(originalContent, options, filePath);
1044
+ return addOptionsToNextScript(originalContent, options, filePath);
1056
1045
  case "vite": return addOptionsToDynamicImport(originalContent, options, filePath);
1057
1046
  case "tanstack": return addOptionsToTanStackImport(originalContent, options, filePath);
1058
1047
  case "webpack": return addOptionsToDynamicImport(originalContent, options, filePath);
@@ -1110,7 +1099,7 @@ const formatActivationKeyDisplay = (activationKey) => {
1110
1099
  };
1111
1100
  //#endregion
1112
1101
  //#region src/commands/configure.ts
1113
- const VERSION$4 = "0.1.44";
1102
+ const VERSION$4 = "0.1.45-dev.b85b9b1";
1114
1103
  const isMac = process.platform === "darwin";
1115
1104
  const META_LABEL = isMac ? "Cmd" : "Win";
1116
1105
  const ALT_LABEL = isMac ? "Option" : "Alt";
@@ -1380,8 +1369,8 @@ const generateSuggestions = (input) => {
1380
1369
  const CONFIG_OPTIONS = [
1381
1370
  {
1382
1371
  id: "activationKey",
1383
- title: "Activation Key",
1384
- description: "The key used to activate React Grab (e.g., g, k, space)"
1372
+ title: "Shortcut",
1373
+ description: "The shortcut used to activate React Grab (e.g., g, k, space)"
1385
1374
  },
1386
1375
  {
1387
1376
  id: "activationMode",
@@ -1416,7 +1405,7 @@ const comboToString = (combo) => {
1416
1405
  }
1417
1406
  return parts.join("+");
1418
1407
  };
1419
- const configure = new Command().name("configure").alias("config").description("configure React Grab options").option("-y, --yes", "skip confirmation prompts", false).option("-k, --key <key>", "activation key (e.g., Meta+K, Ctrl+Shift+G, Space)").option("-m, --mode <mode>", "activation mode (toggle, hold)").option("--hold-duration <ms>", "key hold duration in milliseconds (for hold mode)").option("--allow-input <boolean>", "allow activation inside input fields (true/false)").option("--context-lines <lines>", "max context lines to include").option("--cdn <domain>", "CDN domain (e.g., unpkg.com, custom.react-grab.com)").option("-c, --cwd <cwd>", "working directory (defaults to current directory)", process.cwd()).action(async (opts) => {
1408
+ const configure = new Command().name("configure").alias("config").description("configure React Grab options").option("-y, --yes", "skip confirmation prompts", false).option("-k, --key <key>", "shortcut (e.g., Meta+K, Ctrl+Shift+G, Space)").option("-m, --mode <mode>", "activation mode (toggle, hold)").option("--hold-duration <ms>", "key hold duration in milliseconds (for hold mode)").option("--allow-input <boolean>", "allow activation inside input fields (true/false)").option("--context-lines <lines>", "max context lines to include").option("--cdn <domain>", "CDN domain (e.g., unpkg.com, custom.react-grab.com)").option("-c, --cwd <cwd>", "working directory (defaults to current directory)", process.cwd()).action(async (opts) => {
1420
1409
  console.log(`${pc.magenta("✿")} ${pc.bold("React Grab")} ${pc.gray(VERSION$4)}`);
1421
1410
  console.log();
1422
1411
  try {
@@ -1430,6 +1419,13 @@ const configure = new Command().name("configure").alias("config").description("c
1430
1419
  logger.break();
1431
1420
  process.exit(1);
1432
1421
  }
1422
+ if (!projectInfo.isReactGrabConfigured) {
1423
+ preflightSpinner.fail("React Grab is installed, but setup is missing.");
1424
+ logger.break();
1425
+ logger.error(`Run ${highlighter.info("react-grab init")} to add the setup script/import before configuring options.`);
1426
+ logger.break();
1427
+ process.exit(1);
1428
+ }
1433
1429
  preflightSpinner.succeed();
1434
1430
  if (opts.cdn) {
1435
1431
  const result = previewCdnTransform(projectInfo.projectRoot, projectInfo.framework, projectInfo.nextRouterType, opts.cdn);
@@ -1485,7 +1481,7 @@ const configure = new Command().name("configure").alias("config").description("c
1485
1481
  if (hasFlags) {
1486
1482
  if (opts.key) {
1487
1483
  collectedOptions.activationKey = opts.key;
1488
- logger.log(` Activation key: ${highlighter.info(formatActivationKeyDisplay(collectedOptions.activationKey))}`);
1484
+ logger.log(` Shortcut: ${highlighter.info(formatActivationKeyDisplay(collectedOptions.activationKey))}`);
1489
1485
  }
1490
1486
  if (opts.mode) {
1491
1487
  if (opts.mode !== "toggle" && opts.mode !== "hold") {
@@ -1550,7 +1546,7 @@ const configure = new Command().name("configure").alias("config").description("c
1550
1546
  process.exit(1);
1551
1547
  }
1552
1548
  collectedOptions.activationKey = comboToString(selectedCombo);
1553
- logger.log(` Activation key: ${highlighter.info(formatActivationKeyDisplay(collectedOptions.activationKey))}`);
1549
+ logger.log(` Shortcut: ${highlighter.info(formatActivationKeyDisplay(collectedOptions.activationKey))}`);
1554
1550
  }
1555
1551
  if (selectedOption === "activationMode") {
1556
1552
  const { activationMode } = await prompts({
@@ -1735,7 +1731,7 @@ const isTelemetryEnabled = () => {
1735
1731
  };
1736
1732
  //#endregion
1737
1733
  //#region src/commands/init.ts
1738
- const VERSION$3 = "0.1.44";
1734
+ const VERSION$3 = "0.1.45-dev.b85b9b1";
1739
1735
  const REPORT_URL = "https://react-grab.com/api/report-cli";
1740
1736
  const DOCS_URL = "https://github.com/aidenybai/react-grab";
1741
1737
  const reportToCli = (type, config, error) => {
@@ -1803,7 +1799,7 @@ const failWithManualSetup = (failingSpinner, message, { listSupportedFrameworks
1803
1799
  logger.break();
1804
1800
  process.exit(1);
1805
1801
  };
1806
- const init = new Command().name("init").alias("setup").description("initialize React Grab in your project").option("-y, --yes", "skip confirmation prompts", false).option("-f, --force", "force overwrite existing config", false).option("-k, --key <key>", "activation key (e.g., Meta+K, Ctrl+Shift+G, Space)").option("--skip-install", "skip package installation", false).option("--pkg <pkg>", "custom package URL for CLI (e.g., grab)").option("-c, --cwd <cwd>", "working directory (defaults to current directory)", process.cwd()).option("-g, --global", "install the skill globally instead of in the project", false).action(async (opts) => {
1802
+ const init = new Command().name("init").alias("setup").description("initialize React Grab in your project").option("-y, --yes", "skip confirmation prompts", false).option("-f, --force", "re-run setup checks even when React Grab is already configured", false).option("-k, --key <key>", "shortcut (e.g., Meta+K, Ctrl+Shift+G, Space)").option("--skip-install", "skip package installation", false).option("--pkg <pkg>", "custom package URL for CLI (e.g., grab)").option("-c, --cwd <cwd>", "working directory (defaults to current directory)", process.cwd()).option("-g, --global", "install the skill globally instead of in the project", false).action(async (opts) => {
1807
1803
  console.log(`${pc.magenta("✿")} ${pc.bold("React Grab")} ${pc.gray(VERSION$3)}`);
1808
1804
  console.log();
1809
1805
  try {
@@ -1817,12 +1813,12 @@ const init = new Command().name("init").alias("setup").description("initialize R
1817
1813
  }
1818
1814
  const preflightSpinner = spinner("Preflight checks.").start();
1819
1815
  const projectInfo = await detectProject(cwd);
1820
- if (projectInfo.hasReactGrab && !opts.force) {
1816
+ if (projectInfo.isReactGrabConfigured && !opts.force) {
1821
1817
  preflightSpinner.succeed();
1822
1818
  if (isNonInteractive) {
1823
1819
  logger.break();
1824
1820
  logger.warn("React Grab is already installed.");
1825
- logger.log(`Use ${highlighter.info("--force")} to reconfigure, or remove ${highlighter.info("--yes")} for interactive mode.`);
1821
+ logger.log(`Use ${highlighter.info("--force")} to re-run setup checks, or remove ${highlighter.info("--yes")} for interactive mode.`);
1826
1822
  logger.break();
1827
1823
  process.exit(0);
1828
1824
  }
@@ -1846,12 +1842,12 @@ const init = new Command().name("init").alias("setup").description("initialize R
1846
1842
  const collectedOptions = {};
1847
1843
  if (opts.key) {
1848
1844
  collectedOptions.activationKey = opts.key;
1849
- logger.log(` Activation key: ${highlighter.info(formatActivationKeyDisplay(collectedOptions.activationKey))}`);
1845
+ logger.log(` Shortcut: ${highlighter.info(formatActivationKeyDisplay(collectedOptions.activationKey))}`);
1850
1846
  } else {
1851
1847
  const { wantActivationKey } = await prompts({
1852
1848
  type: "confirm",
1853
1849
  name: "wantActivationKey",
1854
- message: `Configure ${highlighter.info("activation key")}?`,
1850
+ message: `Configure ${highlighter.info("shortcut")}?`,
1855
1851
  initial: false
1856
1852
  });
1857
1853
  if (wantActivationKey === void 0) {
@@ -1862,7 +1858,7 @@ const init = new Command().name("init").alias("setup").description("initialize R
1862
1858
  const { key } = await prompts({
1863
1859
  type: "text",
1864
1860
  name: "key",
1865
- message: "Enter the activation key (e.g., g, k, space):",
1861
+ message: "Enter the shortcut (e.g., g, k, space):",
1866
1862
  initial: ""
1867
1863
  });
1868
1864
  if (key === void 0) {
@@ -1870,7 +1866,7 @@ const init = new Command().name("init").alias("setup").description("initialize R
1870
1866
  process.exit(1);
1871
1867
  }
1872
1868
  collectedOptions.activationKey = key ? key.toLowerCase() : void 0;
1873
- logger.log(` Activation key: ${highlighter.info(formatActivationKeyDisplay(collectedOptions.activationKey))}`);
1869
+ logger.log(` Shortcut: ${highlighter.info(formatActivationKeyDisplay(collectedOptions.activationKey))}`);
1874
1870
  }
1875
1871
  }
1876
1872
  const { activationMode } = await prompts({
@@ -2037,7 +2033,7 @@ const init = new Command().name("init").alias("setup").description("initialize R
2037
2033
  cwd
2038
2034
  });
2039
2035
  }
2040
- const result = previewTransform(projectInfo.projectRoot, finalFramework, finalNextRouterType, false, opts.force);
2036
+ const result = previewTransform(projectInfo.projectRoot, finalFramework, finalNextRouterType, projectInfo.isReactGrabConfigured);
2041
2037
  if (!result.success) {
2042
2038
  logger.break();
2043
2039
  logger.error(result.message);
@@ -2072,7 +2068,8 @@ const init = new Command().name("init").alias("setup").description("initialize R
2072
2068
  if (!opts.skipInstall && shouldInstallReactGrab) await installPackagesWithFeedback(getPackagesToInstall(shouldInstallReactGrab), finalPackageManager, projectInfo.projectRoot);
2073
2069
  if (hasLayoutChanges) applyTransformWithFeedback(result);
2074
2070
  logger.break();
2075
- logger.log(`${highlighter.success("Success!")} React Grab has been installed.`);
2071
+ if (hasLayoutChanges) logger.log(`${highlighter.success("Success!")} React Grab has been installed.`);
2072
+ else logger.log(`${highlighter.success("Success!")} ${result.message}.`);
2076
2073
  logger.log("You may now start your development server.");
2077
2074
  logger.break();
2078
2075
  reportToCli("completed", {
@@ -2685,7 +2682,7 @@ const pull = new Command().name("pull").description("start the watcher if needed
2685
2682
  });
2686
2683
  //#endregion
2687
2684
  //#region src/commands/remove.ts
2688
- const VERSION$2 = "0.1.44";
2685
+ const VERSION$2 = "0.1.45-dev.b85b9b1";
2689
2686
  const remove = new Command().name("remove").description("uninstall the React Grab skill from your agent").option("-c, --cwd <cwd>", "working directory (defaults to current directory)", process.cwd()).option("-g, --global", "remove the globally-installed skill instead of the project's", false).action(async (opts) => {
2690
2687
  console.log(`${pc.magenta("✿")} ${pc.bold("React Grab")} ${pc.gray(VERSION$2)}`);
2691
2688
  console.log();
@@ -2713,7 +2710,7 @@ const stop = new Command().name("stop").description("stop the React Grab watcher
2713
2710
  });
2714
2711
  //#endregion
2715
2712
  //#region src/commands/upgrade.ts
2716
- const VERSION$1 = "0.1.44";
2713
+ const VERSION$1 = "0.1.45-dev.b85b9b1";
2717
2714
  const NPM_REGISTRY_URL = "https://registry.npmjs.org/react-grab/latest";
2718
2715
  const fetchLatestVersion = async () => {
2719
2716
  try {
@@ -2825,7 +2822,7 @@ const watch = new Command().name("watch").description("run the React Grab captur
2825
2822
  });
2826
2823
  //#endregion
2827
2824
  //#region src/cli.ts
2828
- const VERSION = "0.1.44";
2825
+ const VERSION = "0.1.45-dev.b85b9b1";
2829
2826
  const VERSION_API_URL = "https://www.react-grab.com/api/version";
2830
2827
  process.on("SIGINT", () => process.exit(0));
2831
2828
  process.on("SIGTERM", () => process.exit(0));