| 185 | argsSchema = DetectFrontendStackArgs; |
| 186 | |
| 187 | async execute(args: z.infer<typeof DetectFrontendStackArgs>, ctx: ToolContext): Promise<ToolResult> { |
| 188 | const root = args.path ? path.resolve(ctx.cwd, args.path) : ctx.cwd; |
| 189 | const pkgPath = await findFile(root, ['package.json']); |
| 190 | if (!pkgPath) { |
| 191 | return { |
| 192 | content: |
| 193 | `[DETECT_FRONTEND_STACK] No package.json found in ${root}. ` + |
| 194 | `If the project is elsewhere (e.g. a folder the user attached), call this tool again ` + |
| 195 | `with \`path\` set to that folder's absolute path.`, |
| 196 | isError: true, |
| 197 | }; |
| 198 | } |
| 199 | const pkg = await readJsonOrNull(pkgPath); |
| 200 | if (!pkg) return { content: '[DETECT_FRONTEND_STACK] package.json unreadable.', isError: true }; |
| 201 | |
| 202 | const out: string[] = []; |
| 203 | out.push(`# Frontend Stack — ${pkg.name ?? 'project'} ${pkg.version ? `v${pkg.version}` : ''}`); |
| 204 | out.push(''); |
| 205 | |
| 206 | // Framework |
| 207 | out.push(`## Framework`); |
| 208 | let frameworkFound = false; |
| 209 | for (const fw of FRAMEWORKS) { |
| 210 | const ver = hasDep(pkg, fw.pkg); |
| 211 | if (ver) { |
| 212 | const detail = fw.detect ? await fw.detect(root) : null; |
| 213 | out.push(` • ${fw.name} ${ver}${detail ? ` — ${detail}` : ''}`); |
| 214 | frameworkFound = true; |
| 215 | } |
| 216 | } |
| 217 | if (!frameworkFound) { |
| 218 | const react = hasDep(pkg, 'react'); |
| 219 | if (react) out.push(` • React ${react} (no framework — vanilla SPA?)`); |
| 220 | else out.push(` • (no framework detected)`); |
| 221 | } |
| 222 | |
| 223 | // React version + features |
| 224 | const reactVer = hasDep(pkg, 'react'); |
| 225 | if (reactVer) { |
| 226 | const major = parseInt(String(reactVer).replace(/[^\d]/, ''), 10); |
| 227 | out.push(''); |
| 228 | out.push(`## React ${reactVer}`); |
| 229 | if (major >= 19) { |
| 230 | out.push(` • React 19+: Server Components, Actions, useFormStatus, useOptimistic available`); |
| 231 | out.push(` • ref-as-prop (no forwardRef needed for new components)`); |
| 232 | } else if (major === 18) { |
| 233 | out.push(` • React 18: Server Components (in Next.js App), Suspense, useTransition`); |
| 234 | } else { |
| 235 | out.push(` • React ${major}: legacy. Consider upgrade for concurrent features.`); |
| 236 | } |
| 237 | } |
| 238 | |
| 239 | // Each category |
| 240 | const sections: Array<[string, ReturnType<typeof detect>]> = [ |
| 241 | ['## UI library', detect(pkg, UI_LIBS)], |
| 242 | ['## Styling', detect(pkg, STYLING)], |
| 243 | ['## Animation', detect(pkg, ANIMATION)], |
| 244 | ['## 3D / Canvas', detect(pkg, THREE_D)], |