@react-grab/cli 0.1.14 → 0.1.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/cli.cjs +240 -187
  2. package/dist/cli.js +221 -189
  3. package/package.json +5 -2
package/dist/cli.cjs CHANGED
@@ -8,20 +8,56 @@ var child_process = require('child_process');
8
8
  var fs = require('fs');
9
9
  var path = require('path');
10
10
  var ni = require('@antfu/ni');
11
+ var ignore = require('ignore');
11
12
  var os = require('os');
12
13
  var process2 = require('process');
14
+ var jsonc = require('jsonc-parser');
15
+ var TOML = require('smol-toml');
13
16
  var ora = require('ora');
14
17
 
15
18
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
16
19
 
20
+ function _interopNamespace(e) {
21
+ if (e && e.__esModule) return e;
22
+ var n = Object.create(null);
23
+ if (e) {
24
+ Object.keys(e).forEach(function (k) {
25
+ if (k !== 'default') {
26
+ var d = Object.getOwnPropertyDescriptor(e, k);
27
+ Object.defineProperty(n, k, d.get ? d : {
28
+ enumerable: true,
29
+ get: function () { return e[k]; }
30
+ });
31
+ }
32
+ });
33
+ }
34
+ n.default = e;
35
+ return Object.freeze(n);
36
+ }
37
+
17
38
  var pc__default = /*#__PURE__*/_interopDefault(pc);
18
39
  var basePrompts__default = /*#__PURE__*/_interopDefault(basePrompts);
19
40
  var fs__default = /*#__PURE__*/_interopDefault(fs);
20
41
  var path__default = /*#__PURE__*/_interopDefault(path);
42
+ var ignore__default = /*#__PURE__*/_interopDefault(ignore);
21
43
  var os__default = /*#__PURE__*/_interopDefault(os);
22
44
  var process2__default = /*#__PURE__*/_interopDefault(process2);
45
+ var jsonc__namespace = /*#__PURE__*/_interopNamespace(jsonc);
46
+ var TOML__namespace = /*#__PURE__*/_interopNamespace(TOML);
23
47
  var ora__default = /*#__PURE__*/_interopDefault(ora);
24
48
 
49
+ // src/utils/is-non-interactive.ts
50
+ var AGENT_ENVIRONMENT_VARIABLES = [
51
+ "CI",
52
+ "CLAUDECODE",
53
+ "CURSOR_AGENT",
54
+ "CODEX_CI",
55
+ "OPENCODE",
56
+ "AMP_HOME",
57
+ "AMI"
58
+ ];
59
+ var isEnvironmentVariableSet = (variable) => Boolean(process.env[variable]);
60
+ var detectNonInteractive = (yesFlag) => yesFlag || AGENT_ENVIRONMENT_VARIABLES.some(isEnvironmentVariableSet) || !process.stdin.isTTY;
25
61
  var highlighter = {
26
62
  error: pc__default.default.red,
27
63
  warn: pc__default.default.yellow,
@@ -191,18 +227,22 @@ var getWorkspacePatterns = (projectRoot) => {
191
227
  return [...new Set(patterns)];
192
228
  };
193
229
  var expandWorkspacePattern = (projectRoot, pattern) => {
194
- const results = [];
230
+ const isGlob = pattern.endsWith("/*");
195
231
  const cleanPattern = pattern.replace(/\/\*$/, "");
196
232
  const basePath = path.join(projectRoot, cleanPattern);
197
- if (!fs.existsSync(basePath)) return results;
233
+ if (!fs.existsSync(basePath)) return [];
234
+ if (!isGlob) {
235
+ const hasPackageJson = fs.existsSync(path.join(basePath, "package.json"));
236
+ return hasPackageJson ? [basePath] : [];
237
+ }
238
+ const results = [];
198
239
  try {
199
240
  const entries = fs.readdirSync(basePath, { withFileTypes: true });
200
241
  for (const entry of entries) {
201
- if (entry.isDirectory()) {
202
- const packageJsonPath = path.join(basePath, entry.name, "package.json");
203
- if (fs.existsSync(packageJsonPath)) {
204
- results.push(path.join(basePath, entry.name));
205
- }
242
+ if (!entry.isDirectory()) continue;
243
+ const packageJsonPath = path.join(basePath, entry.name, "package.json");
244
+ if (fs.existsSync(packageJsonPath)) {
245
+ results.push(path.join(basePath, entry.name));
206
246
  }
207
247
  }
208
248
  } catch {
@@ -224,35 +264,92 @@ var hasReactDependency = (projectPath) => {
224
264
  return false;
225
265
  }
226
266
  };
267
+ var buildReactProject = (projectPath) => {
268
+ const framework = detectFramework(projectPath);
269
+ const hasReact = hasReactDependency(projectPath);
270
+ if (!hasReact && framework === "unknown") return null;
271
+ let name = path.basename(projectPath);
272
+ const packageJsonPath = path.join(projectPath, "package.json");
273
+ try {
274
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
275
+ name = packageJson.name || name;
276
+ } catch {
277
+ }
278
+ return { name, path: projectPath, framework, hasReact };
279
+ };
227
280
  var findWorkspaceProjects = (projectRoot) => {
228
281
  const patterns = getWorkspacePatterns(projectRoot);
229
282
  const projects = [];
230
283
  for (const pattern of patterns) {
231
- const projectPaths = expandWorkspacePattern(projectRoot, pattern);
232
- for (const projectPath of projectPaths) {
233
- const framework = detectFramework(projectPath);
234
- const hasReact = hasReactDependency(projectPath);
235
- if (hasReact || framework !== "unknown") {
236
- const packageJsonPath = path.join(projectPath, "package.json");
237
- let name = path.basename(projectPath);
238
- try {
239
- const packageJson = JSON.parse(
240
- fs.readFileSync(packageJsonPath, "utf-8")
241
- );
242
- name = packageJson.name || name;
243
- } catch {
284
+ for (const projectPath of expandWorkspacePattern(projectRoot, pattern)) {
285
+ const project = buildReactProject(projectPath);
286
+ if (project) projects.push(project);
287
+ }
288
+ }
289
+ return projects;
290
+ };
291
+ var ALWAYS_IGNORED_DIRECTORIES = [
292
+ "node_modules",
293
+ ".git",
294
+ ".next",
295
+ ".cache",
296
+ ".turbo",
297
+ "dist",
298
+ "build",
299
+ "coverage",
300
+ "test-results"
301
+ ];
302
+ var loadGitignore = (projectRoot) => {
303
+ const ignorer = ignore__default.default().add(ALWAYS_IGNORED_DIRECTORIES);
304
+ const gitignorePath = path.join(projectRoot, ".gitignore");
305
+ if (fs.existsSync(gitignorePath)) {
306
+ try {
307
+ ignorer.add(fs.readFileSync(gitignorePath, "utf-8"));
308
+ } catch {
309
+ }
310
+ }
311
+ return ignorer;
312
+ };
313
+ var scanDirectoryForProjects = (rootDirectory, ignorer, maxDepth, currentDepth = 0) => {
314
+ if (currentDepth >= maxDepth) return [];
315
+ if (!fs.existsSync(rootDirectory)) return [];
316
+ const projects = [];
317
+ try {
318
+ const entries = fs.readdirSync(rootDirectory, { withFileTypes: true });
319
+ for (const entry of entries) {
320
+ if (!entry.isDirectory()) continue;
321
+ if (ignorer.ignores(entry.name)) continue;
322
+ const entryPath = path.join(rootDirectory, entry.name);
323
+ const hasPackageJson = fs.existsSync(path.join(entryPath, "package.json"));
324
+ if (hasPackageJson) {
325
+ const project = buildReactProject(entryPath);
326
+ if (project) {
327
+ projects.push(project);
328
+ continue;
244
329
  }
245
- projects.push({
246
- name,
247
- path: projectPath,
248
- framework,
249
- hasReact
250
- });
251
330
  }
331
+ projects.push(
332
+ ...scanDirectoryForProjects(
333
+ entryPath,
334
+ ignorer,
335
+ maxDepth,
336
+ currentDepth + 1
337
+ )
338
+ );
252
339
  }
340
+ } catch {
341
+ return projects;
253
342
  }
254
343
  return projects;
255
344
  };
345
+ var MAX_SCAN_DEPTH = 2;
346
+ var findReactProjects = (projectRoot) => {
347
+ if (detectMonorepo(projectRoot)) {
348
+ return findWorkspaceProjects(projectRoot);
349
+ }
350
+ const ignorer = loadGitignore(projectRoot);
351
+ return scanDirectoryForProjects(projectRoot, ignorer, MAX_SCAN_DEPTH);
352
+ };
256
353
  var hasReactGrabInFile = (filePath) => {
257
354
  if (!fs.existsSync(filePath)) return false;
258
355
  try {
@@ -318,6 +415,8 @@ var AGENT_PACKAGES = [
318
415
  "@react-grab/gemini",
319
416
  "@react-grab/amp",
320
417
  "@react-grab/ami",
418
+ "@react-grab/droid",
419
+ "@react-grab/copilot",
321
420
  "@react-grab/mcp"
322
421
  ];
323
422
  var detectUnsupportedFramework = (projectRoot) => {
@@ -645,113 +744,39 @@ var ensureDirectory = (filePath) => {
645
744
  fs__default.default.mkdirSync(directory, { recursive: true });
646
745
  }
647
746
  };
648
- var indentJson = (json, baseIndent) => json.split("\n").map((line, index) => index === 0 ? line : baseIndent + line).join("\n");
649
- var insertIntoJsonc = (filePath, content, configKey, serverName, serverConfig) => {
650
- if (content.includes(`"${serverName}"`)) return;
651
- const serverJson = indentJson(
652
- JSON.stringify(serverConfig, null, 2),
653
- " "
747
+ var JSONC_FORMAT_OPTIONS = {
748
+ tabSize: 2,
749
+ insertSpaces: true
750
+ };
751
+ var upsertIntoJsonc = (filePath, content, configKey, serverName, serverConfig) => {
752
+ const edits = jsonc__namespace.modify(
753
+ content,
754
+ [configKey, serverName],
755
+ serverConfig,
756
+ { formattingOptions: JSONC_FORMAT_OPTIONS }
654
757
  );
655
- const escapedConfigKey = configKey.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
656
- const keyPattern = new RegExp(`"${escapedConfigKey}"\\s*:\\s*\\{`);
657
- const keyMatch = keyPattern.exec(content);
658
- if (keyMatch) {
659
- const insertPosition = keyMatch.index + keyMatch[0].length;
660
- const entry = `
661
- "${serverName}": ${serverJson},`;
662
- fs__default.default.writeFileSync(
663
- filePath,
664
- content.slice(0, insertPosition) + entry + content.slice(insertPosition)
665
- );
666
- return;
667
- }
668
- const lastBrace = content.lastIndexOf("}");
669
- if (lastBrace === -1) return;
670
- const beforeBrace = content.slice(0, lastBrace).trimEnd();
671
- const withoutComments = beforeBrace.replace(/\/\/.*$/, "").trimEnd();
672
- const lastChar = withoutComments[withoutComments.length - 1];
673
- const needsComma = lastChar !== void 0 && lastChar !== "{" && lastChar !== ",";
674
- const section = `${needsComma ? "," : ""}
675
- "${configKey}": {
676
- "${serverName}": ${serverJson}
677
- }`;
678
- fs__default.default.writeFileSync(filePath, beforeBrace + section + "\n}\n");
758
+ fs__default.default.writeFileSync(filePath, jsonc__namespace.applyEdits(content, edits));
679
759
  };
680
760
  var installJsonClient = (client) => {
681
761
  ensureDirectory(client.configPath);
682
- if (!fs__default.default.existsSync(client.configPath)) {
683
- const config = {
684
- [client.configKey]: { [SERVER_NAME]: client.serverConfig }
685
- };
686
- fs__default.default.writeFileSync(client.configPath, JSON.stringify(config, null, 2) + "\n");
687
- return;
688
- }
689
- const content = fs__default.default.readFileSync(client.configPath, "utf8");
690
- try {
691
- const config = JSON.parse(content);
692
- const servers = config[client.configKey] ?? {};
693
- servers[SERVER_NAME] = client.serverConfig;
694
- config[client.configKey] = servers;
695
- fs__default.default.writeFileSync(client.configPath, JSON.stringify(config, null, 2) + "\n");
696
- } catch {
697
- insertIntoJsonc(
698
- client.configPath,
699
- content,
700
- client.configKey,
701
- SERVER_NAME,
702
- client.serverConfig
703
- );
704
- }
705
- };
706
- var buildTomlSection = (configKey, serverConfig) => {
707
- const lines = [`[${configKey}.${SERVER_NAME}]`];
708
- for (const [key, value] of Object.entries(serverConfig)) {
709
- if (typeof value === "string") {
710
- lines.push(`${key} = "${value}"`);
711
- } else if (Array.isArray(value)) {
712
- const items = value.map((item) => `"${item}"`).join(", ");
713
- lines.push(`${key} = [${items}]`);
714
- }
715
- }
716
- return lines.join("\n");
762
+ const content = fs__default.default.existsSync(client.configPath) ? fs__default.default.readFileSync(client.configPath, "utf8") : "{}";
763
+ upsertIntoJsonc(
764
+ client.configPath,
765
+ content,
766
+ client.configKey,
767
+ SERVER_NAME,
768
+ client.serverConfig
769
+ );
717
770
  };
718
771
  var installTomlClient = (client) => {
719
772
  ensureDirectory(client.configPath);
720
- const sectionHeader = `[${client.configKey}.${SERVER_NAME}]`;
721
- const newSection = buildTomlSection(client.configKey, client.serverConfig);
722
- if (!fs__default.default.existsSync(client.configPath)) {
723
- fs__default.default.writeFileSync(client.configPath, newSection + "\n");
724
- return;
725
- }
726
- const content = fs__default.default.readFileSync(client.configPath, "utf8");
727
- if (!content.includes(sectionHeader)) {
728
- fs__default.default.writeFileSync(
729
- client.configPath,
730
- content.trimEnd() + "\n\n" + newSection + "\n"
731
- );
732
- return;
733
- }
734
- const lines = content.split("\n");
735
- const resultLines = [];
736
- let isInsideOurSection = false;
737
- let didInsertReplacement = false;
738
- for (const line of lines) {
739
- if (line.trim() === sectionHeader) {
740
- isInsideOurSection = true;
741
- if (!didInsertReplacement) {
742
- resultLines.push(newSection);
743
- didInsertReplacement = true;
744
- }
745
- continue;
746
- }
747
- if (isInsideOurSection && line.startsWith("[")) {
748
- isInsideOurSection = false;
749
- }
750
- if (!isInsideOurSection) {
751
- resultLines.push(line);
752
- }
753
- }
754
- fs__default.default.writeFileSync(client.configPath, resultLines.join("\n"));
773
+ const existingConfig = fs__default.default.existsSync(
774
+ client.configPath
775
+ ) ? TOML__namespace.parse(fs__default.default.readFileSync(client.configPath, "utf8")) : {};
776
+ const serverSection = existingConfig[client.configKey] ?? {};
777
+ serverSection[SERVER_NAME] = client.serverConfig;
778
+ existingConfig[client.configKey] = serverSection;
779
+ fs__default.default.writeFileSync(client.configPath, TOML__namespace.stringify(existingConfig));
755
780
  };
756
781
  var getMcpClientNames = () => getClients().map((client) => client.name);
757
782
  var installMcpServers = (selectedClients) => {
@@ -853,7 +878,8 @@ var AGENTS = [
853
878
  "gemini",
854
879
  "amp",
855
880
  "ami",
856
- "droid"
881
+ "droid",
882
+ "copilot"
857
883
  ];
858
884
  var AGENT_NAMES = {
859
885
  "claude-code": "Claude Code",
@@ -863,7 +889,8 @@ var AGENT_NAMES = {
863
889
  gemini: "Gemini",
864
890
  amp: "Amp",
865
891
  ami: "Ami",
866
- droid: "Droid"
892
+ droid: "Droid",
893
+ copilot: "Copilot"
867
894
  };
868
895
  var getAgentDisplayName = (agent) => {
869
896
  if (agent === "mcp") return "MCP";
@@ -1598,7 +1625,9 @@ var AGENT_PACKAGES2 = {
1598
1625
  opencode: "@react-grab/opencode@latest",
1599
1626
  codex: "@react-grab/codex@latest",
1600
1627
  gemini: "@react-grab/gemini@latest",
1601
- amp: "@react-grab/amp@latest"
1628
+ amp: "@react-grab/amp@latest",
1629
+ droid: "@react-grab/droid@latest",
1630
+ copilot: "@react-grab/copilot@latest"
1602
1631
  };
1603
1632
  var getAgentPrefix = (agent, packageManager) => {
1604
1633
  const agentPackage = AGENT_PACKAGES2[agent];
@@ -2194,7 +2223,7 @@ var previewCdnTransform = (projectRoot, framework, nextRouterType, targetCdnDoma
2194
2223
  };
2195
2224
 
2196
2225
  // src/commands/add.ts
2197
- var VERSION = "0.1.14";
2226
+ var VERSION = "0.1.16";
2198
2227
  var formatInstalledAgentNames = (agents) => agents.map((agent) => AGENT_NAMES[agent] || agent).join(", ");
2199
2228
  var add = new commander.Command().name("add").alias("install").description("connect React Grab to your agent").argument("[agent]", `agent to connect (${AGENTS.join(", ")}, mcp)`).option("-y, --yes", "skip confirmation prompts", false).option(
2200
2229
  "-c, --cwd <cwd>",
@@ -2207,7 +2236,7 @@ var add = new commander.Command().name("add").alias("install").description("conn
2207
2236
  console.log();
2208
2237
  try {
2209
2238
  const cwd = opts.cwd;
2210
- const isNonInteractive = opts.yes;
2239
+ const isNonInteractive = detectNonInteractive(opts.yes);
2211
2240
  const preflightSpinner = spinner("Preflight checks.").start();
2212
2241
  const projectInfo = await detectProject(cwd);
2213
2242
  if (!projectInfo.hasReactGrab) {
@@ -2555,7 +2584,7 @@ var MAX_KEY_HOLD_DURATION_MS = 2e3;
2555
2584
  var MAX_CONTEXT_LINES = 50;
2556
2585
 
2557
2586
  // src/commands/configure.ts
2558
- var VERSION2 = "0.1.14";
2587
+ var VERSION2 = "0.1.16";
2559
2588
  var isMac = process.platform === "darwin";
2560
2589
  var META_LABEL = isMac ? "Cmd" : "Win";
2561
2590
  var ALT_LABEL = isMac ? "Option" : "Alt";
@@ -3111,7 +3140,7 @@ var uninstallPackagesWithFeedback = (packages, packageManager, projectRoot) => {
3111
3140
  };
3112
3141
 
3113
3142
  // src/commands/init.ts
3114
- var VERSION3 = "0.1.14";
3143
+ var VERSION3 = "0.1.16";
3115
3144
  var REPORT_URL = "https://react-grab.com/api/report-cli";
3116
3145
  var DOCS_URL = "https://github.com/aidenybai/react-grab";
3117
3146
  var reportToCli = (type, config, error) => {
@@ -3148,6 +3177,34 @@ var UNSUPPORTED_FRAMEWORK_NAMES = {
3148
3177
  gatsby: "Gatsby"
3149
3178
  };
3150
3179
  var getAgentName = getAgentDisplayName;
3180
+ var sortProjectsByFramework = (projects) => [...projects].sort((projectA, projectB) => {
3181
+ if (projectA.framework === "unknown" && projectB.framework !== "unknown")
3182
+ return 1;
3183
+ if (projectA.framework !== "unknown" && projectB.framework === "unknown")
3184
+ return -1;
3185
+ return 0;
3186
+ });
3187
+ var printSubprojects = (searchRoot, sortedProjects) => {
3188
+ logger.break();
3189
+ logger.log("Found the following projects:");
3190
+ logger.break();
3191
+ for (const project of sortedProjects) {
3192
+ const frameworkLabel = project.framework !== "unknown" ? ` ${highlighter.dim(`(${FRAMEWORK_NAMES[project.framework]})`)}` : "";
3193
+ const relativePath = path.relative(searchRoot, project.path);
3194
+ logger.log(
3195
+ ` ${highlighter.info(project.name)}${frameworkLabel} ${highlighter.dim(relativePath)}`
3196
+ );
3197
+ }
3198
+ logger.break();
3199
+ logger.log(
3200
+ `Re-run with ${highlighter.info("-c <path>")} to specify a project:`
3201
+ );
3202
+ logger.break();
3203
+ logger.log(
3204
+ ` ${highlighter.dim("$")} npx -y grab@latest init -c ${path.relative(searchRoot, sortedProjects[0].path)}`
3205
+ );
3206
+ logger.break();
3207
+ };
3151
3208
  var formatActivationKeyDisplay2 = (activationKey) => {
3152
3209
  if (!activationKey) return "Default (Option/Alt)";
3153
3210
  return activationKey.split("+").map((part) => {
@@ -3176,8 +3233,14 @@ var init = new commander.Command().name("init").description("initialize React Gr
3176
3233
  );
3177
3234
  console.log();
3178
3235
  try {
3179
- const cwd = opts.cwd;
3180
- const isNonInteractive = opts.yes;
3236
+ const cwd = path.resolve(opts.cwd);
3237
+ const isNonInteractive = detectNonInteractive(opts.yes);
3238
+ if (!fs.existsSync(cwd)) {
3239
+ logger.break();
3240
+ logger.error(`Directory does not exist: ${highlighter.info(cwd)}`);
3241
+ logger.break();
3242
+ process.exit(1);
3243
+ }
3181
3244
  const preflightSpinner = spinner("Preflight checks.").start();
3182
3245
  const projectInfo = await detectProject(cwd);
3183
3246
  const removeAgents = async (agentsToRemove2, skipInstall = false) => {
@@ -3704,58 +3767,48 @@ var init = new commander.Command().name("init").description("initialize React Gr
3704
3767
  process.exit(1);
3705
3768
  }
3706
3769
  if (projectInfo.framework === "unknown") {
3707
- if (projectInfo.isMonorepo && !isNonInteractive) {
3708
- frameworkSpinner.info("Verifying framework. Found monorepo.");
3709
- const workspaceProjects = findWorkspaceProjects(
3710
- projectInfo.projectRoot
3711
- );
3712
- const reactProjects = workspaceProjects.filter(
3713
- (project) => project.hasReact || project.framework !== "unknown"
3770
+ let searchRoot = cwd;
3771
+ let reactProjects = findReactProjects(searchRoot);
3772
+ if (reactProjects.length === 0 && cwd !== process.cwd()) {
3773
+ searchRoot = process.cwd();
3774
+ reactProjects = findReactProjects(searchRoot);
3775
+ }
3776
+ if (reactProjects.length > 0) {
3777
+ frameworkSpinner.info(
3778
+ `Verifying framework. Found ${reactProjects.length} project${reactProjects.length === 1 ? "" : "s"}.`
3714
3779
  );
3715
- if (reactProjects.length > 0) {
3716
- logger.break();
3717
- const sortedProjects = [...reactProjects].sort(
3718
- (projectA, projectB) => {
3719
- if (projectA.framework === "unknown" && projectB.framework !== "unknown")
3720
- return 1;
3721
- if (projectA.framework !== "unknown" && projectB.framework === "unknown")
3722
- return -1;
3723
- return 0;
3724
- }
3725
- );
3726
- const { selectedProject } = await prompts({
3727
- type: "select",
3728
- name: "selectedProject",
3729
- message: "Select a project to install React Grab:",
3730
- choices: [
3731
- ...sortedProjects.map((project) => {
3732
- const frameworkLabel = project.framework !== "unknown" ? ` ${highlighter.dim(`(${FRAMEWORK_NAMES[project.framework]})`)}` : "";
3733
- return {
3734
- title: `${project.name}${frameworkLabel}`,
3735
- value: project.path
3736
- };
3737
- }),
3738
- { title: "Skip", value: "skip" }
3739
- ]
3740
- });
3741
- if (!selectedProject || selectedProject === "skip") {
3742
- logger.break();
3743
- process.exit(0);
3744
- }
3745
- process.chdir(selectedProject);
3746
- const newProjectInfo = await detectProject(selectedProject);
3747
- Object.assign(projectInfo, newProjectInfo);
3748
- const newFrameworkSpinner = spinner("Verifying framework.").start();
3749
- newFrameworkSpinner.succeed(
3750
- `Verifying framework. Found ${highlighter.info(FRAMEWORK_NAMES[newProjectInfo.framework])}.`
3751
- );
3752
- } else {
3753
- frameworkSpinner.fail("Could not detect a supported framework.");
3754
- logger.break();
3755
- logger.log(`Visit ${highlighter.info(DOCS_URL)} for manual setup.`);
3756
- logger.break();
3780
+ const sortedProjects = sortProjectsByFramework(reactProjects);
3781
+ if (isNonInteractive) {
3782
+ printSubprojects(searchRoot, sortedProjects);
3757
3783
  process.exit(1);
3758
3784
  }
3785
+ logger.break();
3786
+ const { selectedProject } = await prompts({
3787
+ type: "select",
3788
+ name: "selectedProject",
3789
+ message: "Select a project to install React Grab:",
3790
+ choices: [
3791
+ ...sortedProjects.map((project) => {
3792
+ const frameworkLabel = project.framework !== "unknown" ? ` ${highlighter.dim(`(${FRAMEWORK_NAMES[project.framework]})`)}` : "";
3793
+ return {
3794
+ title: `${project.name}${frameworkLabel}`,
3795
+ value: project.path
3796
+ };
3797
+ }),
3798
+ { title: "Skip", value: "skip" }
3799
+ ]
3800
+ });
3801
+ if (!selectedProject || selectedProject === "skip") {
3802
+ logger.break();
3803
+ process.exit(0);
3804
+ }
3805
+ process.chdir(selectedProject);
3806
+ const newProjectInfo = await detectProject(selectedProject);
3807
+ Object.assign(projectInfo, newProjectInfo);
3808
+ const newFrameworkSpinner = spinner("Verifying framework.").start();
3809
+ newFrameworkSpinner.succeed(
3810
+ `Verifying framework. Found ${highlighter.info(FRAMEWORK_NAMES[newProjectInfo.framework])}.`
3811
+ );
3759
3812
  } else {
3760
3813
  frameworkSpinner.fail("Could not detect a supported framework.");
3761
3814
  logger.break();
@@ -3945,7 +3998,7 @@ var init = new commander.Command().name("init").description("initialize React Gr
3945
3998
  reportToCli("error", void 0, error);
3946
3999
  }
3947
4000
  });
3948
- var VERSION4 = "0.1.14";
4001
+ var VERSION4 = "0.1.16";
3949
4002
  var remove = new commander.Command().name("remove").description("disconnect React Grab from your agent").argument("[agent]", `agent to disconnect (${AGENTS.join(", ")}, mcp)`).option("-y, --yes", "skip confirmation prompts", false).option(
3950
4003
  "-c, --cwd <cwd>",
3951
4004
  "working directory (defaults to current directory)",
@@ -3957,7 +4010,7 @@ var remove = new commander.Command().name("remove").description("disconnect Reac
3957
4010
  console.log();
3958
4011
  try {
3959
4012
  const cwd = opts.cwd;
3960
- const isNonInteractive = opts.yes;
4013
+ const isNonInteractive = detectNonInteractive(opts.yes);
3961
4014
  const preflightSpinner = spinner("Preflight checks.").start();
3962
4015
  const projectInfo = await detectProject(cwd);
3963
4016
  if (!projectInfo.hasReactGrab) {
@@ -4121,7 +4174,7 @@ var remove = new commander.Command().name("remove").description("disconnect Reac
4121
4174
  });
4122
4175
 
4123
4176
  // src/cli.ts
4124
- var VERSION5 = "0.1.14";
4177
+ var VERSION5 = "0.1.16";
4125
4178
  var VERSION_API_URL = "https://www.react-grab.com/api/version";
4126
4179
  process.on("SIGINT", () => process.exit(0));
4127
4180
  process.on("SIGTERM", () => process.exit(0));
package/dist/cli.js CHANGED
@@ -3,13 +3,28 @@ import { Command } from 'commander';
3
3
  import pc from 'picocolors';
4
4
  import basePrompts from 'prompts';
5
5
  import { execSync } from 'child_process';
6
- import fs, { readFileSync, existsSync, writeFileSync, accessSync, constants, readdirSync } from 'fs';
7
- import path, { join, basename } from 'path';
6
+ import fs, { existsSync, readFileSync, writeFileSync, accessSync, constants, readdirSync } from 'fs';
7
+ import path, { resolve, join, relative, basename } from 'path';
8
8
  import { detect } from '@antfu/ni';
9
+ import ignore from 'ignore';
9
10
  import os from 'os';
10
11
  import process2 from 'process';
12
+ import * as jsonc from 'jsonc-parser';
13
+ import * as TOML from 'smol-toml';
11
14
  import ora from 'ora';
12
15
 
16
+ // src/utils/is-non-interactive.ts
17
+ var AGENT_ENVIRONMENT_VARIABLES = [
18
+ "CI",
19
+ "CLAUDECODE",
20
+ "CURSOR_AGENT",
21
+ "CODEX_CI",
22
+ "OPENCODE",
23
+ "AMP_HOME",
24
+ "AMI"
25
+ ];
26
+ var isEnvironmentVariableSet = (variable) => Boolean(process.env[variable]);
27
+ var detectNonInteractive = (yesFlag) => yesFlag || AGENT_ENVIRONMENT_VARIABLES.some(isEnvironmentVariableSet) || !process.stdin.isTTY;
13
28
  var highlighter = {
14
29
  error: pc.red,
15
30
  warn: pc.yellow,
@@ -179,18 +194,22 @@ var getWorkspacePatterns = (projectRoot) => {
179
194
  return [...new Set(patterns)];
180
195
  };
181
196
  var expandWorkspacePattern = (projectRoot, pattern) => {
182
- const results = [];
197
+ const isGlob = pattern.endsWith("/*");
183
198
  const cleanPattern = pattern.replace(/\/\*$/, "");
184
199
  const basePath = join(projectRoot, cleanPattern);
185
- if (!existsSync(basePath)) return results;
200
+ if (!existsSync(basePath)) return [];
201
+ if (!isGlob) {
202
+ const hasPackageJson = existsSync(join(basePath, "package.json"));
203
+ return hasPackageJson ? [basePath] : [];
204
+ }
205
+ const results = [];
186
206
  try {
187
207
  const entries = readdirSync(basePath, { withFileTypes: true });
188
208
  for (const entry of entries) {
189
- if (entry.isDirectory()) {
190
- const packageJsonPath = join(basePath, entry.name, "package.json");
191
- if (existsSync(packageJsonPath)) {
192
- results.push(join(basePath, entry.name));
193
- }
209
+ if (!entry.isDirectory()) continue;
210
+ const packageJsonPath = join(basePath, entry.name, "package.json");
211
+ if (existsSync(packageJsonPath)) {
212
+ results.push(join(basePath, entry.name));
194
213
  }
195
214
  }
196
215
  } catch {
@@ -212,35 +231,92 @@ var hasReactDependency = (projectPath) => {
212
231
  return false;
213
232
  }
214
233
  };
234
+ var buildReactProject = (projectPath) => {
235
+ const framework = detectFramework(projectPath);
236
+ const hasReact = hasReactDependency(projectPath);
237
+ if (!hasReact && framework === "unknown") return null;
238
+ let name = basename(projectPath);
239
+ const packageJsonPath = join(projectPath, "package.json");
240
+ try {
241
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
242
+ name = packageJson.name || name;
243
+ } catch {
244
+ }
245
+ return { name, path: projectPath, framework, hasReact };
246
+ };
215
247
  var findWorkspaceProjects = (projectRoot) => {
216
248
  const patterns = getWorkspacePatterns(projectRoot);
217
249
  const projects = [];
218
250
  for (const pattern of patterns) {
219
- const projectPaths = expandWorkspacePattern(projectRoot, pattern);
220
- for (const projectPath of projectPaths) {
221
- const framework = detectFramework(projectPath);
222
- const hasReact = hasReactDependency(projectPath);
223
- if (hasReact || framework !== "unknown") {
224
- const packageJsonPath = join(projectPath, "package.json");
225
- let name = basename(projectPath);
226
- try {
227
- const packageJson = JSON.parse(
228
- readFileSync(packageJsonPath, "utf-8")
229
- );
230
- name = packageJson.name || name;
231
- } catch {
251
+ for (const projectPath of expandWorkspacePattern(projectRoot, pattern)) {
252
+ const project = buildReactProject(projectPath);
253
+ if (project) projects.push(project);
254
+ }
255
+ }
256
+ return projects;
257
+ };
258
+ var ALWAYS_IGNORED_DIRECTORIES = [
259
+ "node_modules",
260
+ ".git",
261
+ ".next",
262
+ ".cache",
263
+ ".turbo",
264
+ "dist",
265
+ "build",
266
+ "coverage",
267
+ "test-results"
268
+ ];
269
+ var loadGitignore = (projectRoot) => {
270
+ const ignorer = ignore().add(ALWAYS_IGNORED_DIRECTORIES);
271
+ const gitignorePath = join(projectRoot, ".gitignore");
272
+ if (existsSync(gitignorePath)) {
273
+ try {
274
+ ignorer.add(readFileSync(gitignorePath, "utf-8"));
275
+ } catch {
276
+ }
277
+ }
278
+ return ignorer;
279
+ };
280
+ var scanDirectoryForProjects = (rootDirectory, ignorer, maxDepth, currentDepth = 0) => {
281
+ if (currentDepth >= maxDepth) return [];
282
+ if (!existsSync(rootDirectory)) return [];
283
+ const projects = [];
284
+ try {
285
+ const entries = readdirSync(rootDirectory, { withFileTypes: true });
286
+ for (const entry of entries) {
287
+ if (!entry.isDirectory()) continue;
288
+ if (ignorer.ignores(entry.name)) continue;
289
+ const entryPath = join(rootDirectory, entry.name);
290
+ const hasPackageJson = existsSync(join(entryPath, "package.json"));
291
+ if (hasPackageJson) {
292
+ const project = buildReactProject(entryPath);
293
+ if (project) {
294
+ projects.push(project);
295
+ continue;
232
296
  }
233
- projects.push({
234
- name,
235
- path: projectPath,
236
- framework,
237
- hasReact
238
- });
239
297
  }
298
+ projects.push(
299
+ ...scanDirectoryForProjects(
300
+ entryPath,
301
+ ignorer,
302
+ maxDepth,
303
+ currentDepth + 1
304
+ )
305
+ );
240
306
  }
307
+ } catch {
308
+ return projects;
241
309
  }
242
310
  return projects;
243
311
  };
312
+ var MAX_SCAN_DEPTH = 2;
313
+ var findReactProjects = (projectRoot) => {
314
+ if (detectMonorepo(projectRoot)) {
315
+ return findWorkspaceProjects(projectRoot);
316
+ }
317
+ const ignorer = loadGitignore(projectRoot);
318
+ return scanDirectoryForProjects(projectRoot, ignorer, MAX_SCAN_DEPTH);
319
+ };
244
320
  var hasReactGrabInFile = (filePath) => {
245
321
  if (!existsSync(filePath)) return false;
246
322
  try {
@@ -306,6 +382,8 @@ var AGENT_PACKAGES = [
306
382
  "@react-grab/gemini",
307
383
  "@react-grab/amp",
308
384
  "@react-grab/ami",
385
+ "@react-grab/droid",
386
+ "@react-grab/copilot",
309
387
  "@react-grab/mcp"
310
388
  ];
311
389
  var detectUnsupportedFramework = (projectRoot) => {
@@ -633,113 +711,39 @@ var ensureDirectory = (filePath) => {
633
711
  fs.mkdirSync(directory, { recursive: true });
634
712
  }
635
713
  };
636
- var indentJson = (json, baseIndent) => json.split("\n").map((line, index) => index === 0 ? line : baseIndent + line).join("\n");
637
- var insertIntoJsonc = (filePath, content, configKey, serverName, serverConfig) => {
638
- if (content.includes(`"${serverName}"`)) return;
639
- const serverJson = indentJson(
640
- JSON.stringify(serverConfig, null, 2),
641
- " "
714
+ var JSONC_FORMAT_OPTIONS = {
715
+ tabSize: 2,
716
+ insertSpaces: true
717
+ };
718
+ var upsertIntoJsonc = (filePath, content, configKey, serverName, serverConfig) => {
719
+ const edits = jsonc.modify(
720
+ content,
721
+ [configKey, serverName],
722
+ serverConfig,
723
+ { formattingOptions: JSONC_FORMAT_OPTIONS }
642
724
  );
643
- const escapedConfigKey = configKey.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
644
- const keyPattern = new RegExp(`"${escapedConfigKey}"\\s*:\\s*\\{`);
645
- const keyMatch = keyPattern.exec(content);
646
- if (keyMatch) {
647
- const insertPosition = keyMatch.index + keyMatch[0].length;
648
- const entry = `
649
- "${serverName}": ${serverJson},`;
650
- fs.writeFileSync(
651
- filePath,
652
- content.slice(0, insertPosition) + entry + content.slice(insertPosition)
653
- );
654
- return;
655
- }
656
- const lastBrace = content.lastIndexOf("}");
657
- if (lastBrace === -1) return;
658
- const beforeBrace = content.slice(0, lastBrace).trimEnd();
659
- const withoutComments = beforeBrace.replace(/\/\/.*$/, "").trimEnd();
660
- const lastChar = withoutComments[withoutComments.length - 1];
661
- const needsComma = lastChar !== void 0 && lastChar !== "{" && lastChar !== ",";
662
- const section = `${needsComma ? "," : ""}
663
- "${configKey}": {
664
- "${serverName}": ${serverJson}
665
- }`;
666
- fs.writeFileSync(filePath, beforeBrace + section + "\n}\n");
725
+ fs.writeFileSync(filePath, jsonc.applyEdits(content, edits));
667
726
  };
668
727
  var installJsonClient = (client) => {
669
728
  ensureDirectory(client.configPath);
670
- if (!fs.existsSync(client.configPath)) {
671
- const config = {
672
- [client.configKey]: { [SERVER_NAME]: client.serverConfig }
673
- };
674
- fs.writeFileSync(client.configPath, JSON.stringify(config, null, 2) + "\n");
675
- return;
676
- }
677
- const content = fs.readFileSync(client.configPath, "utf8");
678
- try {
679
- const config = JSON.parse(content);
680
- const servers = config[client.configKey] ?? {};
681
- servers[SERVER_NAME] = client.serverConfig;
682
- config[client.configKey] = servers;
683
- fs.writeFileSync(client.configPath, JSON.stringify(config, null, 2) + "\n");
684
- } catch {
685
- insertIntoJsonc(
686
- client.configPath,
687
- content,
688
- client.configKey,
689
- SERVER_NAME,
690
- client.serverConfig
691
- );
692
- }
693
- };
694
- var buildTomlSection = (configKey, serverConfig) => {
695
- const lines = [`[${configKey}.${SERVER_NAME}]`];
696
- for (const [key, value] of Object.entries(serverConfig)) {
697
- if (typeof value === "string") {
698
- lines.push(`${key} = "${value}"`);
699
- } else if (Array.isArray(value)) {
700
- const items = value.map((item) => `"${item}"`).join(", ");
701
- lines.push(`${key} = [${items}]`);
702
- }
703
- }
704
- return lines.join("\n");
729
+ const content = fs.existsSync(client.configPath) ? fs.readFileSync(client.configPath, "utf8") : "{}";
730
+ upsertIntoJsonc(
731
+ client.configPath,
732
+ content,
733
+ client.configKey,
734
+ SERVER_NAME,
735
+ client.serverConfig
736
+ );
705
737
  };
706
738
  var installTomlClient = (client) => {
707
739
  ensureDirectory(client.configPath);
708
- const sectionHeader = `[${client.configKey}.${SERVER_NAME}]`;
709
- const newSection = buildTomlSection(client.configKey, client.serverConfig);
710
- if (!fs.existsSync(client.configPath)) {
711
- fs.writeFileSync(client.configPath, newSection + "\n");
712
- return;
713
- }
714
- const content = fs.readFileSync(client.configPath, "utf8");
715
- if (!content.includes(sectionHeader)) {
716
- fs.writeFileSync(
717
- client.configPath,
718
- content.trimEnd() + "\n\n" + newSection + "\n"
719
- );
720
- return;
721
- }
722
- const lines = content.split("\n");
723
- const resultLines = [];
724
- let isInsideOurSection = false;
725
- let didInsertReplacement = false;
726
- for (const line of lines) {
727
- if (line.trim() === sectionHeader) {
728
- isInsideOurSection = true;
729
- if (!didInsertReplacement) {
730
- resultLines.push(newSection);
731
- didInsertReplacement = true;
732
- }
733
- continue;
734
- }
735
- if (isInsideOurSection && line.startsWith("[")) {
736
- isInsideOurSection = false;
737
- }
738
- if (!isInsideOurSection) {
739
- resultLines.push(line);
740
- }
741
- }
742
- fs.writeFileSync(client.configPath, resultLines.join("\n"));
740
+ const existingConfig = fs.existsSync(
741
+ client.configPath
742
+ ) ? TOML.parse(fs.readFileSync(client.configPath, "utf8")) : {};
743
+ const serverSection = existingConfig[client.configKey] ?? {};
744
+ serverSection[SERVER_NAME] = client.serverConfig;
745
+ existingConfig[client.configKey] = serverSection;
746
+ fs.writeFileSync(client.configPath, TOML.stringify(existingConfig));
743
747
  };
744
748
  var getMcpClientNames = () => getClients().map((client) => client.name);
745
749
  var installMcpServers = (selectedClients) => {
@@ -841,7 +845,8 @@ var AGENTS = [
841
845
  "gemini",
842
846
  "amp",
843
847
  "ami",
844
- "droid"
848
+ "droid",
849
+ "copilot"
845
850
  ];
846
851
  var AGENT_NAMES = {
847
852
  "claude-code": "Claude Code",
@@ -851,7 +856,8 @@ var AGENT_NAMES = {
851
856
  gemini: "Gemini",
852
857
  amp: "Amp",
853
858
  ami: "Ami",
854
- droid: "Droid"
859
+ droid: "Droid",
860
+ copilot: "Copilot"
855
861
  };
856
862
  var getAgentDisplayName = (agent) => {
857
863
  if (agent === "mcp") return "MCP";
@@ -1586,7 +1592,9 @@ var AGENT_PACKAGES2 = {
1586
1592
  opencode: "@react-grab/opencode@latest",
1587
1593
  codex: "@react-grab/codex@latest",
1588
1594
  gemini: "@react-grab/gemini@latest",
1589
- amp: "@react-grab/amp@latest"
1595
+ amp: "@react-grab/amp@latest",
1596
+ droid: "@react-grab/droid@latest",
1597
+ copilot: "@react-grab/copilot@latest"
1590
1598
  };
1591
1599
  var getAgentPrefix = (agent, packageManager) => {
1592
1600
  const agentPackage = AGENT_PACKAGES2[agent];
@@ -2182,7 +2190,7 @@ var previewCdnTransform = (projectRoot, framework, nextRouterType, targetCdnDoma
2182
2190
  };
2183
2191
 
2184
2192
  // src/commands/add.ts
2185
- var VERSION = "0.1.14";
2193
+ var VERSION = "0.1.16";
2186
2194
  var formatInstalledAgentNames = (agents) => agents.map((agent) => AGENT_NAMES[agent] || agent).join(", ");
2187
2195
  var add = new Command().name("add").alias("install").description("connect React Grab to your agent").argument("[agent]", `agent to connect (${AGENTS.join(", ")}, mcp)`).option("-y, --yes", "skip confirmation prompts", false).option(
2188
2196
  "-c, --cwd <cwd>",
@@ -2195,7 +2203,7 @@ var add = new Command().name("add").alias("install").description("connect React
2195
2203
  console.log();
2196
2204
  try {
2197
2205
  const cwd = opts.cwd;
2198
- const isNonInteractive = opts.yes;
2206
+ const isNonInteractive = detectNonInteractive(opts.yes);
2199
2207
  const preflightSpinner = spinner("Preflight checks.").start();
2200
2208
  const projectInfo = await detectProject(cwd);
2201
2209
  if (!projectInfo.hasReactGrab) {
@@ -2543,7 +2551,7 @@ var MAX_KEY_HOLD_DURATION_MS = 2e3;
2543
2551
  var MAX_CONTEXT_LINES = 50;
2544
2552
 
2545
2553
  // src/commands/configure.ts
2546
- var VERSION2 = "0.1.14";
2554
+ var VERSION2 = "0.1.16";
2547
2555
  var isMac = process.platform === "darwin";
2548
2556
  var META_LABEL = isMac ? "Cmd" : "Win";
2549
2557
  var ALT_LABEL = isMac ? "Option" : "Alt";
@@ -3099,7 +3107,7 @@ var uninstallPackagesWithFeedback = (packages, packageManager, projectRoot) => {
3099
3107
  };
3100
3108
 
3101
3109
  // src/commands/init.ts
3102
- var VERSION3 = "0.1.14";
3110
+ var VERSION3 = "0.1.16";
3103
3111
  var REPORT_URL = "https://react-grab.com/api/report-cli";
3104
3112
  var DOCS_URL = "https://github.com/aidenybai/react-grab";
3105
3113
  var reportToCli = (type, config, error) => {
@@ -3136,6 +3144,34 @@ var UNSUPPORTED_FRAMEWORK_NAMES = {
3136
3144
  gatsby: "Gatsby"
3137
3145
  };
3138
3146
  var getAgentName = getAgentDisplayName;
3147
+ var sortProjectsByFramework = (projects) => [...projects].sort((projectA, projectB) => {
3148
+ if (projectA.framework === "unknown" && projectB.framework !== "unknown")
3149
+ return 1;
3150
+ if (projectA.framework !== "unknown" && projectB.framework === "unknown")
3151
+ return -1;
3152
+ return 0;
3153
+ });
3154
+ var printSubprojects = (searchRoot, sortedProjects) => {
3155
+ logger.break();
3156
+ logger.log("Found the following projects:");
3157
+ logger.break();
3158
+ for (const project of sortedProjects) {
3159
+ const frameworkLabel = project.framework !== "unknown" ? ` ${highlighter.dim(`(${FRAMEWORK_NAMES[project.framework]})`)}` : "";
3160
+ const relativePath = relative(searchRoot, project.path);
3161
+ logger.log(
3162
+ ` ${highlighter.info(project.name)}${frameworkLabel} ${highlighter.dim(relativePath)}`
3163
+ );
3164
+ }
3165
+ logger.break();
3166
+ logger.log(
3167
+ `Re-run with ${highlighter.info("-c <path>")} to specify a project:`
3168
+ );
3169
+ logger.break();
3170
+ logger.log(
3171
+ ` ${highlighter.dim("$")} npx -y grab@latest init -c ${relative(searchRoot, sortedProjects[0].path)}`
3172
+ );
3173
+ logger.break();
3174
+ };
3139
3175
  var formatActivationKeyDisplay2 = (activationKey) => {
3140
3176
  if (!activationKey) return "Default (Option/Alt)";
3141
3177
  return activationKey.split("+").map((part) => {
@@ -3164,8 +3200,14 @@ var init = new Command().name("init").description("initialize React Grab in your
3164
3200
  );
3165
3201
  console.log();
3166
3202
  try {
3167
- const cwd = opts.cwd;
3168
- const isNonInteractive = opts.yes;
3203
+ const cwd = resolve(opts.cwd);
3204
+ const isNonInteractive = detectNonInteractive(opts.yes);
3205
+ if (!existsSync(cwd)) {
3206
+ logger.break();
3207
+ logger.error(`Directory does not exist: ${highlighter.info(cwd)}`);
3208
+ logger.break();
3209
+ process.exit(1);
3210
+ }
3169
3211
  const preflightSpinner = spinner("Preflight checks.").start();
3170
3212
  const projectInfo = await detectProject(cwd);
3171
3213
  const removeAgents = async (agentsToRemove2, skipInstall = false) => {
@@ -3692,58 +3734,48 @@ var init = new Command().name("init").description("initialize React Grab in your
3692
3734
  process.exit(1);
3693
3735
  }
3694
3736
  if (projectInfo.framework === "unknown") {
3695
- if (projectInfo.isMonorepo && !isNonInteractive) {
3696
- frameworkSpinner.info("Verifying framework. Found monorepo.");
3697
- const workspaceProjects = findWorkspaceProjects(
3698
- projectInfo.projectRoot
3699
- );
3700
- const reactProjects = workspaceProjects.filter(
3701
- (project) => project.hasReact || project.framework !== "unknown"
3737
+ let searchRoot = cwd;
3738
+ let reactProjects = findReactProjects(searchRoot);
3739
+ if (reactProjects.length === 0 && cwd !== process.cwd()) {
3740
+ searchRoot = process.cwd();
3741
+ reactProjects = findReactProjects(searchRoot);
3742
+ }
3743
+ if (reactProjects.length > 0) {
3744
+ frameworkSpinner.info(
3745
+ `Verifying framework. Found ${reactProjects.length} project${reactProjects.length === 1 ? "" : "s"}.`
3702
3746
  );
3703
- if (reactProjects.length > 0) {
3704
- logger.break();
3705
- const sortedProjects = [...reactProjects].sort(
3706
- (projectA, projectB) => {
3707
- if (projectA.framework === "unknown" && projectB.framework !== "unknown")
3708
- return 1;
3709
- if (projectA.framework !== "unknown" && projectB.framework === "unknown")
3710
- return -1;
3711
- return 0;
3712
- }
3713
- );
3714
- const { selectedProject } = await prompts({
3715
- type: "select",
3716
- name: "selectedProject",
3717
- message: "Select a project to install React Grab:",
3718
- choices: [
3719
- ...sortedProjects.map((project) => {
3720
- const frameworkLabel = project.framework !== "unknown" ? ` ${highlighter.dim(`(${FRAMEWORK_NAMES[project.framework]})`)}` : "";
3721
- return {
3722
- title: `${project.name}${frameworkLabel}`,
3723
- value: project.path
3724
- };
3725
- }),
3726
- { title: "Skip", value: "skip" }
3727
- ]
3728
- });
3729
- if (!selectedProject || selectedProject === "skip") {
3730
- logger.break();
3731
- process.exit(0);
3732
- }
3733
- process.chdir(selectedProject);
3734
- const newProjectInfo = await detectProject(selectedProject);
3735
- Object.assign(projectInfo, newProjectInfo);
3736
- const newFrameworkSpinner = spinner("Verifying framework.").start();
3737
- newFrameworkSpinner.succeed(
3738
- `Verifying framework. Found ${highlighter.info(FRAMEWORK_NAMES[newProjectInfo.framework])}.`
3739
- );
3740
- } else {
3741
- frameworkSpinner.fail("Could not detect a supported framework.");
3742
- logger.break();
3743
- logger.log(`Visit ${highlighter.info(DOCS_URL)} for manual setup.`);
3744
- logger.break();
3747
+ const sortedProjects = sortProjectsByFramework(reactProjects);
3748
+ if (isNonInteractive) {
3749
+ printSubprojects(searchRoot, sortedProjects);
3745
3750
  process.exit(1);
3746
3751
  }
3752
+ logger.break();
3753
+ const { selectedProject } = await prompts({
3754
+ type: "select",
3755
+ name: "selectedProject",
3756
+ message: "Select a project to install React Grab:",
3757
+ choices: [
3758
+ ...sortedProjects.map((project) => {
3759
+ const frameworkLabel = project.framework !== "unknown" ? ` ${highlighter.dim(`(${FRAMEWORK_NAMES[project.framework]})`)}` : "";
3760
+ return {
3761
+ title: `${project.name}${frameworkLabel}`,
3762
+ value: project.path
3763
+ };
3764
+ }),
3765
+ { title: "Skip", value: "skip" }
3766
+ ]
3767
+ });
3768
+ if (!selectedProject || selectedProject === "skip") {
3769
+ logger.break();
3770
+ process.exit(0);
3771
+ }
3772
+ process.chdir(selectedProject);
3773
+ const newProjectInfo = await detectProject(selectedProject);
3774
+ Object.assign(projectInfo, newProjectInfo);
3775
+ const newFrameworkSpinner = spinner("Verifying framework.").start();
3776
+ newFrameworkSpinner.succeed(
3777
+ `Verifying framework. Found ${highlighter.info(FRAMEWORK_NAMES[newProjectInfo.framework])}.`
3778
+ );
3747
3779
  } else {
3748
3780
  frameworkSpinner.fail("Could not detect a supported framework.");
3749
3781
  logger.break();
@@ -3933,7 +3965,7 @@ var init = new Command().name("init").description("initialize React Grab in your
3933
3965
  reportToCli("error", void 0, error);
3934
3966
  }
3935
3967
  });
3936
- var VERSION4 = "0.1.14";
3968
+ var VERSION4 = "0.1.16";
3937
3969
  var remove = new Command().name("remove").description("disconnect React Grab from your agent").argument("[agent]", `agent to disconnect (${AGENTS.join(", ")}, mcp)`).option("-y, --yes", "skip confirmation prompts", false).option(
3938
3970
  "-c, --cwd <cwd>",
3939
3971
  "working directory (defaults to current directory)",
@@ -3945,7 +3977,7 @@ var remove = new Command().name("remove").description("disconnect React Grab fro
3945
3977
  console.log();
3946
3978
  try {
3947
3979
  const cwd = opts.cwd;
3948
- const isNonInteractive = opts.yes;
3980
+ const isNonInteractive = detectNonInteractive(opts.yes);
3949
3981
  const preflightSpinner = spinner("Preflight checks.").start();
3950
3982
  const projectInfo = await detectProject(cwd);
3951
3983
  if (!projectInfo.hasReactGrab) {
@@ -4109,7 +4141,7 @@ var remove = new Command().name("remove").description("disconnect React Grab fro
4109
4141
  });
4110
4142
 
4111
4143
  // src/cli.ts
4112
- var VERSION5 = "0.1.14";
4144
+ var VERSION5 = "0.1.16";
4113
4145
  var VERSION_API_URL = "https://www.react-grab.com/api/version";
4114
4146
  process.on("SIGINT", () => process.exit(0));
4115
4147
  process.on("SIGTERM", () => process.exit(0));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@react-grab/cli",
3
- "version": "0.1.14",
3
+ "version": "0.1.16",
4
4
  "bin": {
5
5
  "react-grab": "./dist/cli.js"
6
6
  },
@@ -18,9 +18,12 @@
18
18
  "dependencies": {
19
19
  "@antfu/ni": "^0.23.0",
20
20
  "commander": "^14.0.0",
21
+ "ignore": "^7.0.5",
22
+ "jsonc-parser": "^3.3.1",
21
23
  "ora": "^8.2.0",
22
24
  "picocolors": "^1.1.1",
23
- "prompts": "^2.4.2"
25
+ "prompts": "^2.4.2",
26
+ "smol-toml": "^1.6.0"
24
27
  },
25
28
  "devDependencies": {
26
29
  "@types/prompts": "^2.4.9",