@qodfy/core 0.1.0 → 0.1.2

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 ADDED
@@ -0,0 +1,52 @@
1
+ # @qodfy/core
2
+
3
+ Scanner engine for Qodfy.
4
+
5
+ Most developers should use the CLI:
6
+
7
+ ```bash
8
+ npx qodfy scan
9
+ ```
10
+
11
+ This package contains the deterministic local scanner used by the `qodfy` CLI. It is published separately so Qodfy can later support more report formats, integrations, and programmatic usage without coupling everything to terminal output.
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ npm install @qodfy/core
17
+ ```
18
+
19
+ ## Usage
20
+
21
+ ```ts
22
+ import { scanProject } from "@qodfy/core";
23
+
24
+ const report = await scanProject(process.cwd());
25
+
26
+ console.log(report.score);
27
+ console.log(report.issues);
28
+ ```
29
+
30
+ ## What It Returns
31
+
32
+ `scanProject(projectPath)` returns a launch-readiness report with:
33
+
34
+ - project path
35
+ - Next.js detection result
36
+ - score from `0` to `100`
37
+ - issues with severity, title, message, and optional file path
38
+ - stats for scanned files, API routes, AI-related files, and large files
39
+
40
+ ## Local-Only
41
+
42
+ `@qodfy/core` reads files from the local project path you provide. It does not call AI APIs, upload code, require login, or contact an external Qodfy server.
43
+
44
+ ## Repository
45
+
46
+ GitHub: https://github.com/yassinifguisse1/qodfy
47
+
48
+ Issues and feedback: https://github.com/yassinifguisse1/qodfy/issues
49
+
50
+ ## License
51
+
52
+ MIT
package/dist/index.d.ts CHANGED
@@ -4,6 +4,7 @@ type Issue = {
4
4
  title: string;
5
5
  message: string;
6
6
  file?: string;
7
+ suggestion?: string;
7
8
  };
8
9
  type ScanReport = {
9
10
  projectPath: string;
package/dist/index.js CHANGED
@@ -1,56 +1,98 @@
1
1
  // src/index.ts
2
- import path from "path";
3
2
  import fs from "fs/promises";
3
+ import path from "path";
4
4
  import fg from "fast-glob";
5
- async function fileExists(filePath) {
6
- try {
7
- await fs.access(filePath);
8
- return true;
9
- } catch {
10
- return false;
11
- }
12
- }
13
- async function readJson(filePath) {
14
- const content = await fs.readFile(filePath, "utf-8");
15
- return JSON.parse(content);
16
- }
5
+ var sourceFilePatterns = ["**/*.{ts,tsx,js,jsx,mts,cts,mjs,cjs}"];
6
+ var ignoredPaths = [
7
+ "node_modules/**",
8
+ ".next/**",
9
+ "dist/**",
10
+ "build/**",
11
+ ".turbo/**",
12
+ ".vercel/**"
13
+ ];
14
+ var aiKeywords = [
15
+ "openai",
16
+ "@ai-sdk",
17
+ "ai/react",
18
+ "anthropic",
19
+ "gemini",
20
+ "generateText",
21
+ "streamText",
22
+ "useChat"
23
+ ];
17
24
  async function scanProject(projectPath) {
25
+ const resolvedProjectPath = path.resolve(projectPath);
18
26
  const issues = [];
19
- const packageJsonPath = path.join(projectPath, "package.json");
27
+ const packageJsonPath = path.join(resolvedProjectPath, "package.json");
20
28
  const hasPackageJson = await fileExists(packageJsonPath);
29
+ let isNextProject = false;
21
30
  if (!hasPackageJson) {
22
31
  issues.push({
23
32
  severity: "critical",
24
33
  title: "Missing package.json",
25
- message: "Qodfy could not find a package.json file in this project."
34
+ message: "Qodfy could not find a package.json file in this project.",
35
+ suggestion: "Run Qodfy from the project root or pass --path to the app folder."
26
36
  });
27
- }
28
- let isNextProject = false;
29
- if (hasPackageJson) {
30
- const packageJson = await readJson(packageJsonPath);
31
- const deps = {
32
- ...packageJson.dependencies,
33
- ...packageJson.devDependencies
34
- };
35
- isNextProject = Boolean(deps.next);
36
- if (!isNextProject) {
37
+ } else {
38
+ const packageJsonResult = await safeReadJson(packageJsonPath);
39
+ if (!packageJsonResult.ok) {
37
40
  issues.push({
38
- severity: "warning",
39
- title: "Next.js not detected",
40
- message: "This first version of Qodfy is optimized for Next.js projects."
41
+ severity: "critical",
42
+ title: "Could not read package.json",
43
+ message: packageJsonResult.reason,
44
+ file: "package.json",
45
+ suggestion: "Fix package.json so Qodfy can detect the framework and dependencies."
46
+ });
47
+ } else if (!isPackageJsonObject(packageJsonResult.data)) {
48
+ issues.push({
49
+ severity: "critical",
50
+ title: "Invalid package.json",
51
+ message: "package.json must contain a JSON object at the top level.",
52
+ file: "package.json",
53
+ suggestion: "Fix package.json so Qodfy can detect the framework and dependencies."
41
54
  });
55
+ } else {
56
+ const deps = {
57
+ ...packageJsonResult.data.dependencies,
58
+ ...packageJsonResult.data.devDependencies
59
+ };
60
+ isNextProject = Boolean(deps.next);
61
+ if (!isNextProject) {
62
+ issues.push({
63
+ severity: "warning",
64
+ title: "Next.js not detected",
65
+ message: "This first version of Qodfy is optimized for Next.js projects.",
66
+ suggestion: "If this is a monorepo, scan the Next.js app folder directly."
67
+ });
68
+ }
42
69
  }
43
70
  }
44
- const envExamplePath = path.join(projectPath, ".env.example");
71
+ const envExamplePath = path.join(resolvedProjectPath, ".env.example");
45
72
  const hasEnvExample = await fileExists(envExamplePath);
73
+ let envExampleVariables = null;
46
74
  if (!hasEnvExample) {
47
75
  issues.push({
48
76
  severity: "warning",
49
77
  title: "Missing .env.example",
50
- message: "Add a .env.example file so future developers know which environment variables are required."
78
+ message: "Add a .env.example file so future developers know which environment variables are required.",
79
+ suggestion: "Document required variable names only, never real secret values."
51
80
  });
81
+ } else {
82
+ const envExampleResult = await safeReadFile(envExamplePath);
83
+ if (!envExampleResult.ok) {
84
+ issues.push({
85
+ severity: "warning",
86
+ title: "Could not read .env.example",
87
+ message: envExampleResult.reason,
88
+ file: ".env.example",
89
+ suggestion: "Make sure .env.example is readable and contains variable names without real secret values."
90
+ });
91
+ } else {
92
+ envExampleVariables = getEnvExampleVariables(envExampleResult.content);
93
+ }
52
94
  }
53
- const hasReadme = await fileExists(path.join(projectPath, "README.md"));
95
+ const hasReadme = await fileExists(path.join(resolvedProjectPath, "README.md"));
54
96
  if (!hasReadme) {
55
97
  issues.push({
56
98
  severity: "info",
@@ -58,35 +100,38 @@ async function scanProject(projectPath) {
58
100
  message: "A README helps other developers understand how to run and maintain the project."
59
101
  });
60
102
  }
61
- const files = await fg(["**/*.{ts,tsx,js,jsx}"], {
62
- cwd: projectPath,
63
- ignore: ["node_modules/**", ".next/**", "dist/**", "build/**"],
64
- absolute: true
65
- });
103
+ const files = await getSourceFiles(resolvedProjectPath, issues);
66
104
  const apiRoutes = files.filter((file) => {
67
105
  return file.includes(`${path.sep}app${path.sep}api${path.sep}`) || file.includes(`${path.sep}pages${path.sep}api${path.sep}`);
68
106
  });
69
- const aiKeywords = [
70
- "openai",
71
- "@ai-sdk",
72
- "ai/react",
73
- "anthropic",
74
- "gemini",
75
- "generateText",
76
- "streamText"
77
- ];
107
+ const apiRouteSet = new Set(apiRoutes);
108
+ const readableFiles = /* @__PURE__ */ new Map();
109
+ const envExampleWarningKeys = /* @__PURE__ */ new Set();
110
+ const clientSecretWarningKeys = /* @__PURE__ */ new Set();
78
111
  let aiFiles = 0;
79
112
  let largeFiles = 0;
80
113
  for (const file of files) {
81
- const content = await fs.readFile(file, "utf-8");
82
- const relativeFile = path.relative(projectPath, file);
114
+ const fileResult = await safeReadFile(file);
115
+ const relativeFile = normalizePath(path.relative(resolvedProjectPath, file));
116
+ if (!fileResult.ok) {
117
+ issues.push({
118
+ severity: "info",
119
+ title: "File could not be read",
120
+ message: fileResult.reason,
121
+ file: relativeFile
122
+ });
123
+ continue;
124
+ }
125
+ const content = fileResult.content;
126
+ readableFiles.set(file, content);
83
127
  if (content.length > 15e3) {
84
128
  largeFiles++;
85
129
  issues.push({
86
130
  severity: "info",
87
131
  title: "Large file detected",
88
132
  message: "Large files are harder to maintain and often appear in AI-generated codebases.",
89
- file: relativeFile
133
+ file: relativeFile,
134
+ suggestion: "Consider splitting this file into smaller modules if it mixes unrelated responsibilities."
90
135
  });
91
136
  }
92
137
  const usesAI = aiKeywords.some(
@@ -95,26 +140,68 @@ async function scanProject(projectPath) {
95
140
  if (usesAI) {
96
141
  aiFiles++;
97
142
  const hasRateLimit = content.includes("rateLimit") || content.includes("ratelimit") || content.includes("upstash") || content.includes("limiter");
98
- if (!hasRateLimit) {
143
+ if (apiRouteSet.has(file) && !hasRateLimit) {
99
144
  issues.push({
100
145
  severity: "critical",
101
146
  title: "AI route may be missing rate limiting",
102
147
  message: "AI routes can create real API costs. Add rate limiting or usage limits before launch.",
103
- file: relativeFile
148
+ file: relativeFile,
149
+ suggestion: "Add rate limiting, usage limits, or per-user quotas before launch."
104
150
  });
105
151
  }
106
152
  }
153
+ const usedEnvVariables = getUsedEnvVariables(content);
154
+ if (envExampleVariables) {
155
+ for (const variableName of usedEnvVariables) {
156
+ if (!envExampleVariables.has(variableName)) {
157
+ const warningKey = `${relativeFile}:${variableName}`;
158
+ if (envExampleWarningKeys.has(warningKey)) {
159
+ continue;
160
+ }
161
+ envExampleWarningKeys.add(warningKey);
162
+ issues.push({
163
+ severity: "warning",
164
+ title: "Environment variable missing from .env.example",
165
+ message: `${variableName} is used in ${relativeFile} but is not documented in .env.example.`,
166
+ file: relativeFile,
167
+ suggestion: `Add ${variableName}= to .env.example without including a real value.`
168
+ });
169
+ }
170
+ }
171
+ }
172
+ if (isClientSideFile(relativeFile, content)) {
173
+ for (const variableName of usedEnvVariables) {
174
+ if (!variableName.startsWith("NEXT_PUBLIC_")) {
175
+ const warningKey = `${relativeFile}:${variableName}`;
176
+ if (clientSecretWarningKeys.has(warningKey)) {
177
+ continue;
178
+ }
179
+ clientSecretWarningKeys.add(warningKey);
180
+ issues.push({
181
+ severity: "warning",
182
+ title: "Possible server secret used in client-side code",
183
+ message: `${variableName} appears in a client-side file. Server secrets should not be exposed to the browser.`,
184
+ file: relativeFile,
185
+ suggestion: "Move server-only environment variable access to a server component, API route, or server action."
186
+ });
187
+ }
188
+ }
189
+ }
107
190
  }
108
191
  for (const route of apiRoutes) {
109
- const content = await fs.readFile(route, "utf-8");
110
- const relativeFile = path.relative(projectPath, route);
192
+ const content = readableFiles.get(route);
193
+ if (!content) {
194
+ continue;
195
+ }
196
+ const relativeFile = normalizePath(path.relative(resolvedProjectPath, route));
111
197
  const hasAuth = content.includes("auth(") || content.includes("getServerSession") || content.includes("currentUser") || content.includes("clerkClient") || content.includes("session");
112
198
  if (!hasAuth) {
113
199
  issues.push({
114
200
  severity: "warning",
115
201
  title: "API route may be missing authentication",
116
202
  message: "This API route does not appear to contain an auth/session check.",
117
- file: relativeFile
203
+ file: relativeFile,
204
+ suggestion: "Confirm the route is public, or add an auth/session check before handling user data."
118
205
  });
119
206
  }
120
207
  }
@@ -122,7 +209,7 @@ async function scanProject(projectPath) {
122
209
  const warningCount = issues.filter((issue) => issue.severity === "warning").length;
123
210
  const score = Math.max(0, 100 - criticalCount * 20 - warningCount * 8);
124
211
  return {
125
- projectPath,
212
+ projectPath: resolvedProjectPath,
126
213
  isNextProject,
127
214
  score,
128
215
  issues,
@@ -134,6 +221,121 @@ async function scanProject(projectPath) {
134
221
  }
135
222
  };
136
223
  }
224
+ async function fileExists(filePath) {
225
+ try {
226
+ await fs.access(filePath);
227
+ return true;
228
+ } catch {
229
+ return false;
230
+ }
231
+ }
232
+ async function safeReadFile(filePath) {
233
+ try {
234
+ return {
235
+ ok: true,
236
+ content: await fs.readFile(filePath, "utf-8")
237
+ };
238
+ } catch (error) {
239
+ const code = getErrorCode(error);
240
+ if (code === "ENOENT") {
241
+ return {
242
+ ok: false,
243
+ code,
244
+ reason: "The file disappeared while Qodfy was scanning it."
245
+ };
246
+ }
247
+ if (code === "EACCES" || code === "EPERM") {
248
+ return {
249
+ ok: false,
250
+ code,
251
+ reason: "Qodfy does not have permission to read this file."
252
+ };
253
+ }
254
+ return {
255
+ ok: false,
256
+ code,
257
+ reason: "Qodfy could not read this file."
258
+ };
259
+ }
260
+ }
261
+ async function safeReadJson(filePath) {
262
+ const fileResult = await safeReadFile(filePath);
263
+ if (!fileResult.ok) {
264
+ return fileResult;
265
+ }
266
+ try {
267
+ return {
268
+ ok: true,
269
+ data: JSON.parse(fileResult.content)
270
+ };
271
+ } catch {
272
+ return {
273
+ ok: false,
274
+ reason: "package.json is not valid JSON."
275
+ };
276
+ }
277
+ }
278
+ async function getSourceFiles(projectPath, issues) {
279
+ try {
280
+ return await fg(sourceFilePatterns, {
281
+ cwd: projectPath,
282
+ ignore: ignoredPaths,
283
+ absolute: true,
284
+ onlyFiles: true,
285
+ dot: false
286
+ });
287
+ } catch {
288
+ issues.push({
289
+ severity: "critical",
290
+ title: "Could not scan source files",
291
+ message: "Qodfy could not list source files in this project.",
292
+ suggestion: "Check that the project path exists and is readable."
293
+ });
294
+ return [];
295
+ }
296
+ }
297
+ function getEnvExampleVariables(content) {
298
+ const variables = /* @__PURE__ */ new Set();
299
+ for (const line of content.split(/\r?\n/)) {
300
+ const trimmedLine = line.trim();
301
+ if (!trimmedLine || trimmedLine.startsWith("#")) {
302
+ continue;
303
+ }
304
+ const match = trimmedLine.match(/^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*(?:=|$)/);
305
+ if (match) {
306
+ variables.add(match[1]);
307
+ }
308
+ }
309
+ return variables;
310
+ }
311
+ function getUsedEnvVariables(content) {
312
+ const variables = /* @__PURE__ */ new Set();
313
+ const dotAccessPattern = /\bprocess\.env\.([A-Za-z_][A-Za-z0-9_]*)/g;
314
+ const bracketAccessPattern = /\bprocess\.env\[['"`]([A-Za-z_][A-Za-z0-9_]*)['"`]\]/g;
315
+ for (const match of content.matchAll(dotAccessPattern)) {
316
+ variables.add(match[1]);
317
+ }
318
+ for (const match of content.matchAll(bracketAccessPattern)) {
319
+ variables.add(match[1]);
320
+ }
321
+ return variables;
322
+ }
323
+ function isClientSideFile(relativeFile, content) {
324
+ const fileName = path.basename(relativeFile);
325
+ return fileName.includes(".client.") || /(^|\n)\s*["']use client["'];?/.test(content);
326
+ }
327
+ function isPackageJsonObject(data) {
328
+ return typeof data === "object" && data !== null && !Array.isArray(data);
329
+ }
330
+ function normalizePath(filePath) {
331
+ return filePath.split(path.sep).join("/");
332
+ }
333
+ function getErrorCode(error) {
334
+ if (error && typeof error === "object" && "code" in error && typeof error.code === "string") {
335
+ return error.code;
336
+ }
337
+ return void 0;
338
+ }
137
339
  export {
138
340
  scanProject
139
341
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qodfy/core",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Scanner engine for Qodfy, an open-source launch readiness scanner for AI-built apps.",
5
5
  "keywords": [
6
6
  "qodfy",
@@ -33,7 +33,8 @@
33
33
  }
34
34
  },
35
35
  "files": [
36
- "dist"
36
+ "dist",
37
+ "README.md"
37
38
  ],
38
39
  "license": "MIT",
39
40
  "engines": {