@nhonh/qabot 0.1.0

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.
@@ -0,0 +1,298 @@
1
+ import path from "node:path";
2
+ import { listDirs, fileExists, safeReadFile } from "../utils/file-utils.js";
3
+
4
+ export async function detectFeatures(projectDir, projectProfile) {
5
+ const result = {
6
+ pages: [],
7
+ features: [],
8
+ components: {
9
+ shared: { count: 0, path: null },
10
+ page: { count: 0, path: null },
11
+ },
12
+ routes: [],
13
+ };
14
+
15
+ const detectors = {
16
+ "react-spa": detectReactSpa,
17
+ nextjs: detectNextjs,
18
+ vue: detectVue,
19
+ angular: detectAngular,
20
+ dotnet: detectDotnet,
21
+ };
22
+
23
+ const detect = detectors[projectProfile.type] || detectGeneric;
24
+ const detected = await detect(projectDir, projectProfile);
25
+ return { ...result, ...detected };
26
+ }
27
+
28
+ async function detectReactSpa(dir) {
29
+ const result = {
30
+ pages: [],
31
+ features: [],
32
+ components: {
33
+ shared: { count: 0, path: null },
34
+ page: { count: 0, path: null },
35
+ },
36
+ routes: [],
37
+ };
38
+ const srcDir = path.join(dir, "src");
39
+
40
+ const containerCandidates = [
41
+ "src/view/containers",
42
+ "src/pages",
43
+ "src/views",
44
+ "src/containers",
45
+ "src/screens",
46
+ ];
47
+ for (const candidate of containerCandidates) {
48
+ const fullPath = path.join(dir, candidate);
49
+ const dirs = await listDirs(fullPath);
50
+ if (dirs.length > 0) {
51
+ result.pages = dirs.map((d) => ({
52
+ name: d,
53
+ path: path.join(candidate, d),
54
+ route: guessRouteFromPageName(d),
55
+ }));
56
+ result.components.page = { count: dirs.length, path: candidate };
57
+ break;
58
+ }
59
+ }
60
+
61
+ const featureCandidates = [
62
+ "src/view/shared/features",
63
+ "src/features",
64
+ "src/modules",
65
+ ];
66
+ for (const candidate of featureCandidates) {
67
+ const fullPath = path.join(dir, candidate);
68
+ const dirs = await listDirs(fullPath);
69
+ if (dirs.length > 0) {
70
+ result.features = dirs.map((d) => ({
71
+ name: d,
72
+ path: path.join(candidate, d),
73
+ type: "feature-module",
74
+ }));
75
+ break;
76
+ }
77
+ }
78
+
79
+ const sharedCandidates = [
80
+ "src/view/shared/components",
81
+ "src/components",
82
+ "src/shared/components",
83
+ "src/ui",
84
+ ];
85
+ for (const candidate of sharedCandidates) {
86
+ const dirs = await listDirs(path.join(dir, candidate));
87
+ if (dirs.length > 0) {
88
+ result.components.shared = { count: dirs.length, path: candidate };
89
+ break;
90
+ }
91
+ }
92
+
93
+ const routeFile = await findRouteFile(dir);
94
+ if (routeFile) {
95
+ result.routes = await parseSimpleRoutes(routeFile);
96
+ }
97
+
98
+ return result;
99
+ }
100
+
101
+ async function detectNextjs(dir) {
102
+ const result = {
103
+ pages: [],
104
+ features: [],
105
+ components: {
106
+ shared: { count: 0, path: null },
107
+ page: { count: 0, path: null },
108
+ },
109
+ routes: [],
110
+ };
111
+
112
+ const appDir = (await fileExists(path.join(dir, "app")))
113
+ ? "app"
114
+ : (await fileExists(path.join(dir, "src", "app")))
115
+ ? "src/app"
116
+ : null;
117
+ const pagesDir = (await fileExists(path.join(dir, "pages")))
118
+ ? "pages"
119
+ : (await fileExists(path.join(dir, "src", "pages")))
120
+ ? "src/pages"
121
+ : null;
122
+
123
+ const routeDir = appDir || pagesDir;
124
+ if (routeDir) {
125
+ const dirs = await listDirs(path.join(dir, routeDir));
126
+ const filtered = dirs.filter(
127
+ (d) => !d.startsWith("_") && !d.startsWith(".") && d !== "api",
128
+ );
129
+ result.pages = filtered.map((d) => ({
130
+ name: d,
131
+ path: path.join(routeDir, d),
132
+ route: `/${d}`,
133
+ }));
134
+ result.components.page = { count: filtered.length, path: routeDir };
135
+ }
136
+
137
+ const compDirs =
138
+ (await listDirs(path.join(dir, "components"))) ||
139
+ (await listDirs(path.join(dir, "src", "components")));
140
+ if (compDirs.length > 0) {
141
+ result.components.shared = { count: compDirs.length, path: "components" };
142
+ }
143
+
144
+ return result;
145
+ }
146
+
147
+ async function detectVue(dir) {
148
+ const result = {
149
+ pages: [],
150
+ features: [],
151
+ components: {
152
+ shared: { count: 0, path: null },
153
+ page: { count: 0, path: null },
154
+ },
155
+ routes: [],
156
+ };
157
+
158
+ for (const candidate of ["src/views", "src/pages"]) {
159
+ const dirs = await listDirs(path.join(dir, candidate));
160
+ if (dirs.length > 0) {
161
+ result.pages = dirs.map((d) => ({
162
+ name: d,
163
+ path: path.join(candidate, d),
164
+ route: `/${d.toLowerCase()}`,
165
+ }));
166
+ result.components.page = { count: dirs.length, path: candidate };
167
+ break;
168
+ }
169
+ }
170
+
171
+ const compDirs = await listDirs(path.join(dir, "src", "components"));
172
+ if (compDirs.length > 0) {
173
+ result.components.shared = {
174
+ count: compDirs.length,
175
+ path: "src/components",
176
+ };
177
+ }
178
+
179
+ return result;
180
+ }
181
+
182
+ async function detectAngular(dir) {
183
+ const result = {
184
+ pages: [],
185
+ features: [],
186
+ components: {
187
+ shared: { count: 0, path: null },
188
+ page: { count: 0, path: null },
189
+ },
190
+ routes: [],
191
+ };
192
+
193
+ const appDirs = await listDirs(path.join(dir, "src", "app"));
194
+ const pageDirs = appDirs.filter(
195
+ (d) => d.endsWith("-page") || d.startsWith("page-"),
196
+ );
197
+ const featureDirs = appDirs.filter(
198
+ (d) =>
199
+ !d.startsWith("_") &&
200
+ !pageDirs.includes(d) &&
201
+ d !== "shared" &&
202
+ d !== "core",
203
+ );
204
+
205
+ result.pages = pageDirs.map((d) => ({
206
+ name: d,
207
+ path: `src/app/${d}`,
208
+ route: `/${d.replace(/-page$/, "")}`,
209
+ }));
210
+ result.features = featureDirs.map((d) => ({
211
+ name: d,
212
+ path: `src/app/${d}`,
213
+ type: "angular-module",
214
+ }));
215
+ result.components.page = { count: pageDirs.length, path: "src/app" };
216
+
217
+ return result;
218
+ }
219
+
220
+ async function detectDotnet(dir) {
221
+ const result = {
222
+ pages: [],
223
+ features: [],
224
+ components: {
225
+ shared: { count: 0, path: null },
226
+ page: { count: 0, path: null },
227
+ },
228
+ routes: [],
229
+ };
230
+ const dirs = await listDirs(dir);
231
+ const projectDirs = dirs.filter(
232
+ (d) =>
233
+ !d.includes(".Tests") &&
234
+ !d.startsWith(".") &&
235
+ !d.includes("node_modules"),
236
+ );
237
+ const testDirs = dirs.filter((d) => d.includes(".Tests"));
238
+
239
+ result.features = projectDirs.map((d) => ({
240
+ name: d,
241
+ path: d,
242
+ type: "dotnet-project",
243
+ }));
244
+ return result;
245
+ }
246
+
247
+ async function detectGeneric(dir) {
248
+ return {
249
+ pages: [],
250
+ features: [],
251
+ components: {
252
+ shared: { count: 0, path: null },
253
+ page: { count: 0, path: null },
254
+ },
255
+ routes: [],
256
+ };
257
+ }
258
+
259
+ function guessRouteFromPageName(name) {
260
+ const cleaned = name
261
+ .replace(/^Page/, "")
262
+ .replace(/([A-Z])/g, "-$1")
263
+ .toLowerCase()
264
+ .replace(/^-/, "");
265
+ return `/${cleaned}`;
266
+ }
267
+
268
+ async function findRouteFile(dir) {
269
+ const candidates = [
270
+ "src/routes/routesData.js",
271
+ "src/routes/index.js",
272
+ "src/router/index.js",
273
+ "src/routes.js",
274
+ "src/App.routes.js",
275
+ ];
276
+ for (const c of candidates) {
277
+ const full = path.join(dir, c);
278
+ if (await fileExists(full)) return full;
279
+ }
280
+ return null;
281
+ }
282
+
283
+ async function parseSimpleRoutes(filePath) {
284
+ const content = await safeReadFile(filePath);
285
+ if (!content) return [];
286
+
287
+ const routes = [];
288
+ const pathRegex = /path:\s*["'`]([^"'`]+)["'`]/g;
289
+ let match;
290
+ while ((match = pathRegex.exec(content)) !== null) {
291
+ routes.push({
292
+ path: match[1],
293
+ component: null,
294
+ isPrivate: content.includes(`isPrivate: true`),
295
+ });
296
+ }
297
+ return routes;
298
+ }
@@ -0,0 +1,157 @@
1
+ import path from "node:path";
2
+ import { fileExists, readJSON, safeReadFile } from "../utils/file-utils.js";
3
+ import { FRAMEWORK_DETECT_MAP } from "../core/constants.js";
4
+
5
+ export async function analyzeProject(projectDir) {
6
+ const profile = {
7
+ name: path.basename(projectDir),
8
+ type: "unknown",
9
+ techStack: {
10
+ language: "unknown",
11
+ framework: null,
12
+ bundler: null,
13
+ stateManagement: null,
14
+ styling: [],
15
+ apiClient: null,
16
+ authProvider: null,
17
+ },
18
+ paths: { root: projectDir, src: null, tests: null, config: null },
19
+ packageManager: "npm",
20
+ };
21
+
22
+ const pkg = await detectPackageJson(projectDir);
23
+ if (pkg) {
24
+ profile.name = pkg.name || profile.name;
25
+ profile.techStack = detectTechStack(pkg);
26
+ profile.type = detectProjectType(pkg, projectDir);
27
+ profile.packageManager = await detectPackageManager(projectDir);
28
+ profile.paths.src = await detectSrcDir(projectDir);
29
+ }
30
+
31
+ const dotnet = await detectDotnet(projectDir);
32
+ if (dotnet) {
33
+ profile.type = "dotnet";
34
+ profile.techStack.language = "csharp";
35
+ profile.techStack.framework = "dotnet";
36
+ }
37
+
38
+ const python = await detectPython(projectDir);
39
+ if (python) {
40
+ profile.type = "python";
41
+ profile.techStack.language = "python";
42
+ }
43
+
44
+ return profile;
45
+ }
46
+
47
+ async function detectPackageJson(dir) {
48
+ try {
49
+ return await readJSON(path.join(dir, "package.json"));
50
+ } catch {
51
+ return null;
52
+ }
53
+ }
54
+
55
+ function detectProjectType(pkg, dir) {
56
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
57
+ if (allDeps.next) return "nextjs";
58
+ if (allDeps["@angular/core"]) return "angular";
59
+ if (allDeps.vue) return "vue";
60
+ if (allDeps.react) return "react-spa";
61
+ if (allDeps.express || allDeps.fastify || allDeps.koa) return "node";
62
+ return "unknown";
63
+ }
64
+
65
+ function detectTechStack(pkg) {
66
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
67
+ const stack = {
68
+ language: allDeps.typescript ? "typescript" : "javascript",
69
+ framework: null,
70
+ bundler: null,
71
+ stateManagement: null,
72
+ styling: [],
73
+ apiClient: null,
74
+ authProvider: null,
75
+ };
76
+
77
+ if (allDeps.next) stack.framework = "nextjs";
78
+ else if (allDeps.react) stack.framework = "react";
79
+ else if (allDeps.vue) stack.framework = "vue";
80
+ else if (allDeps["@angular/core"]) stack.framework = "angular";
81
+ else if (allDeps.express) stack.framework = "express";
82
+
83
+ if (allDeps.webpack) stack.bundler = "webpack";
84
+ else if (allDeps.vite) stack.bundler = "vite";
85
+ else if (allDeps.esbuild) stack.bundler = "esbuild";
86
+ else if (allDeps.next) stack.bundler = "turbopack";
87
+
88
+ if (allDeps["redux-saga"]) stack.stateManagement = "redux-saga";
89
+ else if (allDeps["@reduxjs/toolkit"]) stack.stateManagement = "redux-toolkit";
90
+ else if (allDeps.redux) stack.stateManagement = "redux";
91
+ else if (allDeps.zustand) stack.stateManagement = "zustand";
92
+ else if (allDeps.xstate) stack.stateManagement = "xstate";
93
+ else if (allDeps.vuex) stack.stateManagement = "vuex";
94
+ else if (allDeps.pinia) stack.stateManagement = "pinia";
95
+ else if (allDeps.mobx) stack.stateManagement = "mobx";
96
+
97
+ if (allDeps.tailwindcss) stack.styling.push("tailwind");
98
+ if (allDeps["@mui/material"] || allDeps["@material-ui/core"])
99
+ stack.styling.push("mui");
100
+ if (allDeps["styled-components"]) stack.styling.push("styled-components");
101
+ if (allDeps["@emotion/react"]) stack.styling.push("emotion");
102
+ if (allDeps["@chakra-ui/react"]) stack.styling.push("chakra");
103
+ if (allDeps["antd"]) stack.styling.push("antd");
104
+
105
+ if (allDeps.axios) stack.apiClient = "axios";
106
+ else if (allDeps["graphql-request"] || allDeps["@apollo/client"])
107
+ stack.apiClient = "graphql";
108
+
109
+ if (
110
+ allDeps["auth0-js"] ||
111
+ allDeps["@auth0/nextjs-auth0"] ||
112
+ allDeps["@auth0/auth0-react"]
113
+ )
114
+ stack.authProvider = "auth0";
115
+ else if (allDeps["firebase"]) stack.authProvider = "firebase";
116
+ else if (allDeps["@okta/okta-react"]) stack.authProvider = "okta";
117
+ else if (allDeps["next-auth"]) stack.authProvider = "next-auth";
118
+
119
+ return stack;
120
+ }
121
+
122
+ async function detectPackageManager(dir) {
123
+ if (
124
+ (await fileExists(path.join(dir, "bun.lockb"))) ||
125
+ (await fileExists(path.join(dir, "bun.lock")))
126
+ )
127
+ return "bun";
128
+ if (await fileExists(path.join(dir, "pnpm-lock.yaml"))) return "pnpm";
129
+ if (await fileExists(path.join(dir, "yarn.lock"))) return "yarn";
130
+ return "npm";
131
+ }
132
+
133
+ async function detectSrcDir(dir) {
134
+ const candidates = ["src", "app", "lib", "source"];
135
+ for (const c of candidates) {
136
+ if (await fileExists(path.join(dir, c))) return c;
137
+ }
138
+ return null;
139
+ }
140
+
141
+ async function detectDotnet(dir) {
142
+ const { readdir } = await import("node:fs/promises");
143
+ try {
144
+ const entries = await readdir(dir);
145
+ return entries.some((e) => e.endsWith(".sln") || e.endsWith(".csproj"));
146
+ } catch {
147
+ return false;
148
+ }
149
+ }
150
+
151
+ async function detectPython(dir) {
152
+ return (
153
+ (await fileExists(path.join(dir, "pyproject.toml"))) ||
154
+ (await fileExists(path.join(dir, "requirements.txt"))) ||
155
+ (await fileExists(path.join(dir, "setup.py")))
156
+ );
157
+ }
@@ -0,0 +1,158 @@
1
+ import path from "node:path";
2
+ import { fileExists, readJSON, findFiles } from "../utils/file-utils.js";
3
+ import { RUNNER_DETECT_MAP } from "../core/constants.js";
4
+
5
+ export async function detectTests(projectDir, projectProfile) {
6
+ const result = {
7
+ frameworks: [],
8
+ testFiles: {
9
+ unit: { count: 0, pattern: null, examples: [] },
10
+ integration: { count: 0, pattern: null },
11
+ e2e: { count: 0, pattern: null },
12
+ },
13
+ coverage: { configured: false, thresholds: null, lastReport: null },
14
+ scripts: {},
15
+ ci: { platform: null, testJob: false },
16
+ };
17
+
18
+ const pkg = await readPkg(projectDir);
19
+ if (pkg) {
20
+ result.frameworks = await detectFrameworks(projectDir, pkg);
21
+ result.scripts = detectTestScripts(pkg);
22
+ }
23
+
24
+ for (const fw of result.frameworks) {
25
+ const counts = await countTestFiles(projectDir, fw.name);
26
+ if (fw.name === "playwright" || fw.name === "cypress") {
27
+ result.testFiles.e2e = {
28
+ count: counts.count,
29
+ pattern: counts.pattern,
30
+ examples: counts.examples,
31
+ };
32
+ } else {
33
+ result.testFiles.unit = {
34
+ count: counts.count,
35
+ pattern: counts.pattern,
36
+ examples: counts.examples,
37
+ };
38
+ }
39
+ }
40
+
41
+ const integrationFiles = await findFiles(
42
+ projectDir,
43
+ "**/*.integration.test.{js,ts,jsx,tsx}",
44
+ );
45
+ result.testFiles.integration = {
46
+ count: integrationFiles.length,
47
+ pattern: "**/*.integration.test.*",
48
+ };
49
+
50
+ result.coverage = await detectCoverage(projectDir);
51
+ result.ci = await detectCI(projectDir);
52
+
53
+ return result;
54
+ }
55
+
56
+ async function readPkg(dir) {
57
+ try {
58
+ return await readJSON(path.join(dir, "package.json"));
59
+ } catch {
60
+ return null;
61
+ }
62
+ }
63
+
64
+ async function detectFrameworks(dir, pkg) {
65
+ const frameworks = [];
66
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
67
+
68
+ for (const [name, info] of Object.entries(RUNNER_DETECT_MAP)) {
69
+ const hasDep = info.devDeps.some((d) => allDeps[d]);
70
+ const hasConfig = await Promise.any(
71
+ info.configs.map((c) =>
72
+ fileExists(path.join(dir, c)).then((exists) =>
73
+ exists ? c : Promise.reject(),
74
+ ),
75
+ ),
76
+ ).catch(() => null);
77
+
78
+ if (hasDep || hasConfig) {
79
+ const version =
80
+ info.devDeps.map((d) => allDeps[d]).find(Boolean) || "unknown";
81
+ frameworks.push({ name, version, configFile: hasConfig || null });
82
+ }
83
+ }
84
+
85
+ return frameworks;
86
+ }
87
+
88
+ function detectTestScripts(pkg) {
89
+ const scripts = {};
90
+ const s = pkg.scripts || {};
91
+ if (s.test) scripts.test = `npm test`;
92
+ if (s["test:watch"]) scripts.testWatch = `npm run test:watch`;
93
+ if (s.testcov || s["test:coverage"])
94
+ scripts.testCoverage = `npm run ${s.testcov ? "testcov" : "test:coverage"}`;
95
+ if (s["test:e2e"]) scripts.testE2e = `npm run test:e2e`;
96
+ return scripts;
97
+ }
98
+
99
+ async function countTestFiles(dir, runner) {
100
+ const patterns = {
101
+ jest: "**/*.test.{js,ts,jsx,tsx}",
102
+ vitest: "**/*.{test,spec}.{js,ts,jsx,tsx}",
103
+ playwright: "**/*.spec.{js,ts}",
104
+ cypress: "**/*.cy.{js,ts,jsx,tsx}",
105
+ };
106
+ const pattern = patterns[runner] || "**/*.test.*";
107
+ try {
108
+ const files = await findFiles(dir, pattern);
109
+ const filtered = files.filter((f) => !f.includes("node_modules"));
110
+ return {
111
+ count: filtered.length,
112
+ pattern,
113
+ examples: filtered.slice(0, 3).map((f) => path.relative(dir, f)),
114
+ };
115
+ } catch {
116
+ return { count: 0, pattern, examples: [] };
117
+ }
118
+ }
119
+
120
+ async function detectCoverage(dir) {
121
+ const jestConfig = await readJestConfig(dir);
122
+ if (jestConfig?.coverageThreshold) {
123
+ return {
124
+ configured: true,
125
+ thresholds: jestConfig.coverageThreshold.global || null,
126
+ lastReport: null,
127
+ };
128
+ }
129
+ return { configured: false, thresholds: null, lastReport: null };
130
+ }
131
+
132
+ async function readJestConfig(dir) {
133
+ const configPath = path.join(dir, "jest.config.js");
134
+ if (!(await fileExists(configPath))) return null;
135
+ try {
136
+ const mod = await import(`file://${configPath}`);
137
+ return mod.default || mod;
138
+ } catch {
139
+ return null;
140
+ }
141
+ }
142
+
143
+ async function detectCI(dir) {
144
+ if (await fileExists(path.join(dir, ".github", "workflows"))) {
145
+ const files = await findFiles(
146
+ path.join(dir, ".github", "workflows"),
147
+ "*.{yml,yaml}",
148
+ );
149
+ return { platform: "github-actions", testJob: files.length > 0 };
150
+ }
151
+ if (await fileExists(path.join(dir, ".gitlab-ci.yml")))
152
+ return { platform: "gitlab-ci", testJob: true };
153
+ if (await fileExists(path.join(dir, "Jenkinsfile")))
154
+ return { platform: "jenkins", testJob: true };
155
+ if (await fileExists(path.join(dir, ".circleci")))
156
+ return { platform: "circleci", testJob: true };
157
+ return { platform: null, testJob: false };
158
+ }