@lpm-registry/cli 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/CHANGELOG.md +36 -0
- package/LICENSE +15 -0
- package/README.md +406 -0
- package/bin/lpm.js +334 -0
- package/index.d.ts +131 -0
- package/index.js +31 -0
- package/lib/api.js +324 -0
- package/lib/commands/add.js +1217 -0
- package/lib/commands/audit.js +283 -0
- package/lib/commands/cache.js +209 -0
- package/lib/commands/check-name.js +112 -0
- package/lib/commands/config.js +174 -0
- package/lib/commands/doctor.js +142 -0
- package/lib/commands/info.js +215 -0
- package/lib/commands/init.js +146 -0
- package/lib/commands/install.js +217 -0
- package/lib/commands/login.js +547 -0
- package/lib/commands/logout.js +94 -0
- package/lib/commands/marketplace-compare.js +164 -0
- package/lib/commands/marketplace-earnings.js +89 -0
- package/lib/commands/mcp-setup.js +363 -0
- package/lib/commands/open.js +82 -0
- package/lib/commands/outdated.js +291 -0
- package/lib/commands/pool-stats.js +100 -0
- package/lib/commands/publish.js +707 -0
- package/lib/commands/quality.js +211 -0
- package/lib/commands/remove.js +82 -0
- package/lib/commands/run.js +14 -0
- package/lib/commands/search.js +143 -0
- package/lib/commands/setup.js +92 -0
- package/lib/commands/skills.js +863 -0
- package/lib/commands/token-rotate.js +25 -0
- package/lib/commands/whoami.js +129 -0
- package/lib/config.js +240 -0
- package/lib/constants.js +190 -0
- package/lib/ecosystem.js +501 -0
- package/lib/editors.js +215 -0
- package/lib/import-rewriter.js +364 -0
- package/lib/install-targets/mcp-server.js +245 -0
- package/lib/install-targets/vscode-extension.js +178 -0
- package/lib/install-targets.js +82 -0
- package/lib/integrity.js +179 -0
- package/lib/lpm-config-prompts.js +102 -0
- package/lib/lpm-config.js +408 -0
- package/lib/project-utils.js +152 -0
- package/lib/quality/checks.js +654 -0
- package/lib/quality/display.js +139 -0
- package/lib/quality/score.js +115 -0
- package/lib/quality/swift-checks.js +447 -0
- package/lib/safe-path.js +180 -0
- package/lib/secure-store.js +288 -0
- package/lib/swift-project.js +637 -0
- package/lib/ui.js +40 -0
- package/package.json +74 -0
|
@@ -0,0 +1,654 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quality check definitions for LPM packages.
|
|
3
|
+
* Each check is a pure function that returns { passed, detail }.
|
|
4
|
+
* Checks are grouped by category and scored out of 100 total points.
|
|
5
|
+
*
|
|
6
|
+
* Categories:
|
|
7
|
+
* - documentation: 22 points (6 checks)
|
|
8
|
+
* - code: 31 points (9 checks)
|
|
9
|
+
* - testing: 11 points (2 checks)
|
|
10
|
+
* - health: 36 points (11 checks)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const DEFAULT_TEST_SCRIPT = 'echo "Error: no test" && exit 1'
|
|
14
|
+
|
|
15
|
+
// --- Documentation checks (22 points) ---
|
|
16
|
+
|
|
17
|
+
const hasReadme = {
|
|
18
|
+
id: "has-readme",
|
|
19
|
+
category: "documentation",
|
|
20
|
+
label: "Has README",
|
|
21
|
+
maxPoints: 8,
|
|
22
|
+
run: ({ readme }) => ({
|
|
23
|
+
passed: !!readme && readme.length > 100,
|
|
24
|
+
detail: readme
|
|
25
|
+
? `README found (${readme.length.toLocaleString()} chars)`
|
|
26
|
+
: "No README found",
|
|
27
|
+
}),
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const readmeHasInstall = {
|
|
31
|
+
id: "readme-install",
|
|
32
|
+
category: "documentation",
|
|
33
|
+
label: "README has install section",
|
|
34
|
+
maxPoints: 3,
|
|
35
|
+
run: ({ readme }) => {
|
|
36
|
+
if (!readme) return { passed: false, detail: "No README" }
|
|
37
|
+
const lower = readme.toLowerCase()
|
|
38
|
+
const hasSection =
|
|
39
|
+
lower.includes("## install") ||
|
|
40
|
+
lower.includes("## getting started") ||
|
|
41
|
+
lower.includes("## setup") ||
|
|
42
|
+
lower.includes("npm install") ||
|
|
43
|
+
lower.includes("lpm add") ||
|
|
44
|
+
lower.includes("lpm install")
|
|
45
|
+
return {
|
|
46
|
+
passed: hasSection,
|
|
47
|
+
detail: hasSection
|
|
48
|
+
? "Install instructions found"
|
|
49
|
+
: "No install section found in README",
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const readmeHasUsage = {
|
|
55
|
+
id: "readme-usage",
|
|
56
|
+
category: "documentation",
|
|
57
|
+
label: "README has usage examples",
|
|
58
|
+
maxPoints: 3,
|
|
59
|
+
run: ({ readme }) => {
|
|
60
|
+
if (!readme) return { passed: false, detail: "No README" }
|
|
61
|
+
const lower = readme.toLowerCase()
|
|
62
|
+
const hasUsageSection =
|
|
63
|
+
lower.includes("## usage") || lower.includes("## example")
|
|
64
|
+
const codeBlockCount = (readme.match(/```/g) || []).length / 2
|
|
65
|
+
const hasCodeBlocks = codeBlockCount >= 2
|
|
66
|
+
const passed = hasUsageSection || hasCodeBlocks
|
|
67
|
+
return {
|
|
68
|
+
passed,
|
|
69
|
+
detail: passed
|
|
70
|
+
? `Usage examples found (${Math.floor(codeBlockCount)} code blocks)`
|
|
71
|
+
: "No usage examples found in README",
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const readmeHasApi = {
|
|
77
|
+
id: "readme-api",
|
|
78
|
+
category: "documentation",
|
|
79
|
+
label: "README has API docs",
|
|
80
|
+
maxPoints: 2,
|
|
81
|
+
run: ({ readme }) => {
|
|
82
|
+
if (!readme) return { passed: false, detail: "No README" }
|
|
83
|
+
const lower = readme.toLowerCase()
|
|
84
|
+
const hasApiSection =
|
|
85
|
+
lower.includes("## api") ||
|
|
86
|
+
lower.includes("## reference") ||
|
|
87
|
+
lower.includes("## props") ||
|
|
88
|
+
lower.includes("## parameters") ||
|
|
89
|
+
lower.includes("## options")
|
|
90
|
+
return {
|
|
91
|
+
passed: hasApiSection,
|
|
92
|
+
detail: hasApiSection
|
|
93
|
+
? "API documentation found"
|
|
94
|
+
: "No API/reference section found in README",
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const hasChangelog = {
|
|
100
|
+
id: "has-changelog",
|
|
101
|
+
category: "documentation",
|
|
102
|
+
label: "Has CHANGELOG",
|
|
103
|
+
maxPoints: 3,
|
|
104
|
+
run: ({ files }) => {
|
|
105
|
+
const changelogFiles = files.filter(f => {
|
|
106
|
+
const name = (f.path || f).toLowerCase()
|
|
107
|
+
return name.includes("changelog")
|
|
108
|
+
})
|
|
109
|
+
return {
|
|
110
|
+
passed: changelogFiles.length > 0,
|
|
111
|
+
detail:
|
|
112
|
+
changelogFiles.length > 0
|
|
113
|
+
? "CHANGELOG found"
|
|
114
|
+
: "No CHANGELOG file found",
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const hasLicense = {
|
|
120
|
+
id: "has-license",
|
|
121
|
+
category: "documentation",
|
|
122
|
+
label: "Has LICENSE file",
|
|
123
|
+
maxPoints: 3,
|
|
124
|
+
run: ({ files, packageJson }) => {
|
|
125
|
+
const licenseFiles = files.filter(f => {
|
|
126
|
+
const name = (f.path || f).toLowerCase()
|
|
127
|
+
return name.includes("license") || name.includes("licence")
|
|
128
|
+
})
|
|
129
|
+
const hasLicenseField = !!packageJson.license
|
|
130
|
+
const passed = licenseFiles.length > 0 || hasLicenseField
|
|
131
|
+
return {
|
|
132
|
+
passed,
|
|
133
|
+
detail:
|
|
134
|
+
licenseFiles.length > 0
|
|
135
|
+
? "LICENSE file found"
|
|
136
|
+
: hasLicenseField
|
|
137
|
+
? `License field: ${packageJson.license}`
|
|
138
|
+
: "No LICENSE file or license field",
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// --- Code Quality checks (31 points) ---
|
|
144
|
+
|
|
145
|
+
const hasTypes = {
|
|
146
|
+
id: "has-types",
|
|
147
|
+
category: "code",
|
|
148
|
+
label: "Has TypeScript types",
|
|
149
|
+
maxPoints: 8,
|
|
150
|
+
run: ({ packageJson, files }) => {
|
|
151
|
+
const hasTypesField = !!(packageJson.types || packageJson.typings)
|
|
152
|
+
const hasDtsFiles = files.some(f => {
|
|
153
|
+
const name = f.path || f
|
|
154
|
+
return name.endsWith(".d.ts") || name.endsWith(".d.mts")
|
|
155
|
+
})
|
|
156
|
+
const passed = hasTypesField || hasDtsFiles
|
|
157
|
+
return {
|
|
158
|
+
passed,
|
|
159
|
+
detail: hasTypesField
|
|
160
|
+
? `Types field: ${packageJson.types || packageJson.typings}`
|
|
161
|
+
: hasDtsFiles
|
|
162
|
+
? ".d.ts files found"
|
|
163
|
+
: "No TypeScript type definitions found",
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const intellisenseCoverage = {
|
|
169
|
+
id: "intellisense-coverage",
|
|
170
|
+
category: "code",
|
|
171
|
+
label: "Intellisense coverage",
|
|
172
|
+
maxPoints: 4,
|
|
173
|
+
run: ({ packageJson, files }) => {
|
|
174
|
+
// Full points if .d.ts files exist (complete intellisense)
|
|
175
|
+
const hasTypesField = !!(packageJson.types || packageJson.typings)
|
|
176
|
+
const hasDtsFiles = files.some(f => {
|
|
177
|
+
const name = f.path || f
|
|
178
|
+
return name.endsWith(".d.ts") || name.endsWith(".d.mts")
|
|
179
|
+
})
|
|
180
|
+
if (hasTypesField || hasDtsFiles) {
|
|
181
|
+
return {
|
|
182
|
+
passed: true,
|
|
183
|
+
detail: "TypeScript definitions provide full intellisense",
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
// Partial check: JSDoc detection runs server-side from tarball
|
|
187
|
+
// CLI assumes fail; server can upgrade if JSDoc found
|
|
188
|
+
return {
|
|
189
|
+
passed: false,
|
|
190
|
+
detail: "No .d.ts files or JSDoc detected for intellisense",
|
|
191
|
+
serverOnly: true,
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const hasEsm = {
|
|
197
|
+
id: "esm-exports",
|
|
198
|
+
category: "code",
|
|
199
|
+
label: "ESM exports",
|
|
200
|
+
maxPoints: 3,
|
|
201
|
+
run: ({ packageJson }) => {
|
|
202
|
+
const isModule = packageJson.type === "module"
|
|
203
|
+
const hasModuleField = !!packageJson.module
|
|
204
|
+
const hasExportsField = !!packageJson.exports
|
|
205
|
+
const passed = isModule || hasModuleField || hasExportsField
|
|
206
|
+
return {
|
|
207
|
+
passed,
|
|
208
|
+
detail: isModule
|
|
209
|
+
? '"type": "module" detected'
|
|
210
|
+
: hasExportsField
|
|
211
|
+
? '"exports" field detected'
|
|
212
|
+
: hasModuleField
|
|
213
|
+
? '"module" field detected'
|
|
214
|
+
: "No ESM support detected",
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const treeShakable = {
|
|
220
|
+
id: "tree-shakable",
|
|
221
|
+
category: "code",
|
|
222
|
+
label: "Tree-shakable",
|
|
223
|
+
maxPoints: 3,
|
|
224
|
+
run: ({ packageJson }) => {
|
|
225
|
+
const hasSideEffects = packageJson.sideEffects === false
|
|
226
|
+
const hasExportsField = !!packageJson.exports
|
|
227
|
+
const isModule = packageJson.type === "module"
|
|
228
|
+
const passed = hasSideEffects || (hasExportsField && isModule)
|
|
229
|
+
let detail
|
|
230
|
+
if (hasSideEffects) {
|
|
231
|
+
detail = '"sideEffects": false enables tree-shaking'
|
|
232
|
+
} else if (hasExportsField && isModule) {
|
|
233
|
+
detail = "ESM + exports map enables tree-shaking"
|
|
234
|
+
} else {
|
|
235
|
+
detail = 'No "sideEffects": false in package.json'
|
|
236
|
+
}
|
|
237
|
+
return { passed, detail }
|
|
238
|
+
},
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const noEval = {
|
|
242
|
+
id: "no-eval",
|
|
243
|
+
category: "code",
|
|
244
|
+
label: "No eval/Function() patterns",
|
|
245
|
+
maxPoints: 3,
|
|
246
|
+
run: () => {
|
|
247
|
+
// This check runs server-side from tarball contents.
|
|
248
|
+
// CLI assumes pass; server overrides if eval detected.
|
|
249
|
+
return {
|
|
250
|
+
passed: true,
|
|
251
|
+
detail: "Full check runs server-side",
|
|
252
|
+
serverOnly: true,
|
|
253
|
+
}
|
|
254
|
+
},
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const hasEngines = {
|
|
258
|
+
id: "has-engines",
|
|
259
|
+
category: "code",
|
|
260
|
+
label: 'Has "engines" field',
|
|
261
|
+
maxPoints: 1,
|
|
262
|
+
run: ({ packageJson }) => {
|
|
263
|
+
const hasField = !!packageJson.engines?.node
|
|
264
|
+
return {
|
|
265
|
+
passed: hasField,
|
|
266
|
+
detail: hasField
|
|
267
|
+
? `engines.node: ${packageJson.engines.node}`
|
|
268
|
+
: 'No "engines" field in package.json',
|
|
269
|
+
}
|
|
270
|
+
},
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const hasExportsMap = {
|
|
274
|
+
id: "has-exports-map",
|
|
275
|
+
category: "code",
|
|
276
|
+
label: 'Has "exports" map',
|
|
277
|
+
maxPoints: 4,
|
|
278
|
+
run: ({ packageJson }) => {
|
|
279
|
+
const hasField = !!packageJson.exports
|
|
280
|
+
return {
|
|
281
|
+
passed: hasField,
|
|
282
|
+
detail: hasField
|
|
283
|
+
? '"exports" map defined'
|
|
284
|
+
: 'No "exports" map in package.json',
|
|
285
|
+
}
|
|
286
|
+
},
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const smallDeps = {
|
|
290
|
+
id: "small-deps",
|
|
291
|
+
category: "code",
|
|
292
|
+
label: "Small dependency footprint",
|
|
293
|
+
maxPoints: 4,
|
|
294
|
+
run: ({ packageJson }) => {
|
|
295
|
+
const deps = packageJson.dependencies
|
|
296
|
+
? Object.keys(packageJson.dependencies).length
|
|
297
|
+
: 0
|
|
298
|
+
let points
|
|
299
|
+
if (deps === 0) points = 4
|
|
300
|
+
else if (deps <= 3) points = 3
|
|
301
|
+
else if (deps <= 7) points = 2
|
|
302
|
+
else if (deps <= 15) points = 1
|
|
303
|
+
else points = 0
|
|
304
|
+
return {
|
|
305
|
+
passed: points > 0,
|
|
306
|
+
points,
|
|
307
|
+
detail: `${deps} production ${deps === 1 ? "dependency" : "dependencies"}`,
|
|
308
|
+
}
|
|
309
|
+
},
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const sourceMaps = {
|
|
313
|
+
id: "source-maps",
|
|
314
|
+
category: "code",
|
|
315
|
+
label: "Source maps included",
|
|
316
|
+
maxPoints: 1,
|
|
317
|
+
run: ({ files }) => {
|
|
318
|
+
const mapFiles = files.filter(f => {
|
|
319
|
+
const name = f.path || f
|
|
320
|
+
return name.endsWith(".js.map") || name.endsWith(".mjs.map")
|
|
321
|
+
})
|
|
322
|
+
return {
|
|
323
|
+
passed: mapFiles.length > 0,
|
|
324
|
+
detail:
|
|
325
|
+
mapFiles.length > 0
|
|
326
|
+
? `${mapFiles.length} source map${mapFiles.length !== 1 ? "s" : ""} found`
|
|
327
|
+
: "No source map files found",
|
|
328
|
+
}
|
|
329
|
+
},
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// --- Testing checks (11 points) ---
|
|
333
|
+
|
|
334
|
+
const hasTestFiles = {
|
|
335
|
+
id: "has-test-files",
|
|
336
|
+
category: "testing",
|
|
337
|
+
label: "Has test files",
|
|
338
|
+
maxPoints: 7,
|
|
339
|
+
run: ({ files }) => {
|
|
340
|
+
const testFiles = files.filter(f => {
|
|
341
|
+
const name = (f.path || f).toLowerCase()
|
|
342
|
+
return (
|
|
343
|
+
name.includes(".test.") ||
|
|
344
|
+
name.includes(".spec.") ||
|
|
345
|
+
name.includes("__tests__/") ||
|
|
346
|
+
name.startsWith("test/") ||
|
|
347
|
+
name.startsWith("tests/")
|
|
348
|
+
)
|
|
349
|
+
})
|
|
350
|
+
return {
|
|
351
|
+
passed: testFiles.length > 0,
|
|
352
|
+
detail:
|
|
353
|
+
testFiles.length > 0
|
|
354
|
+
? `${testFiles.length} test file${testFiles.length !== 1 ? "s" : ""} found`
|
|
355
|
+
: "No test files found",
|
|
356
|
+
}
|
|
357
|
+
},
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const hasTestScript = {
|
|
361
|
+
id: "has-test-script",
|
|
362
|
+
category: "testing",
|
|
363
|
+
label: "Has test script",
|
|
364
|
+
maxPoints: 4,
|
|
365
|
+
run: ({ packageJson }) => {
|
|
366
|
+
const testScript = packageJson.scripts?.test
|
|
367
|
+
const hasScript = !!testScript && !testScript.includes(DEFAULT_TEST_SCRIPT)
|
|
368
|
+
return {
|
|
369
|
+
passed: hasScript,
|
|
370
|
+
detail: hasScript
|
|
371
|
+
? `test script: ${testScript}`
|
|
372
|
+
: "No test script in package.json",
|
|
373
|
+
}
|
|
374
|
+
},
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// --- Package Health checks (36 points) ---
|
|
378
|
+
|
|
379
|
+
const hasDescription = {
|
|
380
|
+
id: "has-description",
|
|
381
|
+
category: "health",
|
|
382
|
+
label: "Has description",
|
|
383
|
+
maxPoints: 3,
|
|
384
|
+
run: ({ packageJson }) => {
|
|
385
|
+
const desc = packageJson.description
|
|
386
|
+
const passed = !!desc && desc.length > 10
|
|
387
|
+
return {
|
|
388
|
+
passed,
|
|
389
|
+
detail: passed
|
|
390
|
+
? `Description: "${desc.substring(0, 60)}${desc.length > 60 ? "..." : ""}"`
|
|
391
|
+
: "No meaningful description in package.json",
|
|
392
|
+
}
|
|
393
|
+
},
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const hasKeywords = {
|
|
397
|
+
id: "has-keywords",
|
|
398
|
+
category: "health",
|
|
399
|
+
label: "Has keywords",
|
|
400
|
+
maxPoints: 1,
|
|
401
|
+
run: ({ packageJson }) => {
|
|
402
|
+
const keywords = packageJson.keywords
|
|
403
|
+
const passed = Array.isArray(keywords) && keywords.length > 0
|
|
404
|
+
return {
|
|
405
|
+
passed,
|
|
406
|
+
detail: passed
|
|
407
|
+
? `${keywords.length} keyword${keywords.length !== 1 ? "s" : ""}`
|
|
408
|
+
: "No keywords in package.json",
|
|
409
|
+
}
|
|
410
|
+
},
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const hasRepository = {
|
|
414
|
+
id: "has-repository",
|
|
415
|
+
category: "health",
|
|
416
|
+
label: "Has repository URL",
|
|
417
|
+
maxPoints: 2,
|
|
418
|
+
run: ({ packageJson }) => {
|
|
419
|
+
const repo = packageJson.repository
|
|
420
|
+
const hasRepo = !!(
|
|
421
|
+
repo &&
|
|
422
|
+
(typeof repo === "string" || typeof repo?.url === "string")
|
|
423
|
+
)
|
|
424
|
+
return {
|
|
425
|
+
passed: hasRepo,
|
|
426
|
+
detail: hasRepo
|
|
427
|
+
? `Repository: ${typeof repo === "string" ? repo : repo.url}`
|
|
428
|
+
: "No repository field in package.json",
|
|
429
|
+
}
|
|
430
|
+
},
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const hasHomepage = {
|
|
434
|
+
id: "has-homepage",
|
|
435
|
+
category: "health",
|
|
436
|
+
label: "Has homepage",
|
|
437
|
+
maxPoints: 1,
|
|
438
|
+
run: ({ packageJson }) => {
|
|
439
|
+
const passed = !!packageJson.homepage
|
|
440
|
+
return {
|
|
441
|
+
passed,
|
|
442
|
+
detail: passed
|
|
443
|
+
? `Homepage: ${packageJson.homepage}`
|
|
444
|
+
: "No homepage in package.json",
|
|
445
|
+
}
|
|
446
|
+
},
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const reasonableSize = {
|
|
450
|
+
id: "reasonable-size",
|
|
451
|
+
category: "health",
|
|
452
|
+
label: "Reasonable bundle size",
|
|
453
|
+
maxPoints: 3,
|
|
454
|
+
run: ({ unpackedSize }) => {
|
|
455
|
+
if (!unpackedSize)
|
|
456
|
+
return { passed: true, points: 3, detail: "Size unknown" }
|
|
457
|
+
const kb = unpackedSize / 1024
|
|
458
|
+
const mb = kb / 1024
|
|
459
|
+
let points
|
|
460
|
+
if (mb < 0.1) points = 3
|
|
461
|
+
else if (mb < 0.5) points = 2
|
|
462
|
+
else if (mb < 1) points = 1
|
|
463
|
+
else points = 0
|
|
464
|
+
|
|
465
|
+
const sizeStr = mb >= 1 ? `${mb.toFixed(1)}MB` : `${Math.round(kb)}KB`
|
|
466
|
+
return {
|
|
467
|
+
passed: points > 0,
|
|
468
|
+
points,
|
|
469
|
+
detail: `Unpacked size: ${sizeStr}`,
|
|
470
|
+
}
|
|
471
|
+
},
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const noVulnerabilities = {
|
|
475
|
+
id: "no-vulnerabilities",
|
|
476
|
+
category: "health",
|
|
477
|
+
label: "No known vulnerabilities",
|
|
478
|
+
maxPoints: 5,
|
|
479
|
+
run: () => {
|
|
480
|
+
// This check runs server-side with OSV scan results.
|
|
481
|
+
// CLI assumes pass; server overrides with actual results.
|
|
482
|
+
return {
|
|
483
|
+
passed: true,
|
|
484
|
+
detail: "Full check runs server-side",
|
|
485
|
+
serverOnly: true,
|
|
486
|
+
}
|
|
487
|
+
},
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const maintenanceHealth = {
|
|
491
|
+
id: "maintenance-health",
|
|
492
|
+
category: "health",
|
|
493
|
+
label: "Active maintenance",
|
|
494
|
+
maxPoints: 4,
|
|
495
|
+
run: () => {
|
|
496
|
+
// Server-only: checks if last version was published within 90 days.
|
|
497
|
+
// CLI cannot know this — assumes pass.
|
|
498
|
+
return {
|
|
499
|
+
passed: true,
|
|
500
|
+
detail: "Full check runs server-side",
|
|
501
|
+
serverOnly: true,
|
|
502
|
+
}
|
|
503
|
+
},
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
const semverConsistency = {
|
|
507
|
+
id: "semver-consistency",
|
|
508
|
+
category: "health",
|
|
509
|
+
label: "SemVer consistency",
|
|
510
|
+
maxPoints: 4,
|
|
511
|
+
run: ({ packageJson }) => {
|
|
512
|
+
// CLI can check if the current version is valid semver
|
|
513
|
+
const version = packageJson.version
|
|
514
|
+
if (!version) return { passed: false, detail: "No version in package.json" }
|
|
515
|
+
// Basic semver regex (major.minor.patch with optional pre-release)
|
|
516
|
+
const semverRegex = /^\d+\.\d+\.\d+(-[\w.]+)?(\+[\w.]+)?$/
|
|
517
|
+
const isValid = semverRegex.test(version)
|
|
518
|
+
// Server can also check version history for wild jumps
|
|
519
|
+
return {
|
|
520
|
+
passed: isValid,
|
|
521
|
+
detail: isValid
|
|
522
|
+
? `Valid semver: ${version}`
|
|
523
|
+
: `Invalid semver format: ${version}`,
|
|
524
|
+
serverOnly: !isValid ? undefined : true,
|
|
525
|
+
}
|
|
526
|
+
},
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
const authorVerified = {
|
|
530
|
+
id: "author-verified",
|
|
531
|
+
category: "health",
|
|
532
|
+
label: "Verified author",
|
|
533
|
+
maxPoints: 3,
|
|
534
|
+
run: () => {
|
|
535
|
+
// Server-only: checks if the author has linked social accounts.
|
|
536
|
+
// CLI cannot know this — assumes pass.
|
|
537
|
+
return {
|
|
538
|
+
passed: true,
|
|
539
|
+
detail: "Full check runs server-side",
|
|
540
|
+
serverOnly: true,
|
|
541
|
+
}
|
|
542
|
+
},
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// --- Agent Skills checks (10 points) ---
|
|
546
|
+
|
|
547
|
+
const hasSkills = {
|
|
548
|
+
id: "has-skills",
|
|
549
|
+
category: "health",
|
|
550
|
+
label: "Has Agent Skills",
|
|
551
|
+
maxPoints: 7,
|
|
552
|
+
run: () => {
|
|
553
|
+
// Server-only: checks if package has approved skills.
|
|
554
|
+
// CLI cannot determine this — assumes fail.
|
|
555
|
+
return {
|
|
556
|
+
passed: false,
|
|
557
|
+
detail: "Full check runs server-side",
|
|
558
|
+
serverOnly: true,
|
|
559
|
+
}
|
|
560
|
+
},
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
const skillsComprehensive = {
|
|
564
|
+
id: "skills-comprehensive",
|
|
565
|
+
category: "health",
|
|
566
|
+
label: "Comprehensive Agent Skills",
|
|
567
|
+
maxPoints: 3,
|
|
568
|
+
run: () => {
|
|
569
|
+
// Server-only: checks if package has 3+ approved skills.
|
|
570
|
+
// CLI cannot determine this — assumes fail.
|
|
571
|
+
return {
|
|
572
|
+
passed: false,
|
|
573
|
+
detail: "Full check runs server-side",
|
|
574
|
+
serverOnly: true,
|
|
575
|
+
}
|
|
576
|
+
},
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// --- Export all checks ---
|
|
580
|
+
|
|
581
|
+
/** JS checks (default) */
|
|
582
|
+
export const checks = [
|
|
583
|
+
// Documentation (22)
|
|
584
|
+
hasReadme,
|
|
585
|
+
readmeHasInstall,
|
|
586
|
+
readmeHasUsage,
|
|
587
|
+
readmeHasApi,
|
|
588
|
+
hasChangelog,
|
|
589
|
+
hasLicense,
|
|
590
|
+
// Code Quality (31)
|
|
591
|
+
hasTypes,
|
|
592
|
+
intellisenseCoverage,
|
|
593
|
+
hasEsm,
|
|
594
|
+
treeShakable,
|
|
595
|
+
noEval,
|
|
596
|
+
hasEngines,
|
|
597
|
+
hasExportsMap,
|
|
598
|
+
smallDeps,
|
|
599
|
+
sourceMaps,
|
|
600
|
+
// Testing (11)
|
|
601
|
+
hasTestFiles,
|
|
602
|
+
hasTestScript,
|
|
603
|
+
// Health (36)
|
|
604
|
+
hasDescription,
|
|
605
|
+
hasKeywords,
|
|
606
|
+
hasRepository,
|
|
607
|
+
hasHomepage,
|
|
608
|
+
reasonableSize,
|
|
609
|
+
noVulnerabilities,
|
|
610
|
+
maintenanceHealth,
|
|
611
|
+
semverConsistency,
|
|
612
|
+
authorVerified,
|
|
613
|
+
hasSkills,
|
|
614
|
+
skillsComprehensive,
|
|
615
|
+
]
|
|
616
|
+
|
|
617
|
+
// Export individual universal checks for reuse by ecosystem-specific check sets
|
|
618
|
+
export {
|
|
619
|
+
hasReadme,
|
|
620
|
+
readmeHasUsage,
|
|
621
|
+
readmeHasApi,
|
|
622
|
+
hasChangelog,
|
|
623
|
+
hasLicense,
|
|
624
|
+
hasDescription,
|
|
625
|
+
hasKeywords,
|
|
626
|
+
hasRepository,
|
|
627
|
+
hasHomepage,
|
|
628
|
+
reasonableSize,
|
|
629
|
+
noVulnerabilities,
|
|
630
|
+
maintenanceHealth,
|
|
631
|
+
semverConsistency,
|
|
632
|
+
authorVerified,
|
|
633
|
+
hasSkills,
|
|
634
|
+
skillsComprehensive,
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/**
|
|
638
|
+
* Get source package informational badges (not scored)
|
|
639
|
+
* @param {object} lpmConfig
|
|
640
|
+
* @returns {object|null}
|
|
641
|
+
*/
|
|
642
|
+
export function getSourcePackageInfo(lpmConfig) {
|
|
643
|
+
if (!lpmConfig) return null
|
|
644
|
+
return {
|
|
645
|
+
hasConfig: true,
|
|
646
|
+
hasDefaults: !!lpmConfig.defaultConfig,
|
|
647
|
+
optionCount: lpmConfig.configSchema
|
|
648
|
+
? Object.keys(lpmConfig.configSchema).length
|
|
649
|
+
: 0,
|
|
650
|
+
usesConditionalIncludes: (lpmConfig.files || []).some(
|
|
651
|
+
f => f.include === "when" && f.condition,
|
|
652
|
+
),
|
|
653
|
+
}
|
|
654
|
+
}
|