@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.
Files changed (54) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/LICENSE +15 -0
  3. package/README.md +406 -0
  4. package/bin/lpm.js +334 -0
  5. package/index.d.ts +131 -0
  6. package/index.js +31 -0
  7. package/lib/api.js +324 -0
  8. package/lib/commands/add.js +1217 -0
  9. package/lib/commands/audit.js +283 -0
  10. package/lib/commands/cache.js +209 -0
  11. package/lib/commands/check-name.js +112 -0
  12. package/lib/commands/config.js +174 -0
  13. package/lib/commands/doctor.js +142 -0
  14. package/lib/commands/info.js +215 -0
  15. package/lib/commands/init.js +146 -0
  16. package/lib/commands/install.js +217 -0
  17. package/lib/commands/login.js +547 -0
  18. package/lib/commands/logout.js +94 -0
  19. package/lib/commands/marketplace-compare.js +164 -0
  20. package/lib/commands/marketplace-earnings.js +89 -0
  21. package/lib/commands/mcp-setup.js +363 -0
  22. package/lib/commands/open.js +82 -0
  23. package/lib/commands/outdated.js +291 -0
  24. package/lib/commands/pool-stats.js +100 -0
  25. package/lib/commands/publish.js +707 -0
  26. package/lib/commands/quality.js +211 -0
  27. package/lib/commands/remove.js +82 -0
  28. package/lib/commands/run.js +14 -0
  29. package/lib/commands/search.js +143 -0
  30. package/lib/commands/setup.js +92 -0
  31. package/lib/commands/skills.js +863 -0
  32. package/lib/commands/token-rotate.js +25 -0
  33. package/lib/commands/whoami.js +129 -0
  34. package/lib/config.js +240 -0
  35. package/lib/constants.js +190 -0
  36. package/lib/ecosystem.js +501 -0
  37. package/lib/editors.js +215 -0
  38. package/lib/import-rewriter.js +364 -0
  39. package/lib/install-targets/mcp-server.js +245 -0
  40. package/lib/install-targets/vscode-extension.js +178 -0
  41. package/lib/install-targets.js +82 -0
  42. package/lib/integrity.js +179 -0
  43. package/lib/lpm-config-prompts.js +102 -0
  44. package/lib/lpm-config.js +408 -0
  45. package/lib/project-utils.js +152 -0
  46. package/lib/quality/checks.js +654 -0
  47. package/lib/quality/display.js +139 -0
  48. package/lib/quality/score.js +115 -0
  49. package/lib/quality/swift-checks.js +447 -0
  50. package/lib/safe-path.js +180 -0
  51. package/lib/secure-store.js +288 -0
  52. package/lib/swift-project.js +637 -0
  53. package/lib/ui.js +40 -0
  54. 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
+ }