@produtype/core 0.2.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.
Files changed (107) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +289 -0
  3. package/dist/analyzer/analyzeProject.d.ts +2 -0
  4. package/dist/analyzer/analyzeProject.js +365 -0
  5. package/dist/analyzer/detectAiSafety.d.ts +3 -0
  6. package/dist/analyzer/detectAiSafety.js +74 -0
  7. package/dist/analyzer/detectAuth.d.ts +3 -0
  8. package/dist/analyzer/detectAuth.js +192 -0
  9. package/dist/analyzer/detectBackend.d.ts +6 -0
  10. package/dist/analyzer/detectBackend.js +89 -0
  11. package/dist/analyzer/detectBilling.d.ts +3 -0
  12. package/dist/analyzer/detectBilling.js +78 -0
  13. package/dist/analyzer/detectContext.d.ts +24 -0
  14. package/dist/analyzer/detectContext.js +18 -0
  15. package/dist/analyzer/detectDatabase.d.ts +6 -0
  16. package/dist/analyzer/detectDatabase.js +77 -0
  17. package/dist/analyzer/detectDeployment.d.ts +3 -0
  18. package/dist/analyzer/detectDeployment.js +28 -0
  19. package/dist/analyzer/detectDocker.d.ts +3 -0
  20. package/dist/analyzer/detectDocker.js +43 -0
  21. package/dist/analyzer/detectEnv.d.ts +3 -0
  22. package/dist/analyzer/detectEnv.js +89 -0
  23. package/dist/analyzer/detectFrontend.d.ts +6 -0
  24. package/dist/analyzer/detectFrontend.js +49 -0
  25. package/dist/analyzer/detectGdpr.d.ts +3 -0
  26. package/dist/analyzer/detectGdpr.js +53 -0
  27. package/dist/analyzer/detectJobs.d.ts +3 -0
  28. package/dist/analyzer/detectJobs.js +30 -0
  29. package/dist/analyzer/detectMarketplace.d.ts +3 -0
  30. package/dist/analyzer/detectMarketplace.js +126 -0
  31. package/dist/analyzer/detectNotifications.d.ts +3 -0
  32. package/dist/analyzer/detectNotifications.js +81 -0
  33. package/dist/analyzer/detectObservability.d.ts +3 -0
  34. package/dist/analyzer/detectObservability.js +44 -0
  35. package/dist/analyzer/detectPackageManager.d.ts +14 -0
  36. package/dist/analyzer/detectPackageManager.js +76 -0
  37. package/dist/analyzer/detectSecurity.d.ts +3 -0
  38. package/dist/analyzer/detectSecurity.js +145 -0
  39. package/dist/analyzer/detectStack.d.ts +13 -0
  40. package/dist/analyzer/detectStack.js +36 -0
  41. package/dist/analyzer/detectUploads.d.ts +3 -0
  42. package/dist/analyzer/detectUploads.js +73 -0
  43. package/dist/analyzer/types.d.ts +62 -0
  44. package/dist/analyzer/types.js +2 -0
  45. package/dist/api.d.ts +31 -0
  46. package/dist/api.js +34 -0
  47. package/dist/cli.d.ts +1 -0
  48. package/dist/cli.js +268 -0
  49. package/dist/expectations/evaluateExpectations.d.ts +9 -0
  50. package/dist/expectations/evaluateExpectations.js +377 -0
  51. package/dist/expectations/inferProductProfile.d.ts +3 -0
  52. package/dist/expectations/inferProductProfile.js +39 -0
  53. package/dist/expectations/productProfiles.d.ts +57 -0
  54. package/dist/expectations/productProfiles.js +478 -0
  55. package/dist/expectations/types.d.ts +70 -0
  56. package/dist/expectations/types.js +2 -0
  57. package/dist/index.d.ts +2 -0
  58. package/dist/index.js +9 -0
  59. package/dist/mcp/index.d.ts +2 -0
  60. package/dist/mcp/index.js +9 -0
  61. package/dist/mcp/server.d.ts +3 -0
  62. package/dist/mcp/server.js +155 -0
  63. package/dist/planner/buildPlan.d.ts +3 -0
  64. package/dist/planner/buildPlan.js +187 -0
  65. package/dist/planner/jsonPlan.d.ts +2 -0
  66. package/dist/planner/jsonPlan.js +6 -0
  67. package/dist/planner/markdownPlan.d.ts +2 -0
  68. package/dist/planner/markdownPlan.js +95 -0
  69. package/dist/planner/remediationCatalog.d.ts +3 -0
  70. package/dist/planner/remediationCatalog.js +1200 -0
  71. package/dist/planner/types.d.ts +63 -0
  72. package/dist/planner/types.js +2 -0
  73. package/dist/report/buildReport.d.ts +7 -0
  74. package/dist/report/buildReport.js +198 -0
  75. package/dist/report/businessImpact.d.ts +4 -0
  76. package/dist/report/businessImpact.js +86 -0
  77. package/dist/report/categoryScores.d.ts +21 -0
  78. package/dist/report/categoryScores.js +65 -0
  79. package/dist/report/complianceMapping.d.ts +19 -0
  80. package/dist/report/complianceMapping.js +122 -0
  81. package/dist/report/executiveSummary.d.ts +26 -0
  82. package/dist/report/executiveSummary.js +117 -0
  83. package/dist/report/jsonReport.d.ts +2 -0
  84. package/dist/report/jsonReport.js +6 -0
  85. package/dist/report/markdownReport.d.ts +2 -0
  86. package/dist/report/markdownReport.js +256 -0
  87. package/dist/report/score.d.ts +3 -0
  88. package/dist/report/score.js +29 -0
  89. package/dist/report/types.d.ts +77 -0
  90. package/dist/report/types.js +2 -0
  91. package/dist/rules/ruleEngine.d.ts +3 -0
  92. package/dist/rules/ruleEngine.js +13 -0
  93. package/dist/rules/rules.d.ts +2 -0
  94. package/dist/rules/rules.js +560 -0
  95. package/dist/rules/types.d.ts +16 -0
  96. package/dist/rules/types.js +2 -0
  97. package/dist/utils/fileScanner.d.ts +14 -0
  98. package/dist/utils/fileScanner.js +95 -0
  99. package/dist/utils/pathUtils.d.ts +2 -0
  100. package/dist/utils/pathUtils.js +44 -0
  101. package/dist/utils/readTextFileSafe.d.ts +6 -0
  102. package/dist/utils/readTextFileSafe.js +76 -0
  103. package/dist/utils/textSearch.d.ts +11 -0
  104. package/dist/utils/textSearch.js +36 -0
  105. package/dist/version.d.ts +1 -0
  106. package/dist/version.js +4 -0
  107. package/package.json +91 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Denis Billi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,289 @@
1
+ # ProdKit
2
+
3
+ ProdKit is a deterministic local CLI that analyzes an existing web application repository and generates a production-readiness report or a remediation plan.
4
+
5
+ It is designed for early-stage and AI-generated apps where architecture and security quality can vary significantly.
6
+
7
+ ProdKit MVP is read-only: it never modifies the target repository.
8
+
9
+ ## What ProdKit does
10
+
11
+ ProdKit inspects a target project and detects:
12
+
13
+ - Frontend framework signals
14
+ - Backend framework signals
15
+ - Database technologies
16
+ - Package manager
17
+ - Docker and Docker Compose presence
18
+ - Environment file and env-usage hygiene
19
+ - Authentication and authorization signals
20
+ - Tenant/organization signals
21
+ - GDPR/privacy signals
22
+ - Security hardening signals
23
+ - Upload exposure signals
24
+ - Billing/Stripe signals
25
+ - Observability/logging signals
26
+ - Background jobs/queue signals
27
+ - Deployment readiness signals
28
+
29
+ Then it builds a structured production-readiness report with:
30
+
31
+ - Overall score (0-100)
32
+ - Maturity level (`prototype`, `early`, `partial`, `production_ready`)
33
+ - Findings grouped by category
34
+ - Critical issues and warnings
35
+ - Passed checks
36
+ - Suggested next steps
37
+ - Technical evidence for each finding
38
+
39
+ From the report, ProdKit can also build a deterministic remediation plan with phases, task priorities, effort estimates, and test suggestions.
40
+
41
+ ## Installation
42
+
43
+ ### Local development install
44
+
45
+ 1. Install dependencies:
46
+
47
+ ```bash
48
+ npm install
49
+ ```
50
+
51
+ 2. Build CLI:
52
+
53
+ ```bash
54
+ npm run build
55
+ ```
56
+
57
+ 3. Link globally for local usage:
58
+
59
+ ```bash
60
+ npm link
61
+ ```
62
+
63
+ After linking, use:
64
+
65
+ ```bash
66
+ prodkit --help
67
+ ```
68
+
69
+ ## Local development
70
+
71
+ ```bash
72
+ npm install
73
+ npm run build
74
+ npm test
75
+ ```
76
+
77
+ Optional lint:
78
+
79
+ ```bash
80
+ npm run lint
81
+ ```
82
+
83
+ ## Commands
84
+
85
+ Diagnostic command:
86
+
87
+ ```bash
88
+ prodkit analyze <path-to-project>
89
+ ```
90
+
91
+ Remediation planning command:
92
+
93
+ ```bash
94
+ prodkit plan <path-to-project>
95
+ ```
96
+
97
+ Options:
98
+
99
+ - `--format markdown|json` output format for the report or plan payload
100
+ - `--summary` print summary only
101
+ - `--output <path>` write output to file
102
+ - `--profile <name>` evaluate expected product capabilities (`static-site`, `internal-tool`, `b2c-app`, `b2b-saas`, `ai-saas`, `marketplace`, `auto`, `observed-only`)
103
+ - `--fail-under <score>` (analyze only) exit with code 1 if the overall score is below the threshold — useful as a CI quality gate
104
+ - `--min-maturity <level>` (analyze only) exit with code 1 if maturity is below `prototype|early|partial|production_ready`
105
+ - `--ai` (analyze only) add an AI stack/architecture insight — opt-in, advisory only, does not affect the score
106
+ - `--ai-review` (analyze only) add an AI semantic review of fine-grained code issues — opt-in, advisory only, does not affect the score
107
+
108
+ Optional AI layer:
109
+
110
+ - **A separate package.** The AI features live in `@produtype/ai`, which is commercial
111
+ and is not a dependency of this package. An open source install runs the
112
+ deterministic analysis only, and `--ai` reports that the package is not installed
113
+ rather than failing.
114
+ - ProdKit stays deterministic, offline, and read-only: nothing here makes a network
115
+ call.
116
+ - They require an `ANTHROPIC_API_KEY` and the optional `@anthropic-ai/sdk` package (`npm install @anthropic-ai/sdk`). Without them, the deterministic analysis is unaffected.
117
+ - Repository content is redacted of secrets before anything is sent, and AI output is advisory only — it never changes the deterministic score.
118
+
119
+ Profile warning:
120
+
121
+ - Without a product profile, ProdKit only scores observed deterministic findings. Use `--profile` to evaluate expected product capabilities.
122
+
123
+ Inconclusive assessments:
124
+
125
+ - If no stack signals and no package manifests are detected, the report is marked **inconclusive** and the score is capped at 39 (`prototype`). An unrecognized project is never scored as production ready.
126
+
127
+ ## Examples
128
+
129
+ ```bash
130
+ prodkit analyze ../my-app
131
+ prodkit analyze ../my-app --profile b2b-saas
132
+ prodkit analyze ../my-app --profile auto
133
+ prodkit analyze ../my-app --summary
134
+ prodkit analyze ../my-app --format markdown
135
+ prodkit analyze ../my-app --format json
136
+ prodkit analyze ../my-app --output prodkit-report.md
137
+ prodkit analyze tests/fixtures/express-basic --format json --output report.json
138
+ prodkit analyze ../my-app --fail-under 65
139
+ prodkit analyze ../my-app --min-maturity partial
140
+ prodkit plan ../my-app
141
+ prodkit plan ../my-app --profile b2b-saas
142
+ prodkit plan ../my-app --profile observed-only
143
+ prodkit plan ../my-app --format markdown
144
+ prodkit plan ../my-app --format json
145
+ prodkit plan ../my-app --output prodkit-plan.md
146
+ ```
147
+
148
+ ## Supported stacks in MVP
149
+
150
+ - Backend: Express, NestJS, Fastify, Next.js (Node); Django, FastAPI, Flask (Python)
151
+ - Frontend: React, Vue, Svelte, Angular, Nuxt, Vite
152
+ - Generic unknown app fallback
153
+
154
+ ## Acceptance commands
155
+
156
+ ```bash
157
+ npm install
158
+ npm run build
159
+ npm test
160
+ npm link
161
+ prodkit analyze tests/fixtures/express-basic --format markdown
162
+ prodkit analyze tests/fixtures/express-basic --format json
163
+ prodkit analyze tests/fixtures/express-basic --output report.md
164
+ prodkit plan tests/fixtures/express-basic
165
+ prodkit plan tests/fixtures/express-basic --format json
166
+ prodkit plan tests/fixtures/express-basic --output prodkit-plan.md
167
+ ```
168
+
169
+ ## Current limitations
170
+
171
+ - Deterministic heuristics only, with no AI explanation layer yet
172
+ - Signal-based stack coverage across common Node/Python/JS frameworks + fallback
173
+ - Signal-based detection can produce false positives/negatives
174
+ - Plan output is deterministic and read-only only
175
+ - No cloud dashboard/UI in MVP
176
+
177
+ ## Roadmap
178
+
179
+ 1. Deterministic analyzer
180
+ 2. Deterministic remediation planning
181
+ 3. AI explanation layer
182
+ 4. Apply/generate PRs
183
+ 5. Cloud GitHub integration
184
+ 6. Framework adapters
185
+ 7. Dashboard
186
+
187
+ ## Project layout
188
+
189
+ ```text
190
+ prodkit/
191
+ package.json
192
+ tsconfig.json
193
+ README.md
194
+ src/
195
+ index.ts
196
+ cli.ts
197
+ analyzer/
198
+ planner/
199
+ rules/
200
+ report/
201
+ utils/
202
+ tests/
203
+ fixtures/
204
+ analyzer.test.ts
205
+ report.test.ts
206
+ planner.test.ts
207
+ ```
208
+
209
+ ## GitHub Action
210
+
211
+ Gate a pull request on production readiness:
212
+
213
+ ```yaml
214
+ - uses: denisbilli/prodkit@main
215
+ with:
216
+ profile: b2b-saas
217
+ fail-under: "65"
218
+ ```
219
+
220
+ Inputs: `path`, `profile`, `fail-under`, `min-maturity`, `comment-summary`.
221
+ Outputs: `score`, `maturity`, `launch-ready`.
222
+
223
+ The executive summary is written to the job summary even when the thresholds fail the
224
+ job — a check that fails without saying why is worse than no check at all.
225
+
226
+ Requires the package to be published; until then, point the action at a local
227
+ checkout.
228
+
229
+ ## MCP server
230
+
231
+ ProdKit ships an MCP server so an agent can assess a repository without leaving the
232
+ editor. It exposes the deterministic analysis only, makes no network calls, and the
233
+ code being analysed never leaves the machine.
234
+
235
+ Tools: `analyze_project`, `plan_remediation`, `compare_profiles`, `list_profiles`.
236
+
237
+ ProdKit is not on npm yet — the name `prodkit` is taken by an unrelated package — so
238
+ install it from a local checkout:
239
+
240
+ ```bash
241
+ npm install && npm run build && npm link
242
+ ```
243
+
244
+ Claude Code:
245
+
246
+ ```bash
247
+ claude mcp add prodkit -- prodkit-mcp
248
+ ```
249
+
250
+ Or, in a client that reads a JSON config:
251
+
252
+ ```json
253
+ {
254
+ "mcpServers": {
255
+ "prodkit": {
256
+ "command": "prodkit-mcp"
257
+ }
258
+ }
259
+ }
260
+ ```
261
+
262
+ `compare_profiles` is the one to reach for first: it scores the same repository
263
+ against every product profile in a single call, which is the question ProdKit exists
264
+ to answer.
265
+
266
+ ## Releases
267
+
268
+ Published from CI on a version tag, with
269
+ [npm provenance](https://docs.npmjs.com/generating-provenance-statements): the tarball
270
+ is cryptographically linked to the commit and the workflow run that produced it, so it
271
+ can be verified rather than merely trusted. No publish token ever sits on a developer
272
+ machine.
273
+
274
+ To cut a release:
275
+
276
+ ```bash
277
+ npm version patch # or minor / major — commits and tags
278
+ git push --follow-tags
279
+ ```
280
+
281
+ The workflow refuses to publish if the tag and `package.json` disagree, and runs a
282
+ check that the commercial AI layer is absent from `dist` before anything leaves.
283
+ Release notes are generated from the commits since the previous tag.
284
+
285
+ ## License
286
+
287
+
288
+
289
+ MIT
@@ -0,0 +1,2 @@
1
+ import type { ProjectAnalysis } from './types';
2
+ export declare function analyzeProject(projectPath: string): Promise<ProjectAnalysis>;
@@ -0,0 +1,365 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.analyzeProject = analyzeProject;
37
+ const path = __importStar(require("path"));
38
+ const zod_1 = require("zod");
39
+ const fileScanner_1 = require("../utils/fileScanner");
40
+ const readTextFileSafe_1 = require("../utils/readTextFileSafe");
41
+ const detectPackageManager_1 = require("./detectPackageManager");
42
+ const detectFrontend_1 = require("./detectFrontend");
43
+ const detectBackend_1 = require("./detectBackend");
44
+ const detectDatabase_1 = require("./detectDatabase");
45
+ const detectDocker_1 = require("./detectDocker");
46
+ const detectEnv_1 = require("./detectEnv");
47
+ const detectAuth_1 = require("./detectAuth");
48
+ const detectSecurity_1 = require("./detectSecurity");
49
+ const detectUploads_1 = require("./detectUploads");
50
+ const detectGdpr_1 = require("./detectGdpr");
51
+ const detectBilling_1 = require("./detectBilling");
52
+ const detectObservability_1 = require("./detectObservability");
53
+ const detectJobs_1 = require("./detectJobs");
54
+ const detectMarketplace_1 = require("./detectMarketplace");
55
+ const detectAiSafety_1 = require("./detectAiSafety");
56
+ const detectNotifications_1 = require("./detectNotifications");
57
+ const detectDeployment_1 = require("./detectDeployment");
58
+ const detectStack_1 = require("./detectStack");
59
+ const packageJsonSchema = zod_1.z
60
+ .object({
61
+ name: zod_1.z.string().optional(),
62
+ version: zod_1.z.string().optional(),
63
+ dependencies: zod_1.z.record(zod_1.z.string()).optional(),
64
+ devDependencies: zod_1.z.record(zod_1.z.string()).optional(),
65
+ scripts: zod_1.z.record(zod_1.z.string()).optional(),
66
+ })
67
+ .passthrough();
68
+ function parseRequirements(text) {
69
+ if (!text)
70
+ return [];
71
+ return text
72
+ .split(/\r?\n/)
73
+ .map((l) => l.trim())
74
+ .filter((l) => l && !l.startsWith('#'))
75
+ .map((l) => l.split(/[<=>~![]/)[0].trim().toLowerCase())
76
+ .filter(Boolean);
77
+ }
78
+ function normalizePyDepSpec(spec) {
79
+ return spec.split(/[\s<>=!~;[(]/)[0].trim().toLowerCase();
80
+ }
81
+ function parsePyproject(text) {
82
+ if (!text)
83
+ return [];
84
+ const deps = [];
85
+ let section = '';
86
+ let inDependencyArray = false;
87
+ for (const raw of text.split(/\r?\n/)) {
88
+ const line = raw.trim();
89
+ if (!line || line.startsWith('#'))
90
+ continue;
91
+ const header = line.match(/^\[+([^\]]+)]+$/);
92
+ if (header) {
93
+ section = header[1].trim();
94
+ inDependencyArray = false;
95
+ continue;
96
+ }
97
+ // Poetry-style tables: each key of the section is a dependency name.
98
+ const isPoetryDepsSection = section === 'tool.poetry.dependencies'
99
+ || section === 'tool.poetry.dev-dependencies'
100
+ || /^tool\.poetry\.group\.[^.]+\.dependencies$/.test(section);
101
+ if (isPoetryDepsSection) {
102
+ const match = line.match(/^([A-Za-z0-9_.-]+)\s*=/);
103
+ if (match && match[1].toLowerCase() !== 'python')
104
+ deps.push(match[1].toLowerCase());
105
+ continue;
106
+ }
107
+ // PEP 621 / uv style: dependency specs live inside string arrays.
108
+ const isDepArraySection = section === 'project.optional-dependencies'
109
+ || section === 'dependency-groups'
110
+ || section === 'tool.uv';
111
+ if (section === 'project' || isDepArraySection || inDependencyArray) {
112
+ if (line.includes('include-group'))
113
+ continue;
114
+ const startsArray = section === 'project'
115
+ ? /^dependencies\s*=\s*\[/.test(line)
116
+ : isDepArraySection && /^[A-Za-z0-9_-]+\s*=\s*\[/.test(line);
117
+ if (!inDependencyArray && !startsArray)
118
+ continue;
119
+ for (const quoted of line.matchAll(/["']([^"']+)["']/g)) {
120
+ const name = normalizePyDepSpec(quoted[1]);
121
+ if (name)
122
+ deps.push(name);
123
+ }
124
+ // Only a closing bracket outside quoted specs ends the array — specs
125
+ // like "fastapi[standard]>=0.114" contain brackets of their own.
126
+ const lineWithoutStrings = line.replace(/["'][^"']*["']/g, '');
127
+ inDependencyArray = (inDependencyArray || startsArray) && !lineWithoutStrings.includes(']');
128
+ }
129
+ }
130
+ return deps;
131
+ }
132
+ function isTestOrExamplePath(file) {
133
+ return /(^|\/)(__tests__|tests?|test-data|fixtures|frontend-example)(\/|$)/i.test(file)
134
+ || /(^|\/)test[-_][^/]+\.(ts|tsx|js|jsx|mjs|cjs|py)$/i.test(file)
135
+ || /\.(test|spec)\.(ts|tsx|js|jsx|mjs|cjs|py)$/i.test(file);
136
+ }
137
+ function pickSource(files) {
138
+ return files.filter((f) => /\.(ts|tsx|js|jsx|mjs|cjs|py)$/.test(f) && !isTestOrExamplePath(f));
139
+ }
140
+ function pickConfig(files) {
141
+ return files.filter((f) => /(package\.json|tsconfig|vite\.config|docker|compose|requirements\.txt|pyproject\.toml|settings\.py|\.env)/i.test(f));
142
+ }
143
+ function workspaceRootFromFile(file) {
144
+ const i = file.lastIndexOf('/');
145
+ return i === -1 ? '.' : file.slice(0, i);
146
+ }
147
+ function unique(items) {
148
+ return Array.from(new Set(items));
149
+ }
150
+ function mergeDeps(target, source) {
151
+ if (!source)
152
+ return;
153
+ for (const [k, v] of Object.entries(source))
154
+ target[k.toLowerCase()] = v;
155
+ }
156
+ function detectWorkspaceFrontend(npmDeps) {
157
+ const frameworks = [];
158
+ if (npmDeps.react || npmDeps['react-dom'])
159
+ frameworks.push('react');
160
+ if (npmDeps.vite || npmDeps['@vitejs/plugin-react'])
161
+ frameworks.push('vite');
162
+ if (npmDeps.vue)
163
+ frameworks.push('vue');
164
+ if (npmDeps.nuxt)
165
+ frameworks.push('nuxt');
166
+ if (npmDeps.svelte || npmDeps['@sveltejs/kit'])
167
+ frameworks.push('svelte');
168
+ if (npmDeps['@angular/core'])
169
+ frameworks.push('angular');
170
+ if (npmDeps.electron)
171
+ frameworks.push('electron');
172
+ if (npmDeps['react-router-dom'])
173
+ frameworks.push('react-router-dom');
174
+ if (npmDeps.tailwindcss)
175
+ frameworks.push('tailwindcss');
176
+ return unique(frameworks);
177
+ }
178
+ function detectWorkspaceBackend(npmDeps, pythonDeps) {
179
+ const frameworks = [];
180
+ if (npmDeps.express)
181
+ frameworks.push('express');
182
+ if (npmDeps.next)
183
+ frameworks.push('next');
184
+ if (npmDeps['@nestjs/core'])
185
+ frameworks.push('nestjs');
186
+ if (npmDeps.fastify)
187
+ frameworks.push('fastify');
188
+ if (pythonDeps.includes('django'))
189
+ frameworks.push('django');
190
+ if (pythonDeps.includes('fastapi'))
191
+ frameworks.push('fastapi');
192
+ if (pythonDeps.includes('flask'))
193
+ frameworks.push('flask');
194
+ return unique(frameworks);
195
+ }
196
+ function detectWorkspaceDatabases(npmDeps, pythonDeps) {
197
+ const dbs = [];
198
+ if (npmDeps.pg || pythonDeps.includes('psycopg2'))
199
+ dbs.push('postgres');
200
+ if (npmDeps.redis || npmDeps.ioredis || pythonDeps.includes('redis'))
201
+ dbs.push('redis');
202
+ if (npmDeps.mysql2 || pythonDeps.includes('mysqlclient'))
203
+ dbs.push('mysql');
204
+ if (npmDeps.mongodb || npmDeps.mongoose)
205
+ dbs.push('mongodb');
206
+ if (npmDeps.sqlite3 || npmDeps['better-sqlite3'])
207
+ dbs.push('sqlite');
208
+ return unique(dbs);
209
+ }
210
+ function resolveWorkspacePackageManager(workspace) {
211
+ if (workspace.lockfiles.includes('pnpm-lock.yaml'))
212
+ return { manager: 'pnpm', confidence: 'lockfile', warnings: [] };
213
+ if (workspace.lockfiles.includes('package-lock.json'))
214
+ return { manager: 'npm', confidence: 'lockfile', warnings: [] };
215
+ if (workspace.lockfiles.includes('yarn.lock'))
216
+ return { manager: 'yarn', confidence: 'lockfile', warnings: [] };
217
+ if (workspace.lockfiles.includes('poetry.lock'))
218
+ return { manager: 'poetry', confidence: 'lockfile', warnings: [] };
219
+ if (workspace.packageJsonPath)
220
+ return { manager: 'npm', confidence: 'manifest', warnings: ['package-lock missing'] };
221
+ if (workspace.pyprojectPath)
222
+ return { manager: 'poetry', confidence: 'manifest', warnings: [] };
223
+ if (workspace.requirementsPath)
224
+ return { manager: 'pip', confidence: 'manifest', warnings: [] };
225
+ return { manager: 'unknown', confidence: 'unknown', warnings: [] };
226
+ }
227
+ async function analyzeProject(projectPath) {
228
+ const root = path.resolve(projectPath);
229
+ const allFiles = await (0, fileScanner_1.scanFiles)({ cwd: root });
230
+ const sourceFiles = pickSource(allFiles);
231
+ const configFiles = pickConfig(allFiles);
232
+ const packageJsonRaw = await (0, readTextFileSafe_1.readJsonSafe)(root, 'package.json');
233
+ const packageJson = packageJsonRaw ? packageJsonSchema.parse(packageJsonRaw) : null;
234
+ const packageJsonFiles = allFiles.filter((f) => f.endsWith('package.json'));
235
+ const requirementsFiles = allFiles.filter((f) => /(^|\/)requirements\.txt$/i.test(f));
236
+ const pyprojectFiles = allFiles.filter((f) => /(^|\/)pyproject\.toml$/i.test(f));
237
+ const lockfiles = allFiles.filter((f) => /(^|\/)(pnpm-lock\.yaml|package-lock\.json|yarn\.lock|poetry\.lock)$/i.test(f));
238
+ const workspaceRoots = new Set(['.']);
239
+ for (const file of [...packageJsonFiles, ...requirementsFiles, ...pyprojectFiles, ...lockfiles]) {
240
+ workspaceRoots.add(workspaceRootFromFile(file));
241
+ }
242
+ const workspaces = [];
243
+ for (const wsRoot of Array.from(workspaceRoots).sort()) {
244
+ const packageJsonPath = wsRoot === '.' ? 'package.json' : `${wsRoot}/package.json`;
245
+ const requirementsPath = wsRoot === '.' ? 'requirements.txt' : `${wsRoot}/requirements.txt`;
246
+ const pyprojectPath = wsRoot === '.' ? 'pyproject.toml' : `${wsRoot}/pyproject.toml`;
247
+ const packageJsonRawForWorkspace = packageJsonFiles.includes(packageJsonPath)
248
+ ? await (0, readTextFileSafe_1.readJsonSafe)(root, packageJsonPath)
249
+ : null;
250
+ const packageJsonForWorkspace = packageJsonRawForWorkspace ? packageJsonSchema.parse(packageJsonRawForWorkspace) : null;
251
+ const reqText = requirementsFiles.includes(requirementsPath)
252
+ ? await (0, readTextFileSafe_1.readTextFileSafe)(root, requirementsPath)
253
+ : null;
254
+ const pyprojectText = pyprojectFiles.includes(pyprojectPath)
255
+ ? await (0, readTextFileSafe_1.readTextFileSafe)(root, pyprojectPath)
256
+ : null;
257
+ const wsLockfiles = lockfiles
258
+ .filter((f) => workspaceRootFromFile(f) === wsRoot)
259
+ .map((f) => f.split('/').pop() ?? f);
260
+ workspaces.push({
261
+ root: wsRoot,
262
+ packageJsonPath: packageJsonFiles.includes(packageJsonPath) ? packageJsonPath : undefined,
263
+ packageJson: packageJsonForWorkspace,
264
+ requirementsPath: requirementsFiles.includes(requirementsPath) ? requirementsPath : undefined,
265
+ requirementsDeps: parseRequirements(reqText),
266
+ pyprojectPath: pyprojectFiles.includes(pyprojectPath) ? pyprojectPath : undefined,
267
+ pyprojectDeps: parsePyproject(pyprojectText),
268
+ lockfiles: wsLockfiles,
269
+ });
270
+ }
271
+ const pythonDeps = unique(workspaces.flatMap((w) => [...w.requirementsDeps, ...w.pyprojectDeps]));
272
+ const npmDeps = {};
273
+ for (const workspace of workspaces) {
274
+ mergeDeps(npmDeps, workspace.packageJson?.dependencies);
275
+ mergeDeps(npmDeps, workspace.packageJson?.devDependencies);
276
+ }
277
+ const workspaceStacks = workspaces.map((workspace) => {
278
+ const wsNpmDeps = {};
279
+ mergeDeps(wsNpmDeps, workspace.packageJson?.dependencies);
280
+ mergeDeps(wsNpmDeps, workspace.packageJson?.devDependencies);
281
+ const wsPythonDeps = unique([...workspace.requirementsDeps, ...workspace.pyprojectDeps]);
282
+ const pm = resolveWorkspacePackageManager(workspace);
283
+ return {
284
+ root: workspace.root,
285
+ frontend: detectWorkspaceFrontend(wsNpmDeps),
286
+ backend: detectWorkspaceBackend(wsNpmDeps, wsPythonDeps),
287
+ databases: detectWorkspaceDatabases(wsNpmDeps, wsPythonDeps),
288
+ packageManager: pm.manager,
289
+ packageManagerConfidence: pm.confidence,
290
+ warnings: pm.warnings,
291
+ };
292
+ }).filter((w) => w.frontend.length > 0
293
+ || w.backend.length > 0
294
+ || w.databases.length > 0
295
+ || w.packageManager !== 'unknown');
296
+ const ctx = {
297
+ root,
298
+ files: { all: allFiles, source: sourceFiles, config: configFiles },
299
+ packageJson,
300
+ pythonDeps,
301
+ npmDeps,
302
+ workspaces,
303
+ };
304
+ const [pm, frontend, backend, database, docker, env, auth, security, uploads, gdpr, billing, observability, jobs, marketplace, aiSafety, engagement, deployment] = await Promise.all([
305
+ (0, detectPackageManager_1.detectPackageManager)(ctx),
306
+ (0, detectFrontend_1.detectFrontend)(ctx),
307
+ (0, detectBackend_1.detectBackend)(ctx),
308
+ (0, detectDatabase_1.detectDatabase)(ctx),
309
+ (0, detectDocker_1.detectDocker)(ctx),
310
+ (0, detectEnv_1.detectEnv)(ctx),
311
+ (0, detectAuth_1.detectAuth)(ctx),
312
+ (0, detectSecurity_1.detectSecurity)(ctx),
313
+ (0, detectUploads_1.detectUploads)(ctx),
314
+ (0, detectGdpr_1.detectGdpr)(ctx),
315
+ (0, detectBilling_1.detectBilling)(ctx),
316
+ (0, detectObservability_1.detectObservability)(ctx),
317
+ (0, detectJobs_1.detectJobs)(ctx),
318
+ (0, detectMarketplace_1.detectMarketplace)(ctx),
319
+ (0, detectAiSafety_1.detectAiSafety)(ctx),
320
+ (0, detectNotifications_1.detectEngagement)(ctx),
321
+ (0, detectDeployment_1.detectDeployment)(ctx),
322
+ ]);
323
+ const detectors = (0, detectStack_1.mergeDetectors)([
324
+ pm.result,
325
+ frontend.result,
326
+ backend.result,
327
+ database.result,
328
+ docker,
329
+ ...env,
330
+ ...auth,
331
+ security,
332
+ uploads,
333
+ ...gdpr,
334
+ ...billing,
335
+ observability,
336
+ jobs,
337
+ ...marketplace,
338
+ ...aiSafety,
339
+ ...engagement,
340
+ deployment,
341
+ ]);
342
+ return {
343
+ projectPath: root,
344
+ scannedAt: new Date().toISOString(),
345
+ stack: (0, detectStack_1.buildStackInfo)({
346
+ frontend: frontend.frameworks,
347
+ backend: backend.frameworks,
348
+ databases: database.databases,
349
+ packageManager: pm.manager,
350
+ packageManagerConfidence: pm.confidence,
351
+ warnings: pm.warnings,
352
+ workspaces: workspaceStacks,
353
+ files: allFiles,
354
+ }),
355
+ packageJson,
356
+ pythonDeps,
357
+ workspaceStacks,
358
+ files: {
359
+ all: allFiles,
360
+ source: sourceFiles,
361
+ config: configFiles,
362
+ },
363
+ detectors,
364
+ };
365
+ }
@@ -0,0 +1,3 @@
1
+ import type { DetectorResult } from './types';
2
+ import type { DetectContext } from './detectContext';
3
+ export declare function detectAiSafety(ctx: DetectContext): Promise<DetectorResult[]>;