@react-grab/cli 0.1.46 → 0.1.47-dev.bc3a591
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/README.md +88 -0
- package/dist/api.cjs +70 -0
- package/dist/api.d.cts +121 -0
- package/dist/api.d.cts.map +1 -0
- package/dist/api.d.ts +121 -0
- package/dist/api.d.ts.map +1 -0
- package/dist/api.js +53 -0
- package/dist/api.js.map +1 -0
- package/dist/cli.cjs +42 -911
- package/dist/cli.js +19 -865
- package/dist/cli.js.map +1 -1
- package/dist/install-Car5vKPk.js +864 -0
- package/dist/install-Car5vKPk.js.map +1 -0
- package/dist/install-DlD3sLtm.cjs +1004 -0
- package/package.json +6 -1
package/dist/cli.js
CHANGED
|
@@ -1,15 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { a as previewCdnTransform, c as agentLabel, d as detectAvailableAgents, h as detectProject, i as hasFrameworkEntryPoint, l as installSkill, n as installPackages, o as previewOptionsTransform, r as applyTransform, s as previewTransform, t as getPackagesToInstall, u as removeSkill, y as findReactProjects } from "./install-Car5vKPk.js";
|
|
2
3
|
import { Command } from "commander";
|
|
3
|
-
import path, {
|
|
4
|
+
import path, { join, relative, resolve } from "node:path";
|
|
4
5
|
import pc from "picocolors";
|
|
5
|
-
import fs, {
|
|
6
|
-
import { detect } from "package-manager-detector/detect";
|
|
7
|
-
import ignore from "ignore";
|
|
6
|
+
import fs, { existsSync, readFileSync } from "node:fs";
|
|
8
7
|
import { fileURLToPath } from "node:url";
|
|
9
|
-
import { add, detectInstalledSkillAgents, getCanonicalSkillsDir, getSkillAgentConfig, getSkillAgentDir, getSkillAgentTypes, isUniversalSkillAgent } from "agent-install/skill";
|
|
10
8
|
import basePrompts from "prompts";
|
|
11
9
|
import ora from "ora";
|
|
12
|
-
import { x } from "tinyexec";
|
|
13
10
|
import { spawn, spawnSync } from "node:child_process";
|
|
14
11
|
import { createHash } from "node:crypto";
|
|
15
12
|
//#region src/utils/is-non-interactive.ts
|
|
@@ -25,353 +22,6 @@ const AGENT_ENVIRONMENT_VARIABLES = [
|
|
|
25
22
|
const isEnvironmentVariableSet = (variable) => Boolean(process.env[variable]);
|
|
26
23
|
const detectNonInteractive = (yesFlag) => yesFlag || AGENT_ENVIRONMENT_VARIABLES.some(isEnvironmentVariableSet) || !process.stdin.isTTY;
|
|
27
24
|
//#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
|
|
93
|
-
//#region src/utils/detect.ts
|
|
94
|
-
const VALID_PACKAGE_MANAGERS = new Set([
|
|
95
|
-
"npm",
|
|
96
|
-
"yarn",
|
|
97
|
-
"pnpm",
|
|
98
|
-
"bun"
|
|
99
|
-
]);
|
|
100
|
-
const detectPackageManager = async (projectRoot) => {
|
|
101
|
-
const result = await detect({ cwd: projectRoot });
|
|
102
|
-
if (result?.agent) {
|
|
103
|
-
const managerName = result.agent.split("@")[0];
|
|
104
|
-
if (VALID_PACKAGE_MANAGERS.has(managerName)) return managerName;
|
|
105
|
-
}
|
|
106
|
-
return "npm";
|
|
107
|
-
};
|
|
108
|
-
const CONFIG_EXTENSIONS = [
|
|
109
|
-
"ts",
|
|
110
|
-
"mts",
|
|
111
|
-
"cts",
|
|
112
|
-
"js",
|
|
113
|
-
"mjs",
|
|
114
|
-
"cjs"
|
|
115
|
-
];
|
|
116
|
-
const hasConfigFile = (projectRoot, configBaseName) => CONFIG_EXTENSIONS.some((extension) => existsSync(join(projectRoot, `${configBaseName}.${extension}`)));
|
|
117
|
-
const readMergedDependencies = (projectRoot) => {
|
|
118
|
-
const packageJsonPath = join(projectRoot, "package.json");
|
|
119
|
-
if (!existsSync(packageJsonPath)) return null;
|
|
120
|
-
try {
|
|
121
|
-
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
122
|
-
return {
|
|
123
|
-
...packageJson.dependencies,
|
|
124
|
-
...packageJson.devDependencies
|
|
125
|
-
};
|
|
126
|
-
} catch {
|
|
127
|
-
return null;
|
|
128
|
-
}
|
|
129
|
-
};
|
|
130
|
-
const detectFrameworkFromDependencies = (dependencies) => {
|
|
131
|
-
if (!dependencies) return "unknown";
|
|
132
|
-
if (dependencies["next"]) return "next";
|
|
133
|
-
if (dependencies["@tanstack/react-start"]) return "tanstack";
|
|
134
|
-
if (dependencies["vite"]) return "vite";
|
|
135
|
-
if (dependencies["webpack"]) return "webpack";
|
|
136
|
-
return "unknown";
|
|
137
|
-
};
|
|
138
|
-
const detectFrameworkFromConfigFiles = (projectRoot) => {
|
|
139
|
-
if (hasConfigFile(projectRoot, "next.config")) return "next";
|
|
140
|
-
if (hasConfigFile(projectRoot, "app.config")) return "tanstack";
|
|
141
|
-
if (hasConfigFile(projectRoot, "vite.config")) return "vite";
|
|
142
|
-
if (hasConfigFile(projectRoot, "webpack.config")) return "webpack";
|
|
143
|
-
return "unknown";
|
|
144
|
-
};
|
|
145
|
-
const findEnclosingMonorepoRoot = (projectRoot) => {
|
|
146
|
-
let currentDirectory = dirname(projectRoot);
|
|
147
|
-
while (currentDirectory !== dirname(currentDirectory)) {
|
|
148
|
-
if (detectMonorepo(currentDirectory)) return currentDirectory;
|
|
149
|
-
currentDirectory = dirname(currentDirectory);
|
|
150
|
-
}
|
|
151
|
-
return null;
|
|
152
|
-
};
|
|
153
|
-
const detectFramework = (projectRoot) => {
|
|
154
|
-
const localFramework = detectFrameworkFromDependencies(readMergedDependencies(projectRoot));
|
|
155
|
-
if (localFramework !== "unknown") return localFramework;
|
|
156
|
-
return detectFrameworkFromConfigFiles(projectRoot);
|
|
157
|
-
};
|
|
158
|
-
const detectFrameworkFromMonorepoRoot = (projectRoot) => {
|
|
159
|
-
const monorepoRoot = findEnclosingMonorepoRoot(projectRoot);
|
|
160
|
-
if (!monorepoRoot) return "unknown";
|
|
161
|
-
return detectFrameworkFromDependencies(readMergedDependencies(monorepoRoot));
|
|
162
|
-
};
|
|
163
|
-
const detectNextRouterType = (projectRoot) => {
|
|
164
|
-
const hasAppDir = existsSync(join(projectRoot, "app"));
|
|
165
|
-
const hasSrcAppDir = existsSync(join(projectRoot, "src", "app"));
|
|
166
|
-
const hasPagesDir = existsSync(join(projectRoot, "pages"));
|
|
167
|
-
const hasSrcPagesDir = existsSync(join(projectRoot, "src", "pages"));
|
|
168
|
-
if (hasAppDir || hasSrcAppDir) return "app";
|
|
169
|
-
if (hasPagesDir || hasSrcPagesDir) return "pages";
|
|
170
|
-
return "unknown";
|
|
171
|
-
};
|
|
172
|
-
const detectMonorepo = (projectRoot) => {
|
|
173
|
-
if (existsSync(join(projectRoot, "pnpm-workspace.yaml"))) return true;
|
|
174
|
-
if (existsSync(join(projectRoot, "lerna.json"))) return true;
|
|
175
|
-
const packageJsonPath = join(projectRoot, "package.json");
|
|
176
|
-
if (existsSync(packageJsonPath)) try {
|
|
177
|
-
if (JSON.parse(readFileSync(packageJsonPath, "utf-8")).workspaces) return true;
|
|
178
|
-
} catch {
|
|
179
|
-
return false;
|
|
180
|
-
}
|
|
181
|
-
return false;
|
|
182
|
-
};
|
|
183
|
-
const getWorkspacePatterns = (projectRoot) => {
|
|
184
|
-
const patterns = [];
|
|
185
|
-
const pnpmWorkspacePath = join(projectRoot, "pnpm-workspace.yaml");
|
|
186
|
-
if (existsSync(pnpmWorkspacePath)) {
|
|
187
|
-
const lines = readFileSync(pnpmWorkspacePath, "utf-8").split("\n");
|
|
188
|
-
let inPackages = false;
|
|
189
|
-
for (const line of lines) {
|
|
190
|
-
if (line.match(/^packages:\s*$/)) {
|
|
191
|
-
inPackages = true;
|
|
192
|
-
continue;
|
|
193
|
-
}
|
|
194
|
-
if (inPackages) {
|
|
195
|
-
if (line.match(/^[a-zA-Z]/) || line.trim() === "") {
|
|
196
|
-
if (line.match(/^[a-zA-Z]/)) inPackages = false;
|
|
197
|
-
continue;
|
|
198
|
-
}
|
|
199
|
-
const match = line.match(/^\s*-\s*['"]?([^'"#\n]+?)['"]?\s*$/);
|
|
200
|
-
if (match) patterns.push(match[1].trim());
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
const lernaJsonPath = join(projectRoot, "lerna.json");
|
|
205
|
-
if (existsSync(lernaJsonPath)) try {
|
|
206
|
-
const lernaJson = JSON.parse(readFileSync(lernaJsonPath, "utf-8"));
|
|
207
|
-
if (Array.isArray(lernaJson.packages)) patterns.push(...lernaJson.packages);
|
|
208
|
-
} catch {}
|
|
209
|
-
const packageJsonPath = join(projectRoot, "package.json");
|
|
210
|
-
if (existsSync(packageJsonPath)) try {
|
|
211
|
-
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
212
|
-
if (Array.isArray(packageJson.workspaces)) patterns.push(...packageJson.workspaces);
|
|
213
|
-
else if (packageJson.workspaces?.packages) patterns.push(...packageJson.workspaces.packages);
|
|
214
|
-
} catch {}
|
|
215
|
-
return [...new Set(patterns)];
|
|
216
|
-
};
|
|
217
|
-
const expandWorkspacePattern = (projectRoot, pattern) => {
|
|
218
|
-
const isGlob = pattern.endsWith("/*");
|
|
219
|
-
const basePath = join(projectRoot, pattern.replace(/\/\*$/, ""));
|
|
220
|
-
if (!existsSync(basePath)) return [];
|
|
221
|
-
if (!isGlob) return existsSync(join(basePath, "package.json")) ? [basePath] : [];
|
|
222
|
-
const results = [];
|
|
223
|
-
try {
|
|
224
|
-
const entries = readdirSync(basePath, { withFileTypes: true });
|
|
225
|
-
for (const entry of entries) {
|
|
226
|
-
if (!entry.isDirectory()) continue;
|
|
227
|
-
if (existsSync(join(basePath, entry.name, "package.json"))) results.push(join(basePath, entry.name));
|
|
228
|
-
}
|
|
229
|
-
} catch {
|
|
230
|
-
return results;
|
|
231
|
-
}
|
|
232
|
-
return results;
|
|
233
|
-
};
|
|
234
|
-
const hasReactDependency = (projectPath) => {
|
|
235
|
-
const dependencies = readMergedDependencies(projectPath);
|
|
236
|
-
if (!dependencies) return false;
|
|
237
|
-
return Boolean(dependencies["react"] || dependencies["react-dom"]);
|
|
238
|
-
};
|
|
239
|
-
const buildReactProject = (projectPath) => {
|
|
240
|
-
const framework = detectFramework(projectPath);
|
|
241
|
-
if (!hasReactDependency(projectPath) && framework === "unknown") return null;
|
|
242
|
-
let name = basename(projectPath);
|
|
243
|
-
const packageJsonPath = join(projectPath, "package.json");
|
|
244
|
-
try {
|
|
245
|
-
name = JSON.parse(readFileSync(packageJsonPath, "utf-8")).name || name;
|
|
246
|
-
} catch {}
|
|
247
|
-
return {
|
|
248
|
-
name,
|
|
249
|
-
path: projectPath,
|
|
250
|
-
framework
|
|
251
|
-
};
|
|
252
|
-
};
|
|
253
|
-
const findWorkspaceProjects = (projectRoot) => {
|
|
254
|
-
const patterns = getWorkspacePatterns(projectRoot);
|
|
255
|
-
const projects = [];
|
|
256
|
-
for (const pattern of patterns) for (const projectPath of expandWorkspacePattern(projectRoot, pattern)) {
|
|
257
|
-
const project = buildReactProject(projectPath);
|
|
258
|
-
if (project) projects.push(project);
|
|
259
|
-
}
|
|
260
|
-
return projects;
|
|
261
|
-
};
|
|
262
|
-
const ALWAYS_IGNORED_DIRECTORIES = [
|
|
263
|
-
"node_modules",
|
|
264
|
-
".git",
|
|
265
|
-
".next",
|
|
266
|
-
".cache",
|
|
267
|
-
".turbo",
|
|
268
|
-
"dist",
|
|
269
|
-
"build",
|
|
270
|
-
"coverage",
|
|
271
|
-
"test-results"
|
|
272
|
-
];
|
|
273
|
-
const loadGitignore = (projectRoot) => {
|
|
274
|
-
const ignorer = ignore().add(ALWAYS_IGNORED_DIRECTORIES);
|
|
275
|
-
const gitignorePath = join(projectRoot, ".gitignore");
|
|
276
|
-
if (existsSync(gitignorePath)) try {
|
|
277
|
-
ignorer.add(readFileSync(gitignorePath, "utf-8"));
|
|
278
|
-
} catch {}
|
|
279
|
-
return ignorer;
|
|
280
|
-
};
|
|
281
|
-
const scanDirectoryForProjects = (rootDirectory, ignorer, maxDepth, currentDepth = 0) => {
|
|
282
|
-
if (currentDepth >= maxDepth) return [];
|
|
283
|
-
if (!existsSync(rootDirectory)) return [];
|
|
284
|
-
const projects = [];
|
|
285
|
-
try {
|
|
286
|
-
const entries = readdirSync(rootDirectory, { withFileTypes: true });
|
|
287
|
-
for (const entry of entries) {
|
|
288
|
-
if (!entry.isDirectory()) continue;
|
|
289
|
-
if (ignorer.ignores(entry.name)) continue;
|
|
290
|
-
const entryPath = join(rootDirectory, entry.name);
|
|
291
|
-
if (existsSync(join(entryPath, "package.json"))) {
|
|
292
|
-
const project = buildReactProject(entryPath);
|
|
293
|
-
if (project) {
|
|
294
|
-
projects.push(project);
|
|
295
|
-
continue;
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
projects.push(...scanDirectoryForProjects(entryPath, ignorer, maxDepth, currentDepth + 1));
|
|
299
|
-
}
|
|
300
|
-
} catch {
|
|
301
|
-
return projects;
|
|
302
|
-
}
|
|
303
|
-
return projects;
|
|
304
|
-
};
|
|
305
|
-
const MAX_SCAN_DEPTH = 2;
|
|
306
|
-
const normalizePathForComparison = (filePath) => filePath.replace(/\\/g, "/");
|
|
307
|
-
const findReactProjects = (projectRoot) => {
|
|
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;
|
|
314
|
-
}
|
|
315
|
-
const scannedProjects = scanDirectoryForProjects(projectRoot, loadGitignore(projectRoot), MAX_SCAN_DEPTH);
|
|
316
|
-
if (scannedProjects.length > 0) return scannedProjects;
|
|
317
|
-
let currentDirectory = dirname(projectRoot);
|
|
318
|
-
while (currentDirectory !== dirname(currentDirectory)) {
|
|
319
|
-
const parentProject = buildReactProject(currentDirectory);
|
|
320
|
-
if (parentProject) return [parentProject];
|
|
321
|
-
currentDirectory = dirname(currentDirectory);
|
|
322
|
-
}
|
|
323
|
-
return [];
|
|
324
|
-
};
|
|
325
|
-
const hasReactGrabSetupInFile = (filePath) => {
|
|
326
|
-
if (!existsSync(filePath)) return false;
|
|
327
|
-
try {
|
|
328
|
-
return hasReactGrabSetupCode(readFileSync(filePath, "utf-8"));
|
|
329
|
-
} catch {
|
|
330
|
-
return false;
|
|
331
|
-
}
|
|
332
|
-
};
|
|
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);
|
|
339
|
-
};
|
|
340
|
-
const detectUnsupportedFramework = (projectRoot) => {
|
|
341
|
-
const dependencies = readMergedDependencies(projectRoot);
|
|
342
|
-
if (!dependencies) return null;
|
|
343
|
-
if (dependencies["@remix-run/react"] || dependencies["remix"]) return "remix";
|
|
344
|
-
if (dependencies["astro"]) return "astro";
|
|
345
|
-
if (dependencies["@sveltejs/kit"]) return "sveltekit";
|
|
346
|
-
if (dependencies["gatsby"]) return "gatsby";
|
|
347
|
-
return null;
|
|
348
|
-
};
|
|
349
|
-
const detectReactGrabVersion = (projectRoot) => {
|
|
350
|
-
const installedPackageJsonPath = join(projectRoot, "node_modules", "react-grab", "package.json");
|
|
351
|
-
if (existsSync(installedPackageJsonPath)) try {
|
|
352
|
-
return JSON.parse(readFileSync(installedPackageJsonPath, "utf-8")).version ?? null;
|
|
353
|
-
} catch {}
|
|
354
|
-
return null;
|
|
355
|
-
};
|
|
356
|
-
const detectProject = async (projectRoot = process.cwd()) => {
|
|
357
|
-
const localFramework = detectFramework(projectRoot);
|
|
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);
|
|
362
|
-
return {
|
|
363
|
-
packageManager,
|
|
364
|
-
framework,
|
|
365
|
-
nextRouterType: framework === "next" ? detectNextRouterType(projectRoot) : "unknown",
|
|
366
|
-
isMonorepo,
|
|
367
|
-
projectRoot,
|
|
368
|
-
hasReactGrab: detectReactGrabDependency(projectRoot) || isReactGrabConfigured,
|
|
369
|
-
isReactGrabConfigured,
|
|
370
|
-
reactGrabVersion: detectReactGrabVersion(projectRoot),
|
|
371
|
-
unsupportedFramework: detectUnsupportedFramework(projectRoot)
|
|
372
|
-
};
|
|
373
|
-
};
|
|
374
|
-
//#endregion
|
|
375
25
|
//#region src/utils/highlighter.ts
|
|
376
26
|
const highlighter = {
|
|
377
27
|
error: pc.red,
|
|
@@ -411,39 +61,6 @@ const handleError = (error) => {
|
|
|
411
61
|
process.exit(1);
|
|
412
62
|
};
|
|
413
63
|
//#endregion
|
|
414
|
-
//#region src/utils/detect-agents.ts
|
|
415
|
-
const PATH_BINARIES = {
|
|
416
|
-
"claude-code": ["claude"],
|
|
417
|
-
codex: ["codex"],
|
|
418
|
-
cursor: ["cursor", "cursor-agent"],
|
|
419
|
-
droid: ["droid"],
|
|
420
|
-
"gemini-cli": ["gemini"],
|
|
421
|
-
"github-copilot": ["copilot"],
|
|
422
|
-
opencode: ["opencode"],
|
|
423
|
-
pi: ["pi", "omegon"]
|
|
424
|
-
};
|
|
425
|
-
const isCommandAvailable = (command) => {
|
|
426
|
-
const pathDirectories = (process.env.PATH ?? "").split(delimiter).filter(Boolean);
|
|
427
|
-
for (const directory of pathDirectories) {
|
|
428
|
-
const binaryPath = join(directory, command);
|
|
429
|
-
try {
|
|
430
|
-
if (statSync(binaryPath).isFile()) {
|
|
431
|
-
accessSync(binaryPath, constants.X_OK);
|
|
432
|
-
return true;
|
|
433
|
-
}
|
|
434
|
-
} catch {}
|
|
435
|
-
}
|
|
436
|
-
return false;
|
|
437
|
-
};
|
|
438
|
-
const detectAvailableAgents = async () => {
|
|
439
|
-
const installedAgents = new Set(await detectInstalledSkillAgents());
|
|
440
|
-
return getSkillAgentTypes().filter((agent) => {
|
|
441
|
-
if (agent === "universal") return false;
|
|
442
|
-
if (installedAgents.has(agent)) return true;
|
|
443
|
-
return PATH_BINARIES[agent]?.some(isCommandAvailable) ?? false;
|
|
444
|
-
});
|
|
445
|
-
};
|
|
446
|
-
//#endregion
|
|
447
64
|
//#region src/utils/unref-stdin.ts
|
|
448
65
|
const unrefStdin = () => {
|
|
449
66
|
if (process.stdin.isTTY) return;
|
|
@@ -464,14 +81,7 @@ const prompts = (questions) => {
|
|
|
464
81
|
//#region src/utils/spinner.ts
|
|
465
82
|
const spinner = (text) => ora({ text });
|
|
466
83
|
//#endregion
|
|
467
|
-
//#region src/utils/
|
|
468
|
-
const SKILL_NAME = "react-grab";
|
|
469
|
-
const SKILL_SOURCE = fileURLToPath(new URL("../skills/react-grab", import.meta.url));
|
|
470
|
-
const agentLabel = (agent) => getSkillAgentConfig(agent).displayName;
|
|
471
|
-
const installedSkillDir = (agent, global, cwd) => join(isUniversalSkillAgent(agent) ? getCanonicalSkillsDir(global, cwd) : getSkillAgentDir(agent, {
|
|
472
|
-
global,
|
|
473
|
-
cwd
|
|
474
|
-
}), SKILL_NAME);
|
|
84
|
+
//#region src/utils/prompt-skill-install.ts
|
|
475
85
|
const promptSkillInstall = async ({ yes = false, global = false, cwd = process.cwd() } = {}) => {
|
|
476
86
|
const detectedAgents = await detectAvailableAgents();
|
|
477
87
|
if (detectedAgents.length === 0) {
|
|
@@ -503,12 +113,10 @@ const promptSkillInstall = async ({ yes = false, global = false, cwd = process.c
|
|
|
503
113
|
if (selectedAgents.length === 0) return false;
|
|
504
114
|
}
|
|
505
115
|
const installSpinner = spinner("Installing React Grab skill.").start();
|
|
506
|
-
const { installed, failed } = await
|
|
507
|
-
source: SKILL_SOURCE,
|
|
116
|
+
const { installed, failed } = await installSkill({
|
|
508
117
|
agents: selectedAgents,
|
|
509
118
|
global,
|
|
510
|
-
cwd
|
|
511
|
-
mode: "copy"
|
|
119
|
+
cwd
|
|
512
120
|
});
|
|
513
121
|
if (installed.length === 0) {
|
|
514
122
|
installSpinner.fail("Failed to install React Grab skill.");
|
|
@@ -518,27 +126,10 @@ const promptSkillInstall = async ({ yes = false, global = false, cwd = process.c
|
|
|
518
126
|
for (const record of failed) logger.log(` ${highlighter.error("✗")} ${agentLabel(record.agent)} ${record.error}`);
|
|
519
127
|
return true;
|
|
520
128
|
};
|
|
521
|
-
const removeSkill = async ({ cwd = process.cwd(), global = false } = {}) => {
|
|
522
|
-
const agents = await detectAvailableAgents();
|
|
523
|
-
const removedAgents = [];
|
|
524
|
-
const dirsToRemove = /* @__PURE__ */ new Set();
|
|
525
|
-
for (const agent of agents) {
|
|
526
|
-
const skillDir = installedSkillDir(agent, global, cwd);
|
|
527
|
-
if (!existsSync(skillDir)) continue;
|
|
528
|
-
removedAgents.push(agent);
|
|
529
|
-
dirsToRemove.add(skillDir);
|
|
530
|
-
}
|
|
531
|
-
for (const skillDir of dirsToRemove) rmSync(skillDir, {
|
|
532
|
-
recursive: true,
|
|
533
|
-
force: true
|
|
534
|
-
});
|
|
535
|
-
for (const agent of removedAgents) logger.log(` ${highlighter.success("✓")} ${agentLabel(agent)}`);
|
|
536
|
-
return removedAgents.length;
|
|
537
|
-
};
|
|
538
129
|
//#endregion
|
|
539
130
|
//#region src/commands/add.ts
|
|
540
|
-
const VERSION$5 = "0.1.
|
|
541
|
-
const add
|
|
131
|
+
const VERSION$5 = "0.1.47-dev.bc3a591";
|
|
132
|
+
const add = 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) => {
|
|
542
133
|
console.log(`${pc.magenta("✿")} ${pc.bold("React Grab")} ${pc.gray(VERSION$5)}`);
|
|
543
134
|
console.log();
|
|
544
135
|
try {
|
|
@@ -670,412 +261,6 @@ const printDiff = (filePath, originalContent, newContent) => {
|
|
|
670
261
|
console.log("─".repeat(60));
|
|
671
262
|
};
|
|
672
263
|
//#endregion
|
|
673
|
-
//#region src/utils/templates.ts
|
|
674
|
-
const NEXT_APP_ROUTER_SCRIPT = `{process.env.NODE_ENV === "development" && (
|
|
675
|
-
<Script
|
|
676
|
-
src="//unpkg.com/react-grab/dist/index.global.js"
|
|
677
|
-
crossOrigin="anonymous"
|
|
678
|
-
strategy="beforeInteractive"
|
|
679
|
-
/>
|
|
680
|
-
)}`;
|
|
681
|
-
const VITE_IMPORT = `if (import.meta.env.DEV) {
|
|
682
|
-
import("react-grab");
|
|
683
|
-
}`;
|
|
684
|
-
const WEBPACK_IMPORT = `if (process.env.NODE_ENV === "development") {
|
|
685
|
-
import("react-grab");
|
|
686
|
-
}`;
|
|
687
|
-
const TANSTACK_EFFECT = `useEffect(() => {
|
|
688
|
-
if (import.meta.env.DEV) {
|
|
689
|
-
void import("react-grab");
|
|
690
|
-
}
|
|
691
|
-
}, []);`;
|
|
692
|
-
const SCRIPT_IMPORT = "import Script from \"next/script\";";
|
|
693
|
-
//#endregion
|
|
694
|
-
//#region src/utils/transform.ts
|
|
695
|
-
const hasReactGrabInInstrumentation = (projectRoot) => {
|
|
696
|
-
return findFileWithReactGrabSetup(getInstrumentationFileCandidates(projectRoot)) !== null;
|
|
697
|
-
};
|
|
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
|
-
}
|
|
703
|
-
return null;
|
|
704
|
-
};
|
|
705
|
-
const alreadyConfiguredResult = (filePath) => ({
|
|
706
|
-
success: true,
|
|
707
|
-
filePath,
|
|
708
|
-
message: "React Grab is already configured",
|
|
709
|
-
noChanges: true
|
|
710
|
-
});
|
|
711
|
-
const transformNextAppRouter = (projectRoot, reactGrabAlreadyConfigured) => {
|
|
712
|
-
const layoutPath = findLayoutFile(projectRoot);
|
|
713
|
-
if (!layoutPath) return {
|
|
714
|
-
success: false,
|
|
715
|
-
filePath: "",
|
|
716
|
-
message: "Could not find app/layout.tsx, app/layout.jsx, app/layout.ts, or app/layout.js"
|
|
717
|
-
};
|
|
718
|
-
const originalContent = readFileSync(layoutPath, "utf-8");
|
|
719
|
-
let newContent = originalContent;
|
|
720
|
-
const hasReactGrabInFile = hasReactGrabSetupCode(originalContent);
|
|
721
|
-
const hasReactGrabInInstrumentationFile = hasReactGrabInInstrumentation(projectRoot);
|
|
722
|
-
if (hasReactGrabInFile && reactGrabAlreadyConfigured) return alreadyConfiguredResult(layoutPath);
|
|
723
|
-
if (hasReactGrabInFile || hasReactGrabInInstrumentationFile) return {
|
|
724
|
-
success: true,
|
|
725
|
-
filePath: layoutPath,
|
|
726
|
-
message: "React Grab is already installed" + (hasReactGrabInInstrumentationFile ? " in instrumentation-client" : " in this file"),
|
|
727
|
-
noChanges: true
|
|
728
|
-
};
|
|
729
|
-
if (!newContent.includes("import Script from \"next/script\"")) {
|
|
730
|
-
const importMatch = newContent.match(/^import .+ from ['"].+['"];?\s*$/m);
|
|
731
|
-
if (importMatch) newContent = newContent.replace(importMatch[0], `${importMatch[0]}\n${SCRIPT_IMPORT}`);
|
|
732
|
-
else newContent = `${SCRIPT_IMPORT}\n\n${newContent}`;
|
|
733
|
-
}
|
|
734
|
-
const headMatch = newContent.match(/<head[^>]*>/);
|
|
735
|
-
if (headMatch) newContent = newContent.replace(headMatch[0], `${headMatch[0]}\n ${NEXT_APP_ROUTER_SCRIPT}`);
|
|
736
|
-
else {
|
|
737
|
-
const htmlMatch = newContent.match(/<html[^>]*>/);
|
|
738
|
-
if (htmlMatch) newContent = newContent.replace(htmlMatch[0], `${htmlMatch[0]}\n <head>\n ${NEXT_APP_ROUTER_SCRIPT}\n </head>`);
|
|
739
|
-
}
|
|
740
|
-
return {
|
|
741
|
-
success: true,
|
|
742
|
-
filePath: layoutPath,
|
|
743
|
-
message: "Add React Grab",
|
|
744
|
-
originalContent,
|
|
745
|
-
newContent
|
|
746
|
-
};
|
|
747
|
-
};
|
|
748
|
-
const transformNextPagesRouter = (projectRoot, reactGrabAlreadyConfigured) => {
|
|
749
|
-
const documentPath = findDocumentFile(projectRoot);
|
|
750
|
-
if (!documentPath) return {
|
|
751
|
-
success: false,
|
|
752
|
-
filePath: "",
|
|
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 }"
|
|
754
|
-
};
|
|
755
|
-
const originalContent = readFileSync(documentPath, "utf-8");
|
|
756
|
-
let newContent = originalContent;
|
|
757
|
-
const hasReactGrabInFile = hasReactGrabSetupCode(originalContent);
|
|
758
|
-
const hasReactGrabInInstrumentationFile = hasReactGrabInInstrumentation(projectRoot);
|
|
759
|
-
if (hasReactGrabInFile && reactGrabAlreadyConfigured) return alreadyConfiguredResult(documentPath);
|
|
760
|
-
if (hasReactGrabInFile || hasReactGrabInInstrumentationFile) return {
|
|
761
|
-
success: true,
|
|
762
|
-
filePath: documentPath,
|
|
763
|
-
message: "React Grab is already installed" + (hasReactGrabInInstrumentationFile ? " in instrumentation-client" : " in this file"),
|
|
764
|
-
noChanges: true
|
|
765
|
-
};
|
|
766
|
-
if (!newContent.includes("import Script from \"next/script\"")) {
|
|
767
|
-
const importMatch = newContent.match(/^import .+ from ['"].+['"];?\s*$/m);
|
|
768
|
-
if (importMatch) newContent = newContent.replace(importMatch[0], `${importMatch[0]}\n${SCRIPT_IMPORT}`);
|
|
769
|
-
}
|
|
770
|
-
const headMatch = newContent.match(/<Head[^>]*>/);
|
|
771
|
-
if (headMatch) newContent = newContent.replace(headMatch[0], `${headMatch[0]}\n ${NEXT_APP_ROUTER_SCRIPT}`);
|
|
772
|
-
return {
|
|
773
|
-
success: true,
|
|
774
|
-
filePath: documentPath,
|
|
775
|
-
message: "Add React Grab",
|
|
776
|
-
originalContent,
|
|
777
|
-
newContent
|
|
778
|
-
};
|
|
779
|
-
};
|
|
780
|
-
const checkExistingInstallation = (filePath, reactGrabAlreadyConfigured) => {
|
|
781
|
-
if (!hasReactGrabSetupCode(readFileSync(filePath, "utf-8"))) return null;
|
|
782
|
-
return {
|
|
783
|
-
success: true,
|
|
784
|
-
filePath,
|
|
785
|
-
message: reactGrabAlreadyConfigured ? "React Grab is already configured" : "React Grab is already installed in this file",
|
|
786
|
-
noChanges: true
|
|
787
|
-
};
|
|
788
|
-
};
|
|
789
|
-
const transformVite = (projectRoot, reactGrabAlreadyConfigured) => {
|
|
790
|
-
const entryPath = findEntryFile(projectRoot);
|
|
791
|
-
const indexPath = findIndexHtml(projectRoot);
|
|
792
|
-
if (indexPath) {
|
|
793
|
-
const existingResult = checkExistingInstallation(indexPath, reactGrabAlreadyConfigured);
|
|
794
|
-
if (existingResult) return existingResult;
|
|
795
|
-
}
|
|
796
|
-
if (!entryPath) return {
|
|
797
|
-
success: false,
|
|
798
|
-
filePath: "",
|
|
799
|
-
message: "Could not find entry file (src/index.tsx, src/main.tsx, etc.)"
|
|
800
|
-
};
|
|
801
|
-
const existingResult = checkExistingInstallation(entryPath, reactGrabAlreadyConfigured);
|
|
802
|
-
if (existingResult) return existingResult;
|
|
803
|
-
const originalContent = readFileSync(entryPath, "utf-8");
|
|
804
|
-
return {
|
|
805
|
-
success: true,
|
|
806
|
-
filePath: entryPath,
|
|
807
|
-
message: "Add React Grab",
|
|
808
|
-
originalContent,
|
|
809
|
-
newContent: `${VITE_IMPORT}\n\n${originalContent}`
|
|
810
|
-
};
|
|
811
|
-
};
|
|
812
|
-
const transformWebpack = (projectRoot, reactGrabAlreadyConfigured) => {
|
|
813
|
-
const entryPath = findEntryFile(projectRoot);
|
|
814
|
-
if (!entryPath) return {
|
|
815
|
-
success: false,
|
|
816
|
-
filePath: "",
|
|
817
|
-
message: "Could not find entry file (src/index.tsx, src/main.tsx, etc.)"
|
|
818
|
-
};
|
|
819
|
-
const existingResult = checkExistingInstallation(entryPath, reactGrabAlreadyConfigured);
|
|
820
|
-
if (existingResult) return existingResult;
|
|
821
|
-
const originalContent = readFileSync(entryPath, "utf-8");
|
|
822
|
-
return {
|
|
823
|
-
success: true,
|
|
824
|
-
filePath: entryPath,
|
|
825
|
-
message: "Add React Grab",
|
|
826
|
-
originalContent,
|
|
827
|
-
newContent: `${WEBPACK_IMPORT}\n\n${originalContent}`
|
|
828
|
-
};
|
|
829
|
-
};
|
|
830
|
-
const transformTanStack = (projectRoot, reactGrabAlreadyConfigured) => {
|
|
831
|
-
const rootPath = findTanStackRootFile(projectRoot);
|
|
832
|
-
if (!rootPath) return {
|
|
833
|
-
success: false,
|
|
834
|
-
filePath: "",
|
|
835
|
-
message: "Could not find src/routes/__root.tsx or app/routes/__root.tsx.\n\nTo set up React Grab with TanStack Start, add this to your root route component:\n\n import { useEffect } from \"react\";\n\n useEffect(() => {\n if (import.meta.env.DEV) {\n void import(\"react-grab\");\n }\n }, []);"
|
|
836
|
-
};
|
|
837
|
-
const originalContent = readFileSync(rootPath, "utf-8");
|
|
838
|
-
let newContent = originalContent;
|
|
839
|
-
const hasReactGrabInFile = hasReactGrabSetupCode(originalContent);
|
|
840
|
-
if (hasReactGrabInFile && reactGrabAlreadyConfigured) return alreadyConfiguredResult(rootPath);
|
|
841
|
-
if (hasReactGrabInFile) return {
|
|
842
|
-
success: true,
|
|
843
|
-
filePath: rootPath,
|
|
844
|
-
message: "React Grab is already installed in this file",
|
|
845
|
-
noChanges: true
|
|
846
|
-
};
|
|
847
|
-
if (!/import\s+\{[^}]*useEffect[^}]*\}\s+from\s+["']react["']/.test(newContent)) {
|
|
848
|
-
const reactImportMatch = newContent.match(/import\s+\{([^}]*)\}\s+from\s+["']react["'];?/);
|
|
849
|
-
if (reactImportMatch) {
|
|
850
|
-
const existingImports = reactImportMatch[1];
|
|
851
|
-
newContent = newContent.replace(reactImportMatch[0], `import { ${existingImports.trim()}, useEffect } from "react";`);
|
|
852
|
-
} else {
|
|
853
|
-
const firstImportMatch = newContent.match(/^import .+ from ['"].+['"];?\s*$/m);
|
|
854
|
-
if (firstImportMatch) newContent = newContent.replace(firstImportMatch[0], `import { useEffect } from "react";\n${firstImportMatch[0]}`);
|
|
855
|
-
else newContent = `import { useEffect } from "react";\n\n${newContent}`;
|
|
856
|
-
}
|
|
857
|
-
}
|
|
858
|
-
const componentMatch = newContent.match(/function\s+(\w+)\s*\([^)]*\)\s*\{/);
|
|
859
|
-
if (componentMatch) {
|
|
860
|
-
const insertPosition = componentMatch.index + componentMatch[0].length;
|
|
861
|
-
newContent = newContent.slice(0, insertPosition) + `\n ${TANSTACK_EFFECT}\n` + newContent.slice(insertPosition);
|
|
862
|
-
} else return {
|
|
863
|
-
success: false,
|
|
864
|
-
filePath: rootPath,
|
|
865
|
-
message: "Could not find a component function in the root file"
|
|
866
|
-
};
|
|
867
|
-
return {
|
|
868
|
-
success: true,
|
|
869
|
-
filePath: rootPath,
|
|
870
|
-
message: "Add React Grab",
|
|
871
|
-
originalContent,
|
|
872
|
-
newContent
|
|
873
|
-
};
|
|
874
|
-
};
|
|
875
|
-
const hasFrameworkEntryPoint = (projectRoot, framework, nextRouterType) => {
|
|
876
|
-
switch (framework) {
|
|
877
|
-
case "next": return nextRouterType === "app" ? findLayoutFile(projectRoot) !== null : findDocumentFile(projectRoot) !== null;
|
|
878
|
-
case "vite":
|
|
879
|
-
case "webpack": return findEntryFile(projectRoot) !== null;
|
|
880
|
-
case "tanstack": return findTanStackRootFile(projectRoot) !== null;
|
|
881
|
-
default: return false;
|
|
882
|
-
}
|
|
883
|
-
};
|
|
884
|
-
const previewTransform = (projectRoot, framework, nextRouterType, reactGrabAlreadyConfigured = false) => {
|
|
885
|
-
switch (framework) {
|
|
886
|
-
case "next":
|
|
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);
|
|
892
|
-
default: return {
|
|
893
|
-
success: false,
|
|
894
|
-
filePath: "",
|
|
895
|
-
message: `Unknown framework: ${framework}. Please add React Grab manually.`
|
|
896
|
-
};
|
|
897
|
-
}
|
|
898
|
-
};
|
|
899
|
-
const canWriteToFile = (filePath) => {
|
|
900
|
-
try {
|
|
901
|
-
accessSync(filePath, constants.W_OK);
|
|
902
|
-
return true;
|
|
903
|
-
} catch {
|
|
904
|
-
return false;
|
|
905
|
-
}
|
|
906
|
-
};
|
|
907
|
-
const applyTransform = (result) => {
|
|
908
|
-
if (result.success && result.newContent && result.filePath) {
|
|
909
|
-
if (!canWriteToFile(result.filePath)) return {
|
|
910
|
-
success: false,
|
|
911
|
-
error: `Cannot write to ${result.filePath}. Check file permissions.`
|
|
912
|
-
};
|
|
913
|
-
try {
|
|
914
|
-
writeFileSync(result.filePath, result.newContent);
|
|
915
|
-
return { success: true };
|
|
916
|
-
} catch (error) {
|
|
917
|
-
return {
|
|
918
|
-
success: false,
|
|
919
|
-
error: `Failed to write to ${result.filePath}: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
920
|
-
};
|
|
921
|
-
}
|
|
922
|
-
}
|
|
923
|
-
return { success: true };
|
|
924
|
-
};
|
|
925
|
-
const formatOptionsForNextjs = (options) => {
|
|
926
|
-
const parts = [];
|
|
927
|
-
if (options.activationKey) parts.push(`activationKey: ${JSON.stringify(options.activationKey)}`);
|
|
928
|
-
if (options.activationMode) parts.push(`activationMode: "${options.activationMode}"`);
|
|
929
|
-
if (options.keyHoldDuration !== void 0) parts.push(`keyHoldDuration: ${options.keyHoldDuration}`);
|
|
930
|
-
if (options.allowActivationInsideInput !== void 0) parts.push(`allowActivationInsideInput: ${options.allowActivationInsideInput}`);
|
|
931
|
-
if (options.maxContextLines !== void 0) parts.push(`maxContextLines: ${options.maxContextLines}`);
|
|
932
|
-
return `{ ${parts.join(", ")} }`;
|
|
933
|
-
};
|
|
934
|
-
const formatOptionsAsJson = (options) => {
|
|
935
|
-
const cleanOptions = {};
|
|
936
|
-
if (options.activationKey) cleanOptions.activationKey = options.activationKey;
|
|
937
|
-
if (options.activationMode) cleanOptions.activationMode = options.activationMode;
|
|
938
|
-
if (options.keyHoldDuration !== void 0) cleanOptions.keyHoldDuration = options.keyHoldDuration;
|
|
939
|
-
if (options.allowActivationInsideInput !== void 0) cleanOptions.allowActivationInsideInput = options.allowActivationInsideInput;
|
|
940
|
-
if (options.maxContextLines !== void 0) cleanOptions.maxContextLines = options.maxContextLines;
|
|
941
|
-
return JSON.stringify(cleanOptions);
|
|
942
|
-
};
|
|
943
|
-
const findReactGrabFile = (projectRoot, framework, nextRouterType) => {
|
|
944
|
-
switch (framework) {
|
|
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
|
-
}
|
|
953
|
-
case "vite": {
|
|
954
|
-
const entryFile = findEntryFile(projectRoot);
|
|
955
|
-
const entrySetupFile = findFileWithReactGrabSetup(getEntryFileCandidates(projectRoot));
|
|
956
|
-
if (entrySetupFile) return entrySetupFile;
|
|
957
|
-
const indexHtml = findFileWithReactGrabSetup(getIndexHtmlCandidates(projectRoot));
|
|
958
|
-
if (indexHtml) return indexHtml;
|
|
959
|
-
return entryFile;
|
|
960
|
-
}
|
|
961
|
-
case "tanstack": return findFileWithReactGrabSetup(getTanStackRootFileCandidates(projectRoot)) ?? findTanStackRootFile(projectRoot);
|
|
962
|
-
case "webpack": return findFileWithReactGrabSetup(getEntryFileCandidates(projectRoot)) ?? findEntryFile(projectRoot);
|
|
963
|
-
default: return null;
|
|
964
|
-
}
|
|
965
|
-
};
|
|
966
|
-
const addOptionsToNextScript = (originalContent, options, filePath) => {
|
|
967
|
-
const reactGrabScriptMatch = originalContent.match(/(<Script[\s\S]*?react-grab[\s\S]*?)\s*(\/?>)/i);
|
|
968
|
-
if (!reactGrabScriptMatch) return {
|
|
969
|
-
success: false,
|
|
970
|
-
filePath,
|
|
971
|
-
message: "Could not find React Grab Script tag"
|
|
972
|
-
};
|
|
973
|
-
const scriptTag = reactGrabScriptMatch[0];
|
|
974
|
-
const scriptOpening = reactGrabScriptMatch[1];
|
|
975
|
-
const scriptClosing = reactGrabScriptMatch[2];
|
|
976
|
-
const existingDataOptionsMatch = scriptTag.match(/data-options=\{JSON\.stringify\([^)]+\)\}/);
|
|
977
|
-
const dataOptionsAttr = `data-options={JSON.stringify(\n ${formatOptionsForNextjs(options)}\n )}`;
|
|
978
|
-
let newScriptTag;
|
|
979
|
-
if (existingDataOptionsMatch) newScriptTag = scriptTag.replace(existingDataOptionsMatch[0], dataOptionsAttr);
|
|
980
|
-
else newScriptTag = `${scriptOpening}\n ${dataOptionsAttr}\n ${scriptClosing}`;
|
|
981
|
-
return {
|
|
982
|
-
success: true,
|
|
983
|
-
filePath,
|
|
984
|
-
message: "Update React Grab options",
|
|
985
|
-
originalContent,
|
|
986
|
-
newContent: originalContent.replace(scriptTag, newScriptTag)
|
|
987
|
-
};
|
|
988
|
-
};
|
|
989
|
-
const addOptionsToDynamicImport = (originalContent, options, filePath) => {
|
|
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*\))?/);
|
|
991
|
-
if (!reactGrabImportWithInitMatch) return {
|
|
992
|
-
success: false,
|
|
993
|
-
filePath,
|
|
994
|
-
message: "Could not find React Grab import"
|
|
995
|
-
};
|
|
996
|
-
const optionsJson = formatOptionsAsJson(options);
|
|
997
|
-
const newImport = `${reactGrabImportWithInitMatch[1] ?? ""}import("react-grab").then((m) => m.init(${optionsJson}))`;
|
|
998
|
-
return {
|
|
999
|
-
success: true,
|
|
1000
|
-
filePath,
|
|
1001
|
-
message: "Update React Grab options",
|
|
1002
|
-
originalContent,
|
|
1003
|
-
newContent: originalContent.replace(reactGrabImportWithInitMatch[0], newImport)
|
|
1004
|
-
};
|
|
1005
|
-
};
|
|
1006
|
-
const addOptionsToTanStackImport = (originalContent, options, filePath) => {
|
|
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*\))/);
|
|
1008
|
-
if (!reactGrabImportWithInitMatch) return {
|
|
1009
|
-
success: false,
|
|
1010
|
-
filePath,
|
|
1011
|
-
message: "Could not find React Grab import"
|
|
1012
|
-
};
|
|
1013
|
-
const optionsJson = formatOptionsAsJson(options);
|
|
1014
|
-
const newImport = `${reactGrabImportWithInitMatch[1] ?? reactGrabImportWithInitMatch[2] ?? ""}import("react-grab/core").then(({ init }) => init(${optionsJson}))`;
|
|
1015
|
-
return {
|
|
1016
|
-
success: true,
|
|
1017
|
-
filePath,
|
|
1018
|
-
message: "Update React Grab options",
|
|
1019
|
-
originalContent,
|
|
1020
|
-
newContent: originalContent.replace(reactGrabImportWithInitMatch[0], newImport)
|
|
1021
|
-
};
|
|
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
|
-
};
|
|
1028
|
-
const previewOptionsTransform = (projectRoot, framework, nextRouterType, options) => {
|
|
1029
|
-
const filePath = findReactGrabFile(projectRoot, framework, nextRouterType);
|
|
1030
|
-
if (!filePath) return {
|
|
1031
|
-
success: false,
|
|
1032
|
-
filePath: "",
|
|
1033
|
-
message: "Could not find file containing React Grab configuration"
|
|
1034
|
-
};
|
|
1035
|
-
const originalContent = readFileSync(filePath, "utf-8");
|
|
1036
|
-
if (!hasReactGrabSetupCode(originalContent)) return {
|
|
1037
|
-
success: false,
|
|
1038
|
-
filePath,
|
|
1039
|
-
message: "Could not find React Grab code in the file"
|
|
1040
|
-
};
|
|
1041
|
-
switch (framework) {
|
|
1042
|
-
case "next":
|
|
1043
|
-
if (isInstrumentationFile(filePath)) return addOptionsToAnyImport(originalContent, options, filePath);
|
|
1044
|
-
return addOptionsToNextScript(originalContent, options, filePath);
|
|
1045
|
-
case "vite": return addOptionsToDynamicImport(originalContent, options, filePath);
|
|
1046
|
-
case "tanstack": return addOptionsToTanStackImport(originalContent, options, filePath);
|
|
1047
|
-
case "webpack": return addOptionsToDynamicImport(originalContent, options, filePath);
|
|
1048
|
-
default: return {
|
|
1049
|
-
success: false,
|
|
1050
|
-
filePath,
|
|
1051
|
-
message: `Unknown framework: ${framework}`
|
|
1052
|
-
};
|
|
1053
|
-
}
|
|
1054
|
-
};
|
|
1055
|
-
const previewCdnTransform = (projectRoot, framework, nextRouterType, targetCdnDomain) => {
|
|
1056
|
-
const filePath = findReactGrabFile(projectRoot, framework, nextRouterType);
|
|
1057
|
-
if (!filePath) return {
|
|
1058
|
-
success: false,
|
|
1059
|
-
filePath: "",
|
|
1060
|
-
message: "Could not find React Grab file"
|
|
1061
|
-
};
|
|
1062
|
-
const originalContent = readFileSync(filePath, "utf-8");
|
|
1063
|
-
const newContent = originalContent.replace(/(https?:)?\/\/[^/\s"']+(?=\/(?:@?react-grab))/g, `//${targetCdnDomain}`).replace(/(https?:)?\/\/[^/\s"']*react-grab[^/\s"']*\.com(?=\/script\.js)/g, `//${targetCdnDomain}`);
|
|
1064
|
-
if (newContent === originalContent) return {
|
|
1065
|
-
success: true,
|
|
1066
|
-
filePath,
|
|
1067
|
-
message: "CDN already set",
|
|
1068
|
-
noChanges: true
|
|
1069
|
-
};
|
|
1070
|
-
return {
|
|
1071
|
-
success: true,
|
|
1072
|
-
filePath,
|
|
1073
|
-
message: "Update CDN",
|
|
1074
|
-
originalContent,
|
|
1075
|
-
newContent
|
|
1076
|
-
};
|
|
1077
|
-
};
|
|
1078
|
-
//#endregion
|
|
1079
264
|
//#region src/utils/constants.ts
|
|
1080
265
|
const MAX_KEY_HOLD_DURATION_MS = 2e3;
|
|
1081
266
|
const DEFAULT_WATCH_DIR = ".react-grab";
|
|
@@ -1099,7 +284,7 @@ const formatActivationKeyDisplay = (activationKey) => {
|
|
|
1099
284
|
};
|
|
1100
285
|
//#endregion
|
|
1101
286
|
//#region src/commands/configure.ts
|
|
1102
|
-
const VERSION$4 = "0.1.
|
|
287
|
+
const VERSION$4 = "0.1.47-dev.bc3a591";
|
|
1103
288
|
const isMac = process.platform === "darwin";
|
|
1104
289
|
const META_LABEL = isMac ? "Cmd" : "Win";
|
|
1105
290
|
const ALT_LABEL = isMac ? "Option" : "Alt";
|
|
@@ -1390,7 +575,7 @@ const CONFIG_OPTIONS = [
|
|
|
1390
575
|
{
|
|
1391
576
|
id: "maxContextLines",
|
|
1392
577
|
title: "Max Context Lines",
|
|
1393
|
-
description: "
|
|
578
|
+
description: "Max source-location lines in copied context (raise for large apps)"
|
|
1394
579
|
}
|
|
1395
580
|
];
|
|
1396
581
|
const comboToString = (combo) => {
|
|
@@ -1664,38 +849,6 @@ const configure = new Command().name("configure").alias("config").description("c
|
|
|
1664
849
|
}
|
|
1665
850
|
});
|
|
1666
851
|
//#endregion
|
|
1667
|
-
//#region src/utils/install.ts
|
|
1668
|
-
const installPackages = async (packages, options = {}) => {
|
|
1669
|
-
if (packages.length === 0) return;
|
|
1670
|
-
const detectedAgent = options.packageManager ?? await detectPackageManager(options.cwd ?? process.cwd());
|
|
1671
|
-
const args = [];
|
|
1672
|
-
if (options.preferOffline) args.push("--prefer-offline");
|
|
1673
|
-
if (detectedAgent === "pnpm") {
|
|
1674
|
-
args.push("--prod=false");
|
|
1675
|
-
if (existsSync(resolve(options.cwd ?? process.cwd(), "pnpm-workspace.yaml"))) args.push("-w");
|
|
1676
|
-
}
|
|
1677
|
-
if (options.additionalArgs) args.push(...options.additionalArgs);
|
|
1678
|
-
await x(detectedAgent, [
|
|
1679
|
-
detectedAgent === "npm" ? "install" : "add",
|
|
1680
|
-
...options.isDev !== false ? ["-D"] : [],
|
|
1681
|
-
...args,
|
|
1682
|
-
...packages
|
|
1683
|
-
], {
|
|
1684
|
-
nodeOptions: {
|
|
1685
|
-
stdio: options.silent ? "ignore" : "inherit",
|
|
1686
|
-
cwd: options.cwd,
|
|
1687
|
-
env: {
|
|
1688
|
-
...process.env,
|
|
1689
|
-
REACT_GRAB_INIT: "1"
|
|
1690
|
-
}
|
|
1691
|
-
},
|
|
1692
|
-
throwOnError: true
|
|
1693
|
-
});
|
|
1694
|
-
};
|
|
1695
|
-
const getPackagesToInstall = (includeReactGrab = true) => {
|
|
1696
|
-
return includeReactGrab ? ["react-grab"] : [];
|
|
1697
|
-
};
|
|
1698
|
-
//#endregion
|
|
1699
852
|
//#region src/utils/cli-helpers.ts
|
|
1700
853
|
const applyTransformWithFeedback = (result, message) => {
|
|
1701
854
|
const writeSpinner = spinner(message ?? `Applying changes to ${result.filePath}.`).start();
|
|
@@ -1731,7 +884,7 @@ const isTelemetryEnabled = () => {
|
|
|
1731
884
|
};
|
|
1732
885
|
//#endregion
|
|
1733
886
|
//#region src/commands/init.ts
|
|
1734
|
-
const VERSION$3 = "0.1.
|
|
887
|
+
const VERSION$3 = "0.1.47-dev.bc3a591";
|
|
1735
888
|
const REPORT_URL = "https://react-grab.com/api/report-cli";
|
|
1736
889
|
const DOCS_URL = "https://github.com/aidenybai/react-grab";
|
|
1737
890
|
const reportToCli = (type, config, error) => {
|
|
@@ -2682,19 +1835,20 @@ const pull = new Command().name("pull").description("start the watcher if needed
|
|
|
2682
1835
|
});
|
|
2683
1836
|
//#endregion
|
|
2684
1837
|
//#region src/commands/remove.ts
|
|
2685
|
-
const VERSION$2 = "0.1.
|
|
1838
|
+
const VERSION$2 = "0.1.47-dev.bc3a591";
|
|
2686
1839
|
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) => {
|
|
2687
1840
|
console.log(`${pc.magenta("✿")} ${pc.bold("React Grab")} ${pc.gray(VERSION$2)}`);
|
|
2688
1841
|
console.log();
|
|
2689
1842
|
try {
|
|
2690
1843
|
logger.break();
|
|
2691
|
-
const
|
|
1844
|
+
const removedAgents = await removeSkill({
|
|
2692
1845
|
cwd: resolve(opts.cwd),
|
|
2693
1846
|
global: opts.global
|
|
2694
1847
|
});
|
|
1848
|
+
for (const agent of removedAgents) logger.log(` ${highlighter.success("✓")} ${agentLabel(agent)}`);
|
|
2695
1849
|
logger.break();
|
|
2696
|
-
if (
|
|
2697
|
-
else logger.log(`${highlighter.success("Removed")} the React Grab skill from ${
|
|
1850
|
+
if (removedAgents.length === 0) logger.log("React Grab skill is not installed in any detected agent.");
|
|
1851
|
+
else logger.log(`${highlighter.success("Removed")} the React Grab skill from ${removedAgents.length} agent${removedAgents.length === 1 ? "" : "s"}.`);
|
|
2698
1852
|
logger.break();
|
|
2699
1853
|
} catch (error) {
|
|
2700
1854
|
handleError(error);
|
|
@@ -2710,7 +1864,7 @@ const stop = new Command().name("stop").description("stop the React Grab watcher
|
|
|
2710
1864
|
});
|
|
2711
1865
|
//#endregion
|
|
2712
1866
|
//#region src/commands/upgrade.ts
|
|
2713
|
-
const VERSION$1 = "0.1.
|
|
1867
|
+
const VERSION$1 = "0.1.47-dev.bc3a591";
|
|
2714
1868
|
const NPM_REGISTRY_URL = "https://registry.npmjs.org/react-grab/latest";
|
|
2715
1869
|
const fetchLatestVersion = async () => {
|
|
2716
1870
|
try {
|
|
@@ -2822,7 +1976,7 @@ const watch = new Command().name("watch").description("run the React Grab captur
|
|
|
2822
1976
|
});
|
|
2823
1977
|
//#endregion
|
|
2824
1978
|
//#region src/cli.ts
|
|
2825
|
-
const VERSION = "0.1.
|
|
1979
|
+
const VERSION = "0.1.47-dev.bc3a591";
|
|
2826
1980
|
const VERSION_API_URL = "https://www.react-grab.com/api/version";
|
|
2827
1981
|
process.on("SIGINT", () => process.exit(0));
|
|
2828
1982
|
process.on("SIGTERM", () => process.exit(0));
|
|
@@ -2831,7 +1985,7 @@ try {
|
|
|
2831
1985
|
} catch {}
|
|
2832
1986
|
const program = new Command().name("grab").description("add React Grab to your project").version(VERSION, "-v, --version", "display the version number");
|
|
2833
1987
|
program.addCommand(init);
|
|
2834
|
-
program.addCommand(add
|
|
1988
|
+
program.addCommand(add);
|
|
2835
1989
|
program.addCommand(remove);
|
|
2836
1990
|
program.addCommand(configure);
|
|
2837
1991
|
program.addCommand(upgrade);
|