@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.
- package/LICENSE +21 -0
- package/README.md +289 -0
- package/dist/analyzer/analyzeProject.d.ts +2 -0
- package/dist/analyzer/analyzeProject.js +365 -0
- package/dist/analyzer/detectAiSafety.d.ts +3 -0
- package/dist/analyzer/detectAiSafety.js +74 -0
- package/dist/analyzer/detectAuth.d.ts +3 -0
- package/dist/analyzer/detectAuth.js +192 -0
- package/dist/analyzer/detectBackend.d.ts +6 -0
- package/dist/analyzer/detectBackend.js +89 -0
- package/dist/analyzer/detectBilling.d.ts +3 -0
- package/dist/analyzer/detectBilling.js +78 -0
- package/dist/analyzer/detectContext.d.ts +24 -0
- package/dist/analyzer/detectContext.js +18 -0
- package/dist/analyzer/detectDatabase.d.ts +6 -0
- package/dist/analyzer/detectDatabase.js +77 -0
- package/dist/analyzer/detectDeployment.d.ts +3 -0
- package/dist/analyzer/detectDeployment.js +28 -0
- package/dist/analyzer/detectDocker.d.ts +3 -0
- package/dist/analyzer/detectDocker.js +43 -0
- package/dist/analyzer/detectEnv.d.ts +3 -0
- package/dist/analyzer/detectEnv.js +89 -0
- package/dist/analyzer/detectFrontend.d.ts +6 -0
- package/dist/analyzer/detectFrontend.js +49 -0
- package/dist/analyzer/detectGdpr.d.ts +3 -0
- package/dist/analyzer/detectGdpr.js +53 -0
- package/dist/analyzer/detectJobs.d.ts +3 -0
- package/dist/analyzer/detectJobs.js +30 -0
- package/dist/analyzer/detectMarketplace.d.ts +3 -0
- package/dist/analyzer/detectMarketplace.js +126 -0
- package/dist/analyzer/detectNotifications.d.ts +3 -0
- package/dist/analyzer/detectNotifications.js +81 -0
- package/dist/analyzer/detectObservability.d.ts +3 -0
- package/dist/analyzer/detectObservability.js +44 -0
- package/dist/analyzer/detectPackageManager.d.ts +14 -0
- package/dist/analyzer/detectPackageManager.js +76 -0
- package/dist/analyzer/detectSecurity.d.ts +3 -0
- package/dist/analyzer/detectSecurity.js +145 -0
- package/dist/analyzer/detectStack.d.ts +13 -0
- package/dist/analyzer/detectStack.js +36 -0
- package/dist/analyzer/detectUploads.d.ts +3 -0
- package/dist/analyzer/detectUploads.js +73 -0
- package/dist/analyzer/types.d.ts +62 -0
- package/dist/analyzer/types.js +2 -0
- package/dist/api.d.ts +31 -0
- package/dist/api.js +34 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +268 -0
- package/dist/expectations/evaluateExpectations.d.ts +9 -0
- package/dist/expectations/evaluateExpectations.js +377 -0
- package/dist/expectations/inferProductProfile.d.ts +3 -0
- package/dist/expectations/inferProductProfile.js +39 -0
- package/dist/expectations/productProfiles.d.ts +57 -0
- package/dist/expectations/productProfiles.js +478 -0
- package/dist/expectations/types.d.ts +70 -0
- package/dist/expectations/types.js +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +9 -0
- package/dist/mcp/index.d.ts +2 -0
- package/dist/mcp/index.js +9 -0
- package/dist/mcp/server.d.ts +3 -0
- package/dist/mcp/server.js +155 -0
- package/dist/planner/buildPlan.d.ts +3 -0
- package/dist/planner/buildPlan.js +187 -0
- package/dist/planner/jsonPlan.d.ts +2 -0
- package/dist/planner/jsonPlan.js +6 -0
- package/dist/planner/markdownPlan.d.ts +2 -0
- package/dist/planner/markdownPlan.js +95 -0
- package/dist/planner/remediationCatalog.d.ts +3 -0
- package/dist/planner/remediationCatalog.js +1200 -0
- package/dist/planner/types.d.ts +63 -0
- package/dist/planner/types.js +2 -0
- package/dist/report/buildReport.d.ts +7 -0
- package/dist/report/buildReport.js +198 -0
- package/dist/report/businessImpact.d.ts +4 -0
- package/dist/report/businessImpact.js +86 -0
- package/dist/report/categoryScores.d.ts +21 -0
- package/dist/report/categoryScores.js +65 -0
- package/dist/report/complianceMapping.d.ts +19 -0
- package/dist/report/complianceMapping.js +122 -0
- package/dist/report/executiveSummary.d.ts +26 -0
- package/dist/report/executiveSummary.js +117 -0
- package/dist/report/jsonReport.d.ts +2 -0
- package/dist/report/jsonReport.js +6 -0
- package/dist/report/markdownReport.d.ts +2 -0
- package/dist/report/markdownReport.js +256 -0
- package/dist/report/score.d.ts +3 -0
- package/dist/report/score.js +29 -0
- package/dist/report/types.d.ts +77 -0
- package/dist/report/types.js +2 -0
- package/dist/rules/ruleEngine.d.ts +3 -0
- package/dist/rules/ruleEngine.js +13 -0
- package/dist/rules/rules.d.ts +2 -0
- package/dist/rules/rules.js +560 -0
- package/dist/rules/types.d.ts +16 -0
- package/dist/rules/types.js +2 -0
- package/dist/utils/fileScanner.d.ts +14 -0
- package/dist/utils/fileScanner.js +95 -0
- package/dist/utils/pathUtils.d.ts +2 -0
- package/dist/utils/pathUtils.js +44 -0
- package/dist/utils/readTextFileSafe.d.ts +6 -0
- package/dist/utils/readTextFileSafe.js +76 -0
- package/dist/utils/textSearch.d.ts +11 -0
- package/dist/utils/textSearch.js +36 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +4 -0
- 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,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
|
+
}
|