@react-grab/cli 0.1.43 → 0.1.44-dev.099016d

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
  };
@@ -434,6 +480,13 @@ const promptSkillInstall = async ({ yes = false, global = false, cwd = process.c
434
480
  }
435
481
  let selectedAgents = detectedAgents;
436
482
  if (!yes) {
483
+ const { confirmed } = await prompts({
484
+ type: "confirm",
485
+ name: "confirmed",
486
+ message: `Install the React Grab skill (${global ? "global" : "this project"})?`,
487
+ initial: true
488
+ });
489
+ if (!confirmed) return false;
437
490
  const { agents } = await prompts({
438
491
  type: "multiselect",
439
492
  name: "agents",
@@ -484,7 +537,7 @@ const removeSkill = async ({ cwd = process.cwd(), global = false } = {}) => {
484
537
  };
485
538
  //#endregion
486
539
  //#region src/commands/add.ts
487
- const VERSION$5 = "0.1.43";
540
+ const VERSION$5 = "0.1.44-dev.099016d";
488
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) => {
489
542
  console.log(`${pc.magenta("✿")} ${pc.bold("React Grab")} ${pc.gray(VERSION$5)}`);
490
543
  console.log();
@@ -639,80 +692,14 @@ const TANSTACK_EFFECT = `useEffect(() => {
639
692
  const SCRIPT_IMPORT = "import Script from \"next/script\";";
640
693
  //#endregion
641
694
  //#region src/utils/transform.ts
642
- const hasReactGrabCode = (content) => {
643
- return [
644
- /["'`][^"'`]*react-grab/,
645
- /react-grab[^"'`]*["'`]/,
646
- /<[^>]*react-grab/i,
647
- /import[^;]*react-grab/i,
648
- /require[^)]*react-grab/i,
649
- /from\s+[^;]*react-grab/i,
650
- /src[^>]*react-grab/i,
651
- /href[^>]*react-grab/i
652
- ].some((pattern) => pattern.test(content));
653
- };
654
- const findLayoutFile = (projectRoot) => {
655
- const possiblePaths = [
656
- join(projectRoot, "app", "layout.tsx"),
657
- join(projectRoot, "app", "layout.jsx"),
658
- join(projectRoot, "src", "app", "layout.tsx"),
659
- join(projectRoot, "src", "app", "layout.jsx")
660
- ];
661
- for (const filePath of possiblePaths) if (existsSync(filePath)) return filePath;
662
- return null;
663
- };
664
- const findInstrumentationFile = (projectRoot) => {
665
- const possiblePaths = [
666
- join(projectRoot, "instrumentation-client.ts"),
667
- join(projectRoot, "instrumentation-client.js"),
668
- join(projectRoot, "src", "instrumentation-client.ts"),
669
- join(projectRoot, "src", "instrumentation-client.js")
670
- ];
671
- for (const filePath of possiblePaths) if (existsSync(filePath)) return filePath;
672
- return null;
673
- };
674
695
  const hasReactGrabInInstrumentation = (projectRoot) => {
675
- const instrumentationPath = findInstrumentationFile(projectRoot);
676
- if (!instrumentationPath) return false;
677
- return hasReactGrabCode(readFileSync(instrumentationPath, "utf-8"));
678
- };
679
- const findDocumentFile = (projectRoot) => {
680
- const possiblePaths = [
681
- join(projectRoot, "pages", "_document.tsx"),
682
- join(projectRoot, "pages", "_document.jsx"),
683
- join(projectRoot, "src", "pages", "_document.tsx"),
684
- join(projectRoot, "src", "pages", "_document.jsx")
685
- ];
686
- for (const filePath of possiblePaths) if (existsSync(filePath)) return filePath;
687
- return null;
696
+ return findFileWithReactGrabSetup(getInstrumentationFileCandidates(projectRoot)) !== null;
688
697
  };
689
- const findIndexHtml = (projectRoot) => {
690
- const possiblePaths = [join(projectRoot, "index.html"), join(projectRoot, "public", "index.html")];
691
- for (const filePath of possiblePaths) if (existsSync(filePath)) return filePath;
692
- return null;
693
- };
694
- const findEntryFile = (projectRoot) => {
695
- const possiblePaths = [
696
- join(projectRoot, "src", "index.tsx"),
697
- join(projectRoot, "src", "index.jsx"),
698
- join(projectRoot, "src", "index.ts"),
699
- join(projectRoot, "src", "index.js"),
700
- join(projectRoot, "src", "main.tsx"),
701
- join(projectRoot, "src", "main.jsx"),
702
- join(projectRoot, "src", "main.ts"),
703
- join(projectRoot, "src", "main.js")
704
- ];
705
- for (const filePath of possiblePaths) if (existsSync(filePath)) return filePath;
706
- return null;
707
- };
708
- const findTanStackRootFile = (projectRoot) => {
709
- const possiblePaths = [
710
- join(projectRoot, "src", "routes", "__root.tsx"),
711
- join(projectRoot, "src", "routes", "__root.jsx"),
712
- join(projectRoot, "app", "routes", "__root.tsx"),
713
- join(projectRoot, "app", "routes", "__root.jsx")
714
- ];
715
- 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
+ }
716
703
  return null;
717
704
  };
718
705
  const alreadyConfiguredResult = (filePath) => ({
@@ -721,19 +708,19 @@ const alreadyConfiguredResult = (filePath) => ({
721
708
  message: "React Grab is already configured",
722
709
  noChanges: true
723
710
  });
724
- const transformNextAppRouter = (projectRoot, reactGrabAlreadyConfigured, force = false) => {
711
+ const transformNextAppRouter = (projectRoot, reactGrabAlreadyConfigured) => {
725
712
  const layoutPath = findLayoutFile(projectRoot);
726
713
  if (!layoutPath) return {
727
714
  success: false,
728
715
  filePath: "",
729
- 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"
730
717
  };
731
718
  const originalContent = readFileSync(layoutPath, "utf-8");
732
719
  let newContent = originalContent;
733
- const hasReactGrabInFile = hasReactGrabCode(originalContent);
720
+ const hasReactGrabInFile = hasReactGrabSetupCode(originalContent);
734
721
  const hasReactGrabInInstrumentationFile = hasReactGrabInInstrumentation(projectRoot);
735
- if (!force && hasReactGrabInFile && reactGrabAlreadyConfigured) return alreadyConfiguredResult(layoutPath);
736
- if (!force && (hasReactGrabInFile || hasReactGrabInInstrumentationFile)) return {
722
+ if (hasReactGrabInFile && reactGrabAlreadyConfigured) return alreadyConfiguredResult(layoutPath);
723
+ if (hasReactGrabInFile || hasReactGrabInInstrumentationFile) return {
737
724
  success: true,
738
725
  filePath: layoutPath,
739
726
  message: "React Grab is already installed" + (hasReactGrabInInstrumentationFile ? " in instrumentation-client" : " in this file"),
@@ -758,19 +745,19 @@ const transformNextAppRouter = (projectRoot, reactGrabAlreadyConfigured, force =
758
745
  newContent
759
746
  };
760
747
  };
761
- const transformNextPagesRouter = (projectRoot, reactGrabAlreadyConfigured, force = false) => {
748
+ const transformNextPagesRouter = (projectRoot, reactGrabAlreadyConfigured) => {
762
749
  const documentPath = findDocumentFile(projectRoot);
763
750
  if (!documentPath) return {
764
751
  success: false,
765
752
  filePath: "",
766
- 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 }"
767
754
  };
768
755
  const originalContent = readFileSync(documentPath, "utf-8");
769
756
  let newContent = originalContent;
770
- const hasReactGrabInFile = hasReactGrabCode(originalContent);
757
+ const hasReactGrabInFile = hasReactGrabSetupCode(originalContent);
771
758
  const hasReactGrabInInstrumentationFile = hasReactGrabInInstrumentation(projectRoot);
772
- if (!force && hasReactGrabInFile && reactGrabAlreadyConfigured) return alreadyConfiguredResult(documentPath);
773
- if (!force && (hasReactGrabInFile || hasReactGrabInInstrumentationFile)) return {
759
+ if (hasReactGrabInFile && reactGrabAlreadyConfigured) return alreadyConfiguredResult(documentPath);
760
+ if (hasReactGrabInFile || hasReactGrabInInstrumentationFile) return {
774
761
  success: true,
775
762
  filePath: documentPath,
776
763
  message: "React Grab is already installed" + (hasReactGrabInInstrumentationFile ? " in instrumentation-client" : " in this file"),
@@ -791,7 +778,7 @@ const transformNextPagesRouter = (projectRoot, reactGrabAlreadyConfigured, force
791
778
  };
792
779
  };
793
780
  const checkExistingInstallation = (filePath, reactGrabAlreadyConfigured) => {
794
- if (!hasReactGrabCode(readFileSync(filePath, "utf-8"))) return null;
781
+ if (!hasReactGrabSetupCode(readFileSync(filePath, "utf-8"))) return null;
795
782
  return {
796
783
  success: true,
797
784
  filePath,
@@ -799,24 +786,20 @@ const checkExistingInstallation = (filePath, reactGrabAlreadyConfigured) => {
799
786
  noChanges: true
800
787
  };
801
788
  };
802
- const transformVite = (projectRoot, reactGrabAlreadyConfigured, force = false) => {
789
+ const transformVite = (projectRoot, reactGrabAlreadyConfigured) => {
803
790
  const entryPath = findEntryFile(projectRoot);
804
- if (!force) {
805
- const indexPath = findIndexHtml(projectRoot);
806
- if (indexPath) {
807
- const existingResult = checkExistingInstallation(indexPath, reactGrabAlreadyConfigured);
808
- if (existingResult) return existingResult;
809
- }
791
+ const indexPath = findIndexHtml(projectRoot);
792
+ if (indexPath) {
793
+ const existingResult = checkExistingInstallation(indexPath, reactGrabAlreadyConfigured);
794
+ if (existingResult) return existingResult;
810
795
  }
811
796
  if (!entryPath) return {
812
797
  success: false,
813
798
  filePath: "",
814
799
  message: "Could not find entry file (src/index.tsx, src/main.tsx, etc.)"
815
800
  };
816
- if (!force) {
817
- const existingResult = checkExistingInstallation(entryPath, reactGrabAlreadyConfigured);
818
- if (existingResult) return existingResult;
819
- }
801
+ const existingResult = checkExistingInstallation(entryPath, reactGrabAlreadyConfigured);
802
+ if (existingResult) return existingResult;
820
803
  const originalContent = readFileSync(entryPath, "utf-8");
821
804
  return {
822
805
  success: true,
@@ -826,17 +809,15 @@ const transformVite = (projectRoot, reactGrabAlreadyConfigured, force = false) =
826
809
  newContent: `${VITE_IMPORT}\n\n${originalContent}`
827
810
  };
828
811
  };
829
- const transformWebpack = (projectRoot, reactGrabAlreadyConfigured, force = false) => {
812
+ const transformWebpack = (projectRoot, reactGrabAlreadyConfigured) => {
830
813
  const entryPath = findEntryFile(projectRoot);
831
814
  if (!entryPath) return {
832
815
  success: false,
833
816
  filePath: "",
834
817
  message: "Could not find entry file (src/index.tsx, src/main.tsx, etc.)"
835
818
  };
836
- if (!force) {
837
- const existingResult = checkExistingInstallation(entryPath, reactGrabAlreadyConfigured);
838
- if (existingResult) return existingResult;
839
- }
819
+ const existingResult = checkExistingInstallation(entryPath, reactGrabAlreadyConfigured);
820
+ if (existingResult) return existingResult;
840
821
  const originalContent = readFileSync(entryPath, "utf-8");
841
822
  return {
842
823
  success: true,
@@ -846,7 +827,7 @@ const transformWebpack = (projectRoot, reactGrabAlreadyConfigured, force = false
846
827
  newContent: `${WEBPACK_IMPORT}\n\n${originalContent}`
847
828
  };
848
829
  };
849
- const transformTanStack = (projectRoot, reactGrabAlreadyConfigured, force = false) => {
830
+ const transformTanStack = (projectRoot, reactGrabAlreadyConfigured) => {
850
831
  const rootPath = findTanStackRootFile(projectRoot);
851
832
  if (!rootPath) return {
852
833
  success: false,
@@ -855,9 +836,9 @@ const transformTanStack = (projectRoot, reactGrabAlreadyConfigured, force = fals
855
836
  };
856
837
  const originalContent = readFileSync(rootPath, "utf-8");
857
838
  let newContent = originalContent;
858
- const hasReactGrabInFile = hasReactGrabCode(originalContent);
859
- if (!force && hasReactGrabInFile && reactGrabAlreadyConfigured) return alreadyConfiguredResult(rootPath);
860
- if (!force && hasReactGrabInFile) return {
839
+ const hasReactGrabInFile = hasReactGrabSetupCode(originalContent);
840
+ if (hasReactGrabInFile && reactGrabAlreadyConfigured) return alreadyConfiguredResult(rootPath);
841
+ if (hasReactGrabInFile) return {
861
842
  success: true,
862
843
  filePath: rootPath,
863
844
  message: "React Grab is already installed in this file",
@@ -900,14 +881,14 @@ const hasFrameworkEntryPoint = (projectRoot, framework, nextRouterType) => {
900
881
  default: return false;
901
882
  }
902
883
  };
903
- const previewTransform = (projectRoot, framework, nextRouterType, reactGrabAlreadyConfigured = false, force = false) => {
884
+ const previewTransform = (projectRoot, framework, nextRouterType, reactGrabAlreadyConfigured = false) => {
904
885
  switch (framework) {
905
886
  case "next":
906
- if (nextRouterType === "app") return transformNextAppRouter(projectRoot, reactGrabAlreadyConfigured, force);
907
- return transformNextPagesRouter(projectRoot, reactGrabAlreadyConfigured, force);
908
- case "vite": return transformVite(projectRoot, reactGrabAlreadyConfigured, force);
909
- case "tanstack": return transformTanStack(projectRoot, reactGrabAlreadyConfigured, force);
910
- 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);
911
892
  default: return {
912
893
  success: false,
913
894
  filePath: "",
@@ -961,18 +942,24 @@ const formatOptionsAsJson = (options) => {
961
942
  };
962
943
  const findReactGrabFile = (projectRoot, framework, nextRouterType) => {
963
944
  switch (framework) {
964
- case "next":
965
- if (nextRouterType === "app") return findLayoutFile(projectRoot);
966
- 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
+ }
967
953
  case "vite": {
968
954
  const entryFile = findEntryFile(projectRoot);
969
- if (entryFile && hasReactGrabCode(readFileSync(entryFile, "utf-8"))) return entryFile;
970
- const indexHtml = findIndexHtml(projectRoot);
971
- 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;
972
959
  return entryFile;
973
960
  }
974
- case "tanstack": return findTanStackRootFile(projectRoot);
975
- case "webpack": return findEntryFile(projectRoot);
961
+ case "tanstack": return findFileWithReactGrabSetup(getTanStackRootFileCandidates(projectRoot)) ?? findTanStackRootFile(projectRoot);
962
+ case "webpack": return findFileWithReactGrabSetup(getEntryFileCandidates(projectRoot)) ?? findEntryFile(projectRoot);
976
963
  default: return null;
977
964
  }
978
965
  };
@@ -1000,13 +987,14 @@ const addOptionsToNextScript = (originalContent, options, filePath) => {
1000
987
  };
1001
988
  };
1002
989
  const addOptionsToDynamicImport = (originalContent, options, filePath) => {
1003
- 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*\))?/);
1004
991
  if (!reactGrabImportWithInitMatch) return {
1005
992
  success: false,
1006
993
  filePath,
1007
994
  message: "Could not find React Grab import"
1008
995
  };
1009
- 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}))`;
1010
998
  return {
1011
999
  success: true,
1012
1000
  filePath,
@@ -1016,13 +1004,14 @@ const addOptionsToDynamicImport = (originalContent, options, filePath) => {
1016
1004
  };
1017
1005
  };
1018
1006
  const addOptionsToTanStackImport = (originalContent, options, filePath) => {
1019
- 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*\))/);
1020
1008
  if (!reactGrabImportWithInitMatch) return {
1021
1009
  success: false,
1022
1010
  filePath,
1023
1011
  message: "Could not find React Grab import"
1024
1012
  };
1025
- 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}))`;
1026
1015
  return {
1027
1016
  success: true,
1028
1017
  filePath,
@@ -1031,6 +1020,11 @@ const addOptionsToTanStackImport = (originalContent, options, filePath) => {
1031
1020
  newContent: originalContent.replace(reactGrabImportWithInitMatch[0], newImport)
1032
1021
  };
1033
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
+ };
1034
1028
  const previewOptionsTransform = (projectRoot, framework, nextRouterType, options) => {
1035
1029
  const filePath = findReactGrabFile(projectRoot, framework, nextRouterType);
1036
1030
  if (!filePath) return {
@@ -1039,13 +1033,15 @@ const previewOptionsTransform = (projectRoot, framework, nextRouterType, options
1039
1033
  message: "Could not find file containing React Grab configuration"
1040
1034
  };
1041
1035
  const originalContent = readFileSync(filePath, "utf-8");
1042
- if (!hasReactGrabCode(originalContent)) return {
1036
+ if (!hasReactGrabSetupCode(originalContent)) return {
1043
1037
  success: false,
1044
1038
  filePath,
1045
1039
  message: "Could not find React Grab code in the file"
1046
1040
  };
1047
1041
  switch (framework) {
1048
- 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);
1049
1045
  case "vite": return addOptionsToDynamicImport(originalContent, options, filePath);
1050
1046
  case "tanstack": return addOptionsToTanStackImport(originalContent, options, filePath);
1051
1047
  case "webpack": return addOptionsToDynamicImport(originalContent, options, filePath);
@@ -1103,7 +1099,7 @@ const formatActivationKeyDisplay = (activationKey) => {
1103
1099
  };
1104
1100
  //#endregion
1105
1101
  //#region src/commands/configure.ts
1106
- const VERSION$4 = "0.1.43";
1102
+ const VERSION$4 = "0.1.44-dev.099016d";
1107
1103
  const isMac = process.platform === "darwin";
1108
1104
  const META_LABEL = isMac ? "Cmd" : "Win";
1109
1105
  const ALT_LABEL = isMac ? "Option" : "Alt";
@@ -1373,8 +1369,8 @@ const generateSuggestions = (input) => {
1373
1369
  const CONFIG_OPTIONS = [
1374
1370
  {
1375
1371
  id: "activationKey",
1376
- title: "Activation Key",
1377
- 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)"
1378
1374
  },
1379
1375
  {
1380
1376
  id: "activationMode",
@@ -1409,7 +1405,7 @@ const comboToString = (combo) => {
1409
1405
  }
1410
1406
  return parts.join("+");
1411
1407
  };
1412
- 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) => {
1413
1409
  console.log(`${pc.magenta("✿")} ${pc.bold("React Grab")} ${pc.gray(VERSION$4)}`);
1414
1410
  console.log();
1415
1411
  try {
@@ -1423,6 +1419,13 @@ const configure = new Command().name("configure").alias("config").description("c
1423
1419
  logger.break();
1424
1420
  process.exit(1);
1425
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
+ }
1426
1429
  preflightSpinner.succeed();
1427
1430
  if (opts.cdn) {
1428
1431
  const result = previewCdnTransform(projectInfo.projectRoot, projectInfo.framework, projectInfo.nextRouterType, opts.cdn);
@@ -1478,7 +1481,7 @@ const configure = new Command().name("configure").alias("config").description("c
1478
1481
  if (hasFlags) {
1479
1482
  if (opts.key) {
1480
1483
  collectedOptions.activationKey = opts.key;
1481
- logger.log(` Activation key: ${highlighter.info(formatActivationKeyDisplay(collectedOptions.activationKey))}`);
1484
+ logger.log(` Shortcut: ${highlighter.info(formatActivationKeyDisplay(collectedOptions.activationKey))}`);
1482
1485
  }
1483
1486
  if (opts.mode) {
1484
1487
  if (opts.mode !== "toggle" && opts.mode !== "hold") {
@@ -1543,7 +1546,7 @@ const configure = new Command().name("configure").alias("config").description("c
1543
1546
  process.exit(1);
1544
1547
  }
1545
1548
  collectedOptions.activationKey = comboToString(selectedCombo);
1546
- logger.log(` Activation key: ${highlighter.info(formatActivationKeyDisplay(collectedOptions.activationKey))}`);
1549
+ logger.log(` Shortcut: ${highlighter.info(formatActivationKeyDisplay(collectedOptions.activationKey))}`);
1547
1550
  }
1548
1551
  if (selectedOption === "activationMode") {
1549
1552
  const { activationMode } = await prompts({
@@ -1728,7 +1731,7 @@ const isTelemetryEnabled = () => {
1728
1731
  };
1729
1732
  //#endregion
1730
1733
  //#region src/commands/init.ts
1731
- const VERSION$3 = "0.1.43";
1734
+ const VERSION$3 = "0.1.44-dev.099016d";
1732
1735
  const REPORT_URL = "https://react-grab.com/api/report-cli";
1733
1736
  const DOCS_URL = "https://github.com/aidenybai/react-grab";
1734
1737
  const reportToCli = (type, config, error) => {
@@ -1796,7 +1799,7 @@ const failWithManualSetup = (failingSpinner, message, { listSupportedFrameworks
1796
1799
  logger.break();
1797
1800
  process.exit(1);
1798
1801
  };
1799
- 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) => {
1800
1803
  console.log(`${pc.magenta("✿")} ${pc.bold("React Grab")} ${pc.gray(VERSION$3)}`);
1801
1804
  console.log();
1802
1805
  try {
@@ -1810,12 +1813,12 @@ const init = new Command().name("init").alias("setup").description("initialize R
1810
1813
  }
1811
1814
  const preflightSpinner = spinner("Preflight checks.").start();
1812
1815
  const projectInfo = await detectProject(cwd);
1813
- if (projectInfo.hasReactGrab && !opts.force) {
1816
+ if (projectInfo.isReactGrabConfigured && !opts.force) {
1814
1817
  preflightSpinner.succeed();
1815
1818
  if (isNonInteractive) {
1816
1819
  logger.break();
1817
1820
  logger.warn("React Grab is already installed.");
1818
- 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.`);
1819
1822
  logger.break();
1820
1823
  process.exit(0);
1821
1824
  }
@@ -1839,12 +1842,12 @@ const init = new Command().name("init").alias("setup").description("initialize R
1839
1842
  const collectedOptions = {};
1840
1843
  if (opts.key) {
1841
1844
  collectedOptions.activationKey = opts.key;
1842
- logger.log(` Activation key: ${highlighter.info(formatActivationKeyDisplay(collectedOptions.activationKey))}`);
1845
+ logger.log(` Shortcut: ${highlighter.info(formatActivationKeyDisplay(collectedOptions.activationKey))}`);
1843
1846
  } else {
1844
1847
  const { wantActivationKey } = await prompts({
1845
1848
  type: "confirm",
1846
1849
  name: "wantActivationKey",
1847
- message: `Configure ${highlighter.info("activation key")}?`,
1850
+ message: `Configure ${highlighter.info("shortcut")}?`,
1848
1851
  initial: false
1849
1852
  });
1850
1853
  if (wantActivationKey === void 0) {
@@ -1855,7 +1858,7 @@ const init = new Command().name("init").alias("setup").description("initialize R
1855
1858
  const { key } = await prompts({
1856
1859
  type: "text",
1857
1860
  name: "key",
1858
- message: "Enter the activation key (e.g., g, k, space):",
1861
+ message: "Enter the shortcut (e.g., g, k, space):",
1859
1862
  initial: ""
1860
1863
  });
1861
1864
  if (key === void 0) {
@@ -1863,7 +1866,7 @@ const init = new Command().name("init").alias("setup").description("initialize R
1863
1866
  process.exit(1);
1864
1867
  }
1865
1868
  collectedOptions.activationKey = key ? key.toLowerCase() : void 0;
1866
- logger.log(` Activation key: ${highlighter.info(formatActivationKeyDisplay(collectedOptions.activationKey))}`);
1869
+ logger.log(` Shortcut: ${highlighter.info(formatActivationKeyDisplay(collectedOptions.activationKey))}`);
1867
1870
  }
1868
1871
  }
1869
1872
  const { activationMode } = await prompts({
@@ -2030,7 +2033,7 @@ const init = new Command().name("init").alias("setup").description("initialize R
2030
2033
  cwd
2031
2034
  });
2032
2035
  }
2033
- const result = previewTransform(projectInfo.projectRoot, finalFramework, finalNextRouterType, false, opts.force);
2036
+ const result = previewTransform(projectInfo.projectRoot, finalFramework, finalNextRouterType, projectInfo.isReactGrabConfigured);
2034
2037
  if (!result.success) {
2035
2038
  logger.break();
2036
2039
  logger.error(result.message);
@@ -2065,7 +2068,8 @@ const init = new Command().name("init").alias("setup").description("initialize R
2065
2068
  if (!opts.skipInstall && shouldInstallReactGrab) await installPackagesWithFeedback(getPackagesToInstall(shouldInstallReactGrab), finalPackageManager, projectInfo.projectRoot);
2066
2069
  if (hasLayoutChanges) applyTransformWithFeedback(result);
2067
2070
  logger.break();
2068
- 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}.`);
2069
2073
  logger.log("You may now start your development server.");
2070
2074
  logger.break();
2071
2075
  reportToCli("completed", {
@@ -2678,7 +2682,7 @@ const pull = new Command().name("pull").description("start the watcher if needed
2678
2682
  });
2679
2683
  //#endregion
2680
2684
  //#region src/commands/remove.ts
2681
- const VERSION$2 = "0.1.43";
2685
+ const VERSION$2 = "0.1.44-dev.099016d";
2682
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) => {
2683
2687
  console.log(`${pc.magenta("✿")} ${pc.bold("React Grab")} ${pc.gray(VERSION$2)}`);
2684
2688
  console.log();
@@ -2706,7 +2710,7 @@ const stop = new Command().name("stop").description("stop the React Grab watcher
2706
2710
  });
2707
2711
  //#endregion
2708
2712
  //#region src/commands/upgrade.ts
2709
- const VERSION$1 = "0.1.43";
2713
+ const VERSION$1 = "0.1.44-dev.099016d";
2710
2714
  const NPM_REGISTRY_URL = "https://registry.npmjs.org/react-grab/latest";
2711
2715
  const fetchLatestVersion = async () => {
2712
2716
  try {
@@ -2818,7 +2822,7 @@ const watch = new Command().name("watch").description("run the React Grab captur
2818
2822
  });
2819
2823
  //#endregion
2820
2824
  //#region src/cli.ts
2821
- const VERSION = "0.1.43";
2825
+ const VERSION = "0.1.44-dev.099016d";
2822
2826
  const VERSION_API_URL = "https://www.react-grab.com/api/version";
2823
2827
  process.on("SIGINT", () => process.exit(0));
2824
2828
  process.on("SIGTERM", () => process.exit(0));