@produtype/core 0.3.3 → 0.5.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/README.md +25 -1
- package/dist/analyzer/analyzeProject.js +74 -0
- package/dist/analyzer/catalogue.d.ts +13 -0
- package/dist/analyzer/catalogue.js +38 -2
- package/dist/analyzer/detectContext.d.ts +6 -0
- package/dist/analyzer/detectContext.js +12 -0
- package/dist/analyzer/detectMobile.js +43 -2
- package/dist/analyzer/detectStack.js +5 -8
- package/dist/mcp/server.d.ts +2 -2
- package/dist/mcp/server.js +33 -6
- package/dist/report/buildReport.js +9 -2
- package/package.json +10 -2
package/README.md
CHANGED
|
@@ -40,6 +40,27 @@ From the report, ProdKit can also build a deterministic remediation plan with ph
|
|
|
40
40
|
|
|
41
41
|
## Installation
|
|
42
42
|
|
|
43
|
+
```bash
|
|
44
|
+
npm install -g @produtype/core
|
|
45
|
+
prodkit analyze .
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
The MCP server needs one extra package, and only if you use it:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
npm install @modelcontextprotocol/sdk # for prodkit-mcp
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
It is an optional peer dependency rather than a dependency because it brings 164
|
|
55
|
+
packages with it — nine tenths of what this package used to install — for a server most
|
|
56
|
+
people never run, along with network, shell and eval access that the analyzer itself
|
|
57
|
+
does not use. Installing this package alone brings 19.
|
|
58
|
+
|
|
59
|
+
The command is `prodkit`; the package is `@produtype/core`. They differ on purpose:
|
|
60
|
+
`prodkit` on npm is an unrelated and actively maintained package, so this one is
|
|
61
|
+
published under the `@produtype` scope. `npm i prodkit` installs somebody else's
|
|
62
|
+
project.
|
|
63
|
+
|
|
43
64
|
### Local development install
|
|
44
65
|
|
|
45
66
|
1. Install dependencies:
|
|
@@ -155,7 +176,7 @@ _Generated from the analyzer itself — run `npm run docs:stacks` after changing
|
|
|
155
176
|
|
|
156
177
|
- **Backend:** Express, Next.js, NestJS, Fastify, Hono, Elysia, Koa, AdonisJS, SvelteKit, Remix, Nuxt, Nitro, Astro, Django, Flask, FastAPI, Litestar, Sanic, Tornado, aiohttp, Starlette, Gin, Echo, Fiber, chi, Gorilla, Beego, Go, Rails, Sinatra, Hanami, Roda, Grape, Ruby, Laravel, Symfony, Slim, CodeIgniter, CakePHP, Yii, PHP, ASP.NET Core, .NET
|
|
157
178
|
- **Frontend:** React, Vite, Vue, Nuxt, Svelte, Angular, Astro, Solid, Qwik, Preact, Remix, htmx, Tailwind CSS, Electron
|
|
158
|
-
- **Mobile:** Flutter
|
|
179
|
+
- **Mobile:** Flutter, React Native, iOS (native), Android (native)
|
|
159
180
|
- **Databases:** Postgres, MySQL, SQLite, SQL Server, MongoDB, Redis, Firestore, DynamoDB, Convex
|
|
160
181
|
- **Hosted data platforms:** Supabase, Firebase, PlanetScale, Neon, Vercel Postgres, Turso, Upstash, DynamoDB, Convex
|
|
161
182
|
- **ORMs:** Prisma, Drizzle, TypeORM, Sequelize, Knex, MikroORM, Kysely, SQLAlchemy, Tortoise, Peewee
|
|
@@ -173,6 +194,9 @@ How some of these are decided:
|
|
|
173
194
|
- **ASP.NET Core** — the Microsoft.NET.Sdk.Web SDK attribute
|
|
174
195
|
- **.NET** — a .csproj with no web SDK
|
|
175
196
|
- **Flutter** — pubspec.yaml — classified as a client application, not a backend
|
|
197
|
+
- **React Native** — the react-native or expo dependency
|
|
198
|
+
- **iOS (native)** — Info.plist, Package.swift, a Podfile or an .xcodeproj in the tree
|
|
199
|
+
- **Android (native)** — AndroidManifest.xml, or build.gradle in either dialect
|
|
176
200
|
- **Supabase** — recorded alongside the engine it is — Postgres
|
|
177
201
|
- **Firebase** — Firestore
|
|
178
202
|
- **PlanetScale** — MySQL
|
|
@@ -365,6 +365,78 @@ async function analyzeProject(projectPath) {
|
|
|
365
365
|
dartDeps.push(match[1].toLowerCase());
|
|
366
366
|
}
|
|
367
367
|
}
|
|
368
|
+
/**
|
|
369
|
+
* Gradle, read from build.gradle and build.gradle.kts.
|
|
370
|
+
*
|
|
371
|
+
* Both dialects in one reader because the line that matters is nearly the same in
|
|
372
|
+
* each: `implementation 'group:artifact:version'` in Groovy and
|
|
373
|
+
* `implementation("group:artifact:version")` in Kotlin. Only the coordinate is kept —
|
|
374
|
+
* group and artifact, without the version — because every rule downstream asks which
|
|
375
|
+
* library is used, never which release of it.
|
|
376
|
+
*
|
|
377
|
+
* Version catalogs are read too, from their own file rather than by following the
|
|
378
|
+
* alias. That decision was made the other way this morning and was wrong in
|
|
379
|
+
* practice: pointed at android/nowinandroid — Google's own sample, built to
|
|
380
|
+
* demonstrate offline-first — the analyzer reported no local database, because every
|
|
381
|
+
* module says `implementation(libs.room.runtime)` and the coordinates live in
|
|
382
|
+
* `gradle/libs.versions.toml`. Version catalogs are the recommended practice, so
|
|
383
|
+
* skipping them failed precisely on the projects that follow it.
|
|
384
|
+
*
|
|
385
|
+
* No alias resolution is needed: the catalog declares `group` and `name` outright.
|
|
386
|
+
* The cost is that a library declared in the catalog and used by no module is still
|
|
387
|
+
* reported, which is the direction to err in — the catalog is the project's own
|
|
388
|
+
* statement about what it builds with.
|
|
389
|
+
*/
|
|
390
|
+
const gradleDeps = [];
|
|
391
|
+
for (const file of allFiles.filter((f) => /(^|\/)build\.gradle(\.kts)?$/.test(f))) {
|
|
392
|
+
const raw = (await (0, readTextFileSafe_1.readTextFileSafe)(root, file)) ?? '';
|
|
393
|
+
for (const match of raw.matchAll(/\b(?:implementation|api|compileOnly|runtimeOnly|kapt|ksp|annotationProcessor|testImplementation)\s*[( ]\s*["']([^"']+)["']/g)) {
|
|
394
|
+
const [group, artifact] = match[1].split(':');
|
|
395
|
+
if (group && artifact)
|
|
396
|
+
gradleDeps.push(`${group}:${artifact}`.toLowerCase());
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
for (const file of allFiles.filter((f) => /(^|\/)libs\.versions\.toml$/.test(f))) {
|
|
400
|
+
const raw = (await (0, readTextFileSafe_1.readTextFileSafe)(root, file)) ?? '';
|
|
401
|
+
// Two spellings, both common: group and name as separate keys, or one `module`
|
|
402
|
+
// holding the coordinate. Order within the line varies, so each is matched on its
|
|
403
|
+
// own rather than as one pattern per line.
|
|
404
|
+
for (const line of raw.split('\n')) {
|
|
405
|
+
const module = /module\s*=\s*["']([^"':]+):([^"']+)["']/.exec(line);
|
|
406
|
+
if (module) {
|
|
407
|
+
gradleDeps.push(`${module[1]}:${module[2]}`.toLowerCase());
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
const group = /group\s*=\s*["']([^"']+)["']/.exec(line);
|
|
411
|
+
const name = /\bname\s*=\s*["']([^"']+)["']/.exec(line);
|
|
412
|
+
if (group && name)
|
|
413
|
+
gradleDeps.push(`${group[1]}:${name[1]}`.toLowerCase());
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* Swift packages, from Package.swift and the Podfile.
|
|
418
|
+
*
|
|
419
|
+
* Package.swift is Swift source rather than data, so what is read is the one shape
|
|
420
|
+
* that is always there: `.package(url: "https://github.com/owner/name.git", ...)`.
|
|
421
|
+
* The owner and repository name are kept, which is how a Swift dependency is
|
|
422
|
+
* identified in practice — nobody says "the Alamofire product of the Alamofire
|
|
423
|
+
* package".
|
|
424
|
+
*
|
|
425
|
+
* CocoaPods is simpler and still in wide use: `pod 'Alamofire'`.
|
|
426
|
+
*/
|
|
427
|
+
const swiftDeps = [];
|
|
428
|
+
for (const file of allFiles.filter((f) => /(^|\/)Package\.swift$/.test(f))) {
|
|
429
|
+
const raw = (await (0, readTextFileSafe_1.readTextFileSafe)(root, file)) ?? '';
|
|
430
|
+
for (const match of raw.matchAll(/\.package\s*\(\s*url:\s*["']https?:\/\/[^"']*?\/([^/"']+?)\/([^/"']+?)(?:\.git)?["']/g)) {
|
|
431
|
+
swiftDeps.push(`${match[1]}/${match[2]}`.toLowerCase());
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
for (const file of allFiles.filter((f) => /(^|\/)Podfile$/.test(f))) {
|
|
435
|
+
const raw = (await (0, readTextFileSafe_1.readTextFileSafe)(root, file)) ?? '';
|
|
436
|
+
for (const match of raw.matchAll(/^\s*pod\s+["']([^"'/]+)/gm)) {
|
|
437
|
+
swiftDeps.push(match[1].toLowerCase());
|
|
438
|
+
}
|
|
439
|
+
}
|
|
368
440
|
/**
|
|
369
441
|
* .csproj, which is XML rather than JSON or one-entry-per-line.
|
|
370
442
|
*
|
|
@@ -458,6 +530,8 @@ async function analyzeProject(projectPath) {
|
|
|
458
530
|
dotnetDeps: unique(dotnetDeps),
|
|
459
531
|
dotnetWebSdk,
|
|
460
532
|
dartDeps: unique(dartDeps),
|
|
533
|
+
gradleDeps: unique(gradleDeps),
|
|
534
|
+
swiftDeps: unique(swiftDeps),
|
|
461
535
|
npmDeps,
|
|
462
536
|
workspaces,
|
|
463
537
|
};
|
|
@@ -29,6 +29,19 @@ export declare const PHP_BACKEND_FRAMEWORKS: Array<[string, string[]]>;
|
|
|
29
29
|
export declare const PYTHON_BACKEND_FRAMEWORKS: Array<[string, string]>;
|
|
30
30
|
/** Frontend frameworks settled by their signature dependency. */
|
|
31
31
|
export declare const FRONTEND_FRAMEWORKS: Array<[string, string[]]>;
|
|
32
|
+
/**
|
|
33
|
+
* Source languages, and the extensions that say so.
|
|
34
|
+
*
|
|
35
|
+
* `deriveLanguages` knew about TypeScript, JavaScript and Python, so a Go service or a
|
|
36
|
+
* Kotlin app reported `languages: []` while the README and the website both claimed
|
|
37
|
+
* twelve. One table, read by the detector and by the catalogue, is what stops the
|
|
38
|
+
* claim and the code disagreeing again.
|
|
39
|
+
*/
|
|
40
|
+
export declare const LANGUAGES: Array<{
|
|
41
|
+
id: string;
|
|
42
|
+
label: string;
|
|
43
|
+
extensions: RegExp;
|
|
44
|
+
}>;
|
|
32
45
|
export interface CatalogueEntry {
|
|
33
46
|
/** The identifier the report uses. */
|
|
34
47
|
id: string;
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* that loses users.
|
|
16
16
|
*/
|
|
17
17
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
-
exports.FRONTEND_FRAMEWORKS = exports.PYTHON_BACKEND_FRAMEWORKS = exports.PHP_BACKEND_FRAMEWORKS = exports.RUBY_BACKEND_FRAMEWORKS = exports.GO_BACKEND_FRAMEWORKS = exports.NODE_BACKEND_FRAMEWORKS = void 0;
|
|
18
|
+
exports.LANGUAGES = exports.FRONTEND_FRAMEWORKS = exports.PYTHON_BACKEND_FRAMEWORKS = exports.PHP_BACKEND_FRAMEWORKS = exports.RUBY_BACKEND_FRAMEWORKS = exports.GO_BACKEND_FRAMEWORKS = exports.NODE_BACKEND_FRAMEWORKS = void 0;
|
|
19
19
|
exports.labelFor = labelFor;
|
|
20
20
|
exports.hasLabel = hasLabel;
|
|
21
21
|
exports.supportedStacks = supportedStacks;
|
|
@@ -85,6 +85,28 @@ exports.FRONTEND_FRAMEWORKS = [
|
|
|
85
85
|
['remix', ['@remix-run/react']],
|
|
86
86
|
['htmx', ['htmx.org']],
|
|
87
87
|
];
|
|
88
|
+
/**
|
|
89
|
+
* Source languages, and the extensions that say so.
|
|
90
|
+
*
|
|
91
|
+
* `deriveLanguages` knew about TypeScript, JavaScript and Python, so a Go service or a
|
|
92
|
+
* Kotlin app reported `languages: []` while the README and the website both claimed
|
|
93
|
+
* twelve. One table, read by the detector and by the catalogue, is what stops the
|
|
94
|
+
* claim and the code disagreeing again.
|
|
95
|
+
*/
|
|
96
|
+
exports.LANGUAGES = [
|
|
97
|
+
{ id: 'typescript', label: 'TypeScript', extensions: /\.(ts|tsx)$/ },
|
|
98
|
+
{ id: 'javascript', label: 'JavaScript', extensions: /\.(js|jsx|mjs|cjs)$/ },
|
|
99
|
+
{ id: 'python', label: 'Python', extensions: /\.py$/ },
|
|
100
|
+
{ id: 'php', label: 'PHP', extensions: /\.php$/ },
|
|
101
|
+
{ id: 'go', label: 'Go', extensions: /\.go$/ },
|
|
102
|
+
{ id: 'ruby', label: 'Ruby', extensions: /\.rb$/ },
|
|
103
|
+
{ id: 'java', label: 'Java', extensions: /\.java$/ },
|
|
104
|
+
{ id: 'csharp', label: 'C#', extensions: /\.cs$/ },
|
|
105
|
+
{ id: 'rust', label: 'Rust', extensions: /\.rs$/ },
|
|
106
|
+
{ id: 'kotlin', label: 'Kotlin', extensions: /\.(kt|kts)$/ },
|
|
107
|
+
{ id: 'swift', label: 'Swift', extensions: /\.swift$/ },
|
|
108
|
+
{ id: 'dart', label: 'Dart', extensions: /\.dart$/ },
|
|
109
|
+
];
|
|
88
110
|
/** Display names for ids the tables above produce. */
|
|
89
111
|
const LABELS = {
|
|
90
112
|
next: 'Next.js',
|
|
@@ -133,6 +155,9 @@ const LABELS = {
|
|
|
133
155
|
tailwindcss: 'Tailwind CSS',
|
|
134
156
|
electron: 'Electron',
|
|
135
157
|
flutter: 'Flutter',
|
|
158
|
+
'react-native': 'React Native',
|
|
159
|
+
ios: 'iOS (native)',
|
|
160
|
+
android: 'Android (native)',
|
|
136
161
|
express: 'Express',
|
|
137
162
|
flask: 'Flask',
|
|
138
163
|
fastapi: 'FastAPI',
|
|
@@ -233,6 +258,17 @@ function supportedStacks() {
|
|
|
233
258
|
label: 'Flutter',
|
|
234
259
|
detectedFrom: 'pubspec.yaml — classified as a client application, not a backend',
|
|
235
260
|
},
|
|
261
|
+
{ id: 'react-native', label: 'React Native', detectedFrom: 'the react-native or expo dependency' },
|
|
262
|
+
{
|
|
263
|
+
id: 'ios',
|
|
264
|
+
label: 'iOS (native)',
|
|
265
|
+
detectedFrom: 'Info.plist, Package.swift, a Podfile or an .xcodeproj in the tree',
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
id: 'android',
|
|
269
|
+
label: 'Android (native)',
|
|
270
|
+
detectedFrom: 'AndroidManifest.xml, or build.gradle in either dialect',
|
|
271
|
+
},
|
|
236
272
|
],
|
|
237
273
|
databases: entries(['postgres', 'mysql', 'sqlite', 'sqlserver', 'mongodb', 'redis', 'firestore', 'dynamodb', 'convex']),
|
|
238
274
|
dataPlatforms: [
|
|
@@ -258,6 +294,6 @@ function supportedStacks() {
|
|
|
258
294
|
'tortoise',
|
|
259
295
|
'peewee',
|
|
260
296
|
]),
|
|
261
|
-
languages:
|
|
297
|
+
languages: exports.LANGUAGES.map(({ id, label }) => ({ id, label })),
|
|
262
298
|
};
|
|
263
299
|
}
|
|
@@ -32,6 +32,10 @@ export interface DetectContext {
|
|
|
32
32
|
dotnetWebSdk: boolean;
|
|
33
33
|
/** Package names from pubspec.yaml, lowercase. */
|
|
34
34
|
dartDeps: string[];
|
|
35
|
+
/** Gradle coordinates as `group:artifact`, without the version, lowercase. */
|
|
36
|
+
gradleDeps: string[];
|
|
37
|
+
/** Swift packages as `owner/repo`, and CocoaPods names, lowercase. */
|
|
38
|
+
swiftDeps: string[];
|
|
35
39
|
/** Lowercased combined dependency map (deps + devDeps). */
|
|
36
40
|
npmDeps: Record<string, string>;
|
|
37
41
|
workspaces: WorkspaceManifest[];
|
|
@@ -54,5 +58,7 @@ export declare function hasAnyRubyDep(ctx: DetectContext, names: string[]): stri
|
|
|
54
58
|
* `Npgsql.EntityFrameworkCore.PostgreSQL`, and a rule should be able to ask for
|
|
55
59
|
* `Npgsql` without listing every provider package that starts with it.
|
|
56
60
|
*/
|
|
61
|
+
export declare function hasAnyGradleDep(ctx: DetectContext, names: string[]): string[];
|
|
62
|
+
export declare function hasAnySwiftDep(ctx: DetectContext, names: string[]): string[];
|
|
57
63
|
export declare function hasAnyDartDep(ctx: DetectContext, names: string[]): string[];
|
|
58
64
|
export declare function hasAnyDotnetDep(ctx: DetectContext, names: string[]): string[];
|
|
@@ -8,6 +8,8 @@ exports.hasPhpDep = hasPhpDep;
|
|
|
8
8
|
exports.hasAnyPhpDep = hasAnyPhpDep;
|
|
9
9
|
exports.hasAnyGoDep = hasAnyGoDep;
|
|
10
10
|
exports.hasAnyRubyDep = hasAnyRubyDep;
|
|
11
|
+
exports.hasAnyGradleDep = hasAnyGradleDep;
|
|
12
|
+
exports.hasAnySwiftDep = hasAnySwiftDep;
|
|
11
13
|
exports.hasAnyDartDep = hasAnyDartDep;
|
|
12
14
|
exports.hasAnyDotnetDep = hasAnyDotnetDep;
|
|
13
15
|
function hasDep(ctx, name) {
|
|
@@ -55,6 +57,16 @@ function hasAnyRubyDep(ctx, names) {
|
|
|
55
57
|
* `Npgsql.EntityFrameworkCore.PostgreSQL`, and a rule should be able to ask for
|
|
56
58
|
* `Npgsql` without listing every provider package that starts with it.
|
|
57
59
|
*/
|
|
60
|
+
function hasAnyGradleDep(ctx, names) {
|
|
61
|
+
// Substring rather than equality: a coordinate is `androidx.room:room-runtime`, and
|
|
62
|
+
// a rule asks about `androidx.room` without wanting to name every artifact in it.
|
|
63
|
+
const wanted = names.map((name) => name.toLowerCase());
|
|
64
|
+
return ctx.gradleDeps.filter((dep) => wanted.some((name) => dep.includes(name)));
|
|
65
|
+
}
|
|
66
|
+
function hasAnySwiftDep(ctx, names) {
|
|
67
|
+
const wanted = names.map((name) => name.toLowerCase());
|
|
68
|
+
return ctx.swiftDeps.filter((dep) => wanted.some((name) => dep.includes(name)));
|
|
69
|
+
}
|
|
58
70
|
function hasAnyDartDep(ctx, names) {
|
|
59
71
|
return hasAnyIn(ctx.dartDeps, names);
|
|
60
72
|
}
|
|
@@ -38,6 +38,35 @@ const textSearch_1 = require("../utils/textSearch");
|
|
|
38
38
|
/** Files that say which platform this is, without reading their contents. */
|
|
39
39
|
const IOS_MARKERS = [/(^|\/)Info\.plist$/i, /(^|\/)Podfile$/, /\.xcodeproj\//, /(^|\/)Package\.swift$/];
|
|
40
40
|
const ANDROID_MARKERS = [/(^|\/)AndroidManifest\.xml$/i, /(^|\/)build\.gradle(\.kts)?$/];
|
|
41
|
+
/** Gradle coordinates that put a secret in the Android keystore rather than in a file. */
|
|
42
|
+
const SECURE_STORAGE_GRADLE = ['androidx.security:security-crypto', 'com.scottyab:secure-preferences'];
|
|
43
|
+
/**
|
|
44
|
+
* Swift keychain wrappers, named both ways.
|
|
45
|
+
*
|
|
46
|
+
* Swift Package Manager identifies a dependency as `owner/repo` and CocoaPods as a
|
|
47
|
+
* bare pod name, so the same library arrives under two spellings and a list holding
|
|
48
|
+
* only one of them finds it only half the time — which is how a Podfile declaring
|
|
49
|
+
* KeychainAccess came back as "no secure storage".
|
|
50
|
+
*/
|
|
51
|
+
const SECURE_STORAGE_SWIFT = [
|
|
52
|
+
'kishikawakatsumi/keychainaccess',
|
|
53
|
+
'keychainaccess',
|
|
54
|
+
'evgenyneu/keychain-swift',
|
|
55
|
+
'keychainswift',
|
|
56
|
+
'square/valet',
|
|
57
|
+
'valet',
|
|
58
|
+
];
|
|
59
|
+
/** Local databases, by platform, that let an app open without a network. */
|
|
60
|
+
const OFFLINE_GRADLE = ['androidx.room', 'io.realm', 'io.objectbox', 'com.squareup.sqldelight', 'app.cash.sqldelight'];
|
|
61
|
+
const OFFLINE_SWIFT = [
|
|
62
|
+
'groue/grdb.swift',
|
|
63
|
+
'grdb.swift',
|
|
64
|
+
'stephencelis/sqlite.swift',
|
|
65
|
+
'sqlite.swift',
|
|
66
|
+
'realm/realm-swift',
|
|
67
|
+
'realm/realm-cocoa',
|
|
68
|
+
'realmswift',
|
|
69
|
+
];
|
|
41
70
|
/** Dependencies that put a secret somewhere the operating system protects. */
|
|
42
71
|
const SECURE_STORAGE_DEPS = [
|
|
43
72
|
'flutter_secure_storage',
|
|
@@ -160,9 +189,16 @@ async function detectMobile(ctx) {
|
|
|
160
189
|
}
|
|
161
190
|
const secureStorage = (0, detectContext_1.hasAnyDep)(ctx, SECURE_STORAGE_DEPS);
|
|
162
191
|
const secureStorageDart = (0, detectContext_1.hasAnyDartDep)(ctx, ['flutter_secure_storage']);
|
|
192
|
+
const secureStorageNative = [
|
|
193
|
+
...(0, detectContext_1.hasAnyGradleDep)(ctx, SECURE_STORAGE_GRADLE),
|
|
194
|
+
...(0, detectContext_1.hasAnySwiftDep)(ctx, SECURE_STORAGE_SWIFT),
|
|
195
|
+
];
|
|
163
196
|
const keychainInSource = await (0, textSearch_1.searchInFiles)(ctx.root, ctx.files.source, [/KeychainAccess/, /kSecClass/, /EncryptedSharedPreferences/, /AndroidKeyStore/, /SecItemAdd/], 3);
|
|
164
197
|
const credentialEvidence = [
|
|
165
|
-
...[...secureStorage, ...secureStorageDart].map((dep) => ({
|
|
198
|
+
...[...secureStorage, ...secureStorageDart, ...secureStorageNative].map((dep) => ({
|
|
199
|
+
type: 'dependency',
|
|
200
|
+
value: dep,
|
|
201
|
+
})),
|
|
166
202
|
...keychainInSource.map((match) => ({
|
|
167
203
|
type: 'snippet',
|
|
168
204
|
value: match.snippet,
|
|
@@ -170,7 +206,12 @@ async function detectMobile(ctx) {
|
|
|
170
206
|
line: match.line,
|
|
171
207
|
})),
|
|
172
208
|
];
|
|
173
|
-
const offlineDeps = [
|
|
209
|
+
const offlineDeps = [
|
|
210
|
+
...(0, detectContext_1.hasAnyDep)(ctx, OFFLINE_DEPS),
|
|
211
|
+
...(0, detectContext_1.hasAnyDartDep)(ctx, OFFLINE_DEPS),
|
|
212
|
+
...(0, detectContext_1.hasAnyGradleDep)(ctx, OFFLINE_GRADLE),
|
|
213
|
+
...(0, detectContext_1.hasAnySwiftDep)(ctx, OFFLINE_SWIFT),
|
|
214
|
+
];
|
|
174
215
|
/**
|
|
175
216
|
* A local database is the strong signal; knowing the network dropped is the weak
|
|
176
217
|
* one. Both are recorded, because an app that stores nothing but tells the user the
|
|
@@ -3,18 +3,15 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.deriveLanguages = deriveLanguages;
|
|
4
4
|
exports.buildStackInfo = buildStackInfo;
|
|
5
5
|
exports.mergeDetectors = mergeDetectors;
|
|
6
|
+
const catalogue_1 = require("./catalogue");
|
|
6
7
|
function unique(items) {
|
|
7
8
|
return Array.from(new Set(items));
|
|
8
9
|
}
|
|
9
10
|
function deriveLanguages(files) {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
out.push('javascript');
|
|
15
|
-
if (files.some((f) => /\.py$/.test(f)))
|
|
16
|
-
out.push('python');
|
|
17
|
-
return unique(out);
|
|
11
|
+
// Read from the catalogue rather than from three hand-written lines. It knew about
|
|
12
|
+
// TypeScript, JavaScript and Python only, so a Go service reported no languages at
|
|
13
|
+
// all while the README listed twelve.
|
|
14
|
+
return unique(catalogue_1.LANGUAGES.filter(({ extensions }) => files.some((file) => extensions.test(file))).map(({ id }) => id));
|
|
18
15
|
}
|
|
19
16
|
function buildStackInfo(input) {
|
|
20
17
|
return {
|
package/dist/mcp/server.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
-
export declare function createProdkitMcpServer(): McpServer
|
|
1
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
export declare function createProdkitMcpServer(): Promise<McpServer>;
|
|
3
3
|
export declare function startProdkitMcpServer(): Promise<void>;
|
package/dist/mcp/server.js
CHANGED
|
@@ -2,8 +2,6 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.createProdkitMcpServer = createProdkitMcpServer;
|
|
4
4
|
exports.startProdkitMcpServer = startProdkitMcpServer;
|
|
5
|
-
const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
6
|
-
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
7
5
|
const zod_1 = require("zod");
|
|
8
6
|
const analyzeProject_1 = require("../analyzer/analyzeProject");
|
|
9
7
|
const buildReport_1 = require("../report/buildReport");
|
|
@@ -36,8 +34,36 @@ function errorResult(error) {
|
|
|
36
34
|
content: [{ type: 'text', text: `ProdKit could not analyse that path: ${message}` }],
|
|
37
35
|
};
|
|
38
36
|
}
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
/**
|
|
38
|
+
* The MCP SDK, loaded when the server starts rather than when this package is imported.
|
|
39
|
+
*
|
|
40
|
+
* It was a plain dependency, and it brought 164 of this package's 183 installed
|
|
41
|
+
* packages with it — so everyone installing the analyzer as a library or a CLI paid
|
|
42
|
+
* for a server they may never run, and inherited its supply-chain surface: network,
|
|
43
|
+
* shell and eval, none of which the analyzer itself does.
|
|
44
|
+
*
|
|
45
|
+
* The same shape the AI layer uses for the Anthropic SDK: a variable specifier so the
|
|
46
|
+
* TypeScript build does not need it, a webpackIgnore hint so a bundler does not try to
|
|
47
|
+
* resolve it, and an error that says exactly what to install when it is absent.
|
|
48
|
+
*/
|
|
49
|
+
const SDK_MCP = '@modelcontextprotocol/sdk/server/mcp.js';
|
|
50
|
+
const SDK_STDIO = '@modelcontextprotocol/sdk/server/stdio.js';
|
|
51
|
+
async function loadMcpSdk() {
|
|
52
|
+
try {
|
|
53
|
+
const [mcp, stdio] = await Promise.all([
|
|
54
|
+
import(/* webpackIgnore: true */ SDK_MCP),
|
|
55
|
+
import(/* webpackIgnore: true */ SDK_STDIO),
|
|
56
|
+
]);
|
|
57
|
+
return { McpServer: mcp.McpServer, StdioServerTransport: stdio.StdioServerTransport };
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
throw new Error('The MCP server needs the optional @modelcontextprotocol/sdk package. '
|
|
61
|
+
+ 'Install it alongside this one: npm install @modelcontextprotocol/sdk');
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
async function createProdkitMcpServer() {
|
|
65
|
+
const { McpServer: Server } = await loadMcpSdk();
|
|
66
|
+
const server = new Server({ name: 'prodkit', version: version_1.PRODKit_VERSION });
|
|
41
67
|
server.registerTool('list_profiles', {
|
|
42
68
|
title: 'List product profiles',
|
|
43
69
|
description: 'Lists the product profiles ProdKit can evaluate a repository against, with the capabilities each one expects.',
|
|
@@ -150,6 +176,7 @@ function createProdkitMcpServer() {
|
|
|
150
176
|
return server;
|
|
151
177
|
}
|
|
152
178
|
async function startProdkitMcpServer() {
|
|
153
|
-
const
|
|
154
|
-
|
|
179
|
+
const { StdioServerTransport } = await loadMcpSdk();
|
|
180
|
+
const server = await createProdkitMcpServer();
|
|
181
|
+
await server.connect(new StdioServerTransport());
|
|
155
182
|
}
|
|
@@ -108,14 +108,21 @@ function buildReport(analysis, options) {
|
|
|
108
108
|
const combinedScore = expectationScore === undefined
|
|
109
109
|
? observedScore
|
|
110
110
|
: Math.max(0, Math.min(100, Math.round((observedScore * 0.6) + (expectationScore * 0.4))));
|
|
111
|
+
// A recognised mobile platform counts as a stack signal. Without this an Android
|
|
112
|
+
// project with an AndroidManifest.xml and a build.gradle was reported as
|
|
113
|
+
// "inconclusive — no package manifests found", which is the analyzer saying it
|
|
114
|
+
// understood nothing about a project it had in fact identified, and capping the score
|
|
115
|
+
// at 39 on that basis.
|
|
116
|
+
const mobileDetected = analysis.detectors['mobile.platform']?.present === true;
|
|
111
117
|
const stackDetected = analysis.stack.frontend.length > 0
|
|
112
118
|
|| analysis.stack.backend.length > 0
|
|
113
|
-
|| analysis.stack.databases.length > 0
|
|
119
|
+
|| analysis.stack.databases.length > 0
|
|
120
|
+
|| mobileDetected;
|
|
114
121
|
const inconclusive = !stackDetected && analysis.workspaceStacks.length === 0;
|
|
115
122
|
const inconclusiveReasons = [];
|
|
116
123
|
if (inconclusive) {
|
|
117
124
|
inconclusiveReasons.push('No frontend, backend, or database stack signals were detected.');
|
|
118
|
-
inconclusiveReasons.push('No package
|
|
125
|
+
inconclusiveReasons.push('No package manifest was found in any format this analyzer reads.');
|
|
119
126
|
if (analysis.files.source.length === 0) {
|
|
120
127
|
inconclusiveReasons.push('No recognizable source files were found.');
|
|
121
128
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@produtype/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Deterministic CLI and library that analyzes a web application repository and reports how far it is from production-ready for the kind of product it is meant to be.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"bin": {
|
|
@@ -43,12 +43,20 @@
|
|
|
43
43
|
"node": ">=18"
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
|
-
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
47
46
|
"commander": "^12.1.0",
|
|
48
47
|
"fast-glob": "^3.3.2",
|
|
49
48
|
"zod": "^3.23.8"
|
|
50
49
|
},
|
|
50
|
+
"peerDependencies": {
|
|
51
|
+
"@modelcontextprotocol/sdk": "^1.30.0"
|
|
52
|
+
},
|
|
53
|
+
"peerDependenciesMeta": {
|
|
54
|
+
"@modelcontextprotocol/sdk": {
|
|
55
|
+
"optional": true
|
|
56
|
+
}
|
|
57
|
+
},
|
|
51
58
|
"devDependencies": {
|
|
59
|
+
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
52
60
|
"@types/node": "^22.20.3",
|
|
53
61
|
"@typescript-eslint/eslint-plugin": "^7.16.0",
|
|
54
62
|
"@typescript-eslint/parser": "^7.16.0",
|